Java Best Practices
IntermediateBattle-tested Java best practices from Effective Java and industry experience — writing clean, maintainable, production-quality code.
Overview
Java best practices distil decades of community experience into actionable rules. The canonical reference is Joshua Bloch's Effective Java. Key themes: prefer composition over inheritance, program to interfaces, minimise mutability, use enums instead of int constants, avoid raw types, prefer static factory methods, and write self-documenting code. These practices reduce bugs, improve readability, and make code easier to maintain.
API Design and Encapsulation
Design APIs from the caller's perspective first. Make classes and members as private as possible — expand access only when needed. Prefer immutable objects. Return empty collections rather than null. Validate constructor and method preconditions eagerly.
The principle of least surprise: APIs should do what their name implies, with no surprising side effects.
// BAD — public mutable fields, no validation
public class Range {
public int start;
public int end;
}
// GOOD — encapsulated, validated, immutable, fluent factory
public final class Range {
private final int start;
private final int end;
private Range(int start, int end) {
if (start > end) throw new IllegalArgumentException(
"start (%d) must be <= end (%d)".formatted(start, end));
this.start = start;
this.end = end;
}
public static Range of(int start, int end) {
return new Range(start, end);
}
public int start() { return start; }
public int end() { return end; }
public int length() { return end - start; }
public boolean contains(int value) {
return value >= start && value < end;
}
// Return empty collection, not null
public List<Integer> toList() {
if (start >= end) return List.of();
return IntStream.range(start, end).boxed().collect(Collectors.toList());
}
}Prefer Composition over Inheritance
Inheritance is powerful but brittle — subclasses are tightly coupled to superclass implementation details. If the superclass changes, subclasses can break silently.
Composition (wrapping via delegation) is more flexible and robust. The decorator and strategy patterns both use composition. Use inheritance only for genuine IS-A relationships with stable superclasses.
// BAD — inheritance just for code reuse (no IS-A relationship)
public class LoggingArrayList<E> extends ArrayList<E> {
@Override public boolean add(E e) {
log("Adding " + e);
return super.add(e); // fragile — coupled to ArrayList internals
}
// addAll() calls add() in ArrayList? Not guaranteed across versions!
}
// GOOD — composition (delegation)
public class LoggingList<E> implements List<E> {
private final List<E> delegate;
private final Logger log;
public LoggingList(List<E> delegate, Logger log) {
this.delegate = delegate;
this.log = log;
}
@Override public boolean add(E e) {
log.info("Adding: {}", e);
return delegate.add(e); // always correct regardless of delegate impl
}
@Override public boolean addAll(Collection<? extends E> c) {
log.info("Adding {} items", c.size());
return delegate.addAll(c); // delegates correctly
}
// Delegate all other methods...
}Effective Use of Optionals and Exceptions
Optional<T> communicates "this may be absent" in the return type — better than returning null. Never use Optional for fields, parameters, or collections — only return types.
Exceptions: use checked exceptions for recoverable conditions, runtime for programming errors. Prefer specific exception types over Exception. Include useful context in messages. Never swallow exceptions silently.
// Optional — for return types only
public Optional<User> findUserByEmail(String email) {
return userRepo.findByEmail(email); // may be absent
}
// Caller — explicit handling
Optional<User> userOpt = findUserByEmail("alice@example.com");
// BAD — pointless get() without check
User user = userOpt.get(); // may throw NoSuchElementException
// GOOD — use Optional API
String name = userOpt
.map(User::getName)
.orElse("Anonymous");
User userOrThrow = userOpt
.orElseThrow(() -> new UserNotFoundException("alice@example.com"));
// Exceptions — include context
// BAD
throw new IllegalArgumentException("Invalid value");
// GOOD — enough context to diagnose without logs
throw new IllegalArgumentException(
"Age must be between 0 and 150, got: " + age);
// NEVER swallow exceptions
try {
riskyOperation();
} catch (Exception e) {
// log.error("Operation failed", e); // at minimum, log it
// empty catch is a silent failure — very dangerous
}Key Points to Remember
- Make classes and members as private as possible — expand access only when required.
- Return empty collections (List.of()) instead of null — prevents NullPointerException.
- Prefer composition over inheritance for code reuse — inheritance is for true IS-A relationships.
- Optional is for return types only — not fields, parameters, or collection elements.
- Exceptions must include enough context to diagnose without needing to reproduce the bug.
Practice Java Best Practices in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhat does "prefer composition over inheritance" mean in practice?
When should you use Optional and when should you not?
What is the difference between checked and unchecked exceptions?
Why should you return empty collections instead of null?
What are the key principles from Effective Java you apply daily?
Ask Aria about Java Best Practices
Your personal AI tutor — ask anything about this concept