What is ConcurrentModificationException?
subList does not create a new list — it is a view onto the original. SubList stores the parent's expectedModCount. When you modify the parent list directly, its modCount changes...
🟢 Junior Level
CME (ConcurrentModificationException) — a signal that the code violated the iterator contract: the collection was modified NOT through the iterator’s own methods.
Important: CME is NOT a multi-threading error (as one might think from the name). It is a trap for catching bugs in a single thread.
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
// ❌ Error!
for (String s : list) {
list.remove(s); // → ConcurrentModificationException!
}
// ✅ Correct:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
it.remove(); // OK!
}
Important: More common in a single thread than in multi-threading!
🟡 Middle Level
Mechanics: modCount
// Collection: modCount (number of modifications)
// Iterator: expectedModCount (copy upon creation)
// On next():
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
Tricky scenarios
SubList trap:
List<String> sub = list.subList(0, 2);
list.add("X"); // Modified the parent
sub.size(); // → CME! SubList is sensitive to parent's modCount
subList does not create a new list — it is a view onto the original. SubList stores the parent’s expectedModCount. When you modify the parent list directly, its modCount changes, but the expectedModCount in SubList does not. The check detects the desynchronization.
Last element:
// Sometimes CME does NOT throw when removing the second-to-last!
// → hasNext() returns false → loop terminates
// → Logical error is not caught!
If you remove the second-to-last element via list.remove(), hasNext() may already return false (the iterator “thinks” there are no more elements). The loop terminates without checking modCount → CME does not throw, but data is already corrupted. This is not a CME bug — it’s a best-effort detector.
Solutions
// 1. Iterator.remove()
it.remove(); // Updates expectedModCount
// 2. removeIf (Java 8+)
list.removeIf(e -> condition(e));
// 3. Collect to remove
List<String> toRemove = new ArrayList<>();
for (String s : list) {
if (condition(s)) toRemove.add(s);
}
list.removeAll(toRemove);
// 4. Concurrent collections
CopyOnWriteArrayList<String> safe = new CopyOnWriteArrayList<>(list);
// → Iterator = snapshot, does not throw CME
🔴 Senior Level
CME ≠ multi-threading
CME is more common in a SINGLE thread!
→ Removal in a for-each cycle
In multi-threading without synchronization:
→ modCount is not volatile
→ Thread may NOT see the change
→ ArrayIndexOutOfBoundsException instead of CME!
Views are sensitive to modCount
// keySet, entrySet, subList → view onto the original
// Modification of the original → CME in the view!
Map<String, Integer> map = new HashMap<>();
Set<String> keys = map.keySet();
map.put("A", 1); // Modified map
for (String k : keys) { ... } // → CME!
View (representation) — an object that does not store its own data, but shows data from the original collection. Changes in the original are instantly visible through the view, and vice versa.
Best Practices
- CME = violation of iterator contract
- Best-effort → not guaranteed in multi-threading
- Views (subList, keySet) → sensitive to modCount
- removeIf → optimal approach
- iterator.remove() → safe in a loop
- Concurrent collections → weakly consistent, no CME
Summary for Senior
- modCount = change detector
- Best-effort → not 100% in multi-threading
- Views → CME when original is modified
- removeIf > iterator.remove() > collect-and-remove
- Concurrent collections → no CME
🎯 Interview Cheat Sheet
Must know:
- CME occurs when the collection is modified NOT through the iterator’s methods (more often in a single thread!)
- Mechanism: collection’s
modCount!= iterator’sexpectedModCountwhennext()is called - CME is not a multi-threading error, but a trap for catching logic bugs
subList,keySet,entrySet— view objects, sensitive to parent’smodCount- CME may NOT throw when removing the second-to-last element (best-effort detector)
- 4 solutions:
iterator.remove(),removeIf(), collect-and-remove, concurrent collections iterator.remove()updatesexpectedModCount— CME does not occur
Frequent follow-up questions:
- Why is CME more common in a single thread than in multi-threading? — The most common scenario: removing an element in a for-each loop by a single thread.
- Why does subList throw CME when the parent is modified? — SubList stores the parent’s
expectedModCount; direct modification of the parent desynchronizes it. - What happens in multi-threading without synchronization instead of CME? —
ArrayIndexOutOfBoundsException— the thread does not see themodCountchange due to the absence of volatile. - Why might CME not throw when removing the second-to-last element? —
hasNext()may returnfalsebefore checkingmodCount— the loop terminates without detection.
Red flags (DO NOT say):
- “CME is a multi-threading error” — no, it most commonly occurs in a single thread
- “CME guarantees detection of changes in multi-threading” — no, best-effort, modCount is not volatile
- “subList creates an independent list” — no, it’s a view onto the original, sensitive to modCount
- “You can always rely on CME for synchronization” — it’s a bug detector, not a synchronization mechanism
Related topics:
- [[26. What are fail-fast and fail-safe iterators]]
- [[28. How to properly remove elements during iteration]]
- [[25. What is the difference between Iterator and ListIterator]]