It is a quirk of the implementation.
The exception is thrown on a best-effort basis only, there is no guarantee it will be thrown.
To understand why, you need to understand how enhanced for loops are desugared:
for (String i : a) {
a.remove(i);
}
Is compiled to something like:
Iterator<String> it = a.iterator();
while (it.hasNext()) {
String i = a.next();
a.remove(i);
}
Next, the iterator is implemented something like:
int idx = 0;
boolean hasNext() {
return idx < a.size();
}
Object next() {
checkForModification(); // throws exception if list has been modified.
return a.get(idx++);
}
So, if the list has size 2 initially, execution of the loop proceeds thus:
- Check
hasNext(): idx == 0, and 0 < 2 is true, so it returns true.
- Call
next(): retrieve i = a.get(0), increment idx, so idx == 1.
- Remove
i from a. Now a.size() == 1.
The loop has executed once. Now it executes again:
- Check
hasNext(): idx == 1, and 1 < 1 is false, so it returns false.
So execution stops, no ConcurrentModificationException. The list still contains the initial second item.
If the list is any bigger than 2 initially, the loop executes a second time, (because idx == 1, and size() == initial size() - 1, so idx < initial size() - 1), so next() will be called again after the remove(). The list has been modified, so checkForModification() throws an exception.