How do you safely remove elements from a collection while iterating over it, and why is collection.remove() inside a loop dangerous?
answer
- it.remove(), not collection.remove()
- modCount vs expectedModCount -> CME
- fail-fast = debugging aid, not thread-safe
- removeIf(predicate) is the modern one-liner
- remove() without next() -> IllegalStateException
basics
~10 sUse the iterator's own remove() method, not the collection's. Calling list.remove() while looping with a for-each usually throws ConcurrentModificationException because the collection changed behind the iterator's back.
solid answer
~40 sWhen you need to delete elements during traversal, get an explicit Iterator and call it.remove() after it.next(). The iterator's remove() deletes the last element returned and keeps the iterator consistent with the collection. If instead you modify the collection directly (e.g. list.remove(x)) while a for-each loop is running, the iterator detects the structural change via a modCount mismatch and throws ConcurrentModificationException — this is fail-fast behavior, not a thread-safety guarantee. The for-each loop can't help here because it hides the iterator, so you must drop to an explicit while/for loop. Alternatives that avoid manual iteration entirely are Collection.removeIf(predicate) (Java 8+), which is concise and often the preferred choice, or iterating a copy. Iterator.remove() is the canonical low-level answer.
code
java · 10 lines// Safe removal during iteration
Iterator<String> it = names.iterator();
while (it.hasNext()) {
if (it.next().length() == 1) {
it.remove(); // OK: keeps iterator consistent
}
}
// Modern equivalent (Java 8+)
names.removeIf(n -> n.length() == 1);go deeper
Know the rule: to delete during iteration use the iterator's remove(), and that modifying the collection directly throws ConcurrentModificationException.
Explain modCount/expectedModCount, that the behavior is fail-fast and best-effort, why for-each can't remove, and prefer removeIf for clarity.
Detail the IllegalStateException/UnsupportedOperationException contract of remove(), the second-to-last-element quirk, and contrast fail-fast with weakly-consistent concurrent iterators.
Discuss when to choose removeIf vs explicit iterators vs concurrent collections at scale, the cost model (O(n) shifts for ArrayList removal), and why fail-fast is intentionally not a synchronization primitive.
## The hazard Suppose you loop over a list and try to delete matching elements directly: ```java List<String> names = new ArrayList<>(List.of("a", "bob", "c")); for (String n : names) { if (n.length() == 1) names.remove(n); // DANGER } ``` This typically throws **`ConcurrentModificationException`** (CME). Understanding *why* requires understanding how the iterator tracks change. ## modCount and fail-fast Most `java.util` collections keep an internal counter called **`modCount`** (modification count) that is incremented on every *structural* change — an add or remove that changes the size. When you call `iterator()`, the iterator snapshots the current `modCount` into a field called `expectedModCount`. On each `next()` (and inside `remove()`), the iterator runs `checkForComodification()`: it compares the collection's current `modCount` to its saved `expectedModCount`. If they differ — meaning *something* changed the collection outside this iterator — it throws CME immediately. This is called **fail-fast**: rather than risk skipping elements, reading stale data, or producing undefined behavior, the iterator stops loudly at the first sign of interference. Important nuance: fail-fast is a *best-effort debugging aid*, **not** a concurrency guarantee. The check is not synchronized, so you must never rely on CME for correctness across threads — it may or may not fire. Also, removing the second-to-last element can sometimes *not* throw (a known quirk of `ArrayList`'s loop), so direct modification is unreliable in addition to being illegal. ## The fix: iterator.remove() The iterator's own `remove()` performs the deletion *and* re-syncs `expectedModCount = modCount`, so the iterator stays consistent. Use an explicit iterator: ```java Iterator<String> it = names.iterator(); while (it.hasNext()) { if (it.next().length() == 1) { it.remove(); // safe: removes the element next() just returned } } ``` Rules for `it.remove()`: - It deletes the element the *most recent* `next()` returned. - You must call `next()` before each `remove()`; calling `remove()` without a preceding `next()`, or twice in a row, throws **`IllegalStateException`**. - It is an *optional* operation; some iterators throw `UnsupportedOperationException` (e.g. an immutable list's iterator). ## Why for-each can't do it The enhanced for loop desugars to `iterator()/hasNext()/next()` but never exposes the iterator variable, so you cannot call `remove()` on it. That is exactly why you must drop to an explicit loop when deleting. ## Better alternatives - **`removeIf(Predicate)`** (Java 8+): `names.removeIf(n -> n.length() == 1);` — one line, no CME, often the clearest choice. Internally it uses an iterator or an optimized bulk path. - **Iterate a copy**, modify the original: `for (String n : new ArrayList<>(names)) { if (...) names.remove(n); }` — works but allocates and is O(n) per remove on a list. - **Concurrent collections** (e.g. `CopyOnWriteArrayList`) provide weakly-consistent iterators that don't throw CME, suited to multi-threaded reads. ## Term recap - **Structural modification**: any change to the collection's size (add/remove); merely setting an existing element via `set()` is *not* structural. - **modCount / expectedModCount**: the counters whose mismatch triggers CME. - **Fail-fast**: throw immediately on detected concurrent/illegal modification; a debugging aid, not a thread-safety mechanism. - **Weakly consistent**: an iterator (e.g. on concurrent collections) that tolerates modification without throwing, reflecting some but not necessarily all changes.
- Does set() (replacing an element's value) trigger ConcurrentModificationException?No. set() is not a structural modification; it doesn't change size or modCount, so it won't trip the fail-fast check.
- Why is CME called a 'best-effort' mechanism?The modCount check isn't synchronized, so in concurrent scenarios it may or may not detect the modification; it's a debugging aid, not a reliable concurrency control.
saying these in an interview costs you the question
- Claiming ConcurrentModificationException guarantees thread safety (it's only fail-fast/best-effort)
- Thinking direct list.remove() in a loop always throws (the second-to-last case can sneak through)
- Calling it.remove() before it.next()
- Forgetting removeIf() exists and hand-rolling iterator loops needlessly