Java Convention Rules
1. Null Handling
Preferred Patterns
// Use Optional for return types that may be absent
public Optional<User> findByEmail(String email) {
return Optional.ofNullable(userRepository.findByEmail(email));
}
// Chain Optional operations
String displayName = findByEmail(email)
.map(User::getName)
.orElse("Unknown");
// Use Objects.requireNonNull for preconditions
public void process(Order order) {
Objects.requireNonNull(order, "order must not be null");
Objects.requireNonNull(order.getUserId(), "order.userId must not be null");
}
Null Anti-Patterns
// Never return null from a collection method — return empty collection
public List<Order> findOrders(Long userId) {
List<Order> orders = repository.findByUserId(userId);
return orders != null ? orders : List.of(); // Bad: should never be null
}
// Never use Optional as a field or parameter type
private Optional<String> name; // Bad: use @Nullable or default value
public void setName(Optional<String> name) {} // Bad: just use @Nullable
// Never call Optional.get() without isPresent check — use orElseThrow
user.get(); // Bad: may throw NoSuchElementException
user.orElseThrow(() -> new EntityNotFoundException("User", id)); // Good
@Nullable and @NonNull Annotations
// Use JSR-305 or JSpecify annotations at API boundaries
public @NonNull List<User> findAll() { ... }
public @Nullable User findByEmail(@NonNull String email) { ... }
Null Handling Rules
| Scenario |
Recommended Approach |
| Method return (may be absent) |
Optional<T> |
| Collection return |
Empty collection, never null |
| Method parameter |
@Nullable annotation or overload |
| Field |
Default value or @Nullable |
| External API response |
Null-check at boundary |
2. Records, Sealed Classes, and Enums
Records (Java 16+)
// DTOs, request/response objects, value objects
public record UserResponse(
Long id,
String name,
String email,
UserStatus status
) {
// Compact constructor for validation
public UserResponse {
Objects.requireNonNull(name, "name must not be null");
Objects.requireNonNull(email, "email must not be null");
}
// Static factory method
public static UserResponse from(User user) {
return new UserResponse(user.getId(), user.getName(),
user.getEmail(), user.getStatus());
}
}
Sealed Classes (Java 17+)
public sealed interface Result<T> permits Success, Failure {
record Success<T>(T data) implements Result<T> {}
record Failure<T>(ErrorCode errorCode, String message) implements Result<T> {}
}
Type Selection Criteria
| Use |
When |
record |
DTOs, request/response, value objects, immutable data |
class |
Services, entities with mutable state, complex behavior |
sealed interface |
Restricted type hierarchies (results, states, commands) |
enum |
Fixed set of constants with behavior |
Record Rules
- Records are immutable and final — suitable for DTOs and value objects
- Use compact constructors for validation, not custom constructors
- Records auto-generate
equals(), hashCode(), toString() — do not override unless necessary
- Records cannot extend classes (but can implement interfaces)
3. Pattern Matching
Pattern Matching for instanceof (Java 16+)
// Before
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
// After: pattern variable
if (obj instanceof String s) {
System.out.println(s.length());
}
// With negation
if (!(obj instanceof String s)) {
return;
}
// s is in scope here
Pattern Matching for Switch (Java 21+)
// Type pattern matching
public String describe(Object obj) {
return switch (obj) {
case Integer i when i > 0 -> "positive: " + i;
case Integer i -> "non-positive: " + i;
case String s -> "string: " + s;
case null -> "null";
default -> "other: " + obj;
};
}
// Sealed class exhaustive matching
public String handle(Result<?> result) {
return switch (result) {
case Result.Success<?> s -> "Success: " + s.data();
case Result.Failure<?> f -> "Error: " + f.message();
// No default needed — sealed type is exhaustive
};
}
Record Patterns (Java 21+)
// Destructure records in pattern matching
public String format(Object obj) {
return switch (obj) {
case UserResponse(var id, var name, var email, var status) ->
name + " (#" + id + ")";
default -> obj.toString();
};
}
// Nested record patterns
record Address(String city, String street) {}
record Person(String name, Address address) {}
if (obj instanceof Person(var name, Address(var city, var street))) {
System.out.println(name + " lives in " + city);
}
Unnamed Patterns (Java 22+)
// Use _ for unused pattern variables (Java 22+)
if (obj instanceof Person(var name, Address(var city, _))) {
System.out.println(name + " lives in " + city);
}
4. Text Blocks and String Templates
Text Blocks (Java 15+)
// Multi-line strings
String json = """
{
"name": "%s",
"email": "%s"
}
""".formatted(name, email);
// SQL queries
String query = """
SELECT u.id, u.name, u.email
FROM users u
WHERE u.status = 'ACTIVE'
AND u.created_at > :cutoff
ORDER BY u.name
""";
String Formatting
// Prefer formatted() over String.format() for readability
String message = "User %s (ID: %d) logged in".formatted(name, id);
// For simple concatenation, + or StringBuilder is fine
String greeting = "Hello, " + name;
5. Collections and Streams
Immutable Collections
// Factory methods (Java 9+)
List<String> names = List.of("Alice", "Bob", "Charlie");
Set<String> tags = Set.of("java", "spring");
Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 87);
// Unmodifiable copies
List<String> copy = List.copyOf(mutableList);
Stream Patterns
// Transform
List<String> names = users.stream()
.map(User::getName)
.toList(); // Java 16+, replaces collect(Collectors.toList())
// Filter and collect
List<User> activeUsers = users.stream()
.filter(User::isActive)
.toList();
// Group by
Map<UserStatus, List<User>> grouped = users.stream()
.collect(Collectors.groupingBy(User::getStatus));
// Aggregate
long totalAmount = orders.stream()
.mapToLong(Order::getAmount)
.sum();
// Find first
Optional<User> firstActive = users.stream()
.filter(User::isActive)
.findFirst();
Sequenced Collections (Java 21+)
// First and last element access
SequencedCollection<String> list = new ArrayList<>(List.of("a", "b", "c"));
String first = list.getFirst(); // "a"
String last = list.getLast(); // "c"
// Reversed view
SequencedCollection<String> reversed = list.reversed();
// SequencedMap
SequencedMap<String, Integer> map = new LinkedHashMap<>();
Map.Entry<String, Integer> firstEntry = map.firstEntry();
Map.Entry<String, Integer> lastEntry = map.lastEntry();
Collection Rules
- Prefer
List.of(), Set.of(), Map.of() for immutable collections
- Use
.toList() (Java 16+) instead of .collect(Collectors.toList())
- Use
Stream for transformation pipelines; avoid for simple iteration
- Avoid
Stream for single-element operations — use direct method calls
6. Virtual Threads (Java 21+)
Basic Usage
// Create virtual thread
Thread.startVirtualThread(() -> {
processRequest();
});
// Virtual thread executor
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> handleRequest(request));
}
Spring Boot Integration
- For Spring Boot virtual thread configuration, see
spring-framework skill
Virtual Thread Rules
- Virtual threads are cheap — create per-task, do not pool
- Blocking operations (JDBC, file I/O,
Thread.sleep) are fine on virtual threads
- Avoid
synchronized blocks in virtual thread code on Java 21–23 — use ReentrantLock instead
(synchronized pins the carrier thread; resolved in Java 24+ via JEP 491)
- Do not use
ThreadLocal for large objects — virtual threads may have millions of instances
- Virtual threads do not make CPU-bound work faster — they optimize I/O-bound concurrency
7. Naming Conventions
| Element |
Convention |
Example |
| Class |
PascalCase |
UserService, OrderResponse |
| Interface |
PascalCase |
UserRepository, Cacheable |
| Method |
camelCase |
findByEmail, calculateTotal |
| Variable |
camelCase |
userName, isActive |
| Constant |
SCREAMING_SNAKE |
MAX_RETRY_COUNT, DEFAULT_TTL |
| Package |
lowercase dotted |
com.example.user.service |
| Enum value |
SCREAMING_SNAKE |
PENDING, IN_PROGRESS |
| Type parameter |
Single uppercase |
T, E, K, V |
8. Functional Interfaces and Lambdas
Standard Functional Interfaces
| Interface |
Signature |
Use Case |
Function<T, R> |
T -> R |
Transform input to output |
Predicate<T> |
T -> boolean |
Test a condition |
Consumer<T> |
T -> void |
Process without return |
Supplier<T> |
() -> T |
Provide a value lazily |
BiFunction<T,U,R> |
(T, U) -> R |
Two-input transformation |
UnaryOperator<T> |
T -> T |
Same-type transformation |
Lambda Best Practices
// Prefer method references when possible
users.stream().map(User::getName) // Good
users.stream().map(user -> user.getName()) // Acceptable but verbose
// Avoid complex lambdas — extract to named methods
users.stream()
.filter(this::isEligibleForPromotion) // Clear intent
.map(User::toResponse)
.toList();
// Custom functional interface when standard ones are insufficient
@FunctionalInterface
public interface RetryableAction<T> {
T execute() throws Exception;
}
9. Error Handling
Exception Hierarchy
// Base business exception
public abstract class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
protected BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
protected BusinessException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() { return errorCode; }
}
// Specific exceptions
public class EntityNotFoundException extends BusinessException {
public EntityNotFoundException(String entityName, Object id) {
super(ErrorCode.ENTITY_NOT_FOUND,
"%s not found: %s".formatted(entityName, id));
}
}
Try-With-Resources
// Always use try-with-resources for AutoCloseable
try (var connection = dataSource.getConnection();
var statement = connection.prepareStatement(sql)) {
statement.setLong(1, userId);
try (var resultSet = statement.executeQuery()) {
// process results
}
}
// var in try-with-resources (Java 9+)
var inputStream = new FileInputStream(file);
try (inputStream) {
// use inputStream
}
10. Anti-Patterns
- Using raw types (
List instead of List<User>)
- Returning
null from collection methods — return List.of()
- Using
Optional as a field, parameter, or collection element type
- Mutable public fields — use private fields with getters
- Using
Calendar / Date — use java.time API (LocalDate, Instant, etc.)
- Using
StringBuffer in single-threaded context — use StringBuilder
- Catching
Throwable or Error — only catch Exception subtypes
- Empty catch blocks — at minimum log the exception
- Using
== for String comparison — use .equals()
synchronized with virtual threads on Java 21–23 — use ReentrantLock (fixed in Java 24+)
Additional References
- For Java version migration (8 → 25), see references/migration.md
Related Skills
- For TypeScript coding conventions, see
typescript-convention skill