JavaBest Practices

Best Practices

Writing Java that compiles is easy; writing Java that is correct, maintainable, and doesn’t surprise the next person to touch it takes a bit more discipline. The checklist below collects habits that consistently separate solid Java code from code that merely happens to work today.

The checklist
  1. Always override equals() and hashCode() together. They form a contract: equal objects must produce equal hash codes. Overriding only one breaks that contract and causes objects to silently misbehave in HashMaps, HashSets, and anywhere else hashing is used.

  2. Prefer composition over inheritance. Extending a class ties your code to that class's implementation details and lets its future changes ripple into your subclass unexpectedly. Holding a reference to another object (composition) and delegating to it is usually more flexible and easier to change later.

  3. Use interfaces as variable and parameter types, not concrete classes. Declare List<String> names = new ArrayList<>(); rather than ArrayList<String> names = .... Coding to the interface means you can swap the concrete implementation later without touching every call site.

  4. Favor immutability where practical. Mark fields final, avoid exposing setters that don't need to exist, and use record for simple data carriers. Immutable objects are inherently thread-safe, easier to reason about, and can't be left in an inconsistent half-modified state.

  5. Use try-with-resources for anything Closeable/AutoCloseable. Streams, files, sockets, and JDBC connections must be closed; try-with-resources guarantees that even if an exception is thrown, without you writing manual finally blocks.

  6. Avoid catching generic Exception or Throwable. Catch the specific exception types you know how to handle. A blanket catch (Exception e) can silently swallow bugs (like a NullPointerException from unrelated code) that you never intended to handle.

  7. Follow Java naming conventions. PascalCase for classes and interfaces, camelCase for methods and variables, UPPER_SNAKE_CASE for constants, all-lowercase for packages. Consistent naming lets any Java developer read your code without a decoder ring.

  8. Prefer StringBuilder over + concatenation inside loops. Each + on String objects creates a new immutable String; in a loop that runs n times, naive concatenation is O(n²). StringBuilder mutates an internal buffer and appends in amortized O(1) per call.

  9. Use the diamond operator (<>) instead of repeating generic type arguments. new ArrayList<>() lets the compiler infer the type parameter from context, removing the redundant repetition that new ArrayList<String>() on both sides used to require.

equals() and hashCode(), illustrated

Overriding both together

Java
public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point p)) return false;
        return x == p.x && y == p.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
}
StringBuilder vs. concatenation in a loop

Avoid this in a loop

Java
String result = "";
for (int i = 0; i < 10_000; i++) {
    result += i + ","; // creates a brand-new String object every iteration
}

Prefer this

Java
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
    sb.append(i).append(",");
}
String result = sb.toString();
Records give you a lot of this for free
For simple data-holding classes, a `record` automatically generates a correct `equals()`, `hashCode()`, and `toString()`, all fields are implicitly `final`, and there's no setter boilerplate to accidentally write. When a class is really just a bundle of values, reach for a record before writing a class by hand.
Note
None of these are absolute laws — there are legitimate reasons to catch `Exception` at a system boundary (like a top-level request handler that must never crash the whole server), or to extend a class when you genuinely are modeling an is-a relationship. Treat this checklist as the default you deviate from deliberately, not a rule to follow blindly in every situation.