What is the relationship between the equals contract and hashCode, and what concretely goes wrong in a HashMap or HashSet if you override one but not the other?
answer
- Equal objects MUST share hashCode; unequal MAY collide
- hashCode picks the bucket, equals confirms within it
- Override equals only -> different buckets -> get() returns null
- Override hashCode only -> same bucket but identity equals fails
- Same fields, immutable keys, use Objects.hash
basics
~20 sIf two objects are equal by equals(), they must return the same hashCode(). Override equals without hashCode and a HashMap/HashSet may store duplicates or fail to find a value, because it looks in the wrong bucket.
solid answer
~50 sThe equals/hashCode contract says: equal objects must have equal hash codes (the reverse need not hold — unequal objects may collide). Hash-based collections use hashCode to pick a bucket, then equals to confirm a match inside that bucket. If you override equals but inherit Object.hashCode, two value-equal objects almost certainly get different identity-based hashes, land in different buckets, and the collection never even compares them with equals — so map.get(key) returns null, set.add lets a logical duplicate in, and contains lies. Overriding hashCode but not equals is also broken: equals falls back to identity, so distinct-but-equal objects are never considered equal regardless of matching hashes. The fix is to override both together over the same fields, typically with Objects.hash(...) for hashCode and field-by-field comparison in equals. Records and IDE/Lombok generation keep them in sync automatically.
code
java · 14 linesclass PhoneNumber {
final int area, prefix, line;
PhoneNumber(int a,int p,int l){ area=a; prefix=p; line=l; }
@Override public boolean equals(Object o){
return o instanceof PhoneNumber pn
&& pn.area==area && pn.prefix==prefix && pn.line==line;
}
// NOTE: no hashCode override -> inherits identity hash
}
var m = new java.util.HashMap<PhoneNumber,String>();
m.put(new PhoneNumber(707,867,5309), "Jenny");
System.out.println(m.get(new PhoneNumber(707,867,5309))); // prints null!
// Equal keys, different identity hashCodes -> different buckets -> miss.go deeper
Knows you must override hashCode whenever you override equals, and that HashMap breaks otherwise.
Explains the bucket-then-equals mechanism and both failure modes; uses Objects.hash over the same fields.
Adds the mutable-key hazard, the equal⇒same-hash but collisions-allowed nuance, and good hash distribution considerations.
Discusses performance/security of hashing (poor distribution, hash-flooding DoS), immutable value-object design, and enforcing the pair via records/generation across a codebase.
## Two separate contracts that must agree `Object` declares both `equals(Object)` and `hashCode()`. Their Javadoc ties them together: 1. **If `a.equals(b)` is true, then `a.hashCode() == b.hashCode()` must be true.** (Equal ⇒ same hash.) 2. The converse is **not** required: two unequal objects *may* share a hash code (a **collision**) — that's allowed and normal. 3. `hashCode` must be **consistent**: same object returns the same int across calls (unless equals-relevant fields change). ## How a HashMap/HashSet actually uses them A hash-based collection stores entries in an array of **buckets**. To place or find a key it: 1. computes `key.hashCode()`, maps it (via further mixing + modulo) to a **bucket index**; 2. goes to that one bucket and walks its (short) list/tree of entries; 3. for each candidate, calls `equals` to confirm an exact match. The whole point is to avoid scanning all entries — `hashCode` narrows the search to one bucket, `equals` confirms within it. This only works if **equal keys hash to the same bucket**. ## Failure mode 1 — override equals, inherit hashCode Suppose `PhoneNumber` overrides `equals` to compare its digits but leaves `Object.hashCode` (identity hash, essentially the object's address): ```java Map<PhoneNumber,String> m = new HashMap<>(); m.put(new PhoneNumber(707,867,5309), "Jenny"); m.get(new PhoneNumber(707,867,5309)); // null !! ``` The two `PhoneNumber`s are `equals`-equal but have **different** identity hash codes → they map to **different buckets**. `get` searches only the bucket for the *second* object's hash, never finds the first, and returns `null`. Likewise a `HashSet` will happily store both as if they were distinct, so logical duplicates accumulate and `contains` returns false for a value that is logically present. ## Failure mode 2 — override hashCode, inherit equals Now the hashes match, so both objects land in the **same** bucket. But within the bucket the collection still calls `equals`, which is the inherited **identity** equals → the two distinct objects are deemed unequal. So you again get duplicates / failed lookups, just for the opposite reason. Matching hashes are necessary but never sufficient. ## Why 'equal ⇒ same hash', not the reverse Hashing maps a huge value space to a finite int, so collisions are inevitable; demanding unequal objects always differ would be impossible. The collection tolerates collisions by falling back to `equals` within a bucket. What it cannot tolerate is **equal objects with different hashes**, because then it never even visits the right bucket. ## Correct paired implementation ```java public final class PhoneNumber { private final short area, prefix, line; // ... constructor ... @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof PhoneNumber pn)) return false; return area==pn.area && prefix==pn.prefix && line==pn.line; } @Override public int hashCode() { return java.util.Objects.hash(area, prefix, line); // same fields as equals } } ``` Rules of thumb: **use exactly the same fields** in both; never include a mutable field in equals/hashCode if the object will be used as a hash key (changing it after insertion strands the entry in the wrong bucket); and prefer generated implementations — `record`, the IDE, or Lombok `@EqualsAndHashCode` — so the two never drift apart. ## The mutable-key corollary Even a correct pair breaks if you **mutate an equals-relevant field after** putting the object in a hash collection: its hashCode changes, but it's still physically in the old bucket, so it becomes unreachable. Use immutable keys. ## Takeaway equals and hashCode are a package deal: equal objects must hash equally; collisions are fine. Override one and the bucket logic of HashMap/HashSet silently breaks — lost lookups or duplicate entries. Override both over the same immutable fields.
- Is it a contract violation for two unequal objects to have the same hashCode?No. Collisions are explicitly allowed and unavoidable, since hashCode maps an unbounded value space to a 32-bit int. The collection resolves collisions by calling equals within the bucket. Only equal-but-different-hash is a violation.
- Why must you avoid mutable fields in a key's equals/hashCode?If you mutate an equals/hashCode field after inserting the object as a key, its hashCode changes, but the entry stays in its original bucket; lookups now compute the new bucket and never find it, so the entry is effectively lost.
hashCode is the aisle number in a library; equals is reading the spine to find the exact book. If two copies of the same book get different aisle numbers (equal but different hash), you'll never find one by looking in the other's aisle.
saying these in an interview costs you the question
- Saying 'equal objects can have different hash codes' — they must not.
- Saying 'unequal objects must have different hash codes' — collisions are legal.
- Overriding only one of equals/hashCode.
- Using mutable fields in a hash key's equals/hashCode.