One-line Definition
Abstracts the traversal of a collection so clients can iterate over it without knowing its internal data structure.
Problem
Collections store data in vastly different ways: arrays, linked lists, trees, hash maps, and graphs. If client code wants to iterate over a tree, it needs to know about tree traversal algorithms. If it wants to iterate over an array, it uses a simple for loop. This tightly couples the client to the collection's data structure. The Iterator Pattern extracts traversal logic into a separate object, giving clients a uniform way to loop over absolutely any collection.
Real-world Analogy
Think of a Spotify playlist. You press "Next" to move to the next song without knowing whether the playlist is stored locally as an array, streamed as a linked list, or dynamically fetched via a database query. The "Next" button is the iterator — it gives you one item at a time through a uniform interface.
Structure
- Client
- Uses the collection and iterator interfaces to traverse elements.
- Iterator
- Interface with traversal methods (e.g.,
hasNext(),next()).
- Interface with traversal methods (e.g.,
- ConcreteIterator
- Implements the Iterator for a specific collection, tracking the current traversal position.
- Aggregate (Iterable/Collection)
- Interface declaring a method that returns an Iterator.
- ConcreteAggregate
- The actual data structure that creates and returns its compatible iterator.
Diagram
classDiagram
class Client {
}
class Aggregate {
<<interface>>
+createIterator(): Iterator
}
class ConcreteAggregate {
+createIterator(): Iterator
}
class Iterator {
<<interface>>
+hasNext(): boolean
+next(): Object
}
class ConcreteIterator {
-position
+hasNext(): boolean
+next(): Object
}
Client --> Aggregate
Client --> Iterator
ConcreteAggregate ..|> Aggregate
ConcreteIterator ..|> Iterator
ConcreteAggregate ..> ConcreteIterator : instantiates
Code Walkthrough
Notice that Main (the client) never accesses the internal array of the NotificationCollection. It exclusively relies on hasNext() and next().
interface Iterator<T> {
boolean hasNext();
T next();
}
interface Collection<T> {
Iterator<T> createIterator();
}
class Notification {
private final String message;
public Notification(String message) { this.message = message; }
public String getMessage() { return message; }
}
class NotificationCollection implements Collection<Notification> {
private final Notification[] notifications;
private int count = 0;
public NotificationCollection(int capacity) {
notifications = new Notification[capacity];
}
public void addNotification(Notification notification) {
if (count < notifications.length) {
notifications[count++] = notification;
}
}
@Override
public Iterator<Notification> createIterator() {
return new NotificationIterator(notifications, count);
}
}
class NotificationIterator implements Iterator<Notification> {
private final Notification[] notifications;
private final int count;
private int position = 0;
public NotificationIterator(Notification[] notifications, int count) {
this.notifications = notifications;
this.count = count;
}
@Override
public boolean hasNext() { return position < count; }
@Override
public Notification next() { return notifications[position++]; }
}
class Main {
public static void main(String[] args) {
NotificationCollection collection = new NotificationCollection(10);
collection.addNotification(new Notification("New message"));
collection.addNotification(new Notification("Order shipped"));
Iterator<Notification> iterator = collection.createIterator();
while (iterator.hasNext()) {
System.out.println("📩 " + iterator.next().getMessage());
}
}
}
Bad vs Good
Bad Approach
Problems
- Client must know the collection uses an array and access it by an integer index.
- Changing the internal structure (e.g., from an array to a
Listor aTree) instantly breaks all client code.
class Main {
public static void main(String[] args) {
Notification[] notifications = getNotifications();
// Client is tightly coupled to the array implementation
for (int i = 0; i < notifications.length; i++) {
if (notifications[i] != null) {
System.out.println(notifications[i].getMessage());
}
}
}
}
Better Approach
Improvements
- Client uses
hasNext()/next()without knowing the internal structure. - Collection can safely swap its internal data structure in the future without affecting a single line of client code.
Iterator<Notification> it = collection.createIterator();
while (it.hasNext()) {
System.out.println(it.next().getMessage());
}
// Works identically whether collection is an array, tree, or linked list
Pros vs Cons
| Pros | Cons |
|---|---|
| Provides a uniform traversal interface for all collections | Heavy overkill for simple apps using standard Java collections |
| Decouples the client from complex data structures | Requires creating a new iterator class for every collection type |
| Supports multiple simultaneous, independent traversals | The iterator can easily become stale if the collection is modified |
| Enforces the Single Responsibility Principle | |
| Enables different traversal strategies (e.g., DFS vs BFS in trees) |
When to Use
- You want to provide a standard way to iterate over a custom, complex data structure (like a custom Graph or Tree).
- You want to hide the internal representation of a collection from clients.
- You need to support multiple traversal algorithms (e.g., Depth-First vs Breadth-First) across the same data.
When Not to Use
- You are using standard Java collections (Lists, Sets, Maps). Standard Java
Iteratorhandles this perfectly. Re-inventing it is redundant. - The collection is incredibly simple and will never change its internal structure.
Real-world Examples
java.util.Iteratorandjava.lang.Iterable(The backbone of the enhancedfor-eachloop).java.util.Enumeration(Legacy equivalent).java.sql.ResultSet(Acts as an iterator over database records).
Key Takeaway
The Iterator Pattern abstracts the complex logic of moving through a data structure, providing clients with a clean, universal way to loop over elements. It is so fundamental to modern programming that Java baked it directly into the language via Iterable. Use it when building custom data structures, but rely on Java's built-in iterators everywhere else.