Why is using a mutable object as a HashMap key (or HashSet element) dangerous, and how does this relate to equals()/hashCode()?
answer
- Hash bucket is fixed at insertion; collection never re-indexes
- Mutate a hash field -> entry stranded in old bucket
- get/contains/remove all silently fail -> leak + wrong results
- Fix: immutable keys (records, String) or stable-id-only equals
- Or remove-mutate-reinsert around the change
basics
~20 sA hash collection records an object's hashCode() when you add it. If you then mutate a field used by hashCode(), the object's bucket changes, so the collection looks in the old bucket and can no longer find it. Use immutable keys.
solid answer
~40 sHash-based collections place an element in a bucket derived from its hashCode() at insertion time and don't re-index on later changes. If your key's equals()/hashCode() depend on mutable fields and you change one of those fields after insertion, the recomputed hash points to a different bucket than where the entry physically lives. The collection then 'loses' the entry: contains() returns false, get() returns null, and remove() can't remove it — even though it's still in the map. This silently leaks memory and corrupts logic. The fix is to use immutable keys (or at least keys whose equals/hashCode fields never change while in the collection): records, Strings, boxed primitives, or value objects with final fields. If you must key on something mutable, exclude the mutable parts from equals/hashCode, or remove-then-reinsert around the mutation.
go deeper
Knows you should use immutable objects like String as map keys, even if not the precise reason.
Explains that hashCode is captured at insertion and that mutating a hashed field strands the entry, causing get() to return null.
Connects it to the consistency clause of the contract, prescribes immutable keys/records or stable-id-based equals, and knows the remove-mutate-reinsert workaround and the JPA-entity convention.
Sets data-model conventions (value objects as records, entity equality on stable ids), reasons about the resulting memory-leak/correctness blast radius, and reviews APIs to keep mutable state out of keys.
## Background: how a hash collection indexes When you call `set.add(x)` or `map.put(key, v)`, the collection: 1. Calls `x.hashCode()` to get an int. 2. Reduces it (e.g. masks the low bits) to a **bucket index** in its internal array. 3. Stores the entry in that bucket. Crucially, it **does this once, at insertion**. The collection has no way to be notified later that the object changed — there are no listeners on your fields. It assumes the indexing stays valid. ## The failure mode Suppose your key class computes `equals()`/`hashCode()` from a field `name`, and `name` is *mutable*: ``` Key k = new Key("a"); map.put(k, 1); // hashed by "a" -> bucket 3, stored there k.setName("z"); // now hashCode() reflects "z" map.get(k); // hashes "z" -> bucket 9, looks in bucket 9, finds nothing -> null ``` The entry is **physically still in bucket 3**, but every lookup now computes the *new* hash and searches the *wrong* bucket. Consequences: - `containsKey(k)` / `contains(x)` -> `false`. - `get(k)` -> `null`. - `remove(k)` -> can't find it, so it can't be removed -> a **memory leak** (the entry is unreachable through the API yet retained). - Even iterating and comparing can give surprising results because two different views of the object disagree. This is a *silent* corruption: no exception, just wrong answers — the most expensive kind of bug. ## Why it's fundamentally an equals/hashCode design issue The contract additionally requires **consistency**: while an object is a key, the values feeding `equals()`/`hashCode()` must not change. Mutating those fields violates that *temporal* stability the collection relies on. So the rule "don't mutate a key's identity-defining fields while it's in a hash collection" is a direct corollary of the hashCode contract. ## How to avoid it 1. **Prefer immutable keys.** `String`, boxed numbers, `UUID`, **records**, and value objects with `final` fields can't change, so the hash is permanently valid. This is the standard, robust answer. 2. **Exclude mutable fields from equals/hashCode.** If a key has both stable identity (e.g. an `id`) and mutable attributes, base equals/hashCode only on the stable `id`. Then mutating the attributes is harmless. (JPA entities often do this, keying on a stable business or surrogate id.) 3. **Remove-mutate-reinsert.** If you truly must change an indexed field, `map.remove(key)` *before* the mutation and `map.put(...)` after, so the entry is re-indexed. 4. **Use a different structure if ordering, not hashing, is the need** — e.g. a `TreeMap` re-locates by comparison each time (though mutating its keys still corrupts it for the same reason). ## Related gotchas - **Boxed-cache surprise:** small `Integer`s are cached so `==` may look fine, but the lesson stands — value equality, not identity, is what matters. - **JPA/Hibernate:** entities are mutable by nature; a common best practice is to base equals/hashCode on a stable identifier (or use a natural key), precisely to keep them usable in `Set`s during a session.
- How do JPA entities commonly stay safe as Set elements despite being mutable?By implementing equals()/hashCode() on a stable identifier only (a business key, or a surrogate id assigned before the entity enters any Set), so mutating other attributes never moves the hash bucket.
- If you must key on a mutable field, what is the safe sequence?Remove the entry from the map before mutating the field, then re-insert it after — so it gets re-indexed into the correct bucket.
saying these in an interview costs you the question
- Believing the collection re-hashes entries automatically when fields change
- Using a mutable entity with field-based equals as a HashSet element
- Thinking the bug throws an exception (it's silent)
- Including frequently-changing fields in equals/hashCode of a key
- Assuming TreeMap is immune to mutated keys (it isn't)