Question 13 · Section 10

What Happens If You Modify a Key After Adding It to HashMap?

If you modify the fields of a key object after put(), HashMap won't be able to find that element. The object remains in the map but becomes "invisible" — it can't be retrieved,...

Language versions: English Russian Ukrainian

🟢 Junior Level

If you modify the fields of a key object after put(), HashMap won’t be able to find that element. The object remains in the map but becomes “invisible” — it can’t be retrieved, updated, or deleted.

Example:

MutableKey key = new MutableKey(1);
map.put(key, "Hello");  // Landed in bucket #1
key.setValue(2);        // Modified the key — hash is now different
System.out.println(map.get(key));  // null!

// put(key, "Hello"): key.hashCode() = 1 → bucket #1
// key.setValue(2): key.hashCode() now = 2
// get(key): computes hash = 2 → searches in bucket #2 → null!
// Element is physically in bucket #1 — but get() searches in #2

Analogy: You changed your address but didn’t notify the bank. The bank sends you a letter to the old address — you won’t receive it.

🟡 Middle Level

Step-by-Step Failure Mechanics

  1. Addition: map.put(user, "Active")user.hashCode() = 100 → bucket #100
  2. Modification: user.setName("New")user.hashCode() = 200
  3. Search: map.get(user) — computes hash 200 → bucket #200 → null

Paradox: You have a direct reference to the key object, but can’t retrieve the value!

Visibility via Iterator

When iterating over entrySet(), you can see this element:

for (Map.Entry<MutableKey, String> e : map.entrySet()) {
    System.out.println(e.getKey() + " = " + e.getValue());
    // Prints: MutableKey(id=2) = Hello
}

The iterator traverses all physical buckets, so it finds the “lost” element.

How to Fix?

The simplest way — create a new map:

Map<MutableKey, String> newMap = new HashMap<>(oldMap);
// On copying, the actual hash is computed for each key

Alternative: remove the old key via the entrySet() iterator, modify, insert back.

Common Mistakes

  1. Modifying entity ID — the most common case
  2. Modifying name/email — if they participate in hashCode
  3. JPA Entity as a key — Entity changes on persist/merge

🔴 Senior Level

Ghost Entry Pattern

“Ghost” entries are a memory leak that:

  • Isn’t detected by standard profilers (object is reachable)
  • Grows proportionally to the number of modifications
  • Manifests only through OutOfMemoryError
// In production: service updates keys and puts new versions
Map<ConfigKey, Config> cache = new HashMap<>();
// On each update: old key is "lost", new one is added
// After 1000 updates: 999 ghost entries in memory

Comparison with IdentityHashMap

Map<MutableKey, Value> map = new IdentityHashMap<>();
// Uses System.identityHashCode() — object address
// Address doesn't change on field modification → works correctly

But semantics differ: new Key(1) and new Key(1) — different keys.

Internal Mechanics

HashMap on get():

  1. Computes hash(key) — uses the current hashCode
  2. Goes to the bucket by the current index
  3. Doesn’t find the element (it’s in the old bucket)

HashMap doesn’t store a “snapshot” of the key’s hash. Each get() computes the hash fresh.

Edge Cases

  1. Partial modification: Changed a field, but hashCode didn’t change (e.g., field not in hashCode) — works, but fragile
  2. Restoration: If fields are reverted to original state — hashCode matches, element is “found”
  3. Concurrent modification: One thread modifies the key, another searches — race condition. Result depends on thread execution order: lost updates or stale reads are possible.

Production Monitoring

For detection:

  • Compare map.size() with the number of unique keys via iterator
  • Use JFR to analyze bucket distribution
  • Heap dump shows ghost entries (elements with “wrong” hash)

Defensive Strategy

// Pattern: Delete → Modify → Insert
Value v = map.remove(key);
key.modify();
map.put(key, v != null ? v : newValue);

Or use immutable keys:

record Key(String id) {} // Impossible to modify

🎯 Interview Cheat Sheet

Must know:

  • Element stays in the old bucket, get() searches in the new → null
  • Iterator entrySet() SEES the “lost” element — traverses all physical buckets
  • Paradox: you have a direct reference to the key, but can’t retrieve the value
  • HashMap doesn’t store a hash snapshot — each get() computes hashCode fresh
  • Ghost Entry Pattern: memory leak, grows proportionally to modifications
  • Solution: Delete → Modify → Insert pattern, or immutable keys

Common follow-up questions:

  • Can you delete a lost element? — via entrySet() iterator yes, via remove() no
  • If fields are reverted — will it be found? — yes, hashCode matches, element is “found”
  • How does this differ from the mutable-key problem? — it’s a specific case: key was already in Map, then modified
  • Why does the iterator see the element? — iterator physically traverses all buckets, not by hash

Red flags (DO NOT say):

  • “The element is automatically deleted” — no, it stays in memory
  • “HashMap rewrites the element to a new bucket” — no, it stays in the old place
  • “This is safe if changed back” — fragile decision, race condition in multi-threading

Related topics:

  • [[12. Can You Use a Mutable Object as a Key in HashMap]]
  • [[14. What Are the Requirements for a HashMap Key]]
  • [[19. What Happens During Rehashing]]