skip to content

Why does modifying the source collection while streaming it cause a ConcurrentModificationException, and how do you restructure to avoid it?

level: middleimportance: must knowfreq 52%

answer

  1. Streams read the source lazily; editing it mid-traversal = CME
  2. 'concurrent' = concurrent with iteration, even single-threaded
  3. fail-fast via modCount; best-effort, not a guarantee
  4. Fix: collect a new collection, or use removeIf
  5. Real concurrency: CopyOnWriteArrayList / ConcurrentHashMap (weakly consistent)

basics

~20 s

Streams 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 s

A 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 lines
java
List<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

for a junior

Knows that adding/removing from the source during a stream throws ConcurrentModificationException, and uses removeIf or collect instead.

for a middle

Explains fail-fast/modCount, that 'concurrent' means concurrent-with-iteration, and distinguishes structural vs field mutation.

for a senior

Knows fail-fast is best-effort (not for control flow), and when to choose weakly-consistent concurrent collections versus restructuring to immutable transforms.

for a principal

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)

context