1Z0-771 study materials
1Z0-771 study resources

Rate-Card Cache: Mutated Keys, a Comparator That Collapses the Map, and a Failing Cleanup Loop

1Z0-771 · Put your knowledge to work in context.

All case studies

Rate-Card Cache: Mutated Keys, a Comparator That Collapses the Map, and a Failing Cleanup Loop

A pricing service caches rate cards in a HashMap<RateKey, RateCard>. RateKey is a plain class with a String region and an int tier, with equals and hashCode generated over both fields and a public setTier method. A nightly promotion job mutates the tier of 6,000 cached keys in place. The next morning, containsKey returns false for keys the team can plainly see in the map when debugging, lookups re-insert duplicate entries, and the map has grown from 6,000 to roughly 48,000 entries over four nights. Separately, a reporting endpoint builds a TreeMap<RateKey, BigDecimal> and throws ClassCastException on the first put. A third endpoint builds a TreeMap with a Comparator that compares only region; a report that should list 6,000 keys shows 40 rows, one per region, and the value kept is always the last one inserted. Finally, a cleanup loop that iterates the map's entrySet and calls map.remove on duplicates throws ConcurrentModificationException on the 12th iteration. The team must fix all of it before the next release.

1. Explain why the `HashMap` lookups fail and the map keeps growing, and state the minimal correct fix.

Read the worked answer

HashMap locates an entry in three steps: it calls hashCode(), spreads it (h ^ (h >>> 16)), masks it with (capacity - 1) to choose a bucket, then walks that bucket comparing hash and equals. setTier changes a field used by hashCode, so after the mutation the key's contract with the map is broken:

  • get/containsKey compute a new hash and probe a different bucket. equals is never consulted, because no candidate with a matching hash is found, so the result is null/false even though the entry is still in the table.
  • put behaves the same way, so the same logical key is inserted again; 6,000 entries growing to ~48,000 over four nights is exactly this pattern of repeated re-insertion.
  • The stranded entries are unreachable by lookup but still strongly referenced, so they are never collected. Iteration still returns them, which is why debugging 'sees' the key.

Fix: make the key immutable - final fields, no setters, change tier by creating a new RateKey and doing map.remove(oldKey); map.put(newKey, card);. Order matters: if you remove after mutating, the remove probes the new bucket and silently does nothing, and you leak twice. A record gives immutability plus consistent equals/hashCode for free. If mutation is unavoidable, remove first, mutate, then re-insert.

2. Diagnose the two `TreeMap` failures and say what a correct comparator must guarantee.

Read the worked answer

These are two different defects that happen to share a class.

ClassCastException: a TreeMap with no comparator uses natural ordering, i.e. Comparable.compareTo. RateKey does not implement Comparable, so the very first put throws ClassCastException - nothing is ever inserted. (A HashMap would not have complained, which is why the code 'worked' before.)

40 rows instead of 6,000: once a Comparator is supplied, the map's notion of key identity is compare(a, b) == 0, not equals. The region-only comparator returns 0 for every key in a region, so each put is treated as an update of the existing entry: the value is replaced (last write wins) and size() stays at the number of distinct regions. SortedMap documents that its ordering must be consistent with equals precisely to avoid this; with an inconsistent comparator the map violates the Map contract and get can also return another key's value.

Correct comparator: Comparator.comparing(RateKey::getRegion).thenComparingInt(RateKey::getTier). The tie-break chain must include every field that equals distinguishes, or keys collide. The same rule applies if you implement Comparable, and note that HashSet/HashMap do not have this failure mode because they use equals.

3. Explain the `ConcurrentModificationException` and give the safe ways to remove entries while iterating.

Read the worked answer

HashMap's iterators are fail-fast: they snapshot modCount at creation and compare it to the map's count on each next(). map.remove(k) is a structural modification of the very map being iterated, so the next next() or remove() on the iterator detects the mismatch and throws. The failure is at the 12th iteration, not the first, because it only triggers after a removal has actually occurred.

Safe options, in order of preference:

  1. map.entrySet().removeIf(e -> isDuplicate(e.getKey())); - concise and implemented on the iterator.
  2. Iterator<Entry<RateKey, RateCard>> it = map.entrySet().iterator(); while (it.hasNext()) { if (isDuplicate(it.next().getKey())) it.remove(); } - Iterator.remove updates both modCount and expectedModCount.
  3. Collect keys into a separate List during iteration, then map.keySet().removeAll(keys) afterwards.

Unsafe: map.remove or map.put inside a for-each, and mutating the same map inside a computeIfAbsent/compute mapping function (also a ConcurrentModificationException or worse). Note that ConcurrentHashMap iterators are weakly consistent and will not throw, but the same loop is still logically racy; and Collection.removeIf on map.values() follows the same rule as the entrySet version.