Test frameworks grow into large codebases, and they need the same discipline as product code. This guide gathers the Java best practices that matter most: clean naming and small functions, SOLID, logging, the most useful Effective Java rules, immutable objects, a code review checklist, and the anti-patterns to remove when you see them.
Common Java Anti-Patterns to Avoid
// ❌ ANTI-PATTERN 1: Returning null instead of Optional
public User findUser(int id) { return null; } // callers forget null check!
// ✅ Better:
public Optional<User> findUser(int id) { return Optional.ofNullable(...); }
// ❌ ANTI-PATTERN 2: Catching and ignoring exceptions
try { riskyOp(); } catch (Exception e) {} // silent failure is deadly!
// ✅ Better: log it, rethrow, or handle meaningfully
try { riskyOp(); } catch (Exception e) { log.error("Failed", e); throw e; }
// ❌ ANTI-PATTERN 3: String concatenation in loop
String result = "";
for (int i=0; i<1000; i++) result += i; // 1000 String objects!
// ✅ Better:
StringBuilder sb = new StringBuilder();
for (int i=0; i<1000; i++) sb.append(i);
// ❌ ANTI-PATTERN 4: Using raw types
List list = new ArrayList(); // no type safety!
list.add("string"); list.add(42); // mixed types = runtime error
// ✅ Better:
List<String> list = new ArrayList<>();
// ❌ ANTI-PATTERN 5: == for String comparison
if (name == "Alice") { } // might work by coincidence (pool)!
// ✅ Better:
if ("Alice".equals(name)) { } // null-safe & value-correct
// ❌ ANTI-PATTERN 6: Not closing resources
FileReader fr = new FileReader("f.txt"); // never closed = memory leak!
// ✅ Better:
try (FileReader fr = new FileReader("f.txt")) { }
// ❌ ANTI-PATTERN 7: Mutable static state
class Config { public static String env = "prod"; } // shared mutable = threading nightmare
// ✅ Better: immutable config, dependency injection
// ❌ ANTI-PATTERN 8: Premature optimization
// Don't optimize before profiling! First make it correct, then fast.
// ❌ ANTI-PATTERN 9: God class (one class doing everything)
// ✅ Better: Single Responsibility Principle — one class, one job
// ❌ ANTI-PATTERN 10: Deep inheritance hierarchies
// ✅ Better: Favor composition over inheritance
Clean Code Principles
Naming Conventions
// ❌ BAD naming
int d; // what is d?
void proc(List l) {} // proc? l?
boolean flag = true;
String temp;
// ✅ GOOD naming
int daysSinceLastLogin;
void sendPasswordResetEmail(List<User> expiredUsers) {}
boolean isEmailVerified = true;
String currentUserEmail;
// Classes: Noun phrases
class UserService {} // ✅
class Manager {} // ❌ — manager of what?
class DataProcessor {} // ❌ — too generic
class OrderItemProcessor {} // ✅ — specific
// Methods: Verb phrases
getUserById() // ✅
sendEmail() // ✅
isValid() // ✅ (boolean predicate)
process() // ❌ — process what?
doStuff() // ❌ — never!
// Constants: UPPER_SNAKE_CASE
static final int MAX_RETRY_ATTEMPTS = 3;
static final String DEFAULT_ENCODING = "UTF-8";
// Boolean naming: is/has/can/should
boolean isActive = true;
boolean hasPermission = false;
boolean canDelete = false;
boolean shouldSendEmail = true;
Function Design
// ✅ Rule: Functions should do ONE thing
// ❌ BAD: does many things
public void processUser(User user) {
// validate
if (user.getName() == null) throw new Exception("name required");
if (user.getEmail() == null) throw new Exception("email required");
// save to db
userRepo.save(user);
// send email
emailService.sendWelcome(user.getEmail());
// update stats
statsService.incrementUserCount();
}
// ✅ GOOD: each function does one thing
public void registerUser(User user) {
validateUser(user);
userRepo.save(user);
onUserRegistered(user);
}
private void validateUser(User user) { /* validation only */ }
private void onUserRegistered(User user) {
emailService.sendWelcome(user.getEmail());
statsService.incrementUserCount();
}
// ✅ Limit parameters (max 3, ideally 0-2)
// ❌ BAD
void createOrder(String userId, String productId, int qty, String address,
String paymentMethod, String coupon, boolean express) {}
// ✅ GOOD: use a request object
void createOrder(CreateOrderRequest request) {}
// ✅ Avoid side effects
// ❌ BAD: name suggests it just checks but also modifies state
boolean checkAndUpdatePassword(String oldPass, String newPass) {
boolean valid = passwordEncoder.matches(oldPass, user.getPassword());
if (valid) user.setPassword(passwordEncoder.encode(newPass)); // SIDE EFFECT!
return valid;
}
// ✅ GOOD: separate concerns
boolean isPasswordValid(String raw, String encoded) {
return passwordEncoder.matches(raw, encoded);
}
void updatePassword(String newPassword) {
user.setPassword(passwordEncoder.encode(newPassword));
}
SOLID Principles in Practice
// S — Single Responsibility
// ❌ BAD: Report does too many things
class Report {
void generateData() {}
void formatAsHTML() {}
void saveToFile() {}
void sendByEmail() {}
}
// ✅ GOOD: separate classes
class ReportDataGenerator { void generate() {} }
class HtmlFormatter { void format(ReportData data) {} }
class ReportSaver { void save(Report r, String path) {} }
class ReportEmailer { void send(Report r, String to) {} }
// O — Open/Closed Principle
// ❌ BAD: add new shape = modify existing code
double calculateArea(Object shape) {
if (shape instanceof Circle) return Math.PI * ((Circle)shape).radius * ((Circle)shape).radius;
if (shape instanceof Rectangle) return ((Rectangle)shape).width * ((Rectangle)shape).height;
// must modify this method for every new shape!
}
// ✅ GOOD: add new shape = add new class (no modification)
interface Shape { double area(); }
class Circle implements Shape { double area() { return Math.PI * r * r; } }
class Rectangle implements Shape { double area() { return w * h; } }
// New shape: just add new class implementing Shape
// D — Dependency Inversion
// ❌ BAD: high-level depends on low-level
class OrderService {
MySQLOrderRepository repo = new MySQLOrderRepository(); // hardcoded!
}
// ✅ GOOD: depend on abstraction
interface OrderRepository { Order save(Order o); }
class OrderService {
private final OrderRepository repo; // abstraction
OrderService(OrderRepository repo) { this.repo = repo; } // injected
}
// Can swap MySQL → MongoDB without changing OrderService
Java Logging Best Practices
// Use SLF4J API (facade) + Logback implementation
// Never use System.out.println in production!
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserService {
// One logger per class (static final)
private static final Logger log = LoggerFactory.getLogger(UserService.class);
// Or with Lombok: @Slf4j annotation
public User createUser(User user) {
log.debug("Creating user: {}", user.getEmail()); // dev only
try {
User saved = repo.save(user);
log.info("User created successfully: id={}, email={}",
saved.getId(), saved.getEmail());
return saved;
} catch (DataIntegrityViolationException e) {
log.warn("Duplicate email attempt: {}", user.getEmail());
throw new DuplicateEmailException(user.getEmail());
} catch (Exception e) {
log.error("Failed to create user: {}", user.getEmail(), e); // include exception!
throw e;
}
}
}
// Log levels (in order):
// TRACE: finest detail (method entry/exit) — rarely used
// DEBUG: developer info (queries, values) — dev env only
// INFO: key business events (user created, order placed)
// WARN: unexpected but recoverable (retry, fallback used)
// ERROR: failures that need attention (exceptions, data issues)
// application.properties
// logging.level.com.myapp=DEBUG
// logging.level.org.hibernate.SQL=DEBUG
// logging.file.name=logs/app.log
// logging.logback.rollingpolicy.max-file-size=10MB
// logging.logback.rollingpolicy.max-history=30
// ❌ NEVER log sensitive data
log.info("Login: user={} password={}", username, password); // NEVER!
log.info("Card: {}", cardNumber); // NEVER!
Code Review Checklist for Java
- ✅ No raw types (use generics: List<String> not List)
- ✅ Resources closed with try-with-resources
- ✅ equals() and hashCode() both overridden when one is
- ✅ Checked exceptions declared or handled
- ✅ No System.out.println — use proper logger
- ✅ No hardcoded credentials or URLs in code
- ✅ Immutable objects where possible (final fields)
- ✅ Thread-safe collections for shared mutable state
- ✅ StringBuilder for string concatenation in loops
- ✅ Input validated before processing
- ✅ SQL uses PreparedStatement (never string concat)
- ✅ No swallowed exceptions (empty catch blocks)
- ✅ Collections returned as unmodifiable when appropriate
- ✅ Magic numbers replaced with named constants
- ✅ Methods < 20 lines, classes < 300 lines
- ✅ Unit tests for all public methods
- ✅ Meaningful variable/method/class names
- ✅ No unused imports or dead code
- ✅ Null-safe code or Optional used for nullable returns
- ✅ Static utility methods in utility classes are final class + private constructor
Effective Java Best Practices (Bloch)
// ITEM 1: Use static factory methods instead of constructors
// ✅ Benefits: meaningful names, can return cached instances, can return subtype
public class Color {
private static final Color RED = new Color(255, 0, 0);
private Color(int r, int g, int b) { }
public static Color red() { return RED; } // named, cached
public static Color of(int r, int g, int b) { return new Color(r,g,b); }
}
// Boolean.valueOf(true) — doesn't create new object each time!
// ITEM 2: Builder for many parameters
// Already covered — always use Builder over telescoping constructors
// ITEM 3: Singleton with Enum
public enum Singleton { INSTANCE; }
// ITEM 4: Enforce noninstantiability
public class MathUtils {
private MathUtils() { throw new AssertionError("Do not instantiate"); }
public static int add(int a, int b) { return a + b; }
}
// ITEM 5: Prefer dependency injection over hardcoding resources
// Instead of: private final SpellChecker dict = new SpellChecker(DICTIONARY);
// Use: inject Lexicon as constructor parameter
// ITEM 6: Avoid unnecessary object creation
// ❌ BAD: creates new Pattern each call
boolean isRomanNumeral(String s) {
return s.matches("^M{0,3}(CM|CD|D?C{0,3})...");
}
// ✅ GOOD: compile once, reuse
private static final Pattern ROMAN = Pattern.compile("^M{0,3}...");
boolean isRomanNumeral(String s) { return ROMAN.matcher(s).matches(); }
// ITEM 7: Eliminate obsolete object references
// Arrays that grow: null out elements on removal
public T pop() {
T element = elements[--size];
elements[size] = null; // eliminate obsolete reference!
return element;
}
// ITEM 8: Avoid finalizers and cleaners
// Use try-with-resources (AutoCloseable) instead
// ITEM 9: Prefer try-with-resources to try-finally
// Already covered
// ITEM 10: Obey equals contract
// Reflexive: x.equals(x) = true
// Symmetric: x.equals(y) = y.equals(x)
// Transitive: x.equals(y) && y.equals(z) → x.equals(z)
// Consistent: multiple calls return same result
// Non-nullity: x.equals(null) = false
// ITEM 11: Always override hashCode with equals
@Override public int hashCode() {
return Objects.hash(field1, field2, field3);
}
// ITEM 12: Always override toString
@Override public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
// ITEM 13: Override clone judiciously — prefer copy constructor
// ❌ clone() is tricky with inheritance and final fields
// ✅ copy constructor:
public Person(Person original) {
this.name = original.name;
this.skills = new ArrayList<>(original.skills); // deep copy
}
// ITEM 14: Consider implementing Comparable
// Already covered
// ITEM 15: Minimize accessibility (private by default)
// Make everything as private as possible
// Public only what's necessary for API
// ITEM 16: In public classes use accessor methods not public fields
// Always: private field + public getter/setter
// ITEM 17: Minimize mutability (immutable when possible)
// Use final, no setters, defensive copies
// ITEM 18: Favor composition over inheritance
// Inheritance breaks encapsulation (subclass depends on impl details)
// Composition: has-a relationship, more flexible
// ITEM 19: Design and document for inheritance or prohibit it
// If not designed for extension: make it final
public final class UtilClass { }
// ITEM 20: Prefer interfaces to abstract classes
// Interfaces: mixins, multiple inheritance, easy to retrofit
// Abstract class: when sharing implementation is required
Immutable Object Design
// Rules for immutable objects:
// 1. final class (no subclassing)
// 2. All fields private final
// 3. No setters
// 4. Deep copy mutable fields in constructor and getters
// 5. Don't leak 'this' in constructor
public final class ImmutablePerson {
private final String name;
private final int age;
private final List<String> hobbies; // mutable — must deep copy!
private final Date birthDate; // mutable — must deep copy!
public ImmutablePerson(String name, int age,
List<String> hobbies, Date birthDate) {
this.name = name;
this.age = age;
this.hobbies = List.copyOf(hobbies); // unmodifiable copy
this.birthDate = new Date(birthDate.getTime()); // defensive copy
}
public String getName() { return name; }
public int getAge() { return age; }
public List<String> getHobbies() { return hobbies; } // already unmodifiable
public Date getBirthDate() { return new Date(birthDate.getTime()); } // copy on return
// 'With' methods return NEW instance (like Records)
public ImmutablePerson withAge(int newAge) {
return new ImmutablePerson(this.name, newAge, this.hobbies, this.birthDate);
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ImmutablePerson p)) return false;
return age == p.age && Objects.equals(name, p.name);
}
@Override public int hashCode() { return Objects.hash(name, age); }
@Override public String toString() { return name + "(" + age + ")"; }
}
FAQs
What are the SOLID principles?
Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion: five guidelines for classes that are easy to change and test.
Why prefer composition over inheritance?
Inheritance couples a subclass to its parent's implementation; composition combines small objects through interfaces, so behaviour can change without deep class hierarchies.
How do you make a class immutable in Java?
Make the class final, fields private and final, set them only in the constructor, return copies of mutable fields, and provide no setters; or use a record for simple data.