Iterator Pattern explained simply
How do you traverse a collection without exposing its internal structure? You might have an array, a linked list, a tree, or a hash map – but the client code just wants to go through the elements one by one. It shouldn’t have to care about indexes, pointers, or bucket sizes. That is exactly the problem the Iterator Pattern solves.
What is the Iterator Pattern?
- Provides a way to access elements of a collection sequentially without exposing its underlying representation.
- Separates the traversal logic from the collection itself.
- Lets you define multiple traversal strategies for the same collection.
In short:
Give me the next element. I don’t care how you store them.
Real-Life Analogy
Think about a TV remote. You press “Next Channel” and “Previous Channel” to surf through channels. You have no idea how the TV internally stores those channels – maybe it is a sorted list, maybe it is a frequency table, maybe it is pulling from a satellite feed. You don’t care. You just press the button and the next channel shows up.
That is the Iterator Pattern. The remote is your iterator. The TV’s internal channel storage is the collection. And you, the viewer, are the client code that just wants to move through things without worrying about the internals.
Class Diagram
Structure
- Iterator (Interface): Declares
hasNext()andnext()methods for traversing elements. - ConcreteIterator (NotificationBarIterator): Implements the traversal logic. Keeps track of the current position internally.
- Aggregate / IterableCollection (Interface): Declares a method to create an iterator –
createIterator(). - ConcreteAggregate (NotificationBar): Stores elements in some internal data structure and returns an iterator when asked.
The client never touches the internal array, linked list, or whatever the collection uses. It only talks to the iterator.
Example in Java – Building a Notification Inbox
Let’s say you are building a notification system. The NotificationBar stores notifications internally in a plain array (for performance reasons, maybe). But the rest of your application just wants to loop through them without knowing that detail.
Notification Class
public class Notification {
private String message;
private String type;
private long timestamp;
public Notification(String message, String type, long timestamp) {
this.message = message;
this.type = type;
this.timestamp = timestamp;
}
public String getMessage() { return message; }
public String getType() { return type; }
public long getTimestamp() { return timestamp; }
@Override
public String toString() {
return "[" + type + "] " + message;
}
}
A simple POJO. Nothing fancy.
Iterator Interface
public interface Iterator<T> {
boolean hasNext();
T next();
}
Two methods. That is all an iterator needs. hasNext() tells you if there are more elements, and next() gives you the current one and moves forward.
NotificationCollection Interface (Aggregate)
public interface NotificationCollection {
Iterator<Notification> createIterator();
}
Any collection that wants to be iterable just needs to provide a way to create an iterator. The collection decides which iterator to return.
NotificationBar – The Concrete Collection
public class NotificationBar implements NotificationCollection {
private Notification[] notifications;
private int count = 0;
private static final int MAX_SIZE = 100;
public NotificationBar() {
notifications = new Notification[MAX_SIZE];
}
public void addNotification(Notification notification) {
if (count < MAX_SIZE) {
notifications[count] = notification;
count++;
}
}
@Override
public Iterator<Notification> createIterator() {
return new NotificationBarIterator(notifications, count);
}
}
Internally, it uses a fixed-size array. The client code never sees that. It just calls createIterator() and gets back something it can loop through.
NotificationBarIterator – The Concrete Iterator
public class NotificationBarIterator implements Iterator<Notification> {
private Notification[] notifications;
private int position = 0;
private int count;
public NotificationBarIterator(Notification[] notifications, int count) {
this.notifications = notifications;
this.count = count;
}
@Override
public boolean hasNext() {
return position < count;
}
@Override
public Notification next() {
Notification notification = notifications[position];
position++;
return notification;
}
}
This is where the traversal logic lives. It knows how to walk through an array using a position index. If the collection switched to a linked list tomorrow, you would write a different iterator – but the client code wouldn’t change at all.
Putting It All Together
public class MainProgram {
public static void main(String[] args) {
NotificationBar bar = new NotificationBar();
bar.addNotification(new Notification(
"New login from Chrome on Windows", "security", System.currentTimeMillis()));
bar.addNotification(new Notification(
"Your order has been shipped", "order", System.currentTimeMillis()));
bar.addNotification(new Notification(
"John commented on your post", "social", System.currentTimeMillis()));
bar.addNotification(new Notification(
"Password changed successfully", "security", System.currentTimeMillis()));
// client code doesn't know about the internal array
Iterator<Notification> iterator = bar.createIterator();
while (iterator.hasNext()) {
Notification n = iterator.next();
System.out.println(n);
}
}
}
Output:
[security] New login from Chrome on Windows
[order] Your order has been shipped
[social] John commented on your post
[security] Password changed successfully
The main method has no idea that notifications are stored in an array. It could be a linked list, a database cursor, or a file stream. As long as the iterator implements hasNext() and next(), the client code stays exactly the same.
Key Components
- Iterator Interface: The contract –
hasNext()andnext(). - Concrete Iterator: Implements the traversal for a specific data structure.
- Aggregate Interface: Declares
createIterator()so collections can produce iterators. - Concrete Aggregate: The actual collection that holds data and knows which iterator to create.
- Client: Uses only the iterator interface. Never touches the collection’s internals.
When to Use the Iterator Pattern
- When you want to traverse a collection without exposing its internal representation.
- When you need to support multiple traversal strategies (forward, reverse, filtered) over the same collection.
- When you want a uniform interface for iterating over different types of collections (arrays, lists, trees).
- When you want to decouple the traversal algorithm from the collection structure.
Advantages and Disadvantages
Advantages:
- Single Responsibility: The collection stores data. The iterator traverses it. Clean separation.
- Open/Closed Principle: You can add new iterator types without modifying the collection.
- Uniform Access: Client code looks the same regardless of the underlying data structure.
- Multiple Iterators: You can have several iterators traversing the same collection simultaneously, each with its own state.
Disadvantages:
- Overhead for Simple Collections: If you just have a plain list, writing a full iterator class might be overkill.
- Extra Classes: Each collection typically needs its own iterator class, which adds to the codebase.
- Snapshot vs. Live: You need to decide whether the iterator sees modifications made to the collection during iteration. This can get tricky.
Java’s Built-in Iterator
The good news is that you rarely need to build iterators from scratch in Java. The language already bakes this pattern into the Collections Framework.
Java has two key interfaces:
java.lang.Iterable<T>– the aggregate. Any class that implements this can be used in a for-each loop.java.util.Iterator<T>– the iterator itself, withhasNext(),next(), and an optionalremove().
Here is our notification example rewritten using Java’s built-in support:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class NotificationBar implements Iterable<Notification> {
private List<Notification> notifications = new ArrayList<>();
public void addNotification(Notification notification) {
notifications.add(notification);
}
@Override
public Iterator<Notification> iterator() {
return notifications.iterator();
}
}
And now the client code becomes this:
NotificationBar bar = new NotificationBar();
bar.addNotification(new Notification("New login from Chrome", "security", System.currentTimeMillis()));
bar.addNotification(new Notification("Order shipped", "order", System.currentTimeMillis()));
// enhanced for-each loop -- powered by Iterator under the hood
for (Notification n : bar) {
System.out.println(n);
}
That for-each loop is just syntactic sugar. Behind the scenes, Java calls iterator() to get an Iterator, then calls hasNext() and next() in a loop. Same pattern, zero boilerplate.
Real-World Use Cases
- Java Collections Framework: Every
List,Set, andMapimplementsIterable. The enhanced for loop depends on this pattern. - JDBC ResultSet:
resultSet.next()is literally an iterator over database rows. You callnext()to advance and check the return value to know if there are more rows. - Database Cursors: MongoDB cursors, Hibernate
ScrollableResults– all iterator-based. They let you process millions of records without loading everything into memory. - Stream APIs: Java Streams, C# LINQ, Python generators – all built on lazy iteration. Elements are computed on demand, not upfront.
- File I/O:
BufferedReader.readLine()iterates through a file line by line. Same concept. - XML/JSON Parsers: SAX parsers and
JsonParseruse iterator-style pull parsing to process documents without loading the entire thing.
Wrapping Up
The Iterator Pattern is one of those patterns that is so deeply embedded in modern languages that you use it every day without thinking about it. Every time you write a for-each loop, every time you call .next() on a result set, every time you stream through a collection – you are using this pattern.
The core idea is simple: separate what you are iterating over from how you iterate over it. The collection manages storage. The iterator manages traversal. And the client code stays blissfully ignorant of both.
If you are building custom data structures or need to provide multiple ways to traverse a collection, implementing the Iterator Pattern explicitly gives you clean, decoupled, and flexible code.