20.6 Nullability at Boundaries
22. Final Fields and Immutability
24. equals, hashCode, and toString
26. Static Analysis Enforcement
This project follows the Google Java Style Guide.
The Google Java Style Guide is the authoritative source for Java formatting, naming, organization, and style decisions. These guidelines summarize the rules most relevant to the project.
When these guidelines do not explicitly address a situation, follow the Google Java Style Guide.
Do not introduce project-specific formatting conventions that conflict with Google Java Style.
Java files must:
-
Use UTF-8 encoding.
-
Contain a package declaration.
-
Have imports after the package declaration.
-
Contain exactly one top-level class declaration.
-
Use the same name as the top-level class for the .java filename.
-
Try not to use wildcard imports (*).
The standard structure is:
package com.example.users;
import java.util.List;
import java.util.Optional;
public final class UserService {
// ...
}Google Java Style requires exactly one top-level class per source file and specifies the ordering of license information, package declaration, imports, and the class declaration.
Use K&R-style braces where the opening brace is on the same line as the declaration or control structure.
Classes:
public class UserService {
// ...
}Methods:
public User getUser(long userId) {
// ...
}Constructors:
public UserService(UserRepository repository) {
this.repository = repository;
}Interfaces:
public interface UserRepository {
User findById(long userId);
}Control structures:
if (user.isActive()) {
processUser(user);
}
for (User user : users) {
processUser(user);
}
while (hasMoreUsers()) {
processNextUser();
}Do not put opening braces on a separate line.
// Incorrect
public class UserService
{
}
// Incorrect
public void processUser(User user)
{
}Google Java Style explicitly specifies K&R-style braces for non-empty blocks and block-like constructs.
Always use braces for:
-
if
-
else
-
for
-
while
-
do
even when the body has only one statement.
Correct:
if (user.isActive()) {
processUser(user);
}Incorrect:
if (user.isActive())
processUser(user);This prevents bugs caused by adding statements to an apparently single-statement block and is explicitly required by Google Java Style.
Keep else, catch, and finally on the same line as the preceding closing brace.
if (user.isAdmin()) {
grantAdminAccess();
} else {
grantUserAccess();
}
try {
processRequest();
} catch (IOException e) {
handleError(e);
} finally {
cleanup();
}Use 2 spaces per indentation level.
Never use tabs for indentation.
public class UserService {
public User getUser(long userId) {
if (userId <= 0) {
throw new IllegalArgumentException("Invalid user ID");
}
return repository.findById(userId);
}
}Google Java Style specifies two-space indentation for each new block or block-like construct.
Java source code has a 100-character column limit.
Lines exceeding the limit must be wrapped according to Google's line-wrapping rules.
User user = userRepository.findByOrganizationAndEmail(
organizationId, email);The 100-character limit has exceptions for package declarations and imports.
When wrapping a line, prefer breaking at a higher syntactic level.
Continuation lines are indented at least 4 additional spaces.
private static final String MESSAGE = "This is a long message that needs to be "
+ "split across multiple lines.";Do not arbitrarily align tokens with spaces merely for visual alignment.
Google considers horizontal alignment optional and never requires it.
Use one statement per line.
User user = repository.findById(userId);
processUser(user);Do not place multiple statements on the same line.
// Incorrect
User user = repository.findById(userId); processUser(user);Use a single blank line between consecutive class members and initializers.
public class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
public User getUser(long userId) {
return repository.findById(userId);
}
public void deleteUser(long userId) {
repository.deleteById(userId);
}
}A blank line between consecutive fields is optional when it helps create logical groups.
Multiple blank lines should not be used merely for visual spacing.
Follow Google's horizontal whitespace rules.
Examples:
if (condition) {
}
for (User user : users) {
}
while (condition) {
}
return value + otherValue;There is a space:
-
Between keywords and (.
-
Before {.
-
Around binary and ternary operators.
-
After commas.
-
Between a type and its identifier.
There is no unnecessary space:
object.method();
not:
object . method();Declare one variable per declaration.
Correct:
int first;
int second;Incorrect:
int first, second;The exception is the initializer of a for loop, where multiple variables may be declared when appropriate.
Local variables should generally be declared close to where they are first used rather than all at the beginning of a method.
Prefer:
public void processUser(User user) {
validateUser(user);
String userName = user.getName();
sendNotification(userName);
}rather than declaring every local variable at the beginning of the method.
Use Java's normal type-first array syntax.
Correct:
String[] names;Incorrect:
String names[];Array initializers may use Google's supported block-like formatting.
int[] values = {
1,
2,
3,
};Use Google's naming conventions.
Use UpperCamelCase.
public class UserService {
}Examples:
- UserService
- PaymentProcessor
- HttpClient
Use lowerCamelCase.
public User getUserById(long userId) {
}Examples:
- getUser()
- findUserByEmail()
- calculateTotal()
- processPayment()
Use lowerCamelCase.
long userId;
String firstName;
UserRepository userRepository;Constants use UPPER_SNAKE_CASE.
private static final int MAX_RETRY_COUNT = 3;A constant is a static final field whose contents are deeply immutable and whose methods have no detectable side effects.
Not every final field is a constant.
Package names use lowercase characters.
- com.example.users
- com.example.users.repository
- com.example.users.service
Do not use underscores or mixed capitalization in package names.
Wildcard imports are prohibited.
Incorrect:
import java.util.*;Correct:
import java.util.List;
import java.util.Map;
import java.util.Optional;Imports are ordered as:
-
Static imports.
-
Non-static imports.
There is one blank line between those groups when both exist.
Imports within each group are sorted in ASCII order.
Module imports are not used.
Google Java Style explicitly prohibits wildcard imports.
There is no mandatory universal ordering such as:
-
fields
-
constructors
-
public methods
-
private methods
Instead, each class should have a logical and explainable ordering.
Do not simply append every new method to the bottom of the class.
Related members should be grouped where doing so improves readability.
Overloads:
Methods with the same name must appear together without unrelated members between them.
For example:
public User findUser(long userId) {
// ...
}
public User findUser(String email) {
// ...
}
public User findUser(UUID id) {
// ...
}Do not split overloads apart with unrelated methods.
Google explicitly requires overloads and multiple constructors to remain in contiguous groups.
Switch blocks follow normal block indentation.
switch (status) {
case ACTIVE:
processActive();
break;
case INACTIVE:
processInactive();
break;
default:
handleUnknownStatus();
break;
}Every switch must be exhaustive according to Google Java Style.
For traditional switches, a fall-through must be explicitly documented.
switch (status) {
case ACTIVE:
processActive();
// fall through
case PENDING:
processPending();
break;
default:
handleUnknownStatus();
}Modern switch expressions should use the new-style -> syntax.
return switch (status) {
case ACTIVE -> "Active";
case INACTIVE -> "Inactive";
default -> "Unknown";
};Google's current guide requires switches to be exhaustive and requires switch expressions to use the new-style syntax.
Annotations follow Google's annotation formatting rules.
Class-level annotations appear one per line.
@Deprecated
@CheckReturnValue
public final class UserService {
// ...
}Method annotations normally appear one per line.
@Override
@Nullable
public User findUser(long userId) {
// ...
}A single parameterless annotation may appear on the same line as the method declaration:
@Override public int hashCode() {
return id.hashCode();
}Type-use annotations appear immediately before the annotated type:
final @Nullable String name;Implementation comments should be used when they add useful information.
Do not write comments that merely repeat the code.
Avoid:
// Increment the counter.
counter++;Prefer:
// Retry because the service may still be initializing.
retryCount++;Multi-line block comments follow Google's formatting rules:
/*
* This explains why this operation is necessary.
* The implementation should not be changed without
* considering the external service contract.
*/Do not create decorative boxes using *, -, or other characters.
Temporary work should use a TODO comment.
The Google format is:
// TODO: bug-reference - Explain what needs to be done.For example:
// TODO: crbug.com/12345678 - Remove this compatibility workaround.TODO comments should provide context through a bug/reference rather than assigning the work to a person or team.
Javadocs should be used for API documentation.
Javadocs should describe the contract and behavior that users of the API need to understand.
Example:
/**
* Finds a user by their unique identifier.
*
* @param userId the user's unique identifier
* @return the user associated with {@code userId}
* @throws UserNotFoundException if the user does not exist
*/
public User getUser(long userId) {
// ...
}Follow the Google Java Style Guide's Javadoc rules for formatting, paragraphs, @param, @return, @throws, and other tags.
Java itself does not have Kotlin-style nullability in its type system, so null safety must be explicitly specified across APIs using standard annotations (like JSpecify) and enforced through build-time static analysis tools like NullAway.
By default, Java type usages are unspecified. New code should be marked as non-null by default (e.g., using @NullMarked at the package level) unless null is explicitly part of the API contract.
Prefer:
public User getUser(long userId) {
return repository.getUser(userId);
}If the result may legitimately be absent, represent that explicitly:
public Optional<User> findUser(long userId) {
return repository.findUser(userId);
}Do not use null as an implicit "not found" result when Optional or another explicit representation is appropriate.
When null is a legitimate value, its nullability must be explicitly documented or annotated using JSpecify annotations (such as @Nullable).
For example:
public @Nullable User findUser(long userId) {
// ...
}The project standardizes on JSpecify annotations for nullability.
Do not mix different nullability annotation libraries arbitrarily.
Null-safety violations must be detected automatically.
The build should use NullAway with Error Prone (configured for Gradle or Maven) to fail compilation whenever a nullability violation or improper dereference occurs.
Nullness analysis must be part of the normal build/CI process.
New code must not introduce nullness violations.
Do not return null from methods whose contract indicates a non-null result.
Prefer:
public List<User> getUsers() {
return Collections.emptyList();
}rather than:
public List<User> getUsers() {
return null;
}For an optionally present single object:
public Optional<User> findUser(long userId) {
return Optional.ofNullable(repository.findUser(userId));
}Do not add meaningless defensive null checks when the API contract guarantees a value is non-null.
Avoid:
public void processUser(User user) {
if (user == null) {
return;
}
process(user);
}If a user is required, establish and enforce that contract at the API boundary.
External systems may provide nullable or untrusted data.
Examples include:
-
Database APIs
-
HTTP APIs
-
JSON deserialization
-
Third-party libraries
-
Reflection
-
Legacy code
Validate or normalize nullable data at the boundary so the rest of the application can rely on stronger contracts.
Use Optional<T> when a method naturally represents an optionally available return value.
Prefer:
public Optional<User> findUser(long userId) {
return repository.findUser(userId);
}Avoid using Optional merely as a replacement for every nullable field or parameter.
For example, do not automatically create:
private Optional<User> currentUser;when a non-null field, nullable field with a clear contract, or another domain representation is more appropriate.
Prefer immutable objects and final fields where practical.
public final class UserService {
private final UserRepository repository;
public UserService(UserRepository repository) {
this.repository = repository;
}
}Avoid mutable state when it is not required.
Prefer constructing objects in a valid state rather than creating partially initialized objects and filling them in later.
Use exceptions for exceptional conditions rather than normal control flow.
Exception messages should provide useful context.
throw new IllegalArgumentException(
"User ID must be positive: " + userId);Do not silently catch exceptions.
Avoid:
try {
process();
} catch (Exception e) {
}If an exception must be caught, handle it appropriately or propagate it.
Classes that override equals() must also correctly override hashCode().
Use @Override when overriding methods.
@Override
public boolean equals(Object obj) {
// ...
}
@Override
public int hashCode() {
// ...
}
@Override
public String toString() {
// ...
}Formatting should be automated.
The project should use google-java-format as the authoritative Java formatter.
Developers should not manually maintain a separate formatting style.
CI should reject Java code that does not conform to the configured formatter.
Where possible, formatting should be applied automatically before code review.
Google's own Java Style Guide identifies google-java-format as the formatter for Google Java Style.
The build should enforce:
-
Google Java formatting.
-
Compilation with no errors.
-
Null-safety rules.
-
Appropriate static analysis.
-
No unauthorized warning suppressions.
Warnings should not be hidden merely to make CI pass.
Any suppression should be:
-
Necessary.
-
As narrowly scoped as possible.
-
Documented when its reason is not obvious.
Braces:
public class Example {
public void process() {
if (condition) {
doSomething();
} else {
doSomethingElse();
}
for (Item item : items) {
process(item);
}
}
}Indentation: 2 spaces.
Maximum line length: 100 characters.
Imports: No wildcard imports.
Variables: One variable per declaration.
Control structures: Always use braces.
Naming:
- Classes: UpperCamelCase
- Methods: lowerCamelCase
- Variables: lowerCamelCase
- Constants: UPPER_SNAKE_CASE,
- Packages: lowercase
Null safety: Non-null by default. Explicitly represent nullable values. Enforce nullness statically.
Formatting: google-java-format is authoritative.