Records (Java 16+) auto-generate equals, hashCode, and toString from all components. The implementations are guaranteed correct, satisfy both contracts, and are immutable — making records the default choice for value classes.
What the compiler generates
public record Point(int x, int y) {}
Expands (roughly) to:
public final class Point extends java.lang.Record {
private final int x;
private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; }
public int y() { return y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point that)) return false;
return this.x == that.x && this.y == that.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
(Actual implementation uses invokedynamic and ObjectMethods.bootstrap for efficiency, but the semantics are the above.)
Why records dodge every classic equals trap
- Implicitly
final— no subclassing means no Point/ColorPoint symmetry trap. - Components are final — no mutable state, so
hashCodestays consistent forever. - Type check uses
instanceof— handlesnullcorrectly without an explicit null check. - All components included — no "I forgot to add the new field to equals" bug when refactoring.
- Symmetric across all subtypes — there are no subtypes.
You can override if you must
public record CaseInsensitiveName(String value) {
@Override
public boolean equals(Object o) {
return o instanceof CaseInsensitiveName n
&& value.equalsIgnoreCase(n.value);
}
@Override
public int hashCode() {
return value.toLowerCase().hashCode();
}
}
But if you do, you must override both consistently. The compiler doesn't enforce the contract — it just gives you the default if you don't replace it.
When to use records
- Data Transfer Objects (DTOs) for REST/gRPC payloads.
- Map keys requiring stable equals/hashCode (e.g.,
Map<Coordinate, Tile>). - Sealed-type variants (
sealed interface Shape permits Circle, Square— both can be records). - Multi-value returns from a method (
record SearchResult(int hits, Duration took) {}).
When NOT to use records
- Classes that need to extend a non-
Recordsuperclass (records can only implement interfaces). - Classes with significant non-component state (records can have static fields but not instance fields beyond components).
- JPA entities, where mutability and a no-arg constructor are usually required by the framework.