Java's HashMap Key Mutation Trap

2026-05-01

You're building a simple session cache. Each Session object has a mutable role field, and you store sessions in a HashMap for quick lookup. A colleague reports that sessions are "vanishing" from the map — get() returns null even though the map's size confirms the entry is still there.

import java.util.*;

class Session {
    String userId;
    String role;

    Session(String userId, String role) {
        this.userId = userId;
        this.role = role;
    }

    @Override
    public int hashCode() {
        return Objects.hash(userId, role);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Session s)) return false;
        return Objects.equals(userId, s.userId)
            && Objects.equals(role, s.role);
    }
}

// --- usage ---
Map<Session, String> tokenMap = new HashMap<>();
Session sess = new Session("alice", "viewer");
tokenMap.put(sess, "tok_abc123");

// Alice gets promoted
sess.role = "admin";

// Later, try to fetch her token:
System.out.println(tokenMap.size());           // prints 1
System.out.println(tokenMap.get(sess));        // prints null  (!)
System.out.println(tokenMap.containsKey(sess)); // prints false (!)

The map has one entry. The key object is literally the same reference you used to insert it. Yet the map can't find it. What's going on?

The Bug

When you call put(), the HashMap computes the key's hashCode() and uses it to choose a bucket. That hash value is never recalculated — the entry stays in the original bucket forever.

When you mutate sess.role from "viewer" to "admin", you change the object's hashCode(). Now when you call get(sess), the map computes the new hash, looks in the wrong bucket, and finds nothing. The entry is still sitting in the old bucket, unreachable — a ghost that inflates size() but can never be retrieved, updated, or even removed by key.

This is especially insidious because:

The Fix

The rule is simple: HashMap keys must be effectively immutable with respect to equals() and hashCode(). Either make the key class immutable, or only include stable fields in the hash:

class Session {
    final String userId;  // immutable — safe for hashing
    String role;           // mutable — excluded from hash

    Session(String userId, String role) {
        this.userId = userId;
        this.role = role;
    }

    @Override
    public int hashCode() {
        return Objects.hash(userId);  // only stable fields
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Session s)) return false;
        return Objects.equals(userId, s.userId);
    }
}

Now mutating role doesn't affect the hash, and lookups work correctly after mutation. The better design, when possible, is to use a record or make the entire key class immutable and use the mutable data as the value instead.

This same trap applies to HashSet, Python's dict/set (if you override __hash__ on a mutable object), C#'s Dictionary, and any hash-based collection in any language. The data structure trusts that the hash is stable. Break that contract and you get silent data loss.

Key Takeaway: Never mutate an object while it's being used as a key in a hash-based collection — the entry becomes a ghost: present in memory, invisible to lookups, and impossible to remove by key.

All newsletters