Iterator Design Pattern in Java: Complete Guide with Examples

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.

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

package iterator;
public interface BookIterator {
    boolean hasNext();
    Book next();
}
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 int    getYear()  { return year; }
    public String getTitle() { return title; }
    @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.

package iterator;
import java.util.ArrayList;
import java.util.List;
public class BookShelf {
    private final List<Book> books = new ArrayList<>();
    public void addBook(Book book) { books.add(book); }
    public BookIterator iterator() {
        return new ForwardIterator();
    }
    /** Filtered iterator — only books published in the given decade (e.g. 2000 for 2000-2009) */
    public BookIterator iteratorByDecade(int decade) {
        return new DecadeIterator(decade);
    }
    // Inner classes have access to the outer list — no need to expose it
    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();  // find the first matching book upfront
        }
        private void advance() {
            nextBook = null;
            while (index < books.size()) {
                Book b = books.get(index++);
                if (b.getYear() / 10 * 10 == decade) { nextBook = b; return; }
            }
        }
        @Override public boolean hasNext() { return nextBook != null; }
        @Override public Book    next()    { Book b = nextBook; advance(); return b; }
    }
}
public class Main {
    public static void main(String[] args) {
        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", "Feathers", 2004));
        System.out.println("-- All books --");
        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());
    }
}

Console Output

=== Iterator Design Pattern Demo ===

— All books —
  “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 Feathers (2004)

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

=== 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

Leave a Reply

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