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
Always override
equals()andhashCode()together. They form a contract: equal objects must produce equal hash codes. Overriding only one breaks that contract and causes objects to silently misbehave inHashMaps,HashSets, and anywhere else hashing is used.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.
Use interfaces as variable and parameter types, not concrete classes. Declare
List<String> names = new ArrayList<>();rather thanArrayList<String> names = .... Coding to the interface means you can swap the concrete implementation later without touching every call site.Favor immutability where practical. Mark fields
final, avoid exposing setters that don't need to exist, and userecordfor simple data carriers. Immutable objects are inherently thread-safe, easier to reason about, and can't be left in an inconsistent half-modified state.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 manualfinallyblocks.Avoid catching generic
ExceptionorThrowable. Catch the specific exception types you know how to handle. A blanketcatch (Exception e)can silently swallow bugs (like aNullPointerExceptionfrom unrelated code) that you never intended to handle.Follow Java naming conventions.
PascalCasefor classes and interfaces,camelCasefor methods and variables,UPPER_SNAKE_CASEfor constants, all-lowercase for packages. Consistent naming lets any Java developer read your code without a decoder ring.Prefer
StringBuilderover+concatenation inside loops. Each+onStringobjects creates a new immutableString; in a loop that runsntimes, naive concatenation is O(n²).StringBuildermutates an internal buffer and appends in amortized O(1) per call.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 thatnew ArrayList<String>()on both sides used to require.
equals() and hashCode(), illustrated
Overriding both together
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
String result = "";
for (int i = 0; i < 10_000; i++) {
result += i + ","; // creates a brand-new String object every iteration
}Prefer this
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10_000; i++) {
sb.append(i).append(",");
}
String result = sb.toString();