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, so equal objects always hash the same
- State the contract between them in one sentence
- Explain why a HashSet can hold two objects that are equal to each other
- Spot an equals overload that collections never call
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 use hashCode() to decide where to look
before they ever call equals(), and they only call equals on an
entry whose hash code matches. Point above still has Object's
identity hashCode, so two equal points almost always get different
hash codes, 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, and hashCode may only read
fields that equals also compares. A hashCode that reads a field
equals ignores breaks the contract as surely as leaving it out. The
reverse is legal: a hashCode built from fewer fields than equals
only produces more collisions — even return 0; satisfies the
contract, it just turns every hash lookup into a linear scan.
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 entry does not move — the
object is still filed under its old hash code, and a lookup computed
from its current state looks in the wrong place and does not find it.
import java.util.*; public class Solution { static final class Box { int size; // mutable, and read by hashCode Box(int size) { this.size = size; } @Override public boolean equals(Object o) { return o instanceof Box b && b.size == size; } @Override public int hashCode() { return Integer.hashCode(size); } } public static String describe() { Set<Box> set = new HashSet<>(); Box box = new Box(1); set.add(box); box.size = 2; // mutated while inside the set boolean found = set.contains(box); boolean iteratorSeesIt = set.iterator().next() == box; return "size=" + set.size() + " contains=" + found + " iteratorSeesIt=" + iteratorSeesIt; }}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.
Common mistakes
- Writing
public boolean equals(Point other). That is an overload, not an override: collections callequals(Object), which is stillObject's identity check. Annotating it with@Overrideturns the mistake into a compile error, which is the reason to always write the annotation. - Overriding
equalsand nothashCode(the first example): set and map lookups silently disagree withequals. - Mutating a field that
hashCodereads while the object is a key (the second example):containsreturnsfalsefor an object that is visibly in the set. - Building
hashCodefrom a field thatequalsdoes not compare, or from a raw representation when compares a normalised one — the exercise below is exactly that trap.
import java.util.*; public class Solution { static final class Point { final int x, y; Point(int x, int y) { this.x = x; this.y = y; } // No @Override, and the parameter is Point, not Object. public boolean equals(Point other) { return other != null && x == other.x && y == other.y; } @Override public int hashCode() { return Objects.hash(x, y); } } public static String describe() { Point a = new Point(1, 2), b = new Point(1, 2); Set<Point> set = new HashSet<>(List.of(a)); Object asObject = b; return "a.equals(b)=" + a.equals(b) + " a.equals((Object) b)=" + a.equals(asObject) + " set.contains(b)=" + set.contains(b); }}What interviewers ask
- What happens if you override
equalsbut nothashCode? Equal objects get different hash codes, soHashSetkeeps duplicates andHashMap.getwith an equal key returnsnull. - Can two unequal objects have the same hash code? Yes — that is a collision; it costs performance, never correctness. Only equal objects are required to share one.
- Why should a
HashMapkey be immutable? The entry is filed by the hash code at insertion time; if the key's state changes, lookups compute a different hash and miss it. instanceoforgetClass()inequals?getClass()never lets a subclass instance equal a parent instance;instanceofdoes, which is only safe when subclasses cannot change what equality means — one reason value classes are usuallyfinalor records.
Try it yourself
2 visible tests · 3 hidden testsFraction below already has equals: two fractions are equal when
they have the same value, checked by cross-multiplying, so 1/2,
2/4 and -3/-6 are all equal, and so are 1/-2 and -1/2. It has
no hashCode, so a HashSet<Fraction> keeps equal fractions apart.
Add a hashCode that is consistent with that equals: every pair of
equal fractions must get the same hash code. Then implement
distinctValues(fractions): fractions is a List<List<Integer>> of
[numerator, denominator] pairs (denominators are never 0 and may be
negative); build a Fraction from each, add them all to a
HashSet<Fraction>, and return its size as an int.
distinctValues([[1,2],[2,4],[3,4]])distinctValues([[1,3],[2,3],[3,1]])
Sign up to run the tests, check the hidden ones and save your progress. Sign up