You have almost certainly met ConcurrentModificationException already: you removed an element inside an enhanced for loop, the program blew up, and you learned to reach for Iterator.remove() or removeIf instead. That is the right fix and it is where a beginner course stops. This article is about the machinery underneath it.
The machinery is worth knowing because the exception is not the interesting part. The interesting part is that the check which produces it is documented as best-effort, and there is a small, entirely deterministic, single-threaded case where a broken loop silently produces a wrong answer and throws nothing at all. That case is demonstrated below with real output.
![]()
Every output line, stack trace, counter value and disassembly below was produced by compiling and running the code on OpenJDK 21.0.6 (arm64). No threads were started anywhere in this article; the section on concurrent collections says explicitly which claims come from the documented contract rather than from an experiment.
What the Iterator contract actually is
java.util.Iterator is four methods, of which everyday code calls two:
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() { throw new UnsupportedOperationException("remove"); }
default void forEachRemaining(Consumer<? super E> action) { ... }
}
remove and forEachRemaining became default methods in Java 8, which is why a hand-written iterator that implements only hasNext and next still compiles.
The implementation is two int fields. Here is java.util.ArrayList$Itr from the JDK 21 sources, with the comments the JDK authors wrote:
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
...
}
cursor is the index of the next element to return, which means the cursor does not sit on an element — it sits in the gap before one. Three elements therefore have four legal cursor positions, 0 through 3, and hasNext() is nothing more than cursor != size.
lastRet is the other half of the contract. It records the index that next() last returned, and it is what remove() deletes. It starts at -1, and remove() resets it to -1 after each successful deletion.

The three exceptions the contract defines
Those two fields generate all three failures the contract can produce. Calling remove() before any next() leaves lastRet at -1:
import java.util.*;
public class Traces {
public static void main(String[] args) {
List<String> l = new ArrayList<>(List.of("ada", "linus"));
Iterator<String> it = l.iterator();
it.remove();
}
}
Exception in thread "main" java.lang.IllegalStateException
at java.base/java.util.ArrayList$Itr.remove(ArrayList.java:1062)
at Traces.main(Traces.java:6)
Calling remove() twice in a row fails identically, and for exactly the same reason — the first call put lastRet back to -1:
Iterator<String> it = l.iterator();
it.next();
it.remove();
it.remove();
Exception in thread "main" java.lang.IllegalStateException
at java.base/java.util.ArrayList$Itr.remove(ArrayList.java:1062)
at Traces3.main(Traces3.java:8)
Note that IllegalStateException carries no message. There is nothing to read in a log except the frame it came from, so recognise the shape.
The third failure is calling next() when cursor == size:
List<String> l = new ArrayList<>(List.of("ada"));
Iterator<String> it = l.iterator();
it.next();
it.next();
Exception in thread "main" java.util.NoSuchElementException
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1052)
at Traces2.main(Traces2.java:7)
hasNext() is the only thing standing between a loop and that exception. It is a query, not a guard: nothing forces you to call it, and next() does not call it for you.
One more detail from the source, because it matters later. remove() does this:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
Two lines are doing quiet, essential work. cursor = lastRet rewinds the cursor to the hole the deletion left, which is why iterator removal never skips the following element. And expectedModCount = modCount re-synchronises the iterator with the list, which is the entire reason this removal is legal and a direct list.remove(...) is not.
The enhanced for loop is an Iterator, and the bytecode proves it
The enhanced for loop over a Collection is not a language feature with its own runtime support. The compiler rewrites it into iterator calls, and javap -c shows the rewrite exactly:
import java.util.List;
public class ForEach {
static int sum(List<Integer> xs) {
int total = 0;
for (int x : xs) {
total += x;
}
return total;
}
}
javac ForEach.java && javap -c -p ForEach.class
static int sum(java.util.List<java.lang.Integer>);
Code:
0: iconst_0
1: istore_1
2: aload_0
3: invokeinterface #7, 1 // InterfaceMethod java/util/List.iterator:()Ljava/util/Iterator;
8: astore_2
9: aload_2
10: invokeinterface #13, 1 // InterfaceMethod java/util/Iterator.hasNext:()Z
15: ifeq 38
18: aload_2
19: invokeinterface #19, 1 // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
24: checkcast #23 // class java/lang/Integer
27: invokevirtual #25 // Method java/lang/Integer.intValue:()I
30: istore_3
31: iload_1
32: iload_3
33: iadd
34: istore_1
35: goto 9
38: iload_1
39: ireturn
(The default constructor is omitted; everything else is verbatim.)
Read the four highlighted instructions in order. List.iterator() is called once and the result stored in slot 2. Iterator.hasNext() runs at the top of every pass and ifeq jumps out when it returns false. Iterator.next() produces an Object, checkcast casts it to Integer — that is type erasure showing through — and goto 9 closes the loop.
Two consequences fall straight out of that listing:
- The iterator lives in local slot 2 and your source code has no name for it. That is the whole reason you cannot call
remove()safely from inside an enhancedforloop: the object that would let you do it exists, but the language hides it. Writing thefor (Iterator<String> it = list.iterator(); it.hasNext(); )form by hand is not a stylistic downgrade — it is the only way to get a reference to that object. hasNext()is called before everynext(), andnext()is where the comodification check lives. Hold on to that; the next section turns on it.
Iterating an array compiles to something else entirely
The same syntax over an array produces no iterator at all:
static int sum(int[] xs) {
int total = 0;
for (int x : xs) total += x;
return total;
}
3: astore_2
4: aload_2
5: arraylength
6: istore_3
7: iconst_0
8: istore 4
10: iload 4
12: iload_3
13: if_icmpge 33
16: aload_2
17: iload 4
19: iaload
20: istore 5
...
27: iinc 4, 1
30: goto 10
arraylength, an index in slot 4, iaload, iinc. It is an ordinary counted loop. An array cannot be structurally modified, so there is nothing here to detect and no ConcurrentModificationException is possible — which is also why the same syntax over a List and over an array behave so differently when something changes underneath.
ListIterator: the bidirectional, positional iterator only List provides
Iterator is the lowest common denominator: forwards only, no positions, and remove as the single mutation. List offers something larger through listIterator(), and it is the only interface in the collections framework that does — Set and the Map views offer iterator() and no positional variant, because they have no index to talk about.
| Capability | Iterator | ListIterator |
|---|---|---|
| Move forward | hasNext(), next() | hasNext(), next() |
| Move backward | not available | hasPrevious(), previous() |
| Ask where the cursor is | not available | nextIndex(), previousIndex() |
| Delete the last element returned | remove() | remove() |
| Replace the last element returned | not available | set(E) |
| Insert at the cursor | not available | add(E) |
| Available on | any Iterable | List only |
nextIndex() and previousIndex() are just cursor and cursor - 1 exposed as methods, which makes the between-elements model visible from outside:
List<String> words = new ArrayList<>(List.of("ada", "linus", "grace"));
ListIterator<String> it = words.listIterator();
System.out.println("start nextIndex=" + it.nextIndex() + " previousIndex=" + it.previousIndex());
System.out.println("next() -> " + it.next() + " nextIndex=" + it.nextIndex() + " previousIndex=" + it.previousIndex());
System.out.println("next() -> " + it.next() + " nextIndex=" + it.nextIndex() + " previousIndex=" + it.previousIndex());
System.out.println("previous() -> " + it.previous() + " nextIndex=" + it.nextIndex() + " previousIndex=" + it.previousIndex());
start nextIndex=0 previousIndex=-1
next() -> ada nextIndex=1 previousIndex=0
next() -> linus nextIndex=2 previousIndex=1
previous() -> linus nextIndex=1 previousIndex=0
Look at the last line. previous() returned linus — the same element next() had just returned. The cursor moved back over it rather than past it, because a cursor sitting in a gap can be crossed in either direction. Alternating next() and previous() returns the same element forever, and that is correct behaviour, not a bug.
To walk a list backwards, start the cursor at the far end:
List<String> back = new ArrayList<>(List.of("a", "b", "c"));
ListIterator<String> r = back.listIterator(back.size());
while (r.hasPrevious()) System.out.print(r.previous() + " ");
c b a
set replaces what next() last returned
set is the method that has no Iterator equivalent and the one people most often need. It overwrites the element at lastRet without touching the size, so it is not a structural modification and it does not disturb any other cursor state:
List<String> up = new ArrayList<>(List.of("ada", "linus", "grace"));
for (ListIterator<String> j = up.listIterator(); j.hasNext(); ) {
String s = j.next();
if (s.length() > 3) j.set(s.toUpperCase());
}
System.out.println(up);
[ada, LINUS, GRACE]
The alternative is an indexed loop with list.set(i, ...), which works on an ArrayList and is quadratic on a LinkedList because every set(i, ...) walks the chain again. ListIterator.set is positional for free: it already knows where it is.
set has the same precondition as remove — it needs lastRet >= 0 — so calling it before the first next() throws IllegalStateException.
add inserts before the cursor, and the cursor moves past it
add is the subtle one. The Javadoc says the element is inserted immediately before the implicit cursor, and the JDK source shows what that costs the cursor:
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
Three effects, all verifiable. The cursor advances past the new element, so the next next() skips it and the loop cannot insert its way into an infinite loop. lastRet goes back to -1, so set and remove immediately after an add are illegal. And expectedModCount is resynchronised, so the insertion does not invalidate the iterator that performed it.
List<String> ins = new ArrayList<>(List.of("a", "b", "c"));
ListIterator<String> k = ins.listIterator();
while (k.hasNext()) {
String s = k.next();
if (s.equals("b")) {
k.add("B2");
System.out.println("after add(\"B2\") nextIndex=" + k.nextIndex()
+ " previousIndex=" + k.previousIndex() + " list=" + ins);
}
}
System.out.println("after the loop " + ins);
after add("B2") nextIndex=3 previousIndex=2 list=[a, b, B2, c]
after the loop [a, b, B2, c]
nextIndex went from 2 to 3 across the insertion: the cursor stayed on the far side of B2, the loop went on to c, and B2 was never returned by next(). If you actually want to see what you just inserted, previous() returns it — and the third state change proves itself too:
List<String> l = new ArrayList<>(List.of("a", "b"));
ListIterator<String> it = l.listIterator();
it.next();
it.add("x");
try { it.set("y"); } catch (IllegalStateException e) {
System.out.println("set() after add() -> " + e.getClass().getName());
}
try { it.remove(); } catch (IllegalStateException e) {
System.out.println("remove() after add() -> " + e.getClass().getName());
}
System.out.println("list = " + l);
System.out.println("previous() -> " + it.previous());
set() after add() -> java.lang.IllegalStateException
remove() after add() -> java.lang.IllegalStateException
list = [a, x, b]
previous() -> x
How fail-fast actually works: modCount and expectedModCount
The mechanism is two integers, and both can be read out of a running JVM.
AbstractList declares the first one. The Javadoc in the JDK source defines it precisely: "The number of times this list has been structurally modified. Structural modifications are those that change the size of the list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results."
Every iterator copies that value into its own expectedModCount at construction, and checkForComodification compares them:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
Reflection makes the two counters concrete. modCount is protected on AbstractList and expectedModCount is package-private on ArrayList$Itr, so reading them needs the module opened on the command line:
import java.lang.reflect.Field;
import java.util.*;
public class ModCount {
static Field mod, expected;
static String state(List<?> list, Iterator<?> it) throws Exception {
return String.format("modCount=%d expectedModCount=%d", mod.getInt(list), expected.getInt(it));
}
public static void main(String[] args) throws Exception {
mod = java.util.AbstractList.class.getDeclaredField("modCount");
mod.setAccessible(true);
List<String> list = new ArrayList<>(List.of("ada", "linus", "grace"));
Iterator<String> it = list.iterator();
expected = it.getClass().getDeclaredField("expectedModCount");
expected.setAccessible(true);
System.out.println("iterator class " + it.getClass().getName());
System.out.println("after iterator() " + state(list, it));
System.out.println("after next() -> " + it.next() + " " + state(list, it));
list.add("ken");
System.out.println("after list.add(\"ken\") " + state(list, it));
try {
it.next();
} catch (ConcurrentModificationException e) {
System.out.println("next() threw " + e.getClass().getName());
}
}
}
javac ModCount.java && java --add-opens java.base/java.util=ALL-UNNAMED ModCount
iterator class java.util.ArrayList$Itr
after iterator() modCount=0 expectedModCount=0
after next() -> ada modCount=0 expectedModCount=0
after list.add("ken") modCount=1 expectedModCount=0
next() threw java.util.ConcurrentModificationException
The same program run with it.remove() in place of list.add(...) keeps the two in step, because iterator removal writes expectedModCount = modCount on the way out:
after iterator() modCount=0 expectedModCount=0
after next() -> ada modCount=0 expectedModCount=0
after it.remove() modCount=1 expectedModCount=1
after next() -> linus modCount=1 expectedModCount=1
list = [linus, grace]
That is the whole mechanism. There is no thread, no lock, no monitor. It is one counter compared against a snapshot of that counter.
What counts as a structural modification
The definition says "changes the size, or otherwise perturbs it" — and that second clause is not decoration. Probing each operation with the same reflection shows which ones bump modCount:
Operation on an ArrayList | modCount | Structural |
|---|---|---|
list.get(0) | 0 to 0 | no |
list.set(0, "z") | 0 to 0 | no |
list.add("z") | 0 to 1 | yes |
list.remove("a") | 0 to 1 | yes |
list.addAll(List.of("y")) | 0 to 1 | yes |
list.clear() | 0 to 1 | yes |
list.removeIf(s -> false) | 0 to 0 | no |
list.removeIf(s -> true) | 0 to 1 | yes |
list.sort(null) | 0 to 1 | yes |
list.replaceAll(String::trim) | 0 to 1 | yes |
Two rows deserve attention. sort and replaceAll change no sizes at all and are still structural, because reordering a list under a live cursor would make the traversal return the wrong elements — exactly the "perturb" case in the Javadoc. Sorting a list you are iterating throws:
List<String> l = new ArrayList<>(List.of("b", "a", "c"));
Iterator<String> it = l.iterator();
System.out.println("next() -> " + it.next());
l.sort(null);
System.out.println("size unchanged: " + l.size() + ", list = " + l);
it.next();
next() -> b
size unchanged: 3, list = [a, b, c]
Exception in thread "main" java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1095)
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1049)
at SortDuring.main(SortDuring.java:9)
And removeIf(s -> false) does not bump the counter, because ArrayList.removeIf only touches modCount when it actually deletes something. A removeIf that matches nothing leaves every live iterator valid.
Fail-fast is best-effort, not a guarantee
Here is the paragraph in the ArrayList Javadoc that most tutorials never quote:
Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw
ConcurrentModificationExceptionon a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.
That is usually read as a hedge about threads. It is not only about threads. The check lives inside next() and inside no other method — in particular, not inside hasNext(), which is return cursor != size; and nothing else. So a structural modification that makes hasNext() return false ends the loop before next() ever gets the chance to complain.
Removing the second-to-last element of a list inside an enhanced for loop does exactly that:
import java.util.*;
public class Quiet {
public static void main(String[] args) {
List<String> a = new ArrayList<>(List.of("a", "b", "c", "d"));
int seen = 0;
for (String s : a) {
seen++;
System.out.println("visited " + s);
if (s.equals("c")) a.remove(s);
}
System.out.println("no exception, list = " + a + ", visited " + seen + " of 4");
}
}
visited a
visited b
visited c
no exception, list = [a, b, d], visited 3 of 4
Three elements visited out of four. d was never looked at, the loop exited normally, and nothing was thrown. Walk the counters: after next() returned c the cursor was 3 and the size was 4; a.remove("c") dropped the size to 3 and raised modCount to 1; the loop then asked hasNext(), which computed 3 != 3 and returned false. The comparison that would have caught the mismatch was never reached.

In that example the surviving list happens to be correct. Change the data by one element and it is not:
List<String> tags = new ArrayList<>(List.of("keep", "keep", "drop", "drop"));
for (String t : tags) {
if (t.equals("drop")) tags.remove(t);
}
System.out.println("for-each remove -> " + tags);
List<String> ok = new ArrayList<>(List.of("keep", "keep", "drop", "drop"));
ok.removeIf(t -> t.equals("drop"));
System.out.println("removeIf -> " + ok);
for-each remove -> [keep, keep, drop]
removeIf -> [keep, keep]
A filter that was meant to delete every drop left one behind, silently, with no exception and no warning. That is what "best-effort" costs in practice, and it is the single most important thing in this article: ConcurrentModificationException is a bug detector that sometimes misses. LinkedList behaves identically — its ListItr.hasNext() is nextIndex < size, so the same removal produces [a, b, d] there too.
⚠️ Never treat a clean run as proof that a loop does not modify what it iterates. Fail-fast is a smoke alarm, not a type system.
The same hole exists in HashMap
HashMap's iterator has a different hasNext() — it is next != null, walking the bucket table rather than counting an index — but the structure of the hole is the same. Removing the mapping that is last in iteration order ends the walk without a complaint:
Map<String, Integer> m = new LinkedHashMap<>();
m.put("a", 1); m.put("b", 2); m.put("c", 3);
List<String> order = new ArrayList<>(m.keySet());
System.out.println("iteration order " + order);
for (String k : m.keySet()) {
if (k.equals(order.get(order.size() - 1))) m.remove(k);
}
System.out.println("removing the last key in iteration order -> no exception, map = " + m);
iteration order [a, b, c]
removing the last key in iteration order -> no exception, map = {a=1, b=2}
Removing any earlier key throws, as expected. LinkedHashMap is used here only so the iteration order is fixed and the demonstration is reproducible; a plain HashMap has the same hole at whatever entry happens to come last.
Note also what does not throw: overwriting the value of a key that already exists is not a structural modification, because the table's shape is unchanged.
Map<String, Integer> hm2 = new HashMap<>();
hm2.put("a", 1); hm2.put("b", 2);
Iterator<String> h2 = hm2.keySet().iterator();
h2.next();
hm2.put("a", 7);
System.out.println("value-only put -> next() = " + h2.next());
value-only put -> next() = b
Fail-safe iterators, and what the JDK actually calls them
"Fail-safe iterator" is interview vocabulary, not JDK vocabulary. The string fail-safe does not appear anywhere in the java.util or java.util.concurrent sources of JDK 21. What the Javadoc actually distinguishes is two documented behaviours: a snapshot iterator and a weakly consistent one. They are not the same thing, and the difference is visible in a single-threaded program.

The experiment below is the same for all three: build a three-element collection, call next() once, add "d", then finish the walk.
CopyOnWriteArrayList iterates a snapshot
The CopyOnWriteArrayList Javadoc is unusually direct about it: "The 'snapshot' style iterator method uses a reference to the state of the array at the point that the iterator was created. This array never changes during the lifetime of the iterator, so interference is impossible and the iterator is guaranteed not to throw ConcurrentModificationException. The iterator will not reflect additions, removals, or changes to the list since the iterator was created."
CopyOnWriteArrayList<String> cw = new CopyOnWriteArrayList<>(List.of("a", "b", "c"));
Iterator<String> i2 = cw.iterator();
System.out.println(" next() -> " + i2.next());
cw.add("d");
while (i2.hasNext()) System.out.println(" next() -> " + i2.next());
System.out.println(" list = " + cw);
next() -> a
next() -> b
next() -> c
list = [a, b, c, d]
The list ends up with four elements and the iterator returned three. The write was not missed, delayed or lost — it went to a different array, and the iterator is still holding the old one. Notice that "never throws" is not the same as "sees the truth": this iterator is reliably, permanently out of date the moment anyone writes.
Because the array it holds is a frozen copy that the list no longer owns, the mutating methods cannot work at all:
Iterator<String> it2 = cw.iterator();
it2.next();
it2.remove();
Exception in thread "main" java.lang.UnsupportedOperationException
at java.base/java.util.concurrent.CopyOnWriteArrayList$COWIterator.remove(CopyOnWriteArrayList.java:1208)
at CowRemove.main(CowRemove.java:8)
remove, set and add on a COWIterator all throw it, with a null message. That is a real API restriction, not a detail: code written against Iterator.remove() will not survive being handed a CopyOnWriteArrayList.
What the snapshot costs, counted in copies
The name says what the cost is. Every mutating call allocates a whole new backing array and copies the old contents into it. Reflecting on the private array field and watching its identity change counts the copies exactly (this program needs --add-opens java.base/java.util.concurrent=ALL-UNNAMED alongside the java.base/java.util one):
Field arr = CopyOnWriteArrayList.class.getDeclaredField("array");
arr.setAccessible(true);
CopyOnWriteArrayList<Integer> cow = new CopyOnWriteArrayList<>();
Object previous = arr.get(cow);
int newArrays = 0;
long elementsCopied = 0;
for (int i = 0; i < 1000; i++) {
int before = ((Object[]) arr.get(cow)).length;
cow.add(i);
Object now = arr.get(cow);
if (now != previous) { newArrays++; elementsCopied += before; previous = now; }
}
CopyOnWriteArrayList: 1000 add() calls
new backing arrays allocated 1000
element slots copied 499500
ArrayList: 1000 add() calls
new backing arrays allocated 13
element slots copied 2456
One thousand appends, one thousand array allocations, and 499,500 element slots copied — the sum 0 + 1 + ... + 999, which is quadratic in the number of writes. ArrayList allocated 13 arrays over the same thousand appends because it grows geometrically and amortises. The numbers here are allocation counts, not timings; they are exact and reproducible, and they are the reason CopyOnWriteArrayList is documented for collections that are read constantly and written rarely, such as a listener list.
ConcurrentHashMap is weakly consistent, not snapshot-based
A ConcurrentHashMap iterator does not copy anything. It walks the live table, and the guarantee it offers is the one defined in the java.util.concurrent package documentation:
- they may proceed concurrently with other operations
- they will never throw
ConcurrentModificationException- they are guaranteed to traverse elements as they existed upon construction exactly once, and may (but are not guaranteed to) reflect any modifications subsequent to construction.
The third bullet is the one that matters, and "may but are not guaranteed to" means the behaviour is genuinely unspecified. Running the same experiment:
ConcurrentHashMap<String, Integer> ch = new ConcurrentHashMap<>();
ch.put("a", 1); ch.put("b", 2); ch.put("c", 3);
Iterator<Map.Entry<String, Integer>> i3 = ch.entrySet().iterator();
System.out.println(" next() -> " + i3.next());
ch.put("d", 4);
while (i3.hasNext()) System.out.println(" next() -> " + i3.next());
next() -> a=1
next() -> b=2
next() -> c=3
next() -> d=4
d was added after the iterator was created and the iterator returned it anyway. That is the opposite of the CopyOnWriteArrayList result from the same steps, and both are correct — they implement different contracts. Repeating this run gave the same output every time, but repeatability here is a property of one small map on one JVM, not a guarantee; a different table size, a different key, or a real second thread could each change it, and the specification permits all of those outcomes.
Two more facts, both from the contract rather than from a race. Iterator.remove() works on ConcurrentHashMap views and removes from the map, unlike CopyOnWriteArrayList. And none of this makes a compound operation atomic: if (!map.containsKey(k)) map.put(k, v) is still two operations, which is what putIfAbsent and compute exist for.
Nothing in this section was race-tested. A race is not reproducible on demand, so claiming a concurrency result from a single run would be dishonest. Every statement above about concurrent behaviour comes from the documented contract; the outputs shown are single-threaded programs that mutate a collection between calls on their own iterator, which is enough to expose the difference in semantics.
ArrayList | CopyOnWriteArrayList | ConcurrentHashMap | |
|---|---|---|---|
| Common name | fail-fast | fail-safe | fail-safe |
| JDK term | fail-fast | snapshot | weakly consistent |
| Reads | the live array | a copy frozen at iterator() | the live table |
Throws ConcurrentModificationException | best-effort | never | never |
Sees writes made after iterator() | it throws instead | never | may or may not |
Iterator.remove() | supported | UnsupportedOperationException | supported |
| Cost of a write | amortised | a full array copy each time | localised to one bin |
Writing your own Iterable
The contract is small enough that implementing it is unremarkable. A class that implements Iterable works in an enhanced for loop, because that is all the compiler asked for in the bytecode above:
import java.util.*;
class Countdown implements Iterable<Integer> {
private final int from;
Countdown(int from) { this.from = from; }
@Override
public Iterator<Integer> iterator() {
return new Iterator<>() {
private int n = from;
@Override
public boolean hasNext() { return n > 0; }
@Override
public Integer next() {
if (!hasNext()) throw new NoSuchElementException("countdown finished");
return n--;
}
};
}
}
for (int n : new Countdown(4)) System.out.print(n + " ");
System.out.println();
new Countdown(3).forEach(n -> System.out.print("forEach " + n + " "));
4 3 2 1
forEach 3 forEach 2 forEach 1
Two methods, no remove, no counters, and it works with both for and Iterable.forEach. Three things are worth doing deliberately:
- Throw
NoSuchElementExceptionfromnext()past the end, with a message. The contract requires the exception type; the message is yours. - Return a new iterator from every call to
iterator(). Returningthisfrom an object that also holds the cursor means the second loop over the same object finds it already exhausted. - Do not override
removeunless you mean it. The inherited default throwsUnsupportedOperationException("remove"), which is the honest answer for a computed sequence.
Iterator<Integer> r = new Countdown(3).iterator();
r.next();
r.remove();
Exception in thread "main" java.lang.UnsupportedOperationException: remove
at java.base/java.util.Iterator.remove(Iterator.java:102)
at CountdownRemove.main(CountdownRemove.java:33)
If your own class wraps a mutable collection and you want fail-fast behaviour, the recipe is the one AbstractList documents: keep a modCount field, increment it in every structural operation, snapshot it in the iterator, and compare on next().
Practical rules
- Reach for
removeIffirst. It is one call, it expresses the intent, it cannot skip an element, and it touchesmodCountonce instead of once per deletion —removeIfon a four-element list tookmodCountfrom 0 to 1 where twoIterator.remove()calls took it to 2. - If you need the loop, take the iterator explicitly.
for (Iterator<String> it = list.iterator(); it.hasNext(); )is the only form that gives you a name for the object the enhancedforloop hides. - Never structurally modify a collection you are iterating except through that iterator. Not
add, notremove, notclear, and — less obviously — notsortorreplaceAll. - Use
ListIteratorwhen you need to replace or insert during a walk.setis the right tool for an in-place update and it is not even a structural modification. - Do not rely on
ConcurrentModificationExceptionto find these bugs. It is best-effort by specification, it does not fire when the removal makeshasNext()false, and the loop that got away with it silently produced a wrong list. - Choose a concurrent collection for its read/write shape, not for the exception it avoids.
CopyOnWriteArrayListcopies the whole array on every write; that is a good trade for a listener list and a terrible one for a queue. - Prefer a stream or
removeIfto a mutation loop when you can.list.stream().filter(...).toList()produces a new list and never touches the original, which sidesteps the entire question.
FAQ
What is the difference between fail-fast and fail-safe iterators in Java?
A fail-fast iterator reads the live collection and throws ConcurrentModificationException when it detects that the collection was structurally modified behind its back — the java.util collections work this way. A "fail-safe" iterator never throws, because it is not reading the live structure in the same way: CopyOnWriteArrayList iterates a snapshot taken when the iterator was created, and ConcurrentHashMap iterates the live table under a weaker guarantee. "Fail-safe" is a nickname; the JDK Javadoc uses "snapshot" and "weakly consistent", and the string fail-safe appears nowhere in the java.util or java.util.concurrent sources.
Why does ConcurrentModificationException mention concurrency when there is only one thread?
Because "concurrent" here means "at the same time as an iteration in progress", not "on another thread". A single-threaded loop that removes from the list it is walking has two things touching the list at once — the loop's iterator and the loop's body — and that is exactly what the check detects. The stack trace shows it plainly: the throwing frame is ArrayList$Itr.checkForComodification, called from ArrayList$Itr.next.
Can ConcurrentModificationException be missed?
Yes, and this is the important part. The check runs inside next() only. If a structural modification makes hasNext() return false, the loop ends before the check runs. Removing the second-to-last element of an ArrayList inside an enhanced for loop over ["a", "b", "c", "d"] visits three elements, throws nothing, and skips "d" entirely. The Javadoc states that fail-fast behaviour is provided "on a best-effort basis" and should be used "only to detect bugs".
What is a structural modification?
Anything that changes the size of the collection, or otherwise reorders or reshapes it in a way that would make an in-progress iteration return the wrong elements. add, remove, clear and addAll are structural; get and set(index, value) are not. Less obviously, sort and replaceAll are structural on an ArrayList even though the size does not change, and both make a live iterator throw.
What is the difference between Iterator and ListIterator?
Iterator moves forward only and can delete the element it last returned. ListIterator extends it with hasPrevious()/previous() for backward movement, nextIndex()/previousIndex() for position, set(E) to replace the last element returned, and add(E) to insert at the cursor. ListIterator is available only on List, because it is the only collection interface with a meaningful index.
Where does the cursor end up after ListIterator.add?
Immediately after the element you just inserted. The JDK source sets cursor = i + 1, so the next next() returns the element that followed the insertion point, not the new element — which is what stops an insert-during-traversal loop from running forever. The same call sets lastRet = -1, so set and remove right after an add both throw IllegalStateException, and previous() is what returns the element you added.
Is CopyOnWriteArrayList slower than ArrayList?
For writes, structurally so: every mutating call allocates a fresh backing array and copies the existing elements into it. A thousand appends produced a thousand new arrays and 499,500 copied element slots, against 13 arrays and 2,456 copied slots for ArrayList. Reads take no lock and no copy at all. That shape — cheap concurrent reads paid for by an expensive write — is what makes it right for something like a listener list and wrong for anything write-heavy.
Does iterating a ConcurrentHashMap see changes made during the walk?
Maybe, and the specification refuses to promise either way. The documented guarantee is that the iterator never throws ConcurrentModificationException, traverses every element present when it was created exactly once, and "may (but is not guaranteed to) reflect any modifications subsequent to construction". In the single-threaded run above, an entry added after the iterator was created did show up. Do not build logic on that; if you need a consistent view, take one explicitly.
Conclusion
Iteration in Java is a cursor sitting between two elements, plus two int fields that record where it is and what it last returned. Everything else falls out of that: hasNext() is a comparison against size, remove() is legal exactly when lastRet is not -1, and the enhanced for loop is those calls with the iterator hidden in a local slot you cannot name. Fail-fast is one counter compared against a copy of that counter, which makes it cheap, useful, and — as the second-to-last-element case shows in three lines of real output — genuinely fallible. Treat it as a bug detector that sometimes misses, use removeIf or the explicit iterator, and pick a concurrent collection for how it is written to rather than for the exception it declines to throw.
Article 12 turns from walking a collection to ordering one: Comparable versus Comparator, what "natural order" actually obliges a class to promise, why an inconsistent comparator can make sort throw, and how to build custom sorting that composes.