00:00

Iterator Design Pattern in Java

Last updated:

The iterator design pattern in Java gives clients a standard way to walk through the elements of a collection one by one without seeing how the collection stores them. The collection hands out an iterator object with hasNext() and next(), and the client loops with it, whether the data lives in an array, a tree or a database table.

It matters because exposing internal structure is fragile. If callers loop over a raw array, they must know its size field and its empty slots, and every change to the storage breaks them. Java builds this pattern into the language through Iterable, Iterator and the for-each loop, so knowing how to write good iterators pays off daily. It is a behavioral design pattern.

1. What Is the Iterator Design Pattern?

The GoF intent is: "Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation."

The pattern separates two jobs: storing elements, which is the collection's job, and traversing them, which is the iterator's job. The collection, called the aggregate, creates an iterator. The iterator keeps its own position, a cursor, and answers two questions: is there another element, and what is it?

Because each iterator has its own cursor, several traversals can run at once, and one collection can offer several orders, such as all elements, only pending ones, depth-first or breadth-first. In Java, any class implementing Iterable<T> works in a for-each loop. The benefit is encapsulation: storage can change from an array to a tree to a database without touching any caller.

2. Key Components

ComponentResponsibilityIn the Basic Example
IteratorInterface for traversal: hasNext(), next()java.util.Iterator<Stop>
Concrete IteratorTracks the position and knows how to reach the next elementRouteIterator
AggregateInterface that creates iteratorsjava.lang.Iterable<Stop>
Concrete AggregateStores the elements and returns concrete iteratorsDeliveryRoute
ClientLoops using only the iterator interfaceThe for-each loops in main

The client asks the aggregate for an iterator, then calls hasNext() and next() until it is done. The concrete iterator is usually an inner class, so it can read the aggregate's private fields while the client cannot.

3. When Is Iterator Useful?

  • Custom collections: a delivery route stored in an array that callers should loop over without touching the array.
  • Non-linear structures: a product category tree walked depth-first for a menu or breadth-first for a level view.
  • Data too large for memory: years of bank transactions read batch by batch from a database while the caller sees one simple sequence.

4. UML Class Diagram

The diagram shows the Basic Example.

Iterator UML class diagram DeliveryRoute implements Iterable and creates RouteIterator, which implements Iterator. The Client uses only the Iterable and Iterator interfaces. «interface» Iterable<Stop> + iterator(): Iterator<Stop> «interface» Iterator<Stop> + hasNext(): boolean + next(): Stop Client implements implements DeliveryRoute - stops: Stop[] - size, modCount: int + add(stop) + iterator(): Iterator<Stop> + pendingStops(): Iterable<Stop> private inner class RouteIterator - cursor: int - expectedModCount: int «create»
Iterator: the client depends only on Iterable and Iterator; DeliveryRoute creates its own RouteIterator, which reads the private array.
  1. Iterable and Iterator are the JDK interfaces that play the GoF Aggregate and Iterator roles.
  2. DeliveryRoute implements Iterable, so it works in a for-each loop.
  3. RouteIterator implements Iterator and is a private inner class: clients never see its type, only the interface.
  4. The route creates the iterator («create»); the iterator remembers modCount so it can detect changes.

5. Step-by-Step Execution Flow

  1. The client writes for (Stop stop : route). The compiler turns this into a call to route.iterator().
  2. The route creates a RouteIterator with its cursor at 0 and a copy of the current modCount.
  3. The loop calls hasNext(). The iterator checks for changes, skips elements that do not match its filter, and returns true if one remains.
  4. The loop calls next(), which returns the element and advances the cursor.
  5. If the route is modified mid-loop, modCount no longer matches, and the next call throws ConcurrentModificationException.

6. Real-World Use Cases

  • E-commerce: walking category trees, cart lines and search results without exposing their storage.
  • Payments and banking: streaming transactions for statements, reconciliation and fraud scans, batch by batch.
  • Notifications: iterating over recipients drawn from several sources as one sequence.
  • The JDK: every collection's iterator(); ArrayList and HashMap iterators are fail-fast, ConcurrentHashMap iterators are weakly consistent, and CopyOnWriteArrayList iterators read a snapshot. Spliterator (Java 8+) extends the idea for streams and parallel splitting, and Java 21's SequencedCollection.reversed() offers reverse traversal.
  • Spring and JDBC: a JDBC ResultSet is a database cursor; Spring Data repository methods can return a Stream<T> backed by such a cursor, and Spring Data 3.1+ supports keyset scrolling with Window and ScrollPosition.

7. Problem Without the Pattern

A delivery route that exposes its array:

RouteWithoutIterator.java
import java.util.Arrays;

public class RouteWithoutIterator {

    record Stop(String address, boolean delivered) { }

    static final class DeliveryRoute {
        Stop[] stops = new Stop[4];      // exposed so callers can loop over it
        int size = 0;

        void add(Stop stop) {
            if (size == stops.length) stops = Arrays.copyOf(stops, size * 2);
            stops[size++] = stop;
        }
    }

    public static void main(String[] args) {
        DeliveryRoute route = new DeliveryRoute();
        route.add(new Stop("12 MG Road", true));
        route.add(new Stop("4 Park Street", false));
        route.add(new Stop("88 Lake View", false));

        // Every caller must know the internal array, the size field and the null slots
        int pending = 0;
        for (int i = 0; i < route.size; i++) {
            if (!route.stops[i].delivered()) pending++;
        }
        System.out.println("Pending stops: " + pending);

        // Another caller forgets the size field and walks into empty slots
        try {
            for (Stop stop : route.stops) {
                System.out.println(stop.address());
            }
        } catch (NullPointerException e) {
            System.out.println("Crashed: the array has empty slots beyond 'size'");
        }
    }
}
Console
Pending stops: 2
12 MG Road
4 Park Street
88 Lake View
Crashed: the array has empty slots beyond 'size'
  • Broken encapsulation: callers depend on the array, the size field and the growth strategy.
  • Easy mistakes: one caller forgot size and crashed on an empty slot.
  • Duplicated loops: every caller rewrites the same index logic and filters.
  • No protection: callers can modify the array directly.

8. Solution Using the Pattern

Make the array private, implement Iterable<Stop>, and return an iterator that knows about size. Add a second method, pendingStops(), that returns a filtered traversal over the same data.

Callers now write plain for-each loops and cannot touch the array. The empty-slot crash is gone, because only the iterator handles indexes, and the storage can change without affecting any caller. The iterator also detects modification during a loop instead of silently skipping elements.

9. Iterator Design Pattern in Java: Code Examples

Each example is a single file that runs on Java 21 with java FileName.java.

Basic Example: Delivery Route with Its Own Iterators

Problem: fix section 7 so that callers loop over stops safely, with a second filtered traversal and protection against changes mid-loop.

DeliveryRouteDemo.java
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.function.Predicate;

public class DeliveryRouteDemo {

    record Stop(String address, boolean delivered) { }

    // Aggregate: hides its array and hands out iterators instead
    static final class DeliveryRoute implements Iterable<Stop> {
        private Stop[] stops = new Stop[4];
        private int size = 0;
        private int modCount = 0;                          // changes on every structural modification

        void add(Stop stop) {
            if (size == stops.length) stops = Arrays.copyOf(stops, size * 2);
            stops[size++] = stop;
            modCount++;
        }

        @Override
        public Iterator<Stop> iterator() {
            return new RouteIterator(stop -> true);
        }

        // A second traversal over the same data, with a filter
        Iterable<Stop> pendingStops() {
            return () -> new RouteIterator(stop -> !stop.delivered());
        }

        // Concrete iterator: an inner class, so it can read the private array
        private final class RouteIterator implements Iterator<Stop> {
            private final Predicate<Stop> filter;
            private final int expectedModCount = modCount;
            private int cursor = 0;

            RouteIterator(Predicate<Stop> filter) { this.filter = filter; }

            @Override
            public boolean hasNext() {
                checkForChanges();
                while (cursor < size && !filter.test(stops[cursor])) cursor++;   // skip non-matching stops
                return cursor < size;
            }

            @Override
            public Stop next() {
                if (!hasNext()) throw new NoSuchElementException();
                return stops[cursor++];
            }

            private void checkForChanges() {
                if (modCount != expectedModCount) throw new ConcurrentModificationException("route changed during iteration");
            }
        }
    }

    public static void main(String[] args) {
        DeliveryRoute route = new DeliveryRoute();
        route.add(new Stop("12 MG Road", true));
        route.add(new Stop("4 Park Street", false));
        route.add(new Stop("88 Lake View", false));
        route.add(new Stop("7 Hill Lane", true));
        route.add(new Stop("31 Station Road", false));      // forces the array to grow

        System.out.println("All stops:");
        for (Stop stop : route) {
            System.out.println("  " + stop.address() + (stop.delivered() ? " (done)" : ""));
        }

        System.out.println("Pending stops:");
        for (Stop stop : route.pendingStops()) {
            System.out.println("  " + stop.address());
        }

        try {
            for (Stop stop : route) {
                route.add(new Stop("Urgent: 2 Bank Street", false));   // modifying while iterating
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("Fail-fast: " + e.getMessage());
        }
    }
}

Important classes and methods

  • RouteIterator is a private inner class: it reads the private array, and clients only see Iterator<Stop>.
  • pendingStops() returns a lambda; Iterable has a single abstract method, so a lambda can implement it.
  • modCount and expectedModCount make the iterator fail-fast, as the JDK's ArrayList iterator does.
  • next() throws NoSuchElementException past the end, as the Iterator contract requires.

Expected output

Console
All stops:
  12 MG Road (done)
  4 Park Street
  88 Lake View
  7 Hill Lane (done)
  31 Station Road
Pending stops:
  4 Park Street
  88 Lake View
  31 Station Road
Fail-fast: route changed during iteration

How it runs

  1. The fifth stop makes the array grow, which callers never notice.
  2. The filtered iterator skips delivered stops inside hasNext().
  3. Adding a stop during a loop changes modCount, so the next hasNext() throws.

10. Best Practices and Common Mistakes

Best practices

  • Implement Iterable so your collection works with for-each and, through spliterator(), with streams.
  • Honour the contract: hasNext() has no visible side effects and can be called repeatedly; next() throws NoSuchElementException at the end.
  • Make resource-backed iterators AutoCloseable and use them in try-with-resources.
  • Thread safety: iterators are not thread-safe. Give each thread its own, and choose the collection type deliberately: fail-fast, weakly consistent or snapshot.

Common mistakes

  • Removing from a collection inside a for-each loop, which triggers ConcurrentModificationException.
  • Returning null instead of throwing at the end of the sequence.
  • Leaking cursors by never closing database-backed iterators.
  • Overengineering: writing a custom iterator when returning an unmodifiable List view would do.

11. When to Use and When Not to Use

Use it when you design your own data structure, need several traversal orders, want lazy or streaming access to large or remote data, or must hide how elements are stored.

Avoid writing one when a standard collection already fits, since the JDK's iterators are well tested. Also avoid it when callers need random access by index, which a list serves better, or when an internal iteration method such as forEach() or a stream expresses the job more clearly.

12. Advantages and Disadvantages

Advantages

  • Encapsulation: storage can change without breaking callers.
  • One loop style for arrays, trees and databases.
  • Multiple independent traversals at the same time.
  • Laziness: work happens one element at a time.

Disadvantages

  • Extra classes for simple collections.
  • Sequential only: no random access, and reverse traversal needs its own iterator.
  • Concurrency surprises if the collection changes during iteration.

13. Comparison with Similar Patterns

AspectIteratorVisitorCompositeStream API
PurposeWalk elements one by oneApply operations to elements of different typesModel part-whole treesDeclarative pipeline over elements
Who drives the loopThe client (external iteration)The structure or the visitorNot applicableThe library (internal iteration)
Can stop earlyYes, just stop calling next()Usually notNot applicableYes, with short-circuit operations
Typical pairingWalks a compositeVisits a compositeTraversed by iteratorsBuilt on a Spliterator

Choose an Iterator when the client controls the loop, a Visitor when you add many operations over a stable structure of different node types, and the Stream API when a declarative pipeline reads better. A Composite tree is the classic structure that needs iterators. The Adapter tutorial shows how to turn a paginated API into an Iterable.

14. Summary

  • The iterator design pattern in Java gives sequential access to elements without exposing how they are stored.
  • Purpose: encapsulate traversal and give every collection the same loop interface.
  • Components: Iterator, Concrete Iterator, Aggregate (Iterable), Concrete Aggregate, Client.
  • Each iterator keeps its own cursor; hasNext() and next() walk the elements.
  • Used for custom collections, tree traversal, database cursors and the whole JDK collections framework.
  • Benefits: encapsulation, laziness and multiple traversal orders.
  • Honour the contract, close resource-backed iterators, and never modify a collection mid-loop except through the iterator.

15. Java Design Pattern Interview Questions and Answers

Q1. What is the difference between fail-fast and weakly consistent iterators?

Fail-fast iterators, such as those of ArrayList and HashMap, throw ConcurrentModificationException on a best-effort basis when the collection is changed structurally during iteration, other than through the iterator itself. Weakly consistent iterators, such as those of ConcurrentHashMap, never throw; they may or may not reflect changes made after they were created. CopyOnWriteArrayList iterators read a snapshot and never see later changes.

Q2. How do you safely remove elements while iterating over a list?

Use the iterator's own remove(), which updates the expected modification count, or simply removeIf() (Java 8+). Calling list.remove() inside a for-each loop throws ConcurrentModificationException.

orders.removeIf(order -> order.status() == Status.CANCELLED);

Iterator<Order> it = orders.iterator();
while (it.hasNext()) {
    if (it.next().isExpired()) it.remove();   // safe
}
Q3. What is the difference between external and internal iteration?

With external iteration, the client drives the loop by calling hasNext() and next(), so it controls when to stop or pause. With internal iteration, such as forEach() or streams, the collection drives the loop and the client supplies a function. Internal iteration lets the library optimise, for example by running in parallel, while external iteration gives finer control.

Q4. A Spring Data repository method returns Stream<Payment>. What must the caller do?

Consume it inside a transaction and close it, ideally with try-with-resources, because the stream is backed by an open database cursor and connection. Forgetting to close it leaks connections from the pool, and using it outside a transaction fails or behaves unpredictably.

Q5. What does a Spliterator add beyond an Iterator?

A Spliterator (Java 8+) can traverse elements, and can also split itself into two parts so a parallel stream can process them on different threads. It reports characteristics such as ORDERED, SIZED and NONNULL that let the stream library optimise. Every Iterable gets a default, non-splitting implementation through spliterator().