Why HashSet Can't Find Your Object (Mutable Keys)
π― The Questionβ
"You create a custom class
User, insert an instance into aHashSet<User>, and verifyset.contains(user)returnstrue. Then, you modify one field (user.name = 'Bob'). Why doesset.contains(user)suddenly returnfalse, and why does callingset.remove(user)fail to delete it, creating a permanent memory leak?"
β‘ 30-Second Elevator Pitchβ
A HashSet (and HashMap) relies on the Immutability of Hash Codes:
- When you call
set.add(user), the set computesuser.hashCode(), determines the bucket index (e.g. Bucket #4), and places the object in that bucket. - If you mutate a field participating in
hashCode(), the object's hash code changes dynamically. - When you later call
set.contains(user)orset.remove(user), the set calculates the new hash code and looks in a completely different bucket (e.g. Bucket #9). - Bucket #9 is empty! The set reports
false(object not found), even though the exact instance is still sitting right inside Bucket #4.
The object becomes an unreachable "Ghost Object"βit cannot be retrieved, cannot be deleted, and permanently leaks memory!
π§ Under-the-Hood: The Displaced Hash Bucket Trapβ
π¬ The Golden Rule: The hashCode() and equals() Contractβ
Java's official language specification mandates:
- If two objects are equal according to
equals(), they must have identicalhashCode()values. - Hash Code Invariant: The value returned by
hashCode()must remain strictly consistent throughout the lifetime of the application as long as the object is stored inside a hash-based collection.
Production Solution: Enforce Immutable Keysβ
Never use mutable domain classes as keys in HashMaps or elements in HashSets. Use immutable data carriers:
- Java:
recordorfinalfields without setters:public record UserKey(String id, String email) {} // Immutable by design! - Python: Use immutable tuples or frozen dataclasses (
@dataclass(frozen=True)). - C++: Mark key members
const.
π Comparison Matrix: Mutable vs. Immutable Hash Keysβ
| Dimension | Mutable Key (e.g. standard JavaBean / POJO) | Immutable Key (e.g. Java record / frozen class) |
|---|---|---|
hashCode() Consistency | β Changes whenever fields mutate | β 100% Guaranteed constant throughout lifecycle |
| Lookups After Field Update | π₯ Returns false (Ghost Object) | Fields cannot be mutated; lookups always succeed |
Deletion via remove() | β Fails (Permanent heap memory leak) | β Always locates correct bucket and deletes |
| Thread Safety | Prone to data races | Inherently thread-safe |
π‘ What Interviewers Ask Next (Follow-Up Traps)β
-
"What happens if you iterate over the HashSet with a
forloop after mutating the object?"- Answer: The loop will find the object! Iterators do not compute hash codes; they iterate linearly through all buckets. However,
set.contains()andset.remove()will still fail. This discrepancy creates confusing bugs where a set prints an element on screen, but claims it doesn't contain it when checked!
- Answer: The loop will find the object! Iterators do not compute hash codes; they iterate linearly through all buckets. However,
-
"Why should you only use ID fields in
hashCode()instead of all object attributes?"- Answer: If an entity has a unique immutable identifier (like
UUID id), basehashCode()andequals()solely on that immutable ID. That way, mutable attributes (likeuserNameorlastLogin) can update freely without changing the object's hash code or bucket position.
- Answer: If an entity has a unique immutable identifier (like
Interview Answer: Modifying an object after inserting it into a HashSet changes its hashCode. Subsequent lookups compute the new hash and check the wrong bucket, failing to find the object and preventing deletion. Always use immutable types (like Java records or frozen dataclasses) for keys in hash-based collections.