What happens if you modify an ArrayList while iterating over it, and how do you handle it correctly?
answer
- Fail-fast iterator tracks modCount vs expectedModCount
- Structural change (size) outside iterator -> CME
- Name misleads: fires single-threaded too
- set() is non-structural -> safe
- Fix: Iterator.remove / removeIf / iterate copy / CopyOnWriteArrayList
basics
~10 sChanging an ArrayList's size while looping over it with its iterator usually throws ConcurrentModificationException. To remove during iteration, use the iterator's own remove() method, or use removeIf.
solid answer
~40 sArrayList iterators are fail-fast: they track a modCount that increments on every structural change (add/remove that alters size). If the list is structurally modified through anything other than the iterator itself during iteration, the next next()/remove() detects the modCount mismatch and throws ConcurrentModificationException (CME). This is a best-effort bug-detection mechanism, not a thread-safety guarantee, and despite the name it commonly fires from single-threaded mistakes like removing inside a for-each loop. Correct approaches: use Iterator.remove() (it updates modCount in sync) while iterating; or collection.removeIf(predicate) for bulk conditional removal; or iterate over a copy; or in concurrent scenarios use CopyOnWriteArrayList, whose iterator is a weakly-consistent snapshot and never throws CME. Note set(i, v) does not change size, so it does not trip fail-fast.
code
java · 13 linesList<String> list = new ArrayList<>(List.of("x1", "keep", "x2"));
// WRONG: throws ConcurrentModificationException
// for (String s : list) { if (s.startsWith("x")) list.remove(s); }
// RIGHT 1: iterator's own remove()
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().startsWith("x")) it.remove();
}
// RIGHT 2: removeIf (preferred, single pass)
// list.removeIf(s -> s.startsWith("x"));go deeper
Knows that removing from a list while looping over it throws an exception and that the iterator's remove or removeIf is the fix.
Explains modCount/expectedModCount, that fail-fast can fire single-threaded, structural vs non-structural ops, and the correct removal patterns.
Frames fail-fast as a best-effort bug detector (not a guarantee), and chooses CopyOnWriteArrayList vs removeIf vs synchronized list per concurrency needs.
Sets coding standards around safe mutation-during-iteration, reasons about weakly-consistent vs fail-fast iterators across collections, and the concurrency/memory trade-offs of copy-on-write.
## The trap A very common bug: ```java for (String s : list) { // for-each uses list.iterator() if (s.startsWith("x")) { list.remove(s); // structural change behind the iterator's back } } ``` This typically throws **ConcurrentModificationException (CME)** — even in a single thread, even with no concurrency at all. The name is misleading. ## How fail-fast works: modCount ArrayList keeps an internal counter, **`modCount`**, incremented on every **structural modification** — an operation that changes the list's **size** (add, remove, clear). When you create an iterator (which a for-each does implicitly), it snapshots `modCount` into `expectedModCount`. On each `next()` (and `remove()`), the iterator runs `checkForComodification()`: if `modCount != expectedModCount`, it throws CME immediately. When you called `list.remove(s)` directly, `modCount` went up but the iterator's `expectedModCount` did not — mismatch → CME on the next loop step. ## 'Fail-fast' is a bug-detector, not a contract Fail-fast iterators throw on a **best-effort** basis to surface bugs early. The docs explicitly say you must **not** rely on CME for correctness — it can theoretically miss in unsynchronized concurrent use. Treat a CME as 'you have a bug in how you mutate during iteration,' not as a feature to catch. ## Structural vs non-structural - **Structural** (changes size, bumps modCount, can trip fail-fast): `add`, `remove`, `clear`. - **Non-structural** (does not change size, safe during iteration): `set(i, value)` replaces an element in place. So updating values while iterating is fine; adding/removing is not. ## The correct ways 1. **Iterator.remove()** — the iterator updates `expectedModCount` in lockstep, so no CME: ```java Iterator<String> it = list.iterator(); while (it.hasNext()) { if (it.next().startsWith("x")) it.remove(); // safe } ``` 2. **removeIf(predicate)** — cleanest for conditional bulk removal; one pass, no CME: ```java list.removeIf(s -> s.startsWith("x")); ``` 3. **Iterate a copy** — loop over `new ArrayList<>(list)` and mutate the original. 4. **Collect-then-remove** — gather matches in a second list, then `removeAll`. 5. **Concurrency** — use **CopyOnWriteArrayList**: its iterator works on an immutable snapshot taken at creation, so concurrent writes never cause CME (writes copy the whole array; great for read-heavy, write-rare data). ## Why not just catch the exception? Catching CME and continuing is wrong — the iteration state is already inconsistent and you may skip or reprocess elements. Fix the mutation pattern instead.
- Does CopyOnWriteArrayList throw ConcurrentModificationException?No. Its iterator operates on an immutable snapshot of the array taken when the iterator was created, so concurrent modifications are invisible to it and it never throws CME (but the iterator won't see later writes).
- Is calling list.set(i, v) inside a for-each loop safe?Yes. set replaces an element without changing the size, so it is non-structural, does not bump modCount, and does not trigger fail-fast.
saying these in an interview costs you the question
- Removing/adding via the collection inside a for-each loop
- Thinking CME only happens with multiple threads
- Catching CME and continuing the loop
- Believing set(i,v) trips fail-fast (it does not change size)
- Relying on CME as a correctness guarantee rather than a bug detector