What happens if you mutate a field used in hashCode() after an object is already a key in a HashMap, and how do you avoid it?
answer
- HashMap files by hash ONCE at put; never re-files on mutation
- Mutate a hash field => entry orphaned in old bucket => get/remove miss
- Silent: null returned, phantom entry still counts in size()/iteration
- Fix: immutable (final) hash fields; use records/Strings/enums as keys
- Mutable collections as keys are a trap (List.hashCode depends on elements)
basics
~20 sThe object was filed in a bucket based on its old hash. After mutation its hash changes, so a fresh lookup computes the new hash and searches a different bucket. The entry is still in the old bucket, so the map can't find it — the key becomes a 'lost' entry that you can't get or remove normally.
solid answer
~50 sA HashMap places each key in a bucket chosen from its hashCode() at insertion time. If you then mutate a field that participates in hashCode(), the key's hash changes, but the entry physically stays in the old bucket — HashMap doesn't re-file it. A subsequent get()/remove()/containsKey() with the (now-mutated) key computes the *new* hash, probes the *new* bucket, and misses, returning null/false even though the entry exists. The entry is now effectively orphaned: it still occupies memory, may even still be iterable, but is unreachable by key. The same hazard applies to HashSet elements. Avoid it by making hash-participating fields immutable (final) and using immutable value types as keys; if a key genuinely must change, remove() it before mutation and re-put() it after. Records and other immutable types sidestep the problem entirely. This is also why you should never use a collection (whose contents define its hash) as a HashMap key while mutating it.
go deeper
Knows you shouldn't change a key after putting it in a map, even if not sure why.
Explains that the bucket is chosen at insert time and a changed hash sends lookups to the wrong bucket, orphaning the entry.
Connects it to the consistency clause of the equals contract, prescribes immutable keys/records, knows the remove-then-reinsert workaround and the mutable-collection-as-key trap.
Establishes immutability conventions for key types across a codebase, weighs the cost of defensive copying, and anticipates the analogous failure in TreeMap/TreeSet and in distributed/hashed sharding schemes.
## Background: how a key's bucket is fixed When you call `map.put(key, value)`, HashMap calls `key.hashCode()` **once, at that moment**, reduces it to a bucket index `(n-1) & hash`, and stores the entry in that bucket. It also caches the hash inside the entry. Crucially, HashMap **does not watch the key for changes** — there is no callback when a field mutates. The entry stays exactly where it was filed. ## The failure: a moving key in a static filing system Suppose a key's `hashCode()` depends on a field `name`, and you do: ```java Map<User, String> map = new HashMap<>(); User u = new User("alice"); map.put(u, "data"); // filed in bucket for hash("alice") u.setName("bob"); // hashCode() now reflects "bob" String v = map.get(u); // probes bucket for hash("bob") -> MISS -> null ``` - `put` filed the entry in the **"alice" bucket**. - Mutation changed `u`'s hash to the **"bob" hash**, but the entry is still physically in the alice bucket. - `get(u)` computes the **bob** hash, goes to the **bob bucket**, finds nothing, returns `null`. The entry is now **orphaned**: it still lives in the alice bucket (still counts toward `size()`, still shows up in iteration), but **no key-based operation can reach it** — not `get`, not `containsKey`, not `remove(u)`. You also can't `put` a "correct" replacement cleanly because the duplicate-detection probe also lands in the bob bucket. The same logic ruins `HashSet` membership, and breaks `TreeMap`/`TreeSet` differently (those rely on `compareTo`, so mutating a sort field corrupts the tree ordering instead). ## Why this is subtle - It's **silent** — no exception, just `null` and a phantom entry. - It can pass tests if mutation never happens in the test path, then fail in production when a key is updated in place. - A particularly nasty variant: using a **mutable collection** (e.g. an `ArrayList`) as a HashMap key. `List.hashCode()` is defined from its elements, so adding/removing an element silently moves the key — same orphaning. ## How to avoid it 1. **Make hash-participating fields `final` / the key type immutable.** If the fields used in `hashCode()` can't change, the bucket can't go stale. Value types, **records**, and other immutable classes are the natural fix and the standard advice. 2. **Use stable, immutable keys.** Prefer `String`, boxed primitives, enums, or your own immutable value objects as map keys. 3. **If a key truly must change, remove-then-reinsert:** `map.remove(key); /* mutate */ map.put(key, value);` so the entry is re-filed under the new hash. (Awkward and error-prone — a sign you want an immutable key instead.) 4. **Don't use mutable collections as keys** while you still mutate them. ## Relationship to the interdependence rule This is the *dynamic* cousin of the equals/hashCode contract. The static contract says "equal objects must hash equally"; the **consistency** clause of the equals contract also says equals/hashCode must return the same results **as long as the objects don't change in an equals-relevant way**. Mutating a hash field breaks that temporal consistency: the *same object* presents two different hashes over its lifetime, which the snapshot-based bucket placement cannot tolerate. Immutability is the clean guarantee that the snapshot never goes stale.
- After the bad mutation, can you still retrieve the orphaned entry at all?Not by key via get(). You can still see it by iterating entrySet() (it remains in its old bucket), and you could recover access by mutating the key's fields back to their original values so its hash matches the old bucket again. The robust fix is to never mutate hash fields in the first place.
- How does TreeMap behave differently from HashMap if you mutate a key field used for ordering?TreeMap uses compareTo()/Comparator, not hashCode(), so it has no buckets. Mutating a sort-relevant field corrupts the tree's ordering invariant: lookups (which binary-search by comparison) may take a wrong branch and miss the entry, and the structure can become inconsistent. The same lesson applies — keys should be immutable in their ordering/identity fields.
saying these in an interview costs you the question
- Assuming HashMap automatically re-buckets a key when its fields change
- Thinking the orphaned entry is garbage-collected or removed automatically
- Believing iteration also fails to see the entry (it's still in the old bucket)
- Suggesting you can recover the entry with get() using the original field values without restoring object state
- Using a mutable list/set as a HashMap key while mutating it