Java SE 8 Programmer II study materials
Java SE 8 Programmer II study resources

Raw Types, Inconsistent compareTo, and a Mutable HashMap Key in a Shared Library

Java SE 8 Programmer II · Put your knowledge to work in context.

All case studies

Raw Types, Inconsistent compareTo, and a Mutable HashMap Key in a Shared Library

A platform library is in review before release. Catalog declares void importAll(List<? extends Number> source) and inside it calls source.add(Integer.valueOf(1)); the build emits unchecked warnings and someone silenced them by passing a raw List from an adapter, which now throws ClassCastException in production at a line that looks unrelated. PriceIndex keeps Sku keys in a HashMap<Sku, BigDecimal> and calls sku.setRegion(...) after insertion; lookups with the original key object return null while size() keeps climbing overnight. RankBoard stores Result objects in a TreeSet; compareTo compares only score, but equals compares score and id, so two results with the same score but different ids collapse into one entry and one contestant is missing from the published ranking. The team also wants to know whether a varargs helper static <T> T[] toArray(T... values) is safe to reuse.

1. Explain the `? extends` compile error and the raw-type `ClassCastException`, then give corrected signatures using PECS.

Read the worked answer

List<? extends Number> means 'a list of some unknown subtype of Number'. The compiler cannot know whether the runtime object is List<Integer>, List<Double> or List<Number>, so adding any Number - including an Integer - could corrupt a List<Double>. add is therefore rejected; that is the compiler protecting you, not a limitation to work around.

The raw-List workaround compiled with an unchecked warning and erased the check. The ClassCastException then surfaces at an unrelated read site such as Double d = list.get(0) - the classic raw-type failure: the error moves far from the bug and the generic contract is silently broken for every caller.

PECS - Producer Extends, Consumer Super:

static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T t : src) dest.add(t);
}
static void importAll(List<? super Integer> dest) { dest.add(1); } // legal

Read from ? extends, write to ? super, use the exact type when both happen, and prefer a method-level type parameter when T appears in the return type. About the varargs helper: T... creates a Object[] at the call site, so assigning the result to String[] throws ClassCastException; mark such helpers @SafeVarargs only when they never store into or expose the array, and prefer List<T> parameters over generic varargs.

2. Why does the TreeSet drop a contestant, and how should the ordering be fixed?

Read the worked answer

TreeSet (like TreeMap) locates elements by comparison, not by equals/hashCode. If compareTo returns 0 for two results with the same score, the set treats the second as a duplicate and keeps the first, so one contestant silently vanishes. contains and remove then behave according to the comparator, which violates the Set contract, since Set is defined in terms of equals.

Fixes:

  • Make ordering consistent with equals: compareTo must return 0 exactly when equals returns true. Tie-break on the identity field: Comparator.comparingInt(Result::score).thenComparing(Result::id).
  • Or use LinkedHashSet if uniqueness must follow equals and ordering is only for presentation - sort when displaying.
  • Or keep the TreeSet deliberately, document that uniqueness is by score, and expose that rule to callers.

Also audit comparator consistency: two comparators with different tie-breaks produce different 'sets' from identical input, which is a common source of flaky tests and of ranking discrepancies between services. If the comparator is supplied externally, validate that it is a total order (antisymmetry and transitivity) before using it in a sorted collection.

3. Diagnose the disappearing HashMap entries and list the rules that prevent a recurrence.

Read the worked answer

HashMap places each entry in the bucket chosen by hashCode() at insertion time, and get recomputes the hash to find it again. Mutating region after insertion changes the hash, so get(originalKey) searches the wrong bucket and returns null. The entry is still in the table, which is why size() grows and the map leaks until the key object is collected.

Rules:

  1. Make map keys immutable - final fields, no setters, defensive copies of mutable inputs in the constructor.
  2. Derive hashCode and equals from the same fields (Objects.hash, Objects.equals) and never include mutable or derived state.
  3. If a key must change, remove the entry before mutating and re-insert afterwards with the new state; or key by an immutable identifier such as String sku instead of the entity.
  4. Apply the same discipline to HashSet and ConcurrentHashMap; in the concurrent map a stale entry can persist indefinitely and even block eviction logic.

Note that this is also a HashMap corruption vector: two distinct keys that mutate into the same hash make the bucket chain behave in ways that defeat equals assumptions, so the symptom is not only 'missing entry' but occasionally a wrong value. Prefer immutable keys such as String, Integer and enums, and add a unit test that mutates a candidate entity after insertion to prove the class is unsuitable as a key.