Mutable keys and entries that disappear

Collections · lesson 40 of 42 · 6 min read

Change a key after insertion and the value is unreachable but still occupying memory.

Open this lesson in the learning hub

Key points

  • A HashMap places an entry in a bucket derived from the key hash at insertion time. It is never recomputed.
  • Mutating a field used by hashCode after 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 to TreeMap where mutating a field used by compareTo corrupts 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.