skip to content

Explain the snapshot iterator semantics of CopyOnWriteArrayList: why does iteration never throw ConcurrentModificationException, and what are the consequences?

level: middleimportance: must knowfreq 65%

answer

  1. Iterator captures the array reference once = snapshot
  2. Writers make a new array, snapshot never changes
  3. No modCount, so no CME ever
  4. Weakly consistent / stale view
  5. iterator.remove/set/add → UnsupportedOperationException

basics

~20 s

When you start iterating, the iterator grabs the current array and walks that. Later changes go to a new array the iterator never sees, so it can't notice a modification and never throws ConcurrentModificationException. The downside: you may iterate stale data.

solid answer

~40 s

A CopyOnWriteArrayList iterator captures the backing array reference at the moment it is created — that array is its immutable snapshot. Because mutations always create a new array and leave the old one untouched, the iterator's view can never change underneath it. That's why it never throws ConcurrentModificationException: it has no way to detect a concurrent modification, since modifications don't touch its array. Two consequences follow. First, the iteration is weakly consistent / stale: additions or removals made after the iterator was obtained are invisible to it. Second, the iterator does not support mutation — remove(), set(), and add() on it throw UnsupportedOperationException, because mutating a private snapshot would be meaningless. This is a deliberate trade for fail-safe, lock-free traversal that you can do while other threads write freely.

go deeper

for a junior

Knows the iterator never throws ConcurrentModificationException and walks a fixed snapshot.

for a middle

Explains the snapshot mechanism via the immutable array, the staleness consequence, and that iterator.remove throws UnsupportedOperationException.

for a senior

Contrasts snapshot/weakly-consistent vs fail-fast iterators across the JDK, and judges when stale-but-safe iteration is correct for the domain (e.g. dispatching to listeners present at event time).

for a principal

Reasons about memory pinning by long-lived iterators/streams, and sets conventions for safe concurrent traversal patterns across the codebase.

## Background: the fail-fast iterator Most Java collections (`ArrayList`, `HashMap`, …) return **fail-fast** iterators. An **iterator** is the object you get from `list.iterator()` (and that a `for-each` loop uses) to walk elements one at a time. Fail-fast means: the collection keeps an internal `modCount` (modification counter); the iterator records its value when created and re-checks it on every step. If the collection was structurally modified by anyone in the meantime, `modCount` no longer matches and the iterator throws **`ConcurrentModificationException`** (CME). This is a *best-effort bug detector*, not a thread-safety mechanism — it exists to surface the common mistake of modifying a list while looping over it. ## How COW differs: the snapshot iterator `CopyOnWriteArrayList` works the opposite way. When you call `iterator()`, it grabs the **current backing array reference** and hands the iterator that exact array as a private, read-only **snapshot**. Recall the core COW rule: every mutation creates a *new* array and swaps the reference, never editing an existing array. Therefore: - The array the iterator holds is **effectively immutable** — no future writer will ever modify it. - Concurrent `add`/`remove`/`set` produce a *different* array that the iterator simply doesn't reference. So the iterator has **nothing to detect**. There's no `modCount` check and no CME — by construction the snapshot can't change. This style is called **weakly consistent** (or *snapshot* / *fail-safe*) iteration. ## Consequences 1. **Staleness.** You iterate the list as it existed when you *started*. Elements added afterward won't appear; elements removed afterward will still appear. For many uses ("notify everyone who was subscribed when this event fired") that's exactly the desired semantics, but you must not assume the iteration reflects the live, current contents. 2. **No mutation through the iterator.** `iterator.remove()`, `iterator.set(x)`, and `listIterator.add(x)` all throw **`UnsupportedOperationException`**. Why? The iterator owns only a private snapshot; mutating it couldn't affect the real list, so the API forbids it rather than silently doing nothing. To remove during traversal you must call methods on the list itself (e.g. `list.remove(obj)`). 3. **Safety while others write.** The entire point: you can loop for as long as you like, and other threads may freely mutate the list, with no locking, no CME, and no risk of an index-out-of-bounds from a shrinking array — because your array never shrinks. 4. **Memory note.** While an iterator (or a `subList`/stream over it) is alive, it pins the old array, keeping those elements reachable until the iterator is discarded. ## Mental model A snapshot iterator is like taking a **photograph** of the list and then walking the photo. People can rearrange the real scene all they want; your photo is frozen and complete, so you'll never trip over a half-finished change — and you also won't see anyone who walked in after the shutter clicked. ## Contrast table | | Fail-fast (`ArrayList`) | Snapshot (`CopyOnWriteArrayList`) | |---|---|---| | Detects concurrent change | yes, throws CME | no, can't detect | | Sees changes made after creation | sometimes (then throws) | never | | `iterator.remove()` | supported | throws `UnsupportedOperationException` | | Thread-safe to iterate while others write | no | yes |

  • Why does iterator.remove() throw on a CopyOnWriteArrayList?
    The iterator holds only a private snapshot array; mutating it could not affect the real list, so rather than silently no-op the API declares the operation unsupported. Mutate the list directly instead.
  • If you add an element after obtaining the iterator, does the loop see it?
    No. The iterator is bound to the array snapshot taken at creation time; the add produced a new array the iterator never references, so the element is invisible to that iteration.

saying these in an interview costs you the question

  • Saying the iterator sees live updates — it sees a frozen snapshot
  • Claiming you can call iterator.remove() — it throws UnsupportedOperationException
  • Confusing 'never throws CME' with 'always reflects current state'
  • Thinking COW uses a modCount fail-fast check like ArrayList

context