equals, hashCode, and the contract between them
Why overriding one without the other breaks every hash-based collection silently.
After this lesson you can
- Override equals and hashCode together, over the same fields
- State the contract between them in one sentence
- Explain why a HashSet can hold two objects that are equal to each other
The default equals (inherited from Object) compares references —
a.equals(b) is true only when a and b are the exact same
object. For a value type, that is almost never what you want.
public final class Point {
private final int x, 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;
}
}
This alone already breaks something. HashSet, HashMap, and every
hash-based collection find a bucket using hashCode() before they
ever call equals(). Point above still has Object's identity
hashCode, so two equal points almost always land in different
buckets, equals is never consulted, and a HashSet<Point> ends up
holding both.
Try it
import java.util.*; public class Solution { static final class BrokenPoint { final int x, y; BrokenPoint(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof BrokenPoint other)) return false; return x == other.x && y == other.y; } // hashCode is NOT overridden — still the identity hash. } public static String describe() { Set<BrokenPoint> set = new HashSet<>(); set.add(new BrokenPoint(1, 2)); set.add(new BrokenPoint(1, 2)); return "size=" + set.size(); }}The contract
Java's actual rule, from Object's documentation: if two objects are
equal according to equals, they must return the same hashCode.
The reverse is not required — unequal objects may share a hash code,
which is just a collision, and every hash-based collection already
handles collisions.
@Override
public int hashCode() {
return Objects.hash(x, y);
}
Objects.hash(...) combines any number of fields into one hash code
correctly, which is why hand-rolling one is rarely worth it. The two
methods must be overridden together, over the same fields — a
hashCode that reads a field equals ignores (or the reverse) breaks
the contract just as surely as leaving one of them out entirely.
Why the immutability matters
If a field used in hashCode changes after the object is inside a
HashSet or used as a HashMap key, its bucket does not move — the
object is now sitting in the wrong bucket for its current hash code,
and a lookup for it, computed from its current state, will look in the
bucket it should be in and not find it. This is the practical reason a
key type should be immutable: correctness inside a hash-based
collection depends on the hash code never changing while the object is
a member of one.
A Java record generates a correct equals, hashCode, and
toString for you, over exactly its declared fields, which is why a
plain value type is usually better written as a record than as a hand
rolled class.
Try it yourself
2 visible tests · 2 hidden testsPoint below has equals but not hashCode, so a HashSet<Point>
can hold two points that are equal to each other. Add a correct
hashCode, over the same two fields equals uses. Implement
distinctCount(coords), where coords is a List<List<Integer>> of
[x, y] pairs — build a Point from each pair, add them all to a
HashSet<Point>, and return its size.
distinctCount([[1,2],[1,2],[3,4]])distinctCount([[0,0],[1,1],[2,2]])
Sign up to check the hidden tests and save your progress. Sign up