Iterating maps, fail-safe iterators and where they are used, modifying collections inside enhanced for-loops, Iterator.remove vs List.remove, concurrent modification of a HashSet, why most iterators are fail-fast, CopyOnWriteArrayList iteration and write costs, non-comparable elements in sorted collections, Comparable plus Comparator together, what returning 0 means, compareTo consistent with equals, multi-field sorting, stateful comparators, natural vs custom ordering, sorting maps by key or value, and what happens with inconsistent comparators.
Published September 25, 2026
Iterator and comparator bugs are silent correctness bugs: lost elements in a TreeSet, IllegalArgumentException: Comparison method violates its general contract! in production sorts, or ConcurrentModificationException in single-threaded code. Explain the contract each time, and how to satisfy it.
Iterator to iterate over a Map? How?Short answer: A Map isn't Iterable itself. Iterate one of its views:
entrySet(): the best choice when you need both key and value (one lookup per entry);keySet();values().Their iterators support remove(), which removes the mapping from the map.
for (Iterator<Map.Entry<String, Integer>> it = stock.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, Integer> e = it.next();
if (e.getValue() == 0) it.remove(); // safe removal during iteration
}
stock.entrySet().removeIf(e -> e.getValue() == 0); // the same thing, idiomatically
stock.forEach((sku, qty) -> log.info("{}={}", sku, qty));
Common trap: iterating keySet(), then calling map.get(key) for each key. That's a second lookup per entry, and on a TreeMap it's O(log n) each time.
Short answer: "Fail-safe" (not an official JDK term) describes iterators that never throw ConcurrentModificationException when the collection changes during iteration. There are two kinds:
CopyOnWriteArrayList and CopyOnWriteArraySet. They never see later changes, and their remove() is unsupported.ConcurrentHashMap (and its views), ConcurrentSkipListMap/Set, ConcurrentLinkedQueue/Deque, and most BlockingQueue implementations.Short answer: The enhanced for uses the collection's iterator behind the scenes. Calling list.remove(x) or list.add(x) changes the collection's modCount, so the next iterator.next() throws ConcurrentModificationException. Surprisingly, removing the second-to-last element doesn't throw: hasNext() returns false because the size shrank, so the loop silently skips the last element. That's a nasty inconsistency.
Fixes:
removeIf(predicate);Iterator with it.remove();ListIterator for add or set;Modifying the fields of the elements (not the structure) is fine, unless they're hash keys.
Iterator.remove() and List.remove() during iteration?Short answer:
Iterator.remove() removes the element last returned by next(), and updates the iterator's expected modCount. It's the sanctioned way to remove while iterating. It can only be called once per next(), otherwise it throws IllegalStateException.List.remove(index)/remove(Object) change the list behind the iterator's back, and trigger ConcurrentModificationException on the next next().Watch out for the overload trap on List<Integer>: remove(1) removes index 1, while remove(Integer.valueOf(1)) removes the value 1.
HashSet is modified concurrently during iteration?Short answer:
ConcurrentModificationException on the next next().NullPointerException during a resize;Never rely on CME for thread safety. Use ConcurrentHashMap.newKeySet(), CopyOnWriteArraySet, or external synchronisation covering the whole iteration.
Short answer: It's a design trade-off:
modCount integer check per next(). It surfaces bugs early, instead of iterating over a half-modified structure, and producing wrong results later.General-purpose collections optimise for single-threaded speed, and use fail-fast checks as a debugging aid. Concurrent collections pay the extra cost where it's needed.
CopyOnWriteArrayList iteration differ from ArrayList iteration?Short answer:
CopyOnWriteArrayList: every write (add, set, remove) copies the entire internal array, under a lock, and publishes the new array through a volatile reference. Iterators work over the array that existed when they were created. So they never throw CME, see a consistent snapshot, don't see later updates, and don't support iterator.remove().ArrayList: a fail-fast iterator over the live array.CopyOnWriteArrayList?Short answer: Each modification is O(n) in time and allocation (a full array copy), and writes are serialised by a lock. So:
addAll) to copy once.For write-heavy concurrent lists, use a ConcurrentLinkedQueue/Deque, or a lock around an ArrayList, or rethink the data structure.
TreeSet or TreeMap?Short answer: With natural ordering, the TreeMap casts the key to Comparable. An element that doesn't implement Comparable throws ClassCastException. Even the first insertion checks this (it compares the key with itself), since Java 7. The fix is to supply a Comparator at construction. The same exception appears for mixed incomparable types (a String and an Integer).
Comparable and still be sorted with a Comparator?Short answer: Yes, and it's common. Comparable defines the natural ordering (for example Employee by ID), and Comparators provide alternative orderings for specific uses (by salary, by name). When a comparator is supplied, it takes precedence. You can also reuse the natural ordering inside comparators: Comparator.naturalOrder(), or .thenComparing(Comparator.naturalOrder()).
compareTo() or compare() mean?Short answer: It means the two objects are equal in ordering. The consequences:
TreeSet/TreeMap: they're treated as the same element or key. The second isn't added (a set), or it overwrites the value (a map). This is the source of the classic bug: a TreeSet<Employee> compared only by salary silently drops employees with equal salaries.Fix: add tie-breakers (thenComparing(Employee::id)), so distinct objects never compare as 0.
compareTo but not by equals()?Short answer: Yes. The ordering is then inconsistent with equals. The classic example is BigDecimal: new BigDecimal("2.0").compareTo(new BigDecimal("2.00")) == 0, but equals is false (it compares the scale). The consequences:
new HashSet<>(List.of(a, b)) has 2 elements;new TreeSet<>(List.of(a, b)) has 1.Sorted collections follow compareTo, and hash collections follow equals. The Comparable docs strongly recommend consistency, and require you to document it if you break it.
compareTo() contract linked to equals and hashCode?Short answer:
compareTo contract:
sgn(a.compareTo(b)) == -sgn(b.compareTo(a));a equals b in ordering, then a and c compare the same way as b and c.(a.compareTo(b) == 0) == a.equals(b). If you honour that, and equals is consistent with hashCode, then all three agree, and the object behaves identically in HashSet, TreeSet, HashMap and TreeMap.Key points to cover:
equals/hashCode. Make the comparator match the record's components (or use a clearly documented alternative ordering).List<Employee> by several fields?Short answer: Compose comparators with Comparator.comparing(...).thenComparing(...), and control the direction and null handling explicitly:
employees.sort(
Comparator.comparing(Employee::department)
.thenComparing(Employee::salary, Comparator.reverseOrder()) // highest paid first within a department
.thenComparing(Employee::lastName, Comparator.nullsLast(String.CASE_INSENSITIVE_ORDER))
.thenComparingLong(Employee::id)); // deterministic tie-breaker
Key points to cover:
comparingInt/comparingLong/comparingDouble for primitive keys, to avoid boxing.a.age - b.age): it overflows with extreme values, and breaks the contract..sorted(comparator).Learn it in depth → Collectors and groupingBy
Comparator be stateful, or use lambdas? What's the caveat?Short answer: Comparators are usually lambdas or method-reference compositions, and should be stateless and pure: the same inputs always give the same result.
IllegalArgumentException: Comparison method violates its general contract!, or produce a wrong order.Collator for a locale) is fine.TreeMap.Collator isn't thread-safe; clone it per thread).Short answer:
Comparable (numbers ascending, strings lexicographic, dates chronological). It's used implicitly by Collections.sort(list), TreeSet and Arrays.sort(Object[]).Comparator. You can have many (by name, by price descending, locale-aware), including orderings for classes you can't modify.Pick natural ordering only when there's one obvious ordering, consistent with equals.
Short answer: Maps aren't sorted in place, so you build a sorted view or copy:
// By key: a TreeMap with a comparator
SortedMap<String, Integer> byKey = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
byKey.putAll(scores);
// By value (descending), preserving that order in a LinkedHashMap
Map<String, Integer> byValue = scores.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed()
.thenComparing(Map.Entry.comparingByKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(a, b) -> a, LinkedHashMap::new));
Common trap: trying to build a TreeMap ordered by value, with a comparator that looks up values. It breaks as soon as values change, or two keys share a value (compare = 0 means a lost key).
Short answer: The sort's assumptions are violated:
TimSort (used by List.sort, Arrays.sort for objects, and Collections.sort) may detect it, and throw IllegalArgumentException: Comparison method violates its general contract!, typically only for larger inputs, intermittently, depending on the data;TreeMap/TreeSet, you get lost elements, and failed lookups.The common causes are:
NaN (use Double.compare);Test comparators with property-based tests (antisymmetry and transitivity over random samples).
Q: What does a ListIterator add over an Iterator?
A: Bidirectional traversal (hasPrevious/previous), index queries (nextIndex/previousIndex), and set(e) and add(e) at the current position, all without triggering ConcurrentModificationException.
Q: Is Arrays.sort(int[]) stable? And for objects?
A: Primitive arrays use a dual-pivot quicksort, which isn't stable, but stability is meaningless for identical primitives. Object sorts (TimSort) are stable, which is why multi-pass sorting works, and why thenComparing is often cleaner.
Q: How do you get a reversed view of an ordered collection since Java 21?
A: list.reversed(), deque.reversed(), linkedHashMap.reversed(), treeSet.reversed(). These are sequenced-collection views, with no copying.
Q: Why might Collections.sort on a LinkedList still be fast?
A: List.sort copies the elements into an array, sorts the array with TimSort, and writes them back through a ListIterator. So it's O(n log n), and doesn't sort linked nodes in place.