Behavioral Patterns
Iterator
Access elements of an aggregate object sequentially without exposing its underlying representation (list, stack, tree, graph).
The Iterator Pattern is a behavioral design pattern that provides a way to access the elements of a collection sequentially without exposing its underlying storage representation (such as lists, trees, hash maps, or complex graph structures). By extracting the traversal state and algorithms into dedicated iterator objects, it decouples client applications from structural data representations.
1. Learning Objectives
- Understand the Single Responsibility Principle by decoupling traversal algorithms from data containers.
- Differentiate between the mechanics of Fail-Fast and Fail-Safe iterators.
- Trace how the JVM optimizes enhanced for-loop syntax sugars into iterator instances.
- Evaluate the memory overhead of maintaining multiple independent traversers on the same collection.
- Implement custom graph-traversal iterators (BFS/DFS) in Java, Python, and modern C++.
2. Problem & Naive Solution
Suppose you are building a profile connection manager for a social networking platform. Profiles are connected in a graph structure representing friendships. Clients need to traverse the friend network to build recommendation lists or display feeds.
The Naive Solution
In a naive implementation, the client class accesses the internal data structures of the social graph directly and implements its own Depth-First Search (DFS) stack routing:
This direct-access model presents serious design flaws:
- Exposed Internal Structure: If you decide to change the internal graph representation from an adjacency list to an adjacency matrix or a database index query, the client code breaks.
- Violates SRP: The client service is bloated with graph traversal routing, queues, and search stacks instead of focusing on rendering feeds.
- Traversal Code Duplication: If another service (e.g. recommendation engine) needs to traverse the graph, it must copy-paste the DFS/BFS stack logic.
3. Issues
Direct client coupling to concrete collections prevents refactoring. Changing storage collections (e.g., from ArrayList to LinkedList or TreeSet) forces rewrites of client loops. Furthermore, running multiple traversals on the same collection simultaneously is impossible if traversal indices are stored directly inside the collection class.
4. Pattern Introduction & UML
The Iterator Pattern solves this by extracting the traversal logic into a separate ProfileIterator interface. The collection (SocialNetwork) exposes a method to create an iterator. The client calls hasNext() and getNext() on the iterator. The iterator manages the internal stack, current index pointer, and visited nodes, shielding the client from the underlying graph structure.
UML: Graph Iterator System
5. Participants
- Iterable Collection (
SocialNetwork): Declares the factory method for creating the Iterator. - Concrete Collection (
ConcreteSocialNetwork): Implements the factory method, returning an instance of the concrete iterator configured with reference loops. - Iterator (
ProfileIterator): Declares the iteration interface (hasNext(),getNext()). - Concrete Iterator (
ConcreteDFSIterator): Implements traversal (DFS) and tracks the current search index and stack state. - Client (
FeedDisplayService): Calls the iterator methods to traverse the collection.
6. Theory (Fail-Fast vs. Fail-Safe Iterators)
A key consideration in iterator design is handling concurrent modifications to the collection:
- Fail-Fast Iterators: Throw a
ConcurrentModificationExceptionimmediately if the collection is structurally modified (items added or deleted) during traversal. - *How*: The collection maintains amodCountmodification counter. The iterator copies this counter on instantiation. On every step (next()), it checks ifmodCount != expectedModCount. - Fail-Safe (Non-Seismic) Iterators: Traverse a clone of the collection or work on concurrent structures (e.g.
CopyOnWriteArrayList). - *How*: They operate on a copy of the collection, allowing modifications without exceptions, but at the cost of higher memory usage.
7. Syntax Explanation
Language-specific constructs for iteration support:
- Java: Standard interfaces
java.util.Iteratorandjava.lang.Iterableenable clean integration with the native enhanced for-loop. - Python: Overriding
__iter__()(returns the iterator object) and__next__()(returns the next value, raisingStopIterationwhen complete) enables nativefor item in collection:syntax. - C++: STL iterators require overloading operator functions (
operator++,operator*, andoperator!=), enabling integration with range-based for loops.
8. Step-by-Step Implementation
- Step 1: Create the
Iteratorinterface defining methods likehasNext()andgetNext(). - Step 2: Create the
IterableCollectioninterface containing the factory methodcreateIterator(). - Step 3: Implement the
ConcreteCollectionclass containing the actual storage container. - Step 4: Build the
ConcreteIteratorclass, capturing the collection reference and implementing the traversal logic (e.g., stack routing). - Step 5: Modify the client to execute traversals by calling factory methods and looping over the returned iterator.
9. Complete Code (Mini Project)
10. Code Walkthrough
Let's walk through the decoupled graph navigation:
- Encapsulated Traversal Algorithms: The DFS stack is stored entirely inside the
ConcreteDFSIteratorinstance. The client code is free of index checks and stack logic. - Information Hiding: The client does not know how friendships are mapped (adjacency lists, arrays, database queries). It simply iterates over connections.
- Independent Traversals: Because the iterator maintains its own state stack, you can create multiple iterators on the same network graph simultaneously without interference.
11. Execution Flow
- Client Call: Client calls
createFriendsIterator("Alice"). - Iterator Creation: The collection instantiates
ConcreteDFSIterator, loading "Alice" onto its internal stack. - Next Check: Client calls
iterator.hasNext(). The iterator clears already visited items and checks if its stack is empty. - Item Retrieval: Client calls
iterator.getNext(). The iterator pops the top item, records it as visited, loads its neighbors onto the stack, and returns the profile ID.
12. Internal Working (Compiler Optimization & Syntactic Sugar)
Iterators are so common that modern languages implement syntactic sugar for them:
- JVM Syntactic Sugar Conversion: When you write a Java enhanced for loop:
13. Complexity Analysis
- Time Complexity: $O(1)$ constant time per step (
hasNext(),getNext()). Total traversal takes $O(V + E)$ where $V$ is profiles and $E$ is connections. - Space Complexity: $O(V)$ space inside the iterator stack and visited set to track graph vertices.
14. Best Practices
- Clean up resources: If your iterator is fetching data from a database or file, implement
AutoCloseable(Java) or context managers (Python) to close resources when traversal ends. - Prefer Fail-Fast Implementation: Check database/collection modification states to prevent hard-to-debug concurrency issues.
15. Common Mistakes
- Modifying Collections Directly During Iteration: Calling
collection.remove(item)inside an iterator loop instead ofiterator.remove(), which throwsConcurrentModificationException. - Sharing Iterator Instances: Attempting to share a single iterator object across different client threads, causing race conditions in pointer states.
16. Framework Usage
- Java Collections Iterator: Every class in the Java Collections Framework (ArrayList, HashSet) implements
Iterableto support traversal. - Python iter() and next(): Built-in structures support iterator interfaces automatically.
- C++ STL Iterators: STL algorithms like
std::findandstd::sortoperate entirely on iterators, decoupling algorithms from data containers.
17. Interview Discussion
Answer: - Fail-Fast iterators throw a
ConcurrentModificationException immediately if the collection is structurally modified during traversal.
- Fail-Safe iterators operate on a copy of the collection, allowing concurrent updates without raising exceptions, but at the expense of higher memory usage.
Answer: Use concurrent collections (e.g.
ConcurrentHashMap in Java), run traversals inside synchronized/locked blocks, or copy the collection state to a local array during iterator creation.
Answer: The Java compiler converts for-each loops into standard iterator calls behind the scenes. This conversion requires the target collection to implement
Iterable to compile.
18. Practice Exercises
- Easy: Write a Python program containing a custom list iterator that traverses elements in reverse.
- Medium: Design a binary search tree (BST) iterator in Java with an in-order traversal route.
- Hard: Build a dynamic paginated database cursor iterator in C++ that loads data chunks on-demand as the user scrolls.
19. Challenge Problem
Design an Inventory Stock Sheet Multi-Traverser. An warehouse inventory system organizes products in a multi-level category hierarchy (e.g. Electronics -> Computers -> Laptops). You must implement an iterator engine that can traverse this inventory using two strategies: Category Depth-First Search (visiting subcategories recursively) and Alphabetical SKU Search (traversing products alphabetically across all categories). Write a solution in Java, Python, or C++ that supports running both traversal strategies polymorphically.
20. Summary & Cheat Sheet
- Iterator decouples traversal logic from structural data representations.
- Enables multiple independent traversals to run on the same collection concurrently.
- Fail-fast iterators throw exceptions on modifications; fail-safe iterators work on copies.
- Modern languages compile enhanced loop structures into iterator calls automatically.
21. Quiz
1. What is the primary purpose of the Iterator design pattern?
A) To adapt incompatible interfaces
B) To access elements of an aggregate object sequentially without exposing its underlying structure (Correct)
C) To control object instantiation lifecycles
2. Which pattern helps maintain the Single Responsibility Principle during collection traversal?
A) Iterator (Correct)
B) Facade
C) Prototype
3. What does a Fail-Fast iterator do if the underlying collection is modified during loop execution?
A) Creates a copy of the collection
B) Throws a ConcurrentModificationException immediately (Correct)
C) Skips the modified elements
4. How does a Fail-Safe iterator avoid concurrent modification exceptions?
A) It locks the thread
B) It traverses a copy or clone of the collection (Correct)
C) It compiles the collection to read-only bytecodes
5. What does the Java compiler translate enhanced for loops into behind the scenes?
A) Native pointer arrays
B) Standard iterator calls (hasNext(), next()) (Correct)
C) Static global method callbacks
6. Can you run multiple traversals on the same collection instance simultaneously using iterators?
A) No, since index states interfere
B) Yes, because each iterator instance maintains its own traversal state (Correct)
C) Only when using single-threaded environments
7. What is the time complexity of checking if an item exists next (hasNext()) in a list-based iterator?
A) $O(N)$
B) $O(1)$ (Correct)
C) $O(\log N)$
8. Which Python methods must be overridden to support the native iterable protocol?
A) has_next() and get_next()
B) __iter__() and __next__() (Correct)
C) __init__() and __call__()
9. In C++, how does the standard library integrate user-defined custom iterators into range loops?
A) By overloading operators (operator++, operator*, operator!=) (Correct)
B) By declaring global functions
C) Using raw void pointers
10. Why should iterators avoid sharing state across threads?
A) To save compiler allocations
B) To prevent race conditions in index pointer tracking (Correct)
C) To avoid vtable lookups
22. Next Lesson Preview
In the next lesson, we will explore the Observer Pattern. We will learn how to establish a one-to-many subscription model so that when one object changes state, all its dependents are notified automatically!
Related Topics
- StrategyDefine a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients.
- ObserverDefine a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
- CommandEncapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.