Back to HashMap Internals
Q7Question 7 of 10

What happens with a mutable key in HashMap?

Key Concepts
  • If a mutable key is changed after insertion, its hashCode changes but the entry stays in the old bucket computed from the original hash
  • get() computes the new hash, looks in the wrong bucket, and returns null; the entry is lost but still counted in size()
  • The entry cannot be found, updated, or removed by key; it is a memory leak that occupies space but is unreachable
  • Even looking up by the original value fails because equals() is checked against the mutated key
  • HashMap keys must be effectively immutable: their hashCode() and equals() must never change after insertion
  • Safe key types: String, Integer, Long, enum, records with immutable components

Definition

Mutating a HashMap key after insertion changes its hashCode, causing the entry to become permanently unreachable – get() returns null, but the entry still exists as a memory leak.
  • When a mutable key is mutated after put(), its hashCode() changes
  • The entry remains in the bucket computed from the original hash – it does not move
  • get() with the mutated key computes a new hash, goes to a different bucket, and returns null
  • The entry exists in the old bucket (visible during iteration, counted in size()), but cannot be found by get(), containsKey(), or remove()
  • This is a memory leak: unreachable entries occupy memory and are never garbage collected
  • Rule: always use immutable objects as HashMap keys

You file a document in drawer number 7, computed from the account number on the document. Then you change the account number on the document. When you search for it using the new account number, you look in drawer 12 – not found. The document is still in drawer 7, but nobody can find it because the label no longer matches. The filing cabinet has a phantom entry that wastes space forever.

Understanding the Concepts

What happens when you mutate a key after insertion?

The problem unfolds in four steps:

  1. You insert the key. HashMap computes hash(key) and stores the entry in the corresponding bucket.
  2. You mutate the key. The key's hashCode() now returns a different value.
  3. You call get(key). HashMap computes the new hash, goes to a different bucket, and does not find the entry.
  4. The entry is stuck in the old bucket, unreachable by any key-based operation.
JAVA
package com.bank.demo;

import java.util.*;

public class MutableKeyDemo {
    static class Account {
        String id;
        Account(String id) { this.id = id; }

        @Override public int hashCode() { return id.hashCode(); }
        @Override public boolean equals(Object o) {
            return o instanceof Account a && a.id.equals(this.id);
        }
        @Override public String toString() { return "Account(" + id + ")"; }
    }

    public static void main(String[] args) {
        Map<Account, String> map = new HashMap<>();
        Account key = new Account("SAV2001");

        // Step 1: insert with hash of "SAV2001"
        map.put(key, "Rajesh");
        System.out.println("Before mutation: " + map.get(key)); // Rajesh

        // Step 2: MUTATE the key
        key.id = "CUR3001";

        // Step 3: get() computes hash of "CUR3001" → wrong bucket
        System.out.println("After mutation: " + map.get(key)); // null!

        // Try the original value – also fails
        System.out.println("Original lookup: " + map.get(new Account("SAV2001"))); // null!
        // Why? The entry in the old bucket has key.id = "CUR3001" now.
        // equals("SAV2001") fails because the stored key's id is "CUR3001".

        // Try the new value
        System.out.println("New value lookup: " + map.get(new Account("CUR3001"))); // null!
        // Why? The new hash points to a different bucket where no entry exists.

        // The entry is STUCK
        System.out.println("Size: " + map.size()); // 1 – entry exists!
        System.out.println("Contains key: " + map.containsKey(key)); // false!

        // Only visible during iteration
        for (var entry : map.entrySet()) {
            System.out.println("Iteration found: " + entry.getKey() + " → " + entry.getValue());
        }
    }
}
DIAGRAM
Before mutation: Rajesh
After mutation: null
Original lookup: null
New value lookup: null
Size: 1
Contains key: false
Iteration found: Account(CUR3001) → Rajesh

Why does looking up by the original value also fail?

The entry is in the bucket computed from hash("SAV2001"). When you look up new Account("SAV2001"), HashMap goes to the correct bucket. But the stored key's id has been changed to "CUR3001". So equals("SAV2001") checks against "CUR3001" and returns false. The entry is right there in the bucket, but equals() cannot match it.

This creates a truly unreachable entry: no key value can find it.

What types are safe as HashMap keys?

| Type | Safe? | Why |
|---|---|---|
| String | Yes | Immutable – hashCode never changes |
| Integer, Long, Double | Yes | Immutable wrapper types |
| enum | Yes | Enum constants are immutable |
| Java record | Yes* | If all components are immutable |
| Custom immutable class | Yes | All fields final, no setters |
| Mutable class | No | hashCode can change after insertion |
| arrays | No | hashCode is based on identity, equals compares identity |

JAVA
// Safe keys:
Map<String, String> map1 = new HashMap<>();        // String is immutable
Map<Integer, String> map2 = new HashMap<>();       // Integer is immutable
Map<DayOfWeek, String> map3 = new EnumMap<>(DayOfWeek.class); // enum is immutable

// Safe: records with immutable components
record AccountId(String branch, String number) {} // immutable
Map<AccountId, String> map4 = new HashMap<>();

// UNSAFE: mutable class as key
class MutableAccount { String id; /* no final, has setter */ }

How do you make a custom class safe as a HashMap key?

JAVA
package com.bank.model;

import java.util.Objects;

public final class AccountKey {
    private final String branchCode;
    private final String accountNumber;

    public AccountKey(String branchCode, String accountNumber) {
        this.branchCode = Objects.requireNonNull(branchCode);
        this.accountNumber = Objects.requireNonNull(accountNumber);
    }

    @Override
    public int hashCode() {
        return Objects.hash(branchCode, accountNumber);
    }

    @Override
    public boolean equals(Object o) {
        return o instanceof AccountKey ak
            && ak.branchCode.equals(this.branchCode)
            && ak.accountNumber.equals(this.accountNumber);
    }

    @Override
    public String toString() {
        return branchCode + "-" + accountNumber;
    }

    // No setters! Fields are final!
    public String branchCode() { return branchCode; }
    public String accountNumber() { return accountNumber; }
}

Or use a record (Java 16+), which is immutable by default:

JAVA
record AccountKey(String branchCode, String accountNumber) {}
// hashCode, equals, and toString are auto-generated
// Fields are final – cannot be mutated

What are common mistakes?

Mistake: Using a mutable collection as a key

JAVA
// WRONG: List is mutable – hashCode changes if elements change
Map<List<String>, String> map = new HashMap<>();
List<String> key = new ArrayList<>(List.of("SAV", "2001"));
map.put(key, "Rajesh");
key.add("EXTRA"); // hashCode changes!
System.out.println(map.get(key)); // null! Entry lost.

// CORRECT: use an immutable list or convert to String
Map<List<String>, String> map2 = new HashMap<>();
map2.put(List.of("SAV", "2001"), "Rajesh"); // List.of is immutable

What should a senior engineer know?

- String caching. String caches its hashCode after the first call. This makes String extremely fast as a HashMap key – hashCode is computed once and reused.

- Identity vs equality for keys. If you do not override hashCode/equals, HashMap uses Object.hashCode (identity-based). Two different Account objects with the same data are treated as different keys. Always override both methods.

- The hashCode/equals contract. If a.equals(b) is true, then a.hashCode() == b.hashCode() must be true. The reverse is not required: equal hash codes do not imply equals (collisions are allowed).

- TreeMap alternative. If your keys implement Comparable, TreeMap does not use hashCode at all. It uses compareTo for ordering and equality. Mutable keys in TreeMap cause the same lost-entry problem if compareTo results change.

What are common interview follow-up questions?

  • "What happens if you mutate a HashMap key?" – Entry becomes unreachable; get returns null.
  • "Can you use an array as a HashMap key?" – Technically yes, but it uses identity hashCode/equals, so two arrays with the same content are different keys.
  • "What is the hashCode/equals contract?" – Equal objects must have equal hash codes.
  • "Why is String the most common HashMap key?" – Immutable, good hash distribution, cached hashCode.

Visual Overview

DIAGRAM
  Mutable key problem

  Before mutation:
  key.id = "SAV2001"
  hash("SAV2001") → bucket 7
  table[7] → [key=SAV2001, value=Rajesh]
  get(key) → hash("SAV2001") → bucket 7 → found! → "Rajesh"

  After mutation: key.id = "CUR3001"
  hash("CUR3001") → bucket 12
  get(key) → hash("CUR3001") → bucket 12 → EMPTY → null!

  table[7] still has [key=CUR3001, value=Rajesh]
  But bucket 7 can't be reached with hash("CUR3001")
  And equals("SAV2001") fails because key.id is now "CUR3001"
  → Entry is PERMANENTLY LOST (memory leak)

Key Takeaway

If you use a mutable object as a HashMap key and mutate it after insertion, the entry becomes permanently unreachable. The entry stays in the bucket computed from the original hash, but get() computes the new hash and looks in the wrong bucket. Even looking up the original value fails because equals() is checked against the mutated key.

The entry exists – it is counted in size() and visible during iteration – but cannot be found, updated, or removed by any key-based operation. This is a memory leak in disguise.

In the National Bank of India system, Rajesh always uses String account IDs as HashMap keys, never mutable Account objects. Strings are immutable – their hashCode never changes, and they cache the hash value for fast repeated lookups. For composite keys, he uses Java records: record AccountKey(String branch, String number) {} – immutable by default with auto-generated hashCode and equals.

Remember one sentence: HashMap keys must be immutable because mutating a key after insertion makes the entry permanently unreachable.

---

Share
Use ← → arrow keys to navigate