skip to content

Why is using a mutable object as a HashMap key (or HashSet element) dangerous, and how does this relate to equals()/hashCode()?

level: seniorimportance: should knowfreq 55%

answer

  1. Hash bucket is fixed at insertion; collection never re-indexes
  2. Mutate a hash field -> entry stranded in old bucket
  3. get/contains/remove all silently fail -> leak + wrong results
  4. Fix: immutable keys (records, String) or stable-id-only equals
  5. Or remove-mutate-reinsert around the change

basics

~20 s

A 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 s

Hash-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

for a junior

Knows you should use immutable objects like String as map keys, even if not the precise reason.

for a middle

Explains that hashCode is captured at insertion and that mutating a hashed field strands the entry, causing get() to return null.

for a senior

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.

for a principal

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)

context