ExerciseChallenge
Challenge
Break the map with a mutable key
20 minintermediate2–8 yrs
Edge cases. You have to reason, and two valid fixes differ.
What this teaches
- A key's hash must not change while it is in the map
- A stale-hash entry is unreachable but still counted by size()
- Immutable keys vs excluding fields from hashCode are NOT equivalent fixes
Starter
Starter.java
import java.util.*;
/**
* CHALLENGE — break the map with a mutable key, then explain it.
*
* Run as-is. An entry you just inserted becomes unreachable, while size()
* still counts it. Nothing throws.
*
* TASKS
* 1. Predict every line of output BEFORE running. Write your prediction down.
* 2. Run it. Explain precisely why the entry is lost — name the bucket.
* 3. Fix it. There are two valid fixes and they are not equivalent:
* (a) make the key immutable
* (b) exclude the mutable field from hashCode()
* Implement (a). Then say in a comment what (b) silently costs you.
* 4. Prove the entry is still physically present even though get() fails.
*/
public class Starter {
/** A key whose hash depends on a field you can change afterwards. */
static class OrderKey {
String region; // mutable — this is the landmine
final int orderId;
OrderKey(String region, int orderId) {
this.region = region;
this.orderId = orderId;
}
@Override
public int hashCode() {
return Objects.hash(region, orderId);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof OrderKey k)) return false;
return orderId == k.orderId && Objects.equals(region, k.region);
}
@Override
public String toString() {
return region + "#" + orderId;
}
}
public static void main(String[] args) {
Map<OrderKey, String> orders = new HashMap<>();
OrderKey key = new OrderKey("APAC", 1001);
orders.put(key, "Widget order");
System.out.println("get before mutation = " + orders.get(key));
System.out.println("size before = " + orders.size());
// somewhere far away, someone "just updates the region"
key.region = "EMEA";
System.out.println("get after mutation = " + orders.get(key));
System.out.println("containsKey = " + orders.containsKey(key));
System.out.println("size after = " + orders.size());
System.out.println("remove returns = " + orders.remove(key));
System.out.println("size after remove = " + orders.size());
// The entry is unreachable by key — but is it gone?
System.out.print("still iterable = ");
for (Map.Entry<OrderKey, String> e : orders.entrySet()) {
System.out.print(e.getKey() + "=" + e.getValue() + " ");
}
System.out.println();
}
}Run it locally:
cd exercises/java/collections/hashmap-internals/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out StarterHints
Hint 1
The entry sits in the bucket chosen by the hash it had at put() time.
Hint 2
get() computes the CURRENT hash and looks in a different bucket.
Hint 3
Iterate the entrySet. Is the entry really gone, or just unreachable?
Done when
- OrderKey is immutable; get() works after any attempted mutation
- A comment explains what excluding region from hashCode() would cost