Skip to main content

Iterator Design Pattern in Java: Complete Guide with Examples

The Iterator pattern provides a uniform way to traverse a collection without exposing its internal structure. Complete Java guide: custom BookShelf iterator, filtered decade iterator, how Java’s for-each loop works under the hood, Iterable vs Iterator, and when to write a custom iterator vs using Streams.

You have a BookShelf that stores books in an ArrayList internally. You want callers to be able to iterate over books without knowing it’s an ArrayList. Next month you might change it to a linked list or a tree structure sorted by author. If callers work directly with your internal list, every change to your storage structure breaks them. The Iterator pattern gives callers a stable traversal interface that survives any change to the underlying data structure.

This is one of the patterns you already use every day through Java’s for-each loop. Every class that implements Iterable<T> is participating in this pattern. Understanding the mechanics helps you write cleaner custom collections and explains why certain traversal approaches are more maintainable than others.

All code compiles and runs with Java 25. No external dependencies required.

Pattern Structure

Iterator design pattern structure (via refactoring.guru)
Iterator interface decouples traversal from the collection. The Aggregate (collection) creates iterators without exposing internals. Diagram: refactoring.guru
  • Iterator — declares hasNext() and next(). Callers use only this interface.
  • Concrete Iterator — implements the traversal algorithm. Can maintain its own traversal state (current index, filter criteria).
  • Aggregate (Collection) — declares a factory method that creates an Iterator. Hides internal storage.

Custom Iterator Implementation

BookIterator.java
package iterator;

/** Our custom Iterator interface (mirrors java.util.Iterator) */
public interface BookIterator {
    boolean hasNext();
    Book next();
}

Book is the plain data object the shelf stores.

Book.java
package iterator;

public class Book {
    private final String title;
    private final String author;
    private final int year;

    public Book(String title, String author, int year) {
        this.title = title; this.author = author; this.year = year;
    }
    public String getTitle()  { return title; }
    public int    getYear()   { return year; }
    @Override public String toString() { return "\"" + title + "\" by " + author + " (" + year + ")"; }
}

BookShelf exposes two factory methods that return different iterator types. The DecadeIterator is the interesting one — it applies filtering logic as part of traversal, which would be clunky to implement as an external loop.

BookShelf.java
package iterator;

import java.util.ArrayList;
import java.util.List;

/**
 * Aggregate — the collection. Exposes iterators without
 * revealing its internal storage structure.
 */
public class BookShelf {
    private final List<Book> books = new ArrayList<>();

    public void addBook(Book book) { books.add(book); }

    /** Standard forward iterator */
    public BookIterator iterator() {
        return new ForwardIterator();
    }

    /** Filtered iterator — only books from a specific decade */
    public BookIterator iteratorByDecade(int decade) {
        return new DecadeIterator(decade);
    }

    // --- Inner iterator implementations ---

    private class ForwardIterator implements BookIterator {
        private int index = 0;

        @Override public boolean hasNext() { return index < books.size(); }
        @Override public Book    next()    { return books.get(index++); }
    }

    private class DecadeIterator implements BookIterator {
        private final int decade;
        private int index = 0;
        private Book nextBook;

        DecadeIterator(int decade) {
            this.decade = decade;
            advance();
        }

        private void advance() {
            nextBook = null;
            while (index < books.size()) {
                Book b = books.get(index++);
                if (b.getYear() / 10 * 10 == decade) { nextBook = b; break; }
            }
        }

        @Override public boolean hasNext() { return nextBook != null; }
        @Override public Book next() {
            Book b = nextBook; advance(); return b;
        }
    }
}

Wiring it together — Main populates the shelf and exercises both iterators:

Main.java
package iterator;

/**
 * Iterator Design Pattern — Runnable Demo
 * Run: javac iterator/*.java -d out/iterator && java -cp out/iterator iterator.Main
 * Article: https://ankurm.com/iterator-design-pattern-java/
 */
public class Main {
    public static void main(String[] args) {
        System.out.println("=== Iterator Design Pattern Demo ===\n");

        BookShelf shelf = new BookShelf();
        shelf.addBook(new Book("Clean Code",              "Robert Martin", 2008));
        shelf.addBook(new Book("The Pragmatic Programmer","Andrew Hunt",   1999));
        shelf.addBook(new Book("Effective Java",          "Joshua Bloch",  2001));
        shelf.addBook(new Book("Design Patterns",         "Gang of Four",  1994));
        shelf.addBook(new Book("Refactoring",             "Martin Fowler", 2018));
        shelf.addBook(new Book("Working Effectively with Legacy Code", "Michael Feathers", 2004));

        System.out.println("-- All books (forward iterator) --");
        BookIterator it = shelf.iterator();
        while (it.hasNext()) {
            System.out.println("  " + it.next());
        }

        System.out.println("\n-- Books from the 2000s --");
        BookIterator it2000s = shelf.iteratorByDecade(2000);
        while (it2000s.hasNext()) {
            System.out.println("  " + it2000s.next());
        }

        System.out.println("\n-- JDK Iterable: same pattern, different vocabulary --");
        System.out.println("  java.util.Iterator is our BookIterator; for-each uses it under the hood");

        System.out.println("\n=== Demo complete ===");
    }
}

Console Output

=== Iterator Design Pattern Demo ===

— All books (forward iterator) —
  “Clean Code” by Robert Martin (2008)
  “The Pragmatic Programmer” by Andrew Hunt (1999)
  “Effective Java” by Joshua Bloch (2001)
  “Design Patterns” by Gang of Four (1994)
  “Refactoring” by Martin Fowler (2018)
  “Working Effectively with Legacy Code” by Michael Feathers (2004)

— Books from the 2000s —
  “Clean Code” by Robert Martin (2008)
  “Effective Java” by Joshua Bloch (2001)
  “Working Effectively with Legacy Code” by Michael Feathers (2004)

— JDK Iterable: same pattern, different vocabulary —
  java.util.Iterator is our BookIterator; for-each uses it under the hood

=== Demo complete ===

How Java’s For-Each Loop Uses Iterator

The for-each loop is syntactic sugar. for (Book b : shelf) compiles to exactly this:

// What for-each compiles to:
Iterator<Book> it = shelf.iterator();  // calls Iterable.iterator()
while (it.hasNext()) {
    Book b = it.next();
    // loop body
}
// To make BookShelf work with for-each, implement Iterable<Book>:
public class BookShelf implements Iterable<Book> {
    @Override
    public Iterator<Book> iterator() {
        return books.iterator();  // delegate to the internal list's iterator
    }
}

Once BookShelf implements Iterable<Book>, it works natively with for-each, Stream.of(), and any method that accepts an Iterable.

💡 Iterator vs Stream: Java Streams (since Java 8) solve the same traversal problem with more power — lazy evaluation, parallel execution, rich transformation API. When should you write a custom iterator? When you need stateful traversal that Streams can’t express cleanly (cursor-based database iteration, resumable traversal, streaming I/O), or when you’re exposing a collection API that should integrate with for-each without requiring callers to understand Stream APIs. For in-memory collections with filtering and mapping, use Streams.

Iterator in the JDK

java.util.Iterator and java.lang.Iterable are the JDK’s Iterator pattern. Every Collection is Iterable. Scanner implements Iterator<String> — you can iterate over lines of input like any collection. ResultSet in JDBC uses next() and cursor position — the Iterator pattern applied to database rows.

✅ Make Iterators Fail-Fast: Java’s standard collection iterators throw ConcurrentModificationException if the collection is modified during iteration. Implement the same protection in custom iterators by capturing the collection’s modification count when the iterator is created and checking it on every next() call. Without this, mutating the collection mid-iteration produces silent data corruption that’s very hard to debug.

When to Use Iterator

Reach for Iterator when: you want to hide a collection’s internal structure from callers. You need multiple traversal algorithms over the same collection (forward, reverse, filtered). You want your collection to integrate naturally with Java’s for-each and Stream APIs. You’re building a library where clients should not depend on your specific collection implementation.

Avoid building custom iterators when: the collection is a standard java.util type — its built-in iterators already handle everything. The traversal is a one-off query better expressed as a Stream pipeline. You need concurrent access — standard iterators are not thread-safe; use concurrent collections or snapshot iterators instead.

Runnable Code on GitHub

Full source at ankurm.com/git.app/asmhatre/design-patterns under 03-behavioral/iterator/.

javac 03-behavioral/iterator/*.java -d out/iterator
java -cp out/iterator iterator.Main

See Also

Further Reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.