Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

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.

Beginner6 min readUpdated
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 means equals() 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 record generates 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:

  1. Call key.hashCode(), spread the bits, and take the low bits to get an array index.
  2. Walk the entries in that bucket.
  3. Compare each with equals().
hashCode() selects the bucket and equals() selects the entry. If hashCode() disagrees for two equal objects, step 3 never runs.

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.

BrokenKey.java
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.
}
the symptom
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);                         // false

The 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.

RuleMeaning
Reflexivex.equals(x) is always true
Symmetricx.equals(y) implies y.equals(x)
Transitivex.equals(y) and y.equals(z) implies x.equals(z)
ConsistentRepeated calls return the same result while the objects do not change
Null-safex.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

Account.java
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.

the disappearing entry
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 unreachable

The 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:

OrderId.java
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()?
The object still works fine for direct comparison, but every hash-based collection breaks. Two objects you consider equal land in different buckets, so a HashMap lookup with an equal-but-different instance returns null, a HashSet accepts an apparent duplicate, and distinct() in a stream stops removing duplicates. Nothing throws — the data is simply wrong.
Can two unequal objects have the same hashCode?
Yes, and that is legal. The contract is one-directional: equal objects must have equal hash codes, but equal hash codes do not imply equality. That is a hash collision, and HashMap resolves it by comparing with equals() inside the bucket. A hashCode() that always returns 1 is technically correct and turns every map into a linked list.
Should I use instanceof or getClass() in equals()?
Use getClass() when you want strict type equality and are willing to say a subclass is never equal to its parent. Use instanceof when the class is final, or when you deliberately want a subclass to be comparable with its parent — but then you must not add state to the subclass, or you break symmetry. Most codebases are best served by making the class final and using instanceof.

Related tutorials