Zum Hauptinhalt springen

Week 6 Day 4 — Fail-fast Iterators

Goal

Today I want fail-fast as a modCount check, not as “the iterator is angry.”

Main questions:

  1. What is a structural modification?
  2. Why does for-each + remove throw?
  3. How do I remove while iterating?
  4. Fail-fast vs fail-safe?
  5. What about concurrent maps?

1. modCount

ArrayList, HashMap, HashSet keep a counter of structural changes: add/remove that change size or bins, not set on a list index.

The iterator snapshots modCount at creation. Each next() checks that the list’s modCount still matches. If another structural change happened, it throws ConcurrentModificationException.

That check is best effort, even on one thread. It is not a lock. It does not detect every race. It exists to fail loudly when I mutate during for-each, instead of skipping elements silently.

Memory sentence:

Fail-fast iterators detect a changed modCount and throw ConcurrentModificationException.


2. The classic crash


for (Order order : orders) {
if (order.isExpired()) {
orders.remove(order); // CME
}
}

Enhanced for-each uses an iterator. orders.remove is a structural change the iterator did not make.

HashMap during iteration:


for (String key : map.keySet()) {
map.put("new", 1); // CME if it is actually new
}

Replacing the value for the current key via entry.setValue is allowed on HashMap. Adding a new key is structural.



// 1. iterator.remove — after next()
Iterator<Order> it = orders.iterator();
while (it.hasNext()) {
if (it.next().isExpired()) {
it.remove();
}
}

// 2. removeIf (does the iterator dance for me)
orders.removeIf(Order::isExpired);

// 3. collect survivors
List<Order> keep = orders.stream()
.filter(o -> !o.isExpired())
.toList();

Iterator.remove() updates modCount in a way the same iterator understands. Another iterator on the same list still fails.

Indexed loop from the end on ArrayList also works for remove-by-index. I still prefer removeIf.


4. Fail-fast vs snapshot (“fail-safe”)

Interviewers say fail-safe for iterators that do not throw CME because they walk a copy or a weakly consistent view.

KindExampleBehavior
Fail-fastArrayList, HashMap, HashSetCME on structural change during iteration
SnapshotCopyOnWriteArrayListIterates the array at construction; later writes copy-on-write
Weakly consistentConcurrentHashMapMay or may not see later puts; does not throw CME

CopyOnWriteArrayList is for many readers, rare writes (listener lists). Every write copies the array. It is not a general ArrayList replacement.

ConcurrentHashMap iterators never throw CME. They also do not freeze a full snapshot. I do not use them as a substitute for a transaction.


5. Spring connection

  • A @Service with List<Listener> mutated from requests while the list is iterated on another thread: CME or missed listener. CopyOnWriteArrayList is the usual listener pattern.
  • Do not iterate a HashMap cache on a singleton without concurrency control.
  • JPA persistent collections: extra-lazy / persistent-bag iteration plus application remove has its own rules. In the service layer I copy to an ArrayList if I need to mutate freely after load.

Unmodifiable lists (List.of) throw UnsupportedOperationException on remove, which is a different exception from CME.


6. Common traps

Trap 1: Catching ConcurrentModificationException as flow control.

Trap 2: “Fail-safe means thread-safe.” Snapshot iterators still need a story for writes.

Trap 3: list.remove(i) inside for (int i = 0; i < list.size(); i++) skipping the next element when incrementing i after a remove.

Trap 4: Two threads, one iterates ArrayList, one adds. CME is possible, not guaranteed. The bug can be silent corruption.

Trap 5: Calling collection.remove(x) during for-each because “there is only one thread.” Fail-fast is about structural change, not about threads.


Practice Questions and Answers

Question 1

Fail-fast vs fail-safe iterators?

Answer:

Fail-fast (ArrayList, HashMap) record modCount and throw ConcurrentModificationException if the collection changes structurally during iteration. Snapshot / weakly consistent iterators (CopyOnWriteArrayList, ConcurrentHashMap) do not throw CME; they walk a copy or a concurrent view. Fail-fast is a bug detector, not a lock.


Question 2

How do I remove elements while iterating an ArrayList?

Answer:

iterator.remove() after next(), or removeIf(predicate). I do not call list.remove inside enhanced for-each.


Question 3

Does CME always fire on a race between two threads?

Answer:

No. It is best effort. Two threads can still corrupt an ArrayList without a CME. I do not share a fail-fast collection across threads without external synchronization or a concurrent collection.


Question 4

When is CopyOnWriteArrayList the right tool?

Answer:

Many iterations, rare writes — typical event listener lists. Each write copies the array, so it is expensive for frequent adds. It is not a faster ArrayList.


Question 5

Why can map.put during keySet iteration throw even on one thread?

Answer:

A new key is a structural change. The iterator’s modCount no longer matches. Replacing the value of an existing key through Map.Entry.setValue is the allowed in-place update.


Memory sentences

Fail-fast means modCount changed; CME is a detector, not a lock.

Remove with iterator.remove or removeIf, not with list.remove inside for-each.

CopyOnWriteArrayList for rare writes and many readers. ConcurrentHashMap for concurrent maps.

Next: Week 6 Day 5 — Comparable vs Comparator