Iteration Patterns
For-each, explicit iterators, ListIterator, streams, and the unwritten rules about modifying a collection while iterating it
There is more than one way to walk a collection in Java, and the right one depends on what you actually need: just read each element? remove some as you go? insert new ones? walk backwards? process the whole thing as a pipeline?
This chapter is a tour of the five iteration patterns you'll use every week, plus the single most common bug each one introduces.
1. Enhanced for-each
The for-each loop is sugar over an Iterator. It is the right tool
for "do something with each element, in order":
The compiler rewrites for (E x : c) into roughly:
Iterator<E> it = c.iterator();
while (it.hasNext()) {
E x = it.next();
/* body */
}That has one immediate consequence: you cannot remove elements
from the collection inside a for-each loop. Doing so triggers
ConcurrentModificationException on the next it.hasNext().
2. Explicit Iterator and Iterator.remove()
When you need to remove while iterating, use the explicit Iterator
form so you can call it.remove() — the only safe way to mutate
during iteration:
Even better, Java 8 added removeIf on every Collection, which is
the modern one-liner:
3. ListIterator: bidirectional, plus add()
Lists support listIterator(), which is Iterator with
superpowers: you can walk backwards (hasPrevious, previous),
replace the current element (set), and insert a new element at
the cursor (add):
ListIterator is what makes "edit in place" patterns possible on
list-backed structures.
4. forEach with a Consumer
Every Iterable has a default forEach(Consumer<? super E>). It
reads naturally when the action is a simple, terminal side effect:
forEach does not let you stop early — that's a for with a
break's job. And, as with the enhanced for, you cannot modify the
collection from inside the action.
5. Streams: iteration as a pipeline
Streams are the declarative form of iteration. You describe what
should happen to each element (filter, map, sorted,
distinct, flatMap, ...) and end with a terminal operation
(toList, count, reduce, forEach, ...). The library decides
how.
The same logic with a manual loop is twice as long and twice as mistake-prone:
Use a stream when the loop body is a clear pipeline of filter → transform → collect. Use an explicit loop when the body has real control flow (early returns, complex branching, state across iterations).
ConcurrentModificationException: the real villain
The exception's name is misleading: it has nothing to do with
threads. It is raised by Iterator when it notices the structural
modification count of the collection changed since the iterator was
created — i.e. you added or removed something behind its back.
The fix is always one of:
- Use the iterator's own
remove()(orremoveIffor whole-collection deletes). - Collect what you want to remove during iteration, then remove afterwards.
- Iterate a copy:
for (E x : new ArrayList<>(xs)) { xs.remove(...); }.
Multi-file example: filter a feed with three techniques
Three idioms, same output — and the stream version is the easiest to read in five months' time.
Practice
Complete Pruner.removeShorterThan(List<String> words, int minLen) so it removes every word shorter than minLen without triggering ConcurrentModificationException. Use either Iterator.remove() or removeIf.
Expected output:
[alpha, gamma, delta]
Test your understanding
Why does this loop throw ConcurrentModificationException?
for (Integer n : list) if (n % 2 == 0) list.remove(n);
Because list.remove(Object) is O(n)
Because the enhanced-for uses an Iterator, and modifying the underlying list via list.remove invalidates the iterator's internal modCount
Because list is unmodifiable
Because integers are autoboxed
Which iteration form lets you insert new elements at the cursor while walking a List?
Enhanced for-each
forEach(Consumer)
Plain Iterator
ListIterator
When is a stream pipeline the better choice over an explicit loop?
Always, because streams are faster
When the body needs break or return
When the body is a straightforward pipeline of filter / transform / collect with no early exit
When you need to mutate the source collection