Best Practices
Effective Java highlights, SOLID in Java, common pitfalls and anti-patterns
Effective Java by Joshua Bloch remains the definitive guide to writing production-quality Java. Combined with SOLID principles and awareness of common pitfalls, these practices separate code that works from code that is maintainable, performant, and correct under concurrency and edge cases.
Key Points
- Prefer static factory methods over constructors — they have names (valueOf, of, getInstance), can return subtypes, and can cache instances
- Use the Builder pattern when a constructor has 4+ parameters — avoids telescoping constructors and improves readability
- Minimize mutability — immutable objects are inherently thread-safe and simpler to reason about; use Records for data carriers
- Favour composition over inheritance — inheritance breaks encapsulation; implement interfaces, delegate to a private field
- Override equals and hashCode together — violating the contract breaks HashMap, HashSet, and all hash-based structures
- Use try-with-resources for anything that implements AutoCloseable — never rely on finalizers or cleaners for resource release
- Prefer EnumMap and EnumSet over HashMap/HashSet when keys are enums — O(1) backed by array, no boxing
- Synchronize access to shared mutable state — even a single boolean flag needs volatile or synchronization if written/read from multiple threads
- Document thread-safety: @ThreadSafe, @NotThreadSafe, @GuardedBy — callers need to know
| Anti-pattern | Problem | Fix |
|---|---|---|
| new Integer(42) | Creates object always | Integer.valueOf(42) — cached -128 to 127 |
| "" + x in a loop | O(n²) string creation | StringBuilder.append() |
| Float/Double for money | Floating-point rounding errors | BigDecimal or store cents as long |
| catch (Exception e) {} | Swallows errors silently | Log or rethrow; never empty catch |
| public fields | Breaks encapsulation | Private fields + getters (or Records) |
| Mutable static state | Thread-unsafe, hard to test | Stateless classes or thread-local |
| Overriding equals without hashCode | Breaks HashMap contract | Always override both |
| instanceof without null check | Pattern matching (Java 16) handles null | Use pattern instanceof |
Effective Java: static factory, immutable Record, EnumMap, holder-class singleton, try-with-resources
// Static factory — descriptive names + optional caching
public static Optional<User> findById(long id) { ... }
public static List<Permission> of(Permission... perms) { ... }
// Immutable class — Records (Java 16)
public record Money(BigDecimal amount, Currency currency) {
// compact constructor for validation
public Money {
Objects.requireNonNull(amount);
if (amount.compareTo(BigDecimal.ZERO) < 0)
throw new IllegalArgumentException("Negative amount");
}
public Money add(Money other) {
if (!currency.equals(other.currency)) throw new IllegalArgumentException();
return new Money(amount.add(other.amount), currency);
}
}
// EnumMap — faster and more memory-efficient than HashMap<Enum,V>
EnumMap<DayOfWeek, List<Order>> byDay = new EnumMap<>(DayOfWeek.class);
// Checked vs Unchecked exceptions
// Checked: recoverable (IOException, SQLException) — force caller to handle
// Unchecked (RuntimeException): programming errors — let them propagate
// Thread-safe lazy initialisation via holder class (no synchronisation overhead)
class Singleton {
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() { return Holder.INSTANCE; }
}
// Close resources — always use try-with-resources
public String readConfig(Path path) throws IOException {
try (var reader = Files.newBufferedReader(path)) {
return reader.lines().collect(Collectors.joining("
"));
}
}Real-World Example
The most expensive Java bugs in production trace back to a handful of anti-patterns: (1) mutable shared state causing race conditions, (2) resource leaks (connections, file handles) from missing try-with-resources, (3) floating-point arithmetic for financial calculations causing rounding discrepancies, (4) equals/hashCode violations causing silent data loss in collections. These are all preventable with discipline and code review.