$ emrebener

Iterator Pattern

author: emre bener read time: 7 min about: iterator pattern, java repository: https://github.com/Emrebener/GoF-Design-Patterns-Behavioural
published: updated: mentions: software design pattern, behavioral pattern, iterator, java collections framework, design patterns

1. What the iterator pattern is

The Iterator pattern gives a caller sequential access to the elements of an aggregate object without exposing how that aggregate stores them. The aggregate might be an array, a linked list, a hash table, or a tree; the iterator presents a uniform hasNext() / next() cursor over it, and the caller never learns which.

The Gang of Four put it in the behavioural bucket because the load-bearing decision is a runtime responsibility question: who owns the traversal cursor. Keep the cursor on the collection and the collection can only be traversed one way at a time. Move it into a separate object, and traversal state lives apart from the data, so several traversals can run over the same aggregate at once.

That separation is the whole pattern. The rest is mechanics.

2. The aggregate and the iterator

The pattern has exactly two roles. The aggregate is the collection; its single job here is to hand out iterators. The iterator is a separate object that holds the traversal cursor and knows how to advance it. In the GoF book the aggregate exposes createIterator(), and the iterator exposes first(), next(), isDone(), and currentItem().

The reason the iterator is a separate object, rather than a few methods bolted onto the collection, comes down to where the cursor lives:

  • Concurrent traversals. Two nested loops over the same list need two independent cursors. If the cursor were a field on the collection, the inner loop would clobber the outer one’s position.
  • Swappable traversal order. A tree can be walked in-order, pre-order, or breadth-first. Each is a different iterator class over the same unchanged aggregate. The traversal algorithm becomes a thing you pick rather than something the collection hard-codes.
  • A uniform interface across unlike collections. Code written against the iterator interface walks an array and a hash table with identical syntax, even though their internal layouts share nothing.

Keep those three payoffs in mind; they decide, later, whether reaching for the pattern buys you anything.

One aggregate, two independent iteratorsIterator Acursor → AIterator Bcursor → EAggregate (shared collection)ABCDEEach iterator owns its own cursor; the collection itself holds none,so two independent traversals never collide.One aggregate, two independent iteratorsIterator Acursor → AIterator Bcursor → EAggregate (shared collection)ABCDEEach iterator owns its own cursor; the collection itself holds none,so two independent traversals never collide.

3. The pattern in Java: Iterable, Iterator, and for-each

Java ships the pattern as two interfaces in java.util and java.lang, and the GoF roles map onto them almost one to one.

Iterable<T> is the aggregate. It has one method, iterator(), which is the GoF createIterator() under a different name: each call returns a fresh, independent cursor.

Iterator<T> is the iterator. It collapses the GoF’s four methods into two, plus an optional mutator:

public interface Iterator<T> {
    boolean hasNext();           // the inverse of GoF isDone()
    T next();                    // GoF currentItem() + next() in one call
    default void remove() {      // optional; not in the GoF iterator
        throw new UnsupportedOperationException("remove");
    }
}

There is no first(). A Java iterator starts positioned before the first element and is single-use; to restart, you ask the Iterable for a new one. next() does double duty: it returns the current element and advances the cursor, which is why a correct loop calls next() exactly once per iteration.

The for-each loop is pure syntax sugar over Iterable. This:

for (String name : names) {
    System.out.println(name);
}

compiles to essentially this:

Iterator<String> it = names.iterator();
while (it.hasNext()) {
    String name = it.next();
    System.out.println(name);
}

Any type that implements Iterable is for-each-compatible for free. That’s the practical reason to implement the interface rather than invent your own traversal API: you inherit the language’s loop syntax.

3.1. ListIterator and bidirectional traversal

The GoF iterator only moves forward. Java’s ListIterator<T>, returned by List.listIterator(), is the bidirectional variant: it adds hasPrevious() and previous(), reports nextIndex() / previousIndex(), and can mutate during traversal with set() and add().

ListIterator<String> it = list.listIterator(list.size()); // start at the end
while (it.hasPrevious()) {
    System.out.println(it.previous());
}

ListIterator only exists for List, because backward traversal needs an ordered, index-addressable structure. A HashSet has no “previous”. That constraint is itself a small lesson — the iterator interface a collection can offer is bounded by what its internal structure supports.

4. Writing a custom iterator

You implement the pattern by making your type Iterable and returning an Iterator whose fields hold the traversal state. A binary tree is the textbook case, because in-order traversal genuinely cannot be expressed as an index, and the cursor state has to live somewhere.

The aggregate is small. It owns the nodes and hands out iterators; it knows nothing about traversal order:

public class BinaryTree<T> implements Iterable<T> {
    static final class Node<T> {
        final T value;
        Node<T> left;
        Node<T> right;

        Node(T value) {
            this.value = value;
        }
    }

    private Node<T> root;

    public void setRoot(Node<T> root) {
        this.root = root;
    }

    @Override
    public Iterator<T> iterator() {
        return new InOrderIterator<>(root);
    }
}

The iterator carries the cursor. In-order traversal (left subtree, node, right subtree) needs an explicit stack, because after visiting a node you have to remember every ancestor you still owe a visit. That stack is the cursor, and it lives on the iterator, not the tree:

public final class InOrderIterator<T> implements Iterator<T> {
    private final Deque<BinaryTree.Node<T>> stack = new ArrayDeque<>();

    InOrderIterator(BinaryTree.Node<T> root) {
        pushLeftSpine(root);
    }

    private void pushLeftSpine(BinaryTree.Node<T> node) {
        for (; node != null; node = node.left) {
            stack.push(node);
        }
    }

    @Override
    public boolean hasNext() {
        return !stack.isEmpty();
    }

    @Override
    public T next() {
        if (stack.isEmpty()) {
            throw new NoSuchElementException();
        }
        BinaryTree.Node<T> node = stack.pop();
        pushLeftSpine(node.right);
        return node.value;
    }
}

In-order traversal: the stack is the cursor42513The iterator's stack at each step (top of stack drawn uppermost):constructnext() → 1next() → 2next() → 3next() → 4next() → 5124243445emptynext() pops the top node and pushes the left spine of its right child.The stack is the cursor; it lives on the iterator, not the tree.In-order traversal: the stack is the cursor42513The iterator's stack at each step (top of stack drawn uppermost):constructnext() → 1next() → 2next() → 3next() → 4next() → 5124243445emptynext() pops the top node and pushes the left spine of its right child.The stack is the cursor; it lives on the iterator, not the tree.

Two things are worth pulling out. The tree exposes no node references and no traversal methods, yet a for (T v : tree) loop walks every element in in-order sequence — the representation stays hidden, which is §1’s promise made concrete. And the cursor is the stack field, so two for-each loops over the same tree get two InOrderIterator instances with two independent stacks and neither disturbs the other.

Swapping traversal order means swapping the iterator class, nothing else. A PreOrderIterator over the same BinaryTree keeps its own stack and pushes right then left in next(); the tree and its nodes are untouched. That’s the §2 payoff — traversal as a thing you pick — falling straight out of the design.

5. Fail-fast iteration and ConcurrentModificationException

Most JDK collection iterators are fail-fast: if the collection is structurally modified while an iterator is live, the next next() or hasNext() throws ConcurrentModificationException rather than returning a quietly corrupt result.

The mechanism is a modification counter. ArrayList, via its AbstractList superclass, keeps an int modCount that every add and remove increments. When you ask for an iterator, it snapshots that counter, and checks the snapshot on each step:

// inside ArrayList's iterator, simplified
int expectedModCount = modCount;

final void checkForComodification() {
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

So this loop throws on the next step after the remove: it bumped modCount, and the iterator’s snapshot is now stale:

for (String name : names) {
    if (name.isBlank()) {
        names.remove(name);   // ConcurrentModificationException
    }
}

The fix is to mutate through the iterator. Iterator.remove() removes the last element returned by next(), and crucially it updates both modCount and the iterator’s expectedModCount, so the snapshot stays valid:

Iterator<String> it = names.iterator();
while (it.hasNext()) {
    if (it.next().isBlank()) {
        it.remove();          // safe: keeps the iterator's snapshot in sync
    }
}

Two caveats. Fail-fast is best-effort, not a guarantee: it exists to catch bugs, not to provide thread-safe semantics, and the JDK explicitly says you can’t depend on the throw. Not every iterator is fail-fast either — the set of concurrent collections that ship with the JDK, including CopyOnWriteArrayList and ConcurrentHashMap, return weakly consistent iterators that tolerate concurrent modification and may or may not reflect it. Picking between the two is a collection-choice decision, not an iteration one.

Fail-fast: modCount snapshot vs. live counterlist.iterator()modCount = 0, expected = 0list.remove(x)modCount = 1, expected = 0it.next()checkForComodification(): 1 ≠ 0throwConcurrentModificationExceptionin syncmodCount drifts aheadmismatch detectedThe iterator snapshots modCount when created. A structural change bumps thelive counter, and the next call sees the mismatch and fails fast.Fail-fast: modCount snapshot vs. live counterlist.iterator()modCount = 0, expected = 0list.remove(x)modCount = 1, expected = 0it.next()checkForComodification(): 1 ≠ 0throwConcurrentModificationExceptionin syncmodCount drifts aheadmismatch detectedThe iterator snapshots modCount when created. A structural change bumps thelive counter, and the next call sees the mismatch and fails fast.

6. External vs internal iteration, and when not to write one

This section answers two questions: which of Java’s two iteration styles to use, and when not to implement the pattern at all. The short answer to the second is “rarely”: most code consumes an iterator rather than writing one. Take the styles first. Everything so far has been external iteration: the caller drives the loop, pulling one element at a time with hasNext() and next(). Java 8 added internal iteration, where you hand a function to the collection and it runs the loop:

// external: the caller drives the loop
int total = 0;
for (Order o : orders) {
    if (o.isOverdue()) {
        total += o.amount();
    }
}

// internal: the Stream drives the loop
int total = orders.stream()
    .filter(Order::isOverdue)
    .mapToInt(Order::amount)
    .sum();

Internal iteration is still the Iterator pattern at heart (a Stream is consumed by an underlying Spliterator, the parallel-friendly cousin of Iterator), but the cursor is fully hidden and the library is now free to fuse the stages, short-circuit, or parallelise. External iteration keeps that control with you, which you want when the loop has side effects, early returns tied to surrounding code, or stateful logic that does not decompose into stream stages.

When should you implement the pattern yourself? Four signals that you shouldn’t:

  • The type is already a JDK collection. ArrayList, HashMap and friends are Iterable out of the box. Writing your own iterator over them is duplicating the JDK.
  • An index loop says it more plainly. If the structure is a flat array or a List and the traversal is “every element, front to back,” a plain for (int i = ...) is clearer than a custom iterator. The pattern earns its keep when the cursor state is non-trivial — like the tree’s stack in §4 — not when it’s a single int.
  • A Stream already expresses the traversal. Filtering, mapping, and reducing are internal-iteration jobs. Reach for a custom Iterator only when the consumer needs the pull-based control external iteration gives.
  • You only need one traversal, used once, in one place. The pattern buys concurrent cursors, swappable order, and a uniform interface. If you need none of the three, you need a loop, not a pattern.

Where it does pay off is exactly the §4 shape: a type whose internal representation should stay hidden, whose traversal is genuinely non-obvious, or which wants to offer several traversal orders. Implement Iterable, put the cursor state on the returned Iterator, and the for-each loop, the enhanced syntax, and every algorithm written against Iterable come along for free.