Iterators and Iterable

What for-each compiles to, the modCount check behind ConcurrentModificationException, and how to write a class the language can loop over.

6 min read🗂️ Collections and Generics

for (String s : list) is not a loop over a list. It is sugar for an Iterator, and every surprising thing about iteration — why removing an element throws, why it sometimes does not, why a stream can only be consumed once — comes from that one substitution. This lesson is the two interfaces, what the compiler writes for you, and the check that decides whether your loop throws.

What for-each becomes

java
for (String s : names) { … }

compiles to exactly this:

java
for (Iterator<String> it = names.iterator(); it.hasNext(); ) {
    String s = it.next();

}

Two consequences fall straight out. You have no index — the loop variable is a value, not a position. And you have no handle on the iterator, which is why you cannot remove through it without rewriting the loop.

Over an array the compiler emits an indexed loop instead, because an array has no iterator(). Same syntax, different code.

The two interfaces

java
interface Iterable<T> { Iterator<T> iterator(); }
 
interface Iterator<T> {
    boolean hasNext();
    T next();
    default void remove() { throw new UnsupportedOperationException(); }
}

That is all for-each requires: anything with an iterator() can be looped over, including your own types.

java
record Page<T>(List<T> items) implements Iterable<T> {
    @Override public Iterator<T> iterator() { return items.iterator(); }
}

remove() is a default that throws, which is why Arrays.asList(...).iterator().remove() and List.of(...).iterator().remove() both fail: the iterator exists, the operation is not supported.

modCount, and the exception it produces

Every modifiable collection keeps a modCount — a counter incremented on every structural change. An iterator records it at creation and checks it on every next():

iterator()next() → alist.remove(a)hasNext()next() → throw
list[a, b, c]modCount0expected0cursor0

iterator(). The iterator snapshots the list's modCount into its own expectedModCount. Nothing else is copied.

1 / 5

Two things that diagram is there to fix.

The name is wrong. ConcurrentModificationException needs no second thread. One thread modifying a collection while iterating it is enough, and that is the overwhelmingly common case.

hasNext() does not check. It compares the cursor to the size, so removing the second-to-last element makes hasNext() return false and the loop exits cleanly, with an element skipped and no exception. The collections lesson's first exercise is exactly this: same code, different element, different outcome. The check is best-effort by specification — it is a bug detector, not a guarantee.

Removing safely

Three ways, in the order to prefer them:

java
names.removeIf(n -> n.isBlank());              // 1. says what it means
 
Iterator<String> it = names.iterator();        // 2. when the condition needs state
while (it.hasNext()) {
    if (it.next().isBlank()) it.remove();      //    removes through the iterator
}
 
var kept = names.stream()                      // 3. when you want a new collection
        .filter(n -> !n.isBlank()).toList();

it.remove() works because the iterator updates its own expectedModCount after removing — it is the one path that keeps the two counters in step. It also must be called exactly once per next(), and throws IllegalStateException otherwise.

Under the hood: where streams come from

Iterable has a second method, added in Java 8:

java
default Spliterator<T> spliterator() { … }

A Spliterator is an iterator that can split: trySplit() hands back a piece for another thread and keeps the rest. That is what parallelStream() divides work with, and why a collection that knows its size and structure — ArrayList, arrays — parallelises well while a LinkedList does not: splitting it means walking it.

It also carries characteristics — SIZED, ORDERED, DISTINCT, SORTED — which the stream pipeline uses to skip work. List.of(1,2,3).stream().count() never touches an element, because SIZED means the answer is already known. That is the same mechanism behind the streams lesson's exercise where peek prints nothing.

Walkthrough: the loop that skipped every other element

A cleanup job removed expired sessions:

java
for (int i = 0; i < sessions.size(); i++) {
    if (sessions.get(i).isExpired()) sessions.remove(i);
}

No exception — an indexed loop has no iterator and no modCount check. It simply left half the expired sessions behind: removing index i shifts everything down, the next iteration reads i + 1, and the element that moved into i is never examined.

It had run nightly for two years and the count of expired sessions had been slowly climbing, which nobody read as a bug because it also climbed with traffic.

sessions.removeIf(Session::isExpired) is the whole fix. The indexed version needs to iterate backwards to be correct, which is the tell that the loop is fighting the data structure.

Try it yourself

Why does this one not throw?

java
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) if (s.equals("b")) list.remove(s);
System.out.println(list);
Answer

[a, c], and no exception. Removing "b" leaves size 2 with the cursor at 2, so hasNext() — which only compares cursor to size — returns false and the loop ends before next() can check modCount. Remove "a" instead and it throws. Same code, different element: the check is best-effort, and relying on it to catch your mistake is relying on where the element happened to be.

What does remove() need?

java
Iterator<String> it = list.iterator();
it.remove();
Answer

IllegalStateException. remove() removes the element returned by the last next(), and there has not been one. The same exception comes from calling it twice in a row. The iterator tracks a lastRet index and sets it to −1 after removing, which is the state being checked.

Can you loop over it twice?

java
Iterable<String> once = () -> List.of("a", "b").iterator();
for (String s : once) { }
for (String s : once) { }      // ?
Answer

Both loops work, because iterator() is called afresh each time and this lambda returns a new iterator on every call. Write it to return a stored iterator and the second loop sees an exhausted one and does nothing — silently. That is the difference between an Iterable and an Iterator, and why a Stream, which is consumed once, is not an Iterable.

Misconceptions

  • "ConcurrentModificationException means threads." It means modCount moved. One thread is the usual case.
  • "If my loop does not throw, it is correct." hasNext() does not check, so removing near the end exits cleanly with elements skipped.
  • "for-each is slower than an indexed loop." On an ArrayList the JIT produces effectively the same code. On a LinkedList the indexed loop is O(n²) and the for-each is O(n).
  • "I can remove with list.remove() inside a for-each if I break straight after." You can, and it works, and it is a rule about control flow that the next person to edit the loop will not know.

Going deeper

  • ArrayList.Itr in the JDK source — forty lines, and checkForComodification is three of them.
  • Spliterator's javadoc on characteristics, for what the stream pipeline is allowed to skip.
  • Effective Java item 46, on preferring for-each, and the three cases where it says not to.
Progress is saved on this device and to your account when signed in.