equals & hashCode done right
The full contract, a correct hand-written pair, getClass vs instanceof, and how record makes it free.
The equals/hashCode contract
Java's Object gives every class two related methods: equals and hashCode. The default implementations use object identity (pointer comparison), which is almost never what you want for domain objects. The exam and every real codebase expect you to override them correctly.
The contract has three obligations:
- Consistency: if two objects are
equals, they must have the samehashCode. - Hash stability:
hashCodemust return the same value on multiple calls as long as the object's state hasn't changed. - equals contract: reflexive (
x.equals(x)is true), symmetric (x.equals(y)↔y.equals(x)), transitive, and consistent.x.equals(null)must returnfalse, never throw.
Breaking rule 1 silently destroys HashMap and HashSet — the object can be put in but never found again.
A correct hand-written pair
import java.util.Objects;
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 other)) return false;
return x == other.x && y == other.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
Objects.hash(x, y) — handles primitives (auto-boxed) and nulls. Never write the 31-prime multiplication by hand — Objects.hash is idiomatic and less error-prone.
getClass() vs instanceof
The pattern !(o instanceof Point other) allows subclasses of Point to be equal to a Point. That satisfies symmetry only if the subclass doesn't add fields that affect equality. If it does, symmetry breaks.
The safer alternative for classes that may be subclassed is getClass():
if (o == null || getClass() != o.getClass()) return false;
This guarantees only same-class instances are ever equal. For final classes (like Point above), the question is moot — nothing can subclass them, so instanceof is fine.
records make it free
Java 16's record generates a correct equals and hashCode over all record components automatically:
public record Point(int x, int y) {}
// equals, hashCode, and toString are all synthesised
For immutable value objects with no custom logic, prefer record and skip the hand-written pair entirely.