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?
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:
entrySet() will still show the entry, making debugging confusing — "it's right there!"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.
