---
title: 'Iterators and Concurrent Modification'
source: 'https://academia.sh/en/courses/java-standard-library/iterators-and-concurrent-modification'
course: 'The Standard Library and Streams'
language: en
updated: '2026-08-17T18:09:42+00:00'
license: 'CC BY-SA 4.0'
---

# Iterators and Concurrent Modification

The iterator's basic promise — NoSuchElementException once exhausted — comes out byte-for-byte identical across three separate implementations. But if the container changes during iteration, three separate answers appear: one that fails fast, one that walks over a snapshot, one that says nothing at all. Fail-fast itself turns out to be a best effort, not a guarantee: the same removal throws an exception at one position while leaving a loop that finishes silently, one short, at the second-to-last element. The bounding measurement shows the iterator's own remove falls outside this rule.

The previous lesson measured whether a map could find its key, and split the flaw's source
into three: interface, implementation, caller's rule. This lesson tests the same trio not on
the container's **resting** state but **while it is being iterated.** Adding a new element to
the same container, or removing one, while walking a list, a set, or a map with `for` is a
situation all three families share. The library does not give this a single answer.

Iteration always assumes an order: the iterator infers where the next element is from the
internal state the previous call left behind. If the container changes in the middle of
iteration, a mismatch opens between this internal state and the container's real state. The
problem itself is the same across every implementation; what gets measured is how each one
handles this mismatch — silently ignoring it, stopping by name, or being designed so it never
arises at all.

## The Iterator's Interface Promise

Before entering the subject of modification, the iterator's side that does not break is
measured. The `Iterator` interface defines the `hasNext`/`next` pair and also defines what
`next` will do on an exhausted iterator.

- **CO16** — All three containers are filled with the same three elements and walked to
  exhaustion; then one more `next` call is made. The only thing measured is that last call's
  result.

```java
// BasicPromise.java — do all three implementations behave the same once next is exhausted
import java.util.*;

public class BasicPromise {
    static String attempt(Iterable<String> container) {
        Iterator<String> it = container.iterator();
        while (it.hasNext()) it.next();
        try {
            it.next();
            return "next succeeded (not expected)";
        } catch (NoSuchElementException e) {
            return e.getClass().getSimpleName();
        }
    }

    public static void main(String[] args) {
        List<String> source = List.of("a", "b", "c");
        System.out.println("ArrayList  : " + attempt(new ArrayList<>(source)));
        System.out.println("HashSet    : " + attempt(new HashSet<>(source)));
        System.out.println("ArrayDeque : " + attempt(new ArrayDeque<>(source)));
    }
}
```

```
ArrayList  : NoSuchElementException
HashSet    : NoSuchElementException
ArrayDeque : NoSuchElementException
```

All three families give the same exception, with the same name. This is a promise the
`Iterator` interface directly defines: the `next` contract requires throwing this exception
on an exhausted iterator. As long as the container itself does not change — as long as it is
only being iterated — the three implementations cannot be told apart. The distinction begins
once the container **itself** changes during iteration.

This measurement is the same shape as the previous lessons' "answer for a missing key"
measurement: when the container **never changes**, only being queried, three implementations
gather around a single behavior. The distinction always shows up when the container's state
**changes** — writing a key in the previous lesson, the container changing while being
iterated in this one.

## Three Answers: Fail-Fast, Snapshot, Silence

- **CO17** — All four containers are filled with the same four elements, and while being
  walked with a for-each, the moment the second element is processed, a **fifth element** is
  added to the container. What is measured is how this addition affects the iteration: an
  exception, silent continuation, and if it continues, how many elements are seen. The change
  is made at **the same step** in all four containers; only the container changes.

```java
// ThreeAnswers.java — three separate answers if the container changes during iteration
import java.util.*;
import java.util.concurrent.*;

public class ThreeAnswers {
    static String attempt(Iterable<String> container, Runnable change) {
        int counted = 0;
        try {
            for (String s : container) {
                counted++;
                if (counted == 2) change.run();
            }
            return "no exception, counted=" + counted;
        } catch (ConcurrentModificationException e) {
            return e.getClass().getSimpleName() + ", counted=" + counted;
        }
    }

    public static void main(String[] args) {
        List<String> failingList = new ArrayList<>(List.of("a", "b", "c", "d"));
        System.out.println("ArrayList (fail-fast)                     : "
                + attempt(failingList, () -> failingList.add("extra")));

        Set<String> failingSet = new HashSet<>(List.of("a", "b", "c", "d"));
        System.out.println("HashSet (fail-fast)                       : "
                + attempt(failingSet, () -> failingSet.add("extra")));

        List<String> snapshotBased = new CopyOnWriteArrayList<>(List.of("a", "b", "c", "d"));
        System.out.println("CopyOnWriteArrayList (snapshot)           : "
                + attempt(snapshotBased, () -> snapshotBased.add("extra")));

        Map<String, Integer> staysSilent = new ConcurrentHashMap<>();
        staysSilent.put("a", 1); staysSilent.put("b", 2);
        staysSilent.put("c", 3); staysSilent.put("d", 4);
        System.out.println("ConcurrentHashMap (says nothing)          : "
                + attempt(staysSilent.keySet(), () -> staysSilent.put("extra", 5)));
    }
}
```

```
ArrayList (fail-fast)                     : ConcurrentModificationException, counted=2
HashSet (fail-fast)                       : ConcurrentModificationException, counted=2
CopyOnWriteArrayList (snapshot)           : no exception, counted=4
ConcurrentHashMap (says nothing)          : no exception, counted=5
```

Four rows show three separate promises, and none of the three is written in the `Iterator`
interface itself — `Iterator` never defines what will happen to a modification during
iteration, it leaves the definition to the chosen class. `ArrayList`'s and `HashSet`'s
iterators check, on every `next` call, whether the container has changed structurally, and if
it has, they stop immediately, by name: because the addition happened after the second
element, the third `next` call falls in both. That two separate data structures (an
array-based list, a hash-based set) keep the same counter is not a coincidence — `AbstractList`
and `HashMap` (which `HashSet` internally wraps) share the same `modCount` mechanism;
fail-fast is not a single class's trait, it is part of the framework's design pattern.

`CopyOnWriteArrayList`, by contrast, has already taken a copy of the array the moment
iteration starts; whatever gets added to the container, that copy has four elements and the
iterator sees all four, without noticing anything. The fourth row is different from both
previous ones: `ConcurrentHashMap` neither throws an exception nor freezes on a fixed copy —
in this run, the fifth element added also enters the iteration, and a total of five elements
are counted, but this is not a promise this API gives, it is only this run's result; the
library only guarantees it **will not throw** an exception, it does not guarantee which
elements it will see.

## Fail-Fast Is a Best Effort, Not a Guarantee

`ArrayList`'s "fail-fast" behavior shows its own limit in the next measurement. The iterator
does not catch every modification; it only keeps a counter, and only checks that counter when
`next` is called. If the removed element is the list's **second-to-last**, the loop already
finishes without calling `next` again, and the check never runs at all.

- **CO18** — The same five-element list is walked twice; once the middle element (index 2) is
  removed when iteration reaches it, once the second-to-last element (index 3) is. The only
  thing that changes is the removed position.

```java
// SilentEnding.java — fail-fast does not always work
import java.util.*;

public class SilentEnding {
    static void attempt(String label, int indexToRemove) {
        List<String> list = new ArrayList<>(List.of("a", "b", "c", "d", "e"));
        List<String> visited = new ArrayList<>();
        try {
            for (String s : list) {
                visited.add(s);
                if (visited.size() - 1 == indexToRemove) {
                    list.remove(indexToRemove);
                }
            }
            System.out.println(label + " -> no exception, visited=" + visited);
        } catch (ConcurrentModificationException e) {
            System.out.println(label + " -> " + e.getClass().getSimpleName()
                    + ", visited=" + visited);
        }
    }

    public static void main(String[] args) {
        attempt("if index 2 (middle) is removed", 2);
        attempt("if index 3 (second-to-last) is removed", 3);
    }
}
```

```
if index 2 (middle) is removed -> ConcurrentModificationException, visited=[a, b, c]
if index 3 (second-to-last) is removed -> no exception, visited=[a, b, c, d]
```

Same class, same for-each, same single removal — the result says two separate things. When
the middle element is removed, what is expected happens: the next `next` call catches the
counter mismatch and the exception falls. When the second-to-last element is removed, though,
the list's size drops from five to four, and the loop's internal cursor, instead of moving to
the next element, now equals the list's new size; `hasNext` reads this as "iteration is done"
and the loop ends **without calling `next` at all** — the last element (`"e"`) is never seen.
No exception at all, but the iteration is incomplete too: four elements were seen, the fifth
was silently skipped.

This is the misleading side of the name "fail-fast." `ArrayList`'s iterator is not an observer
that catches every modification; it is a **best-effort** check that only does a counter
comparison when `next` is called. Java's own documentation defines this behavior not as a
guarantee but as a tool that helps catch bugs early — and this measurement shows a situation
where even that tool itself can be skipped.

The practical consequence of this is that a program's correctness cannot rest on the sentence
"no exception was thrown." Both runs worked with the same class, the same loop, a single
removal operation; one reported the flaw by name, the other lost an element without saying
anything. Whether the check runs depends on where the removed position sits in the list — and
since this is often information that depends on run-time data, the same source code can look
flawless on some inputs and produce an incomplete iteration on others.

Where the check is done makes this best effort's boundary even clearer. `hasNext` never
performs a consistency check at all; only `next` and a few methods like it check.

- **CO19** — After one element is read, an element is added to the container; right after
  that, `hasNext` is called first, then `next`. What is measured is whether both throw an
  exception at the same time, or only one does.

```java
// HasNextCheck.java — the check only runs inside next()
import java.util.*;

public class HasNextCheck {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(List.of("a", "b", "c"));
        Iterator<String> it = list.iterator();
        it.next();
        list.add("extra");
        System.out.println("hasNext() after the change : " + it.hasNext());
        try {
            it.next();
            System.out.println("next() succeeded (not expected)");
        } catch (ConcurrentModificationException e) {
            System.out.println("next()    -> " + e.getClass().getSimpleName());
        }
    }
}
```

```
hasNext() after the change : true
next()    -> ConcurrentModificationException
```

`hasNext` silently returns `true` even after the change; the check only kicks in when `next`
is called. This makes concrete where the best effort stops: the counter comparison does not
run on every container operation, only at the moments the iterator **advances.** `hasNext`
saying "yes, there is more" is not a guarantee; there is no guarantee at all that the next
step will succeed.

## The Bounding Measurement: The Rule Forbids Changing from Outside the Container

The measurements so far have all been the rule "do not change the container while iterating"
being broken. But what exactly does this rule mean? The iterator's **own** `remove` method
changes the container too — so why does it not throw the same exception?

- **CO20** — The same five-element list is walked with an open `Iterator`, and two elements
  are removed not directly through the list's `remove`, but through the iterator's own
  `remove`.

```java
// IteratorRemove.java — the iterator's own remove does not break the rule
import java.util.*;

public class IteratorRemove {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(List.of("a", "b", "c", "d", "e"));
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            String s = it.next();
            if (s.equals("b") || s.equals("d")) {
                it.remove();
            }
        }
        System.out.println("result with the iterator's own remove : " + list);
    }
}
```

```
result with the iterator's own remove : [a, c, e]
```

No exception falls at all, and the removal completes without a hitch: both elements are
permanently gone. This does not contradict the previous two measurements; it requires reading
the rule more accurately. The rule is not "do not change the collection" — it is "do not
change the collection **from outside the iterator.**" When `Iterator.remove()` is called, the
iterator updates the container's internal counter (`modCount`) itself, and at the same time
advances its own expected value too; at the next `next` call the two are still equal, and the
check passes silently. The list's own `remove`, by contrast, does not know about this second
counter and only changes the first; the difference surfaces at the next `next` call. The
fail-fast mechanism does not ask "did the container change," it asks "did the container change
**through a path the iterator does not know about.**"

This distinction also explains why the rule exists. An iterator tracks which element of the
container it is sitting on through an internal position (an index in array-based containers, a
node reference in linked structures). When the container changes from outside, this internal
position can become invalid — if an element has been removed, the next `next` call can land
either on the wrong element or on a position that no longer exists at all. Because
`Iterator.remove()` updates its own record itself, this danger never arises at all; a `remove`
coming from outside, though, shifts the position without the iterator's knowledge and leaves
it inconsistent. Fail-fast is a design decision that prefers stopping by name over silently
landing on the wrong element in the face of this inconsistency.

This confirms, once again, the three lessons' shared conclusion. The rule does not forbid a
behavior — if it set a blunt prohibition like "do not change the collection," then
`Iterator.remove()` itself would always throw an exception too, yet it does not. The rule
separates **who is informed**: a change coming through the iterator's own path is an informed
change, a change coming through any other path of the container is not. Two calls that
produce the same result (an element permanently removed) are, from the iterator's point of
view, two entirely separate events.

All three answers are internally consistent, but none gives the guarantee the other gives.
Fail-fast says "stop rather than work incorrectly"; snapshot says "whatever the iteration
sees, it is shown a consistent view"; silent continuation says "I will never stop, but I make
no promise about the list of what I saw." All three are legitimate promises, and all three sit
outside the `Iterator` interface, as the chosen class's own decision — the interface only
guarantees the `hasNext`/`next` pair, it never speaks about what will happen to the container
changing during iteration.

## Summary

- The iterator's basic promise — `NoSuchElementException` once `next` is exhausted — came out
  byte-for-byte identical across all three implementations; this is the `Iterator` interface's
  direct promise and does not break as long as the container never changes.
- When the container changed during iteration, three separate answers were measured across
  four implementations: `ArrayList` and `HashSet` fail fast and by name, `CopyOnWriteArrayList`
  stays on the copy taken at the moment iteration started and never sees the change,
  `ConcurrentHashMap` continues without throwing an exception but does not guarantee which
  elements it will see.
- Fail-fast is not a guarantee, it is a best effort: the same removal throws an exception on
  the middle element, but on the second-to-last element it finishes the loop one element short
  with no warning at all — the result depends on where the removed position sits in the list.
- Bounding measurement: the iterator's own `remove` does not break the same rule, because the
  rule is not "do not change," it is "do not change from outside the iterator";
  `Iterator.remove()` updates the container's internal counter and the iterator's expected
  value at the same time.
- None of the three answers is written in the `Iterator` interface; which one is chosen is
  decided only by the chosen class, and all three are internally consistent, independent
  promises.

## Next Step

This lesson measured the container **changing** during iteration; what happens, though, when
the container never changes and only the iteration **order** is in question? In every
measurement so far, a container's own implementation decided which order its own elements
would be given in. The next lesson moves this decision outside the container: the ordering
rule becomes an object the caller writes, independent of the container, and whether that
object is consistent becomes a separate question that has to be tested before the container
itself. The pattern seen in this lesson continues there too: a rule can be written, can
compile, can give the correct result on most inputs, and still fall silently into wrong
behavior on a particular input.
