Explain the equals/hashCode contract and why violating it breaks a HashSet.
answer
- equal objects => equal hashCodes (one direction only)
- Override equals AND hashCode together, same fields
- Forget hashCode => duplicates in HashSet
- Mutate hash field after insert => lost element
- Use immutable fields / records / Objects.hash
basics
~10 sIf two objects are equal by equals(), they must return the same hashCode(). If you break this, a HashSet can store duplicates or fail to find elements you put in.
solid answer
~40 sThe contract links two Object methods. equals() must be reflexive, symmetric, transitive, consistent, and x.equals(null) must be false. hashCode() must return the same int for objects that are equal by equals(), and should ideally spread unequal objects across different values. The binding rule: equal objects MUST have equal hash codes (the reverse is not required — collisions are allowed). HashSet relies on this: it locates an element's bucket by hashCode, then confirms with equals. If you override equals but not hashCode, two 'equal' objects can land in different buckets, so the Set never compares them and stores both — a logical duplicate. Conversely, an unstable hashCode (changing after insertion) makes contains/remove miss the element. Practically, always override equals and hashCode together, base both on the same immutable fields, and prefer Objects.equals/Objects.hash or records.
code
java · 14 lines// Correct, field-based implementation
final class Money {
private final long cents;
private final String currency;
Money(long cents, String currency){ this.cents = cents; this.currency = currency; }
@Override public boolean equals(Object o){
if (this == o) return true;
if (!(o instanceof Money m)) return false;
return cents == m.cents && currency.equals(m.currency);
}
@Override public int hashCode(){ return Objects.hash(cents, currency); }
}
// Or simply: record Money(long cents, String currency) {} // auto equals/hashCodego deeper
Knows you must override equals and hashCode together and that forgetting hashCode breaks HashSet; can use a record or IDE-generated methods.
States all five equals properties and the equal-objects-equal-hashes rule, and explains the duplicate and lost-element failures with a concrete example.
Reasons about hash distribution quality, immutability of equality fields, symmetry pitfalls in inheritance (Liskov), and when to prefer records or Objects.hash.
Considers domain-wide identity strategy (value vs entity equality), API contracts that depend on equality, performance of hashCode under adversarial input (hash-flooding), and migration risk when changing equals.
## Two methods that must cooperate Every Java object has two inherited methods from `Object`: - **`boolean equals(Object o)`** — "are these two objects the same *value*?" - **`int hashCode()`** — "give me an `int` fingerprint of this object." Hash-based collections (`HashSet`, `HashMap`) use **both together**, so Java defines a **contract** they must obey. ## The equals contract For non-null references, `equals` must be: - **Reflexive**: `x.equals(x)` is true. - **Symmetric**: `x.equals(y)` ⇔ `y.equals(x)`. - **Transitive**: if `x.equals(y)` and `y.equals(z)`, then `x.equals(z)`. - **Consistent**: repeated calls give the same answer if the objects don't change. - **Null-safe**: `x.equals(null)` is `false`. ## The hashCode contract - **Self-consistent**: same object, same hash within a run (unless equals-relevant state changes). - **THE key rule**: if `x.equals(y)` is true, then `x.hashCode() == y.hashCode()` **must** hold. - **One-way only**: equal hash codes do **not** imply equal objects. Two different objects sharing a hash is a *collision* — allowed and normal. - **Quality**: a good `hashCode` spreads unequal objects across many values to keep buckets small. ## Why HashSet depends on it `HashSet.add(x)` does: compute `x.hashCode()` → go to that bucket → call `equals` only on elements already there. Two consequences: **Override equals but NOT hashCode** → two "equal" objects may compute *different* hash codes, land in *different* buckets, and never be compared by `equals`. The Set then holds both — a **logical duplicate** that defeats the whole point of a Set. **Unstable hashCode** → you insert `x`, later mutate a field used by `hashCode`, then call `contains(x)`. The Set looks in the *new* bucket, the element is still in the *old* one, so `contains` returns **false** and `remove` silently fails — a "lost" element / memory leak. ## Worked failure ```java class Point { int x, y; public boolean equals(Object o){ return o instanceof Point p && p.x==x && p.y==y; } // forgot hashCode() -> inherits Object's identity hash } Set<Point> s = new HashSet<>(); s.add(new Point(1,1)); s.add(new Point(1,1)); // equal by equals, but different identity hash -> different bucket s.size(); // 2 (BUG: duplicate stored) ``` Adding a matching `hashCode` (e.g. `Objects.hash(x, y)`) fixes it and `size()` becomes 1. ## How to get it right - Override `equals` and `hashCode` **together**, from the **same fields**. - Use only **immutable, equality-defining** fields so the hash never changes after insertion. - Prefer `Objects.equals(a,b)` and `Objects.hash(...)`, or let the compiler do it: a **`record`** auto-generates a correct, field-based `equals`/`hashCode`. - Don't include mutable or derived fields you don't compare in `equals`.
- Does the contract require unequal objects to have unequal hash codes?No. Only equal objects must share a hash code. Unequal objects may collide on the same hash; the Set just resolves it with equals() inside the bucket. Good hashing minimizes collisions for performance, but they are never a correctness bug.
- Why are records a good fit for Set elements?A record generates equals() and hashCode() from all its components automatically and is shallowly immutable, so the contract is satisfied and the hash stays stable after insertion — eliminating the two classic Set bugs.
hashCode is the aisle number in a library, equals is reading the exact title. Equal books must share an aisle, or the librarian (HashSet) walks the wrong aisle and never finds the match.
saying these in an interview costs you the question
- Saying equal hash codes imply equal objects — the implication only runs from equals to hashCode.
- Overriding equals without hashCode (or vice versa).
- Including mutable fields in hashCode, then mutating them after insertion.
- Claiming collisions are a bug — they are expected and handled by equals within a bucket.
- Forgetting the null/instanceof check in equals, causing NullPointerException or ClassCastException.