Iterator Pattern
Learn to expose a uniform way to walk through a collection's elements without leaking its internal storage structure to client code, the same mechanism behind every Java for-each loop.
Learning objectives
- Explain why exposing a collection's raw backing structure tightly couples client code to it
- Design a hasNext()/next() contract that hides a collection's internal storage
- Keep traversal position on the iterator so multiple independent traversals don't interfere
- Swap a collection's backing storage without changing any client-facing traversal code
- Implement java.lang.Iterable to get native for-each loop support
◆ Story
Turning pages in a book works the same way whether the book is hardcover, paperback, or spiral-bound — you just turn to the next page. You never need to understand the binding mechanism to read sequentially through the content. "Turn to the next page" is a completely uniform action, no matter what's actually happening underneath it.
That's the exact idea behind the Iterator pattern, and it's almost certainly the pattern already used the most without realizing it — every for-each loop written in Java is this pattern, already built into the language.
A small library class makes the underlying mechanism concrete. A BookCollection holds books and lets client code loop through every one of them. It might be backed by an ArrayList today, but there's a real chance it becomes a TreeSet once sorting or deduplication becomes a requirement. Building the traversal the direct way first — indexing straight into the underlying list — is what exposes exactly why that change would otherwise break every caller.
This closes out the six behavioral patterns built here on a fitting note: where Memento hid an object's state from an outside caretaker, and Command hid an action's target from an invoker, Iterator hides a collection's storage shape from anything that just wants to walk through its contents — the same underlying instinct, encapsulation, aimed at one more kind of implementation detail.
The obvious starting point: BookCollection holds a List<Book> internally, and exposes a getBooks() method that just returns that list directly, so callers can loop over it themselves. Client code then indexes into it with a standard for-loop, calling .get(i) up to .size().
This runs fine, and prints every book correctly. So is this fine? The real test is what happens the moment BookCollection's author — who might not even be the same person writing the client code — decides the internal storage should become a TreeSet instead, say to keep books automatically sorted.
The client's loop calls .get(i), a method that exists on List but not on Set. The moment BookCollection's internal field changes from a List to a Set, this exact client code stops compiling. The client was never supposed to know or care what data structure backed the collection, but by exposing getBooks()'s raw return type and indexing into it directly, the client's code became tightly coupled to an implementation detail that was never meant to be part of the contract in the first place.
Worse, this coupling isn't confined to one call site. Realistically, getBooks().get(i) is the kind of expression that gets copy-pasted into every place client code needs to read from the collection — a report generator, a search feature, a display screen — and every one of those places would need to be found and rewritten the day the backing storage changes. The fix needs to shrink that surface area down to exactly one place: the collection class itself.
💻 Code example
class BookCollection { private List<Book> books = new ArrayList<>(); // today, a List specifically void addBook(Book book) { books.add(book); } List<Book> getBooks() { return books; } // exposed just so callers can loop over it } // client code, indexing directly into the underlying List BookCollection collection = new BookCollection(); collection.addBook(new Book("C++")); collection.addBook(new Book("Java")); for (int i = 0; i < collection.getBooks().size(); i++) { System.out.println(collection.getBooks().get(i)); // relies on get(i) — a List-specific method } // The moment BookCollection's internal field changes from a List to a Set, // this exact client code stops compiling — .get(i) doesn't exist on Set.
Reason through what traversal actually needs. The client genuinely only needs two things: "is there a next element?" and "give me the next element." Nothing about indexing, nothing about the underlying structure. So there should be a shared, uniform contract for exactly those two operations — one that works identically whether the collection is a list, a set, a tree, or something else entirely. And the logic for what "next" actually means belongs inside BookCollection itself, not the client, since only the collection's own author knows how its internal structure is organized.
This is the Iterator pattern: a uniform interface — hasNext() and next() — for stepping through a collection one element at a time, without the caller ever needing to know or care how that collection actually stores its data internally.
Build the shared contract first: boolean hasNext() and T next(), generic over the element type. Then build the concrete iterator — the only class allowed to actually know BookCollection stores its books in an ArrayList. It holds a reference to that same list, plus a second field, position, tracking where it currently is, completely separately from the list itself. hasNext() answers position < books.size(); next() returns the current book and advances the position in one step, books.get(position++).
Notice position — the entire "where am I in this traversal" state — lives on the iterator, not on BookCollection itself. That's deliberate: it means two different callers can iterate over the same BookCollection at the same time, each with their own independent position, without interfering with each other. BookCollection gains exactly one new method, createIterator(), which returns a fresh BookIterator wired to its own current list — and stops exposing its raw list entirely.
💻 Code example
interface MyIterator<T> { boolean hasNext(); // is there anything left? T next(); // hand back the next element, and advance } class BookIterator implements MyIterator<Book> { private final List<Book> books; // the SAME list BookCollection holds, not a copy private int position = 0; // where we currently are — lives HERE, not on BookCollection BookIterator(List<Book> books) { this.books = books; } public boolean hasNext() { return position < books.size(); } public Book next() { return books.get(position++); // hand back the current book, then advance } } class BookCollection { private final List<Book> books = new ArrayList<>(); void addBook(Book book) { books.add(book); } MyIterator<Book> createIterator() { // the ONLY way a client can now walk this collection return new BookIterator(this.books); } }
Wire it together: a client now calls collection.createIterator() once, then loops while (iterator.hasNext()), calling iterator.next() each time. No .get(i) anywhere in the client — no idea it's an ArrayList underneath at all.
Now prove the actual point: change BookCollection's internal field from a List to a TreeSet, exactly the change that broke the naive version, and check exactly what breaks in the client this time.
BookIterator's internals have to change completely — no more position, no more .get(i), since a Set has no concept of an index at all. The new version wraps Set's own built-in java.util.Iterator internally and just delegates hasNext()/next() to it. That's real, necessary work, but it's contained work: it happens once, inside the one class whose whole job is knowing about the storage. The client's while (iterator.hasNext()) loop is not touched at all — it never knew, and never needed to know, whether it was walking an ArrayList or a TreeSet. That's the concrete proof this pattern was worth building: the exact bug from the naive version is now structurally impossible, not just less likely.
In real Java code, this is rarely hand-rolled from scratch — implementing the standard library's own Iterable<T> interface gets the for-each loop syntax for free, since the compiler translates for (Book b : collection) directly into calls to iterator(), hasNext(), and next() underneath. If the underlying storage is already a standard Java collection, delegating straight to its existing .iterator() is usually all that's needed.
💻 Code example
// swapping the storage — only BookCollection and BookIterator change, the client doesn't class BookCollection { private final Set<Book> books = new TreeSet<>(); // changed — was a List void addBook(Book book) { books.add(book); } MyIterator<Book> createIterator() { return new BookIterator(this.books); // BookIterator now wraps a Set, not a List } } class BookIterator implements MyIterator<Book> { private final Iterator<Book> delegate; // no more position/get(i) — a Set has no index BookIterator(Set<Book> books) { this.delegate = books.iterator(); } public boolean hasNext() { return delegate.hasNext(); } public Book next() { return delegate.next(); } } // doing it Java's way — implement Iterable directly and get the for-each loop for free class BookCollectionIterable implements Iterable<Book> { private final List<Book> books = new ArrayList<>(); void addBook(Book book) { books.add(book); } @Override public Iterator<Book> iterator() { return books.iterator(); } // reuse ArrayList's own iterator } for (Book book : new BookCollectionIterable()) { // compiler calls iterator()/hasNext()/next() for you System.out.println(book); }
▲ Edge case — calling next() without checking hasNext() first
Calling next() once the collection is exhausted is a classic bug: Java's built-in iterators throw NoSuchElementException, while a hand-rolled version like BookIterator above would throw an IndexOutOfBoundsException instead. Always guard every next() call with a hasNext() check, exactly as a for-each loop does automatically underneath.
▲ Edge case — modifying a collection while iterating over it
Adding or removing an element from a collection while an iterator is mid-traversal can silently produce wrong results — skipped or duplicated elements — or throw a ConcurrentModificationException, depending on the backing structure. This is a genuinely common, subtle bug. Java's built-in collections detect and fail fast on it specifically to surface the mistake loudly rather than let it corrupt results silently.
▲ Edge case — switching backing storage can carry hidden requirements
Switching a collection's backing storage from an ArrayList to a TreeSet does more than change the iteration mechanism — a TreeSet keeps its elements sorted, which means it needs to know how to compare two elements. Without the element type implementing Comparable, inserting into the set throws a ClassCastException at runtime. The iteration contract itself is unaffected by this switch, but the storage's own requirements can still ripple outward in ways worth checking for deliberately.
◆ Where this pattern actually shows up
java.util.IteratorandIterable— every collection in the Java Collections Framework (ArrayList,HashSet,TreeMap'skeySet(), and dozens more) implements this pattern directly, which is exactly why they all support for-each syntax identically.- The for-each loop itself —
for (X x : collection)is compiled by the Java compiler into calls toiterator(),hasNext(), andnext(). Every for-each loop ever written is this pattern in action, whether or not the code visibly says so. java.util.Spliterator— introduced in Java 8 alongside streams, it extends the same core idea to support parallel traversal, splitting a source into pieces that can be iterated concurrently.- JDBC
ResultSet— paging through query results row by row withnext(), without loading the entire result set into memory at once, is the identicalhasNext()/next()-shaped traversal applied to database rows. java.util.Enumeration— the legacy predecessor toIterator, still present in some older APIs likeVectorandHashtable, usinghasMoreElements()/nextElement()instead ofhasNext()/next().- File readers — reading a large file line by line with a
BufferedReader, without loading the entire file into memory at once, follows the same "ask for the next piece, don't expose the source" shape this pattern is built around.
Q: What does the Iterator pattern actually hide from the caller? : The collection's internal storage structure — the caller only ever sees a uniform hasNext()/next() interface, regardless of how the data is actually stored underneath.
Q: Why does an iterator's position field live on the iterator itself, not on the collection it walks? : So multiple callers can iterate over the same collection simultaneously, each with their own independent position, without interfering with each other.
Q: What does implementing Java's Iterable interface actually give you? : The ability to use a custom class directly in a standard for-each loop, since the Java compiler translates that loop into calls to iterator(), hasNext(), and next() underneath.
Q: If a collection's backing storage changes from ArrayList to TreeSet, what has to change, and what doesn't? : The collection class and its iterator logic change (and the element type needs to implement Comparable for TreeSet's sorting). Client code that only calls hasNext()/next(), or uses a for-each loop, does not change at all.
Q: What's a real, common bug caused by modifying a collection while iterating over it? : Elements can be silently skipped or duplicated, or, in Java's built-in collections, a ConcurrentModificationException is thrown to surface the problem loudly rather than let results be silently corrupted.
Want a visual for this concept?
Generate a diagram tailored to “Iterator Pattern” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →