LESSON 7.4
Iterator
Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
15 min read
Real-world analogy
A museum handheld audio guide acts as an iterator. As you explore the Louvre, you do not need to study the building's architectural blueprints, HVAC duct routes, or fire escape maps. You press the "Next" button on the handset. The guide advances to the next painting in the exhibit's curated tour. Different tour guides (a "Quick Highlights" guide versus a "Renaissance Masters" guide) traverse the exact same physical museum along completely different pathways without moving any walls.
The problem
You are building a social network graph where users have friends, followers, and organizational group memberships. Internally, the data structure is represented as a complex adjacency list and binary search tree (UserNode with left, right, and friendsList).
If client code (such as the recommendation engine, feed generator, and export tool) traverses this graph by directly accessing internal node pointers, several critical problems emerge:
- Breaks Encapsulation: Private graph pointers and memory structures are exposed to client code.
- Brittle Code: If you refactor the internal data structure from a binary tree to a B-Tree or hash table, all client traversal loops across the codebase break.
- No Support for Multiple Traversals: If the client wants to perform a Breadth-First Search (BFS) or Depth-First Search (DFS), that complex traversal algorithm is repeated across every client service:
// Naive implementation: Client directly coupled to internal node pointers
class UserProfileNode {
constructor(name) {
this.name = name;
this.left = null; // Internal tree pointers leaked to clients!
this.right = null;
}
}
// Client must manually traverse internal pointers
function printAllUsers(root) {
const stack = [root];
while (stack.length > 0) {
const node = stack.pop();
if (node) {
console.log(node.name);
stack.push(node.right);
stack.push(node.left);
}
}
}public class Example {
static class UserNode {
String name;
UserNode left, right; // Leaked internal structure
public UserNode(String name) { this.name = name; }
}
public static void printUsers(UserNode root) {
// Client is tightly coupled to binary tree pointers
Client code should not care whether users are stored in a binary search tree, an array, a hash map, or a remote paginated API.
The solution
The Iterator pattern extracts the traversal behavior into a separate Iterator object. The iterator encapsulates the cursor state (e.g. current index or traversal stack) and provides standard methods (hasNext() and next()). The collection becomes an Iterable, exposing a factory method (createIterator()) that returns an iterator.
- Iterator (
Iterator): Interface declaring operations to access and step through elements (hasNext(),next()). - Concrete Iterator (
InOrderIterator,BreadthFirstIterator): Tracks current position and implements traversal logic. - Aggregate (
IterableCollection): Interface declaring the factory method (createIterator()). - Concrete Aggregate (
UserTree): Holds the actual data and instantiates the iterator.
Step-by-step implementation
Step 1: Define the iterator interface
Establish standard cursor navigation methods.
class Iterator {
hasNext() {
throw new Error("hasNext() must be implemented");
}
next() {
throw new Error("next() must be implemented");
}
}public class Example {
interface CustomIterator<T> {
Step 2: Implement the collection and concrete iterator
The iterator maintains an internal stack or index, keeping cursor state out of the collection.
Step 3: Iterate uniformly in client code
Clients traverse the aggregate without referencing its internal array or tree pointers.
const list = new SimpleList();
list.add("Alice");
list.add("Bob");
const iter = list.getIterator();
while (iter.hasNext()) {
console.log(`Visited: ${iter.next()}`);
}The complete example
Here is the complete runnable example demonstrating an in-order tree iterator and JavaScript generator integration.
--- Classical GoF Iterator Traversal ---
Visited: 20
Visited: 30
Visited: 40
Visited: 50
Visited: 60
Visited: 70
Visited: 80
--- Native ES2022 for...of Loop ---
for...of item: 20
for...of item: 30
for...of item: 40
for...of item: 50
for...of item: 60
Variations
External vs internal iteration
- External Iterator (Active): The client code actively controls iteration pacing using
hasNext()andnext(). The client can pause, skip elements, break early, or interleave iteration across two collections simultaneously. - Internal Iterator (Passive): The collection controls iteration internally, passing each item to a callback function (e.g.
list.forEach(item => ...)or Java Streams). Pacing is managed by the collection, and breaking out of loops early is harder.
Fail-fast iterators and ConcurrentModificationException
In multi-threaded or mutable collections, modifying a collection while an iterator is actively traversing it produces non-deterministic errors.
- Java collections maintain a private counter:
modCount. - When an iterator is created, it captures
expectedModCount = modCount. - On every call to
next(), it checksif (modCount != expectedModCount) throw new ConcurrentModificationException(). - To safely remove items during iteration, use the iterator's own
iterator.remove()method, which updates both counters simultaneously.
Where you've already used it
- Java Enhanced for-loop: Any class implementing
java.lang.Iterablecan be consumed viafor (T item : collection). - JavaScript
Symbol.iterator: Standardizes iteration across Arrays, Maps, Sets, and generator functions withfor (const x of iterable). - Java Database Connectivity (JDBC)
ResultSet: Implements cursor iteration viaresultSet.next(). - Database Cursor Pagination: Paginating through millions of records in MongoDB, PostgreSQL, or DynamoDB using continuation tokens.
Pros and cons
Advantages
- Single Responsibility Principle: Separates traversal algorithms from collection data storage.
- Open/Closed Principle: Add new traversal strategies (BFS, DFS, reverse order) without altering collection classes.
- Parallel Traversals: Multiple independent iterators can traverse the same collection simultaneously.
- Clean Client API: Simplifies client code by providing a uniform traversal interface.
Disadvantages
- Overhead for Simple Data: Using an iterator for a simple sequential fixed array adds unnecessary indirection compared to a simple indexed loop.
- Concurrent Modification Risks: Modifying collections during active iteration requires synchronization or fail-fast protection.
When not to use it
Do not build custom Iterator classes for simple linear arrays or lists where native indexed loops or language-provided iterators already exist. Reserve custom iterators for trees, graphs, circular buffers, or paginated network streams.
Related patterns
| Pattern | Traversal Focus | Relationship |
|---|---|---|
| Iterator | Step-by-step element access | Decouples traversal from storage |
| Composite | Part-whole tree hierarchies | Iterators are frequently used to traverse Composite trees |
| Visitor | Operations on elements | Visitor traverses objects via an iterator to perform operations |
| Memento | State snapshots | Can store iterator cursor positions to save and restore iteration state |
Interview questions
What is the difference between fail-fast and fail-safe iterators?
Fail-fast iterators (like in ArrayList) throw ConcurrentModificationException immediately if the underlying collection is modified during iteration. Fail-safe iterators (like in CopyOnWriteArrayList or ConcurrentHashMap) operate on a clone or snapshot of the collection, allowing modifications without exceptions.
Why is it unsafe to modify a collection using list.remove() during an enhanced for-loop?
Enhanced for-loops compile into an Iterator. Calling list.remove() modifies modCount on the list without updating the iterator's expectedModCount, causing the next iteration step to throw ConcurrentModificationException. Use iterator.remove() instead.
How does an external iterator enable interleaving multiple collections?
Because the client holds two iterator instances (iterA and iterB), the client can call iterA.next() and iterB.next() alternately to merge-sort two sorted streams. Internal iterators (like forEach) cannot be interleaved in this way.
How do JavaScript Generators (function*) relate to the Iterator pattern?
A generator function automatically produces an object that implements the Iterator protocol (next() returning { value, done }), pausing execution at each yield and resuming on the subsequent next() call.
Try it
Resumable Pagination Iterator LLD
Traverse remote pages with bounded prefetch, opaque cursors and explicit resume semantics.