The equals contract has five properties: reflexive, symmetric, transitive, consistent, and non-null. Any class that overrides equals must satisfy all five for hash collections, lists, and Object-method clients to work correctly.
The five properties
- Reflexive —
x.equals(x)must returntruefor every non-nullx. - Symmetric —
x.equals(y)must returntrueif and only ify.equals(x)returnstrue. - Transitive — If
x.equals(y)andy.equals(z), thenx.equals(z). - Consistent — Multiple invocations of
x.equals(y)must return the same result, provided no information used in the comparison changes. - Non-null —
x.equals(null)must returnfalse(never throwNullPointerException).
Canonical implementation
public final class Money {
private final long cents;
private final String currency;
@Override
public boolean equals(Object o) {
if (this == o) return true; // reflexive shortcut
if (!(o instanceof Money other)) return false; // non-null + type check
return cents == other.cents
&& currency.equals(other.currency); // field-by-field
}
@Override
public int hashCode() {
return Objects.hash(cents, currency);
}
}
The instanceof check handles the null case for free: null instanceof Money is always false.
Why each property matters
- Reflexive failure breaks
List.contains(x)whenxis in the list — it walks elements callinge.equals(x), but the implementation might recursex.equals(x)internally. - Symmetric failure breaks bidirectional searches.
set.contains(colorPoint)may behave differently fromcolorPointSet.contains(point). - Transitive failure means
HashSetmay contain three pairwise-equal-ish objects that aren't all considered duplicates. - Consistent failure (e.g., using
System.currentTimeMillis()in equals) makes keys "disappear" fromHashMap. - Non-null failure throws NPE during collection operations, since collections routinely test against null.