Because the iterator captures a reference to the backing array at the moment it's created, and every write produces a brand new array. The iterator's array is never mutated, so there is nothing to detect — and nothing to throw.
How it works
CopyOnWriteArrayList stores its elements in a volatile Object[] array field. The iterator constructor reads that field once and keeps the reference:
// Simplified from JDK source
static final class COWIterator<E> implements ListIterator<E> {
private final Object[] snapshot; // captured at creation
private int cursor;
COWIterator(Object[] elements, int initialCursor) {
this.cursor = initialCursor;
this.snapshot = elements;
}
public boolean hasNext() { return cursor < snapshot.length; }
public E next() { return (E) snapshot[cursor++]; }
}
Writes (add, set, remove) acquire the internal ReentrantLock, build a brand new array with the modification applied, and assign it back to the volatile field. Existing iterators still hold their original snapshot — completely untouched by the new array.
Consequence: iteration is frozen-in-time
List<String> list = new CopyOnWriteArrayList<>(List.of("a", "b", "c"));
Iterator<String> it = list.iterator();
list.add("d"); // backing array is now {a,b,c,d}
list.remove("a"); // backing array is now {b,c,d}
while (it.hasNext()) {
System.out.print(it.next() + " "); // prints: a b c
}
The iterator sees {a, b, c} because that's the array reference it captured. No CME, no surprise.
Side effects
iterator.remove()throwsUnsupportedOperationException. Mutating a frozen snapshot would be meaningless.iterator.set()andadd()on theListIteratoralso throwUOEfor the same reason.- Memory pressure under concurrent iteration + writes. Each long-running iterator pins an old array version, preventing GC. If you iterate slowly while writes flood in, you can accumulate many array generations in memory.
Contrast with fail-fast collections
| Collection | Iterator strategy | Behavior on concurrent modification |
|---|---|---|
ArrayList, HashMap | fail-fast (modCount check) | throws CME on next op |
CopyOnWriteArrayList | snapshot | sees frozen state, never throws |
ConcurrentHashMap, ConcurrentSkipListMap | weakly consistent | reflects some state, never throws |