equals, hashCode & toString done right
The equals contract, the hashCode contract, why they must change together, and a correct implementation.
Why the defaults are not enough
As you saw in Module 5, Object.equals compares identity and Object.hashCode returns a memory-derived number. For value objects — objects where two instances with the same data should be considered equal — you must override both.
The equals contract
A correct equals must satisfy five properties for any non-null references x, y, z:
- Reflexive:
x.equals(x)istrue. - Symmetric: if
x.equals(y)theny.equals(x). - Transitive: if
x.equals(y)andy.equals(z)thenx.equals(z). - Consistent: repeated calls with unchanged objects return the same result.
- Null-safe:
x.equals(null)is alwaysfalse— neverNullPointerException.
The hashCode contract
- Objects that are equals must have the same hashCode.
- Objects with the same hashCode need not be equals (collision is allowed).
Violating this contract causes HashSet to store duplicates and HashMap to lose entries silently.
A correct implementation
import java.util.Objects;
public class Point {
private final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true; // same reference
if (!(o instanceof Point p)) return false; // null-safe + type check
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y); // consistent with equals
}
@Override
public String toString() {
return "Point(" + x + ", " + y + ")";
}
}
Objects.hash() combines multiple fields correctly; it handles nulls safely for reference-type fields.