🔁 Iterator Design Pattern
Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
🌍 Real-World Analogy
Analogy — TV Remote Channel Surfing
A TV remote lets you surf channels by pressing Next and Previous — without knowing whether the channels come from cable, satellite, or streaming. The remote (Iterator) provides a uniform way to traverse channels (elements) regardless of the underlying source (collection type). You don't need to know the internal data structure — you just navigate.
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
R["🎮 Remote"] -->|"next"| C1["📺 News"] -->|"next"| C2["📺 Sports"] -->|"next"| C3["📺 Movies"]
C3 -.->|"prev"| C2
style R fill:#FFF8E1,stroke:#F9A825,stroke-width:2px,color:#000
style C1 fill:#E3F2FD,stroke:#1565C0,color:#000
style C2 fill:#E8F5E9,stroke:#2E7D32,color:#000
style C3 fill:#FFF3E0,stroke:#E65100,color:#000 🏗️ Pattern Structure
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
flowchart LR
Client["👤 Client"] -->|"uses"| Iterator[["🔁 Iterator"]]
Client -->|"asks"| Aggregate[["📦 Aggregate"]]
ConcreteAgg{{"🗂️ Concrete Aggregate"}} -->|"implements"| Aggregate
ConcreteAgg -.->|"creates"| ConcreteIt{{"⚡ Concrete Iterator"}}
ConcreteIt -->|"implements"| Iterator
style Client fill:#E8F5E9,stroke:#2E7D32,color:#000
style Iterator fill:#FFF3E0,stroke:#E65100,color:#000
style Aggregate fill:#FFF3E0,stroke:#E65100,color:#000
style ConcreteAgg fill:#E3F2FD,stroke:#1565C0,color:#000
style ConcreteIt fill:#E3F2FD,stroke:#1565C0,color:#000 UML Class Diagram
%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '13px', 'fontFamily': 'Inter, -apple-system, sans-serif'}, 'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'padding': 12, 'curve': 'basis'}, 'sequence': {'actorMargin': 60, 'messageMargin': 40}, 'class': {'padding': 12}}}%%
classDiagram
class Iterator~T~ {
<<interface>>
+hasNext() boolean
+next() T
+reset() void
}
class IterableCollection~T~ {
<<interface>>
+createIterator() Iterator~T~
+createReverseIterator() Iterator~T~
}
class NotificationList {
-notifications: Notification[]
-count: int
+addNotification(Notification) void
+size() int
+get(int index) Notification
+createIterator() Iterator~Notification~
+createReverseIterator() Iterator~Notification~
}
class ForwardIterator {
-list: NotificationList
-position: int
+hasNext() boolean
+next() Notification
+reset() void
}
class ReverseIterator {
-list: NotificationList
-position: int
+hasNext() boolean
+next() Notification
+reset() void
}
NotificationList ..|> IterableCollection~T~
ForwardIterator ..|> Iterator~T~
ReverseIterator ..|> Iterator~T~
NotificationList ..> ForwardIterator : creates
NotificationList ..> ReverseIterator : creates ❓ The Problem
Collections can have complex internal structures (trees, graphs, stacks, hash tables), but clients need to traverse them without:
- Knowing the internal representation (array? linked list? tree?)
- Duplicating traversal logic across multiple clients
- Exposing the collection's internal structure (breaking encapsulation)
- Being limited to a single traversal algorithm
Example: A social network graph where you want to iterate over a user's friends — by depth-first, breadth-first, or filtered by location — without exposing the graph internals.
Without This Pattern
public class NotificationList {
private Notification[] notifications;
private int count;
// Exposing internals so clients can traverse
public Notification[] getNotifications() { return notifications; }
public int getCount() { return count; }
}
// Client code — tightly coupled to internal structure
public class NotificationRenderer {
public void render(NotificationList list) {
// Client must KNOW it's an array and how count works
for (int i = 0; i < list.getCount(); i++) {
display(list.getNotifications()[i]);
}
// If NotificationList changes to a LinkedList? ALL clients break.
}
}
- Breaks encapsulation — clients access the raw internal array, creating a dependency on the data structure choice
- Duplication — every client that traverses the collection reimplements the same loop logic
- No alternative traversals — you cannot easily support reverse, filtered, or sorted iteration without changing client code
- Fragile to refactoring — changing from an array to a
ListorTreeSetbreaks every caller that usesgetNotifications()[i] - Pain point: When the team decides to change from an array to a database-backed lazy collection, dozens of client classes must be rewritten because they all assume array indexing
✅ The Solution
The Iterator pattern extracts traversal behavior into a separate iterator object:
- Iterator interface — defines
hasNext(),next(), and optionallyremove() - Concrete Iterator — implements traversal logic, tracks current position
- Aggregate interface — declares a factory method
createIterator() - Concrete Aggregate — returns the appropriate iterator for its structure
💻 Implementation
// Iterator interface
public interface Iterator<T> {
boolean hasNext();
T next();
void reset();
}
// Aggregate interface
public interface IterableCollection<T> {
Iterator<T> createIterator();
Iterator<T> createReverseIterator();
}
// Concrete Aggregate — custom notification list
public class NotificationList implements IterableCollection<Notification> {
private final Notification[] notifications;
private int count = 0;
public NotificationList(int capacity) {
this.notifications = new Notification[capacity];
}
public void addNotification(Notification notification) {
if (count < notifications.length) {
notifications[count++] = notification;
}
}
public int size() { return count; }
public Notification get(int index) { return notifications[index]; }
@Override
public Iterator<Notification> createIterator() {
return new ForwardIterator(this);
}
@Override
public Iterator<Notification> createReverseIterator() {
return new ReverseIterator(this);
}
}
// Concrete Iterator — forward traversal
public class ForwardIterator implements Iterator<Notification> {
private final NotificationList list;
private int position = 0;
public ForwardIterator(NotificationList list) {
this.list = list;
}
@Override
public boolean hasNext() {
return position < list.size();
}
@Override
public Notification next() {
if (!hasNext()) throw new NoSuchElementException();
return list.get(position++);
}
@Override
public void reset() { position = 0; }
}
// Concrete Iterator — reverse traversal
public class ReverseIterator implements Iterator<Notification> {
private final NotificationList list;
private int position;
public ReverseIterator(NotificationList list) {
this.list = list;
this.position = list.size() - 1;
}
@Override
public boolean hasNext() {
return position >= 0;
}
@Override
public Notification next() {
if (!hasNext()) throw new NoSuchElementException();
return list.get(position--);
}
@Override
public void reset() { position = list.size() - 1; }
}
// Usage
public class Main {
public static void main(String[] args) {
NotificationList list = new NotificationList(10);
list.addNotification(new Notification("Welcome!"));
list.addNotification(new Notification("New message"));
list.addNotification(new Notification("Update available"));
// Forward iteration
System.out.println("📬 Latest notifications:");
Iterator<Notification> it = list.createIterator();
while (it.hasNext()) {
System.out.println(" → " + it.next().getText());
}
// Reverse iteration
System.out.println("📬 Oldest first:");
Iterator<Notification> reverse = list.createReverseIterator();
while (reverse.hasNext()) {
System.out.println(" → " + reverse.next().getText());
}
}
}
// Binary tree node
public class TreeNode<T> {
T value;
TreeNode<T> left, right;
public TreeNode(T value) { this.value = value; }
}
// Tree with multiple iteration strategies
public class BinaryTree<T> implements IterableCollection<T> {
private TreeNode<T> root;
public void setRoot(TreeNode<T> root) { this.root = root; }
@Override
public Iterator<T> createIterator() {
return new InOrderIterator<>(root);
}
public Iterator<T> createBfsIterator() {
return new BreadthFirstIterator<>(root);
}
}
// DFS In-Order Iterator
public class InOrderIterator<T> implements Iterator<T> {
private final Deque<TreeNode<T>> stack = new ArrayDeque<>();
public InOrderIterator(TreeNode<T> root) {
pushLeft(root);
}
private void pushLeft(TreeNode<T> node) {
while (node != null) {
stack.push(node);
node = node.left;
}
}
@Override
public boolean hasNext() { return !stack.isEmpty(); }
@Override
public T next() {
TreeNode<T> node = stack.pop();
pushLeft(node.right);
return node.value;
}
@Override
public void reset() { /* re-initialize from root */ }
}
// BFS Iterator
public class BreadthFirstIterator<T> implements Iterator<T> {
private final Queue<TreeNode<T>> queue = new LinkedList<>();
public BreadthFirstIterator(TreeNode<T> root) {
if (root != null) queue.add(root);
}
@Override
public boolean hasNext() { return !queue.isEmpty(); }
@Override
public T next() {
TreeNode<T> node = queue.poll();
if (node.left != null) queue.add(node.left);
if (node.right != null) queue.add(node.right);
return node.value;
}
@Override
public void reset() { /* re-initialize */ }
}
🎯 When to Use
- When you want to traverse a collection without exposing its internal structure
- When you need multiple traversal algorithms for the same collection (forward, reverse, filtered, DFS, BFS)
- When you want a uniform interface for traversing different collection types
- When you want to support concurrent iteration with independent iterator instances
- When the collection structure may change but the traversal interface should remain stable
🏭 Real-World Examples
| Framework/Library | Usage |
|---|---|
java.util.Iterator | The core iterator interface in Java Collections |
java.util.Enumeration | Legacy iterator (pre-Java 2) |
java.util.Scanner | Iterates over tokens in input |
java.sql.ResultSet | Iterates over database query results |
java.util.stream.Stream | Internal iteration with forEach, map, filter |
Spring JdbcTemplate.queryForStream() | Lazy row iteration over large result sets |
Kafka ConsumerRecords | Iterable over consumed records |
⚠️ Pitfalls
Common Mistakes
- ConcurrentModificationException — Modifying a collection while iterating. Use
ConcurrentHashMaporCopyOnWriteArrayListfor concurrent access. - Stateful iterators — Iterators hold position state; sharing them between threads without synchronization causes bugs.
- Lazy vs. Eager — Decide if the iterator fetches all elements upfront or lazily. Lazy is better for large/infinite collections.
- Memory leaks — Iterators over database cursors or file streams must be closed. Use try-with-resources.
- External vs. Internal iteration — Java Streams (internal) are often simpler than custom external iterators for standard traversals.
📝 Key Takeaways
Summary
- Iterator separates traversal logic from the collection, following Single Responsibility
- Multiple iterators can traverse the same collection simultaneously and independently
- Java's
Iterable/Iteratorinterfaces enable the for-each loop (for (var x : collection)) - Modern Java prefers Streams for most traversals — use custom iterators for complex structures
- The pattern enables lazy evaluation — elements are computed on demand, crucial for large datasets
- Iterator + Factory Method = collections that create the right iterator without exposing internals