The equals() and hashCode() Contract
Why overriding equals() without hashCode() breaks every hash-based collection, what the five contract rules guarantee, and the mutable-key bug that silently loses data.
On this page
This is the most-asked question in Java interviews, and the one most often answered from memory rather than understanding. "Always override both" is the correct rule and the wrong answer. The interviewer wants to know what breaks if you do not, and that requires knowing how a hash-based collection finds things.
Key Takeaways
hashCode()picks the bucket;equals()picks the entry inside that bucket. Wrong bucket meansequals()is never even called.- Equal objects must have equal hash codes. Unequal objects may share one — that is just a collision.
- Using a mutable field in
hashCode()and then mutating it while the object is a key loses the entry permanently. - A
recordgenerates both correctly from its components. That is the safest default. Objects.hash(...)is readable but allocates a varargs array; hand-written folding is faster on a hot path.
What HashMap actually does
A HashMap is an array of buckets. To store or find a key it performs three steps, in this order:
- Call
key.hashCode(), spread the bits, and take the low bits to get an array index. - Walk the entries in that bucket.
- Compare each with
equals().
Step 3 only ever runs on the bucket step 1 chose. That single fact explains the whole contract: if
two objects you consider equal produce different hash codes, they land in different buckets and
equals() is never consulted. The map does not "fail to find" the key — it never looks where the
key is.
public class BrokenKey {
private final String id;
public BrokenKey(String id) { this.id = id; }
@Override
public boolean equals(Object o) {
return o instanceof BrokenKey other && id.equals(other.id);
}
// hashCode() deliberately not overridden — inherits Object's identity hash.
}Map<BrokenKey, String> map = new HashMap<>();
map.put(new BrokenKey("A-1"), "value");
BrokenKey lookup = new BrokenKey("A-1");
lookup.equals(map.keySet().iterator().next()); // true
map.get(lookup); // null
map.containsKey(lookup); // falseThe two objects are equal by every definition the class gives, and the map cannot find one using the
other. In production this shows up as a cache that never hits, a Set that accumulates duplicates,
or a distinct() that does nothing — all without a single exception.
The five rules
Object.equals documents a contract, and violating any part of it makes a collection's behaviour
undefined rather than merely surprising.
| Rule | Meaning |
|---|---|
| Reflexive | x.equals(x) is always true |
| Symmetric | x.equals(y) implies y.equals(x) |
| Transitive | x.equals(y) and y.equals(z) implies x.equals(z) |
| Consistent | Repeated calls return the same result while the objects do not change |
| Null-safe | x.equals(null) is false, never a NullPointerException |
And the linking rule from hashCode(): if two objects are equal, their hash codes must be equal.
The reverse is explicitly not required.
Symmetry is the one people break, almost always with inheritance. If Point.equals uses instanceof
and ColourPoint extends Point adds a colour to the comparison, then point.equals(colourPoint) can
be true while colourPoint.equals(point) is false. A List.contains will then return different
answers depending on the list's internal ordering.
Writing them by hand
public final class Account {
private final String iban; // identity
private final String currency; // identity
private BigDecimal balance; // deliberately NOT part of equality
@Override
public boolean equals(Object o) {
if (this == o) return true; // cheap fast path
if (!(o instanceof Account other)) return false; // also handles null
return iban.equals(other.iban)
&& currency.equals(other.currency);
}
@Override
public int hashCode() {
// Exactly the fields used in equals(), in the same order.
int result = iban.hashCode();
result = 31 * result + currency.hashCode();
return result;
}
}Two details matter. First, equals() and hashCode() must use the same fields — a field in one
and not the other is the bug, in either direction. Second, balance is excluded on purpose: an
account is the same account whether or not money moved. Deciding which fields carry identity is a
design decision, and interviewers often push on it.
The 31 * multiplier is not magic. It is an odd prime, so it does not throw away bits, and the JIT
compiles 31 * x to (x << 5) - x. Any odd prime works.
Objects.hash(iban, currency) is the readable one-liner and is correct, but it boxes each argument
and allocates a varargs array on every call. On a key used inside a tight loop that allocation is
measurable; everywhere else, prefer the readable form.
The mutable key bug
This one loses data, and it does so silently.
Set<List<String>> set = new HashSet<>();
List<String> key = new ArrayList<>(List.of("a"));
set.add(key);
set.contains(key); // true
key.add("b"); // hashCode of the list has now changed
set.contains(key); // false — the entry sits in the old bucket
set.remove(key); // false — cannot be removed either
set.size(); // 1 — still in there, permanently unreachableThe entry is now a leak: it holds memory, is counted in size(), and can never be retrieved or
removed. Iterating the set still yields it, which makes the bug particularly confusing to debug.
The rule that follows: a key's hashCode() must depend only on immutable state. In practice this
means making key classes immutable, and never using a mutable collection as a map key.
Records, Lombok and IDE generation
A record generates equals(), hashCode() and toString() from all its components, correctly and
consistently. For a value type, this is the right default:
public record OrderId(String tenant, long sequence) { }Lombok's @EqualsAndHashCode works but has sharp edges worth knowing for the interview: by default
it does not call super.equals(), and on JPA entities it will happily include a lazily-loaded
association, triggering a database query — or a LazyInitializationException — from inside
hashCode().
How to answer this in an interview
Lead with the mechanism, not the rule. Something like: "hashCode() selects the bucket and
equals() selects the entry within it, so if equal objects hash differently the map looks in the
wrong bucket and equals() is never called — the lookup returns null even though the key is present."
Then be ready for the three standard follow-ups: what a collision is and why it is legal; what
happens if a key is mutated after insertion; and whether instanceof or getClass() belongs in
equals(). Those three cover almost every version of this question.
Frequently Asked Questions
What happens if I override equals() but not hashCode()?
Can two unequal objects have the same hashCode?
Should I use instanceof or getClass() in equals()?
Related tutorials
- Strings: Immutability, the Pool and StringBuilderWhy String is immutable and what that buys you, how the string pool and intern() really work, why == sometimes appears to work, compact strings, and when concatenation in a loop actually costs you.
- OOP Principles the Interviewer Actually ProbesThe OOP questions behind the textbook four: static vs dynamic dispatch, Liskov violations that compile cleanly, abstract class versus interface, and composition over inheritance.
- Exception Handling, finally and try-with-resourcesChecked versus unchecked and when each is right, how finally silently discards an exception or a return value, and suppressed exceptions in try-with-resources.
- static, final and Class Initialisation OrderThe exact order the JVM initialises a class, why a static final String can survive deleting the class that declared it, effectively final, and how two classes can deadlock while loading.