Stage 4: HashMap internals, lesson 13 of 13

HashMap internals 13: Why mutable keys are dangerous

Intermediate3 min read@since 16Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

When you put a key, HashMap stores it in the bucket chosen by its hash code at that moment, and remembers that hash. If the fields used by hashCode() and equals() change afterwards:

  • get(key) calculates the new hash and looks in a different bucket, returning null even though the entry is still inside.
  • Even if it happened to look in the right bucket, the stored hash no longer matches.
  • containsKey is false, remove does nothing, but size() and iteration still show the entry: a stranded entry and a memory leak.

The rules:

  • Use immutable keys: String, Integer, enums, records with immutable fields.
  • Never change fields used in equals()/hashCode() while an object is a key (or a HashSet element).
  • If you must change it: remove, change, then put back.
HashMap lab

Example

Java
class User {
    private String email;
    User(String email) { this.email = email; }
    void setEmail(String email) { this.email = email; }
    @Override public boolean equals(Object o) { return o instanceof User u && u.email.equals(email); }
    @Override public int hashCode() { return email.hashCode(); }
}

Map<User, String> roles = new HashMap<>();
User asha = new User("asha@old.com");
roles.put(asha, "ADMIN");                     // stored in bucket 4 (of 16)

asha.setEmail("asha@new.com");                // the key changes while it's inside the map

System.out.println(roles.get(asha));          // null   get() now looks in bucket 11
System.out.println(roles.containsKey(asha));  // false
System.out.println(roles.size());             // 1      the entry is still there, stranded
roles.remove(asha);                           // removes nothing

Common mistake

Using an object as a key and later updating a field that equals() and hashCode() depend on.

Under the hood

The same trap hits HashSet (its elements are the keys of an internal HashMap) and TreeMap/TreeSet if fields used by compareTo() change. Records are only shallowly immutable: a record holding an ArrayList is still a dangerous key if the list changes. JPA entities are a classic case: an equals()/hashCode() based on a generated id changes when the entity is first saved, which is why Hibernate's documentation recommends a stable business key.

Check yourself

A key's email (used in hashCode) changes after put(). What does get(sameObject) return?

How this connects

Where this leads

You've reached the end of this thread. Try a learning path for what's next.

Part of HashMap internals, part by part.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.