Why does modifying the source collection while streaming it cause a ConcurrentModificationException, and how do you restructure to avoid it?
answer
- Streams read the source lazily; editing it mid-traversal = CME
- 'concurrent' = concurrent with iteration, even single-threaded
- fail-fast via modCount; best-effort, not a guarantee
- Fix: collect a new collection, or use removeIf
- Real concurrency: CopyOnWriteArrayList / ConcurrentHashMap (weakly consistent)
basics
~20 sStreams read the source lazily through an iterator that detects changes. If you add or remove elements from the source collection while the stream is running, you usually get a ConcurrentModificationException. Build a new collection from the stream instead of editing the old one.
solid answer
~50 sA stream over a collection doesn't snapshot the data; it pulls elements lazily from the source when the terminal operation runs. Most JDK collections are fail-fast: their spliterator/iterator tracks a modification count, and if the structure changes during traversal, they throw ConcurrentModificationException to signal a likely bug. So mutating the same list inside forEach, or having another part of the code add/remove during the stream, trips it. Even single-threaded code does this — the 'concurrent' in the name means concurrent with iteration, not necessarily across threads. The idiomatic fix is to treat streams as producing a new value: collect the desired elements into a new collection and replace, or use collection methods designed for in-place removal like removeIf. If you genuinely need concurrent mutation, use a copy-on-write or concurrent collection whose spliterator is weakly consistent and won't throw, accepting its weaker visibility guarantees.
code
java · 13 linesList<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4));
// WRONG: mutating the source during traversal
// list.stream().forEach(x -> { if (x == 2) list.remove(Integer.valueOf(2)); });
// -> java.util.ConcurrentModificationException
// RIGHT (functional): build a new collection
List<Integer> kept = list.stream()
.filter(x -> x != 2)
.collect(Collectors.toList());
// RIGHT (in-place): purpose-built API
list.removeIf(x -> x == 2);go deeper
Knows that adding/removing from the source during a stream throws ConcurrentModificationException, and uses removeIf or collect instead.
Explains fail-fast/modCount, that 'concurrent' means concurrent-with-iteration, and distinguishes structural vs field mutation.
Knows fail-fast is best-effort (not for control flow), and when to choose weakly-consistent concurrent collections versus restructuring to immutable transforms.
Guides API/data-ownership design so traversal and mutation don't overlap, and reviews for hidden cross-thread mutation of fail-fast collections under load.
## Terms - **Source collection**: the `List`, `Set`, `Map` (etc.) a stream is created from via `.stream()`. - **Lazy traversal**: the stream doesn't copy elements up front; when the **terminal** operation runs, it walks the source through a **spliterator** (the stream-era cousin of an iterator) pulling elements on demand. - **Structural modification**: adding or removing elements (changing the size/structure), as opposed to merely updating a field of an element already present. - **Fail-fast**: a collection that *detects* structural modification during iteration and throws immediately, rather than risking corrupted or undefined results. - **`modCount`**: an internal counter most JDK collections bump on every structural change. The iterator/spliterator remembers the expected value and compares; a mismatch → `ConcurrentModificationException` (CME). ## Why the exception happens Because traversal is lazy and reads the live source, changing that source mid-stream breaks the iterator's assumptions: ```java List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4)); list.stream().forEach(x -> { if (x == 2) list.remove(Integer.valueOf(2)); // structural change DURING traversal }); // throws java.util.ConcurrentModificationException ``` The `remove` bumps `modCount`; the spliterator notices on its next step and throws. The name is misleading: **"concurrent" means concurrent with the iteration**, even in a single thread. It's a *best-effort* safety net, not a guarantee — fail-fast behavior is documented as not to be relied on for correctness, only as a debugging aid. The same happens if a *different* method, or another thread, mutates the collection while your stream is being consumed. ## What does NOT trigger it Mutating a **field of an existing element** is not a structural change and won't throw: ```java people.stream().forEach(p -> p.setActive(false)); // OK structurally (but see side-effect cautions) ``` The pitfall is specifically **add/remove on the source** during traversal. ## How to restructure ### 1. Produce a new collection (the functional way) Don't edit the source while reading it — build the result and replace: ```java List<Integer> kept = list.stream() .filter(x -> x != 2) .collect(Collectors.toList()); // new list, source untouched during traversal ``` ### 2. Use in-place collection APIs designed for it For removal, `removeIf` mutates safely because it manages its own iteration: ```java list.removeIf(x -> x == 2); // no CME, in-place ``` For map values, `replaceAll`/`compute` do controlled in-place updates. ### 3. Use a CME-immune collection when you truly need concurrent mutation Collections like `CopyOnWriteArrayList` or `ConcurrentHashMap` have **weakly consistent** spliterators that tolerate modification during traversal (they won't throw CME) — at the cost of weaker visibility (you may or may not see concurrent changes) and, for copy-on-write, expensive writes. Choose these deliberately for genuine concurrency, not to paper over a single-threaded design mistake. ## Mental model A stream is a **reader** of the source. Don't rewrite a book while you're reading it. Either read it to produce a *new* book (`collect`), or use a tool built to edit in place (`removeIf`).
- Does updating a field of an element during a stream cause CME?No. CME is about structural modification (add/remove changing the collection's structure). Updating a field of an element already in the collection is not structural — though it can still be a questionable side effect.
- How can you remove matching elements without a CME?Use list.removeIf(predicate) for in-place removal, or stream-and-collect into a new filtered collection and replace the original. Both avoid mutating the source during a stream traversal.
saying these in an interview costs you the question
- Removing from the source inside forEach to 'filter in place'
- Thinking CME only happens across threads
- Assuming fail-fast detection is guaranteed/reliable for control flow
- Confusing updating an element's field (fine) with add/remove on the source (CME)