Mutable keys and entries that disappear
Change a key after insertion and the value is unreachable but still occupying memory.
Open this lesson in the learning hubKey points
- A
HashMapplaces an entry in a bucket derived from the key hash at insertion time. It is never recomputed. - Mutating a field used by
hashCodeafter insertion moves the key logical bucket but not its physical one. A lookup goes to the new bucket, finds nothing, and the entry is unreachable. - The entry is still there. It still holds memory, still appears in iteration and in
size(), and it will never be found again - a leak that is also a correctness bug. - The same applies to
HashSet, which is a HashMap underneath, and toTreeMapwhere mutating a field used bycompareTocorrupts the ordering invariant instead. - The rule is simple: keys should be immutable. Records, Strings, boxed primitives and enums are safe; a mutable entity with a generated id is not.
- If a mutable object must be a key, remove it before mutating and re-insert afterwards - which is also a good sign that the design wants a different key.
Example
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
public class MutableKeys {
// Mutable, and uses the mutable field in hashCode. A dangerous key.
static final class Tag {
private String name;
Tag(String name) { this.name = name; }
void rename(String n) { this.name = n; }
@Override public boolean equals(Object o) {
return o instanceof Tag t && Objects.equals(name, t.name);
}
@Override public int hashCode() { return Objects.hash(name); }
@Override public String toString() { return "Tag(" + name + ")"; }
}
public static void main(String[] args) {
Map<Tag, String> map = new HashMap<>();
Tag key = new Tag("draft");
map.put(key, "the value");
System.out.println("before rename:");
System.out.println(" get(key) = " + map.get(key));
System.out.println(" containsKey = " + map.containsKey(key));
key.rename("published"); // hashCode changes; the bucket does not
System.out.println("after rename:");
System.out.println(" get(key) = " + map.get(key) + " <- lost");
System.out.println(" containsKey = " + map.containsKey(key));
System.out.println(" size() = " + map.size() + " <- still there");
System.out.println(" entries = " + map.entrySet());
System.out.println(" even a fresh equal key fails: "
+ map.get(new Tag("published")));
// Same failure in a HashSet.
Set<Tag> set = new HashSet<>();
Tag t = new Tag("a");
set.add(t);
t.rename("b");
System.out.println();
System.out.println("HashSet contains(t) after mutation: " + set.contains(t));
System.out.println(" but iteration still shows it : " + set);
// A mutable VALUE is fine - only the key is hashed.
Map<String, List<String>> safe = new HashMap<>();
safe.computeIfAbsent("k", k -> new ArrayList<>()).add("mutating the value is fine");
System.out.println();
System.out.println("mutable value, immutable key: " + safe);
// If you must mutate a key: remove, mutate, re-insert.
Map<Tag, String> managed = new HashMap<>();
Tag k2 = new Tag("x");
managed.put(k2, "v");
String v = managed.remove(k2);
k2.rename("y");
managed.put(k2, v);
System.out.println("remove-mutate-reinsert works: " + managed.get(k2));
}
}
The bucket is fixed at insertion, so mutating a key strands the entry - reachable by iteration, never by lookup.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Collections course, and every lesson in it is listed on the Collections contents page.