Iterator Pattern Tutorial 🔄 | Iterate Collections Like a Pro ⭐
The Iterator Design Pattern is a behavioral design pattern that allows sequential traversal of elements within a collection without exposing its underlying representation (such as arrays, lists, sets, trees, or maps). By decoupling the traversal algorithms from the data structures themselves, it enables clean, scalable, and highly flexible code that adheres to core object-oriented design principles.
1. The Core Problem: The Mess of Collection Traversal
In software engineering, we constantly work with collections of objects. However, different data structures require entirely different traversal mechanics:
- C++ Vectors: Accessed via direct indexing (
vector[i]). - Java ArrayLists: Accessed via the
.get(i)method. - Sets & Maps: Do not have index-based access; they require hash-bucket traversal or tree-node traversal.
If client applications had to understand the internal mechanics of every data structure they traversed, the code would become highly coupled and fragile.
The Problem of Leaking Internal Structure
Without a unified traversal interface, any change to the underlying data structure (e.g., switching an ArrayList to a LinkedList or a HashSet) forces you to rewrite the traversal logic across your entire codebase.
The Iterator Pattern solves this by providing a uniform interface—typically centered around two simple operations: hasNext() and next()—shielding the client from the internal complexities of the collection.
2. Real-Life Analogy: The Modern Music Playlist
Imagine you are building a modern music streaming application like Spotify, Apple Music, or YouTube Music.
The Real-World Analogy
A user's playlist is a collection of songs. However, users do not always want to listen to songs in a simple, sequential order. They expect multiple playback strategies:
- Sequential Playback: Playing songs one by one from index to .
- Random Shuffle: Shuffling the songs randomly so the next song is unpredictable.
- Favorites Only: Skipping non-favorite songs and playing only the ones marked with a ❤️.
- Seasonal/Custom Filters: Playing only "March Favorites" or "Year-End Hits".
If we bundle all these traversal algorithms directly inside the
Playlistclass, the class will quickly bloat, violating clean architecture principles.
3. The Naive Approach & Its Architectural Pitfalls
In a naive implementation, a developer might attempt to handle all playback modes directly within the Playlist class using conditional logic.
Naive Code Implementation
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
class Song {
private String title;
private boolean isFavorite;
public Song(String title, boolean isFavorite) {
this.title = title;
this.isFavorite = isFavorite;
}
public String getTitle() { return title; }
public boolean isFavorite() { return isFavorite; }
}
// Naive Playlist class handling its own traversal logic
class NaivePlaylist {
private List<Song> songs = new ArrayList<>();
public void addSong(Song song) {
songs.add(song);
}
// Naive traversal method cluttered with conditional logic
public void playSongs(String mode) {
if (mode.equals("SEQUENTIAL")) {
for (int i = 0; i < songs.size(); i++) {
System.out.println("Playing: " + songs.get(i).getTitle());
}
} else if (mode.equals("SHUFFLE")) {
List<Song> shuffled = new ArrayList<>(songs);
Collections.shuffle(shuffled);
for (Song song : shuffled) {
System.out.println("Playing shuffled: " + song.getTitle());
}
} else if (mode.equals("FAVORITES")) {
for (Song song : songs) {
if (song.isFavorite()) {
System.out.println("Playing favorite: " + song.getTitle());
}
}
}
}
}
Why This Naive Approach Fails in Production
- Violation of the Single Responsibility Principle (SRP): The
NaivePlaylistclass is now responsible for two distinct tasks: managing the collection of songs, and implementing multiple traversal algorithms. - Violation of the Open/Closed Principle (OCP): Every time a product manager requests a new playback mode (e.g., "Skip Explicit Songs" or "Reverse Order"), you must modify the core
NaivePlaylistclass. This introduces regression risks to existing playback modes. - Tight Coupling: The client cannot implement custom traversal strategies without modifying the collection class itself.
- Poor Scalability: As the number of traversal strategies grows, the
if-elseblocks expand, making the code unreadable and difficult to maintain.
4. The Iterator Pattern Mental Model
The Iterator Pattern decouples the collection from its traversal algorithms by extracting the traversal state and logic into dedicated Iterator objects.
Clean Vertical Workflow
Client Application
|
| 1. Requests an Iterator (e.g., getShuffleIterator())
v
Playlist Collection (Iterable)
|
| 2. Instantiates and returns the Concrete Iterator (passing 'this' reference)
v
Concrete Iterator (e.g., ShufflePlaylistIterator)
|
| 3. Client calls hasNext() / next() on the Iterator
v
Client receives elements sequentially without knowing the internal structure
By using this workflow, the Playlist class only needs to expose a way to hand over an iterator. The client interacts exclusively with the abstract iterator interface.
5. Step-by-Step Evolution: Designing the Iterator Interface & Concrete Iterators
Let's refactor the naive design into a highly decoupled, production-grade implementation using the Iterator Pattern.
Step 1: Define the Iterator Interface
This interface defines the contract for traversing any collection.
public interface PlaylistIterator {
boolean hasNext();
Song next();
}
Step 2: Implement the Concrete Iterators
Each concrete iterator encapsulates its own traversal state (such as the current tracking index) and algorithm.
1. Sequential Playlist Iterator
This iterator traverses the playlist linearly from start to finish.
public class SimplePlaylistIterator implements PlaylistIterator {
private List<Song> songs;
private int position = 0;
public SimplePlaylistIterator(List<Song> songs) {
this.songs = songs;
}
@Override
public boolean hasNext() {
return position < songs.size();
}
@Override
public Song next() {
if (!hasNext()) {
throw new java.util.NoSuchElementException("No more songs in the playlist!");
}
return songs.get(position++);
}
}
2. Shuffle Playlist Iterator
This iterator shuffles a copy of the playlist upon initialization, allowing randomized playback while keeping the original playlist order intact.
import java.util.Collections;
public class ShufflePlaylistIterator implements PlaylistIterator {
private List<Song> shuffledSongs;
private int position = 0;
public ShufflePlaylistIterator(List<Song> songs) {
// Create a shallow copy to avoid modifying the original playlist
this.shuffledSongs = new ArrayList<>(songs);
Collections.shuffle(this.shuffledSongs);
}
@Override
public boolean hasNext() {
return position < shuffledSongs.size();
}
@Override
public Song next() {
if (!hasNext()) {
throw new java.util.NoSuchElementException("No more songs in the shuffled playlist!");
}
return shuffledSongs.get(position++);
}
}
3. Favorite Playlist Iterator
This iterator filters the collection on the fly, returning only the songs marked as favorites.
public class FavoritePlaylistIterator implements PlaylistIterator {
private List<Song> songs;
private int position = 0;
public FavoritePlaylistIterator(List<Song> songs) {
this.songs = songs;
}
@Override
public boolean hasNext() {
// Scan ahead to find the next favorite song
while (position < songs.size()) {
if (songs.get(position).isFavorite()) {
return true;
}
position++;
}
return false;
}
@Override
public Song next() {
if (!hasNext()) {
throw new java.util.NoSuchElementException("No more favorite songs!");
}
return songs.get(position++);
}
}
6. Integrating Iterators with the Collection (The Playlist Class)
Now, we define the collection interface and implement the concrete Playlist class. The playlist is responsible for storing the elements and returning the appropriate iterator instance.
Step 1: Define the Aggregate Interface
public interface SongCollection {
PlaylistIterator createSequentialIterator();
PlaylistIterator createShuffleIterator();
PlaylistIterator createFavoriteIterator();
}
Step 2: Implement the Concrete Playlist Class
The playlist class passes its internal list of songs to the iterators using the this reference or by passing the internal list directly.
public class Playlist implements SongCollection {
private List<Song> songs;
public Playlist() {
this.songs = new ArrayList<>();
}
public void addSong(Song song) {
songs.add(song);
}
@Override
public PlaylistIterator createSequentialIterator() {
return new SimplePlaylistIterator(this.songs);
}
@Override
public PlaylistIterator createShuffleIterator() {
return new ShufflePlaylistIterator(this.songs);
}
@Override
public PlaylistIterator createFavoriteIterator() {
return new FavoritePlaylistIterator(this.songs);
}
}
7. Complete End-to-End Flow Walkthrough
Let's see how a client application utilizes these decoupled components to play music using different strategies.
public class Main {
public static void main(String[] args) {
// 1. Create a playlist and populate it with songs
Playlist myPlaylist = new Playlist();
myPlaylist.addSong(new Song("Hotel California", true));
myPlaylist.addSong(new Song("Bohemian Rhapsody", false));
myPlaylist.addSong(new Song("Stairway to Heaven", true));
myPlaylist.addSong(new Song("Billie Jean", false));
// 2. Traverse Sequentially
System.out.println("--- Playing Songs Sequentially ---");
PlaylistIterator sequentialIterator = myPlaylist.createSequentialIterator();
printPlaylist(sequentialIterator);
// 3. Traverse in Shuffled Order
System.out.println("\n--- Playing Songs in Shuffled Order ---");
PlaylistIterator shuffleIterator = myPlaylist.createShuffleIterator();
printPlaylist(shuffleIterator);
// 4. Traverse Favorites Only
System.out.println("\n--- Playing Favorite Songs Only ---");
PlaylistIterator favoriteIterator = myPlaylist.createFavoriteIterator();
printPlaylist(favoriteIterator);
}
private static void printPlaylist(PlaylistIterator iterator) {
while (iterator.hasNext()) {
Song song = iterator.next();
System.out.println("Now Playing: " + song.getTitle() + " (Favorite: " + song.isFavorite() + ")");
}
}
}
Expected Output
--- Playing Songs Sequentially ---
Now Playing: Hotel California (Favorite: true)
Now Playing: Bohemian Rhapsody (Favorite: false)
Now Playing: Stairway to Heaven (Favorite: true)
Now Playing: Billie Jean (Favorite: false)
--- Playing Songs in Shuffled Order ---
Now Playing: Stairway to Heaven (Favorite: true)
Now Playing: Billie Jean (Favorite: false)
Now Playing: Hotel California (Favorite: true)
Now Playing: Bohemian Rhapsody (Favorite: false)
--- Playing Favorite Songs Only ---
Now Playing: Hotel California (Favorite: true)
Now Playing: Stairway to Heaven (Favorite: true)
8. Built-in Language Support: The Java Iterator Ecosystem
Modern programming languages have built-in support for the Iterator Pattern. In Java, this is standardized via the java.util.Iterator interface and the java.lang.Iterable interface.
Whenever you use a Java collection (like ArrayList, HashSet, or HashMap), you can retrieve an iterator directly:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class JavaBuiltInIteratorDemo {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Mango");
// Obtaining the built-in iterator
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println("Fruit: " + fruit);
}
}
}
Note: The Java "for-each" loop (
for (String fruit : fruits)) is syntactic sugar over the standardIteratorinterface. Under the hood, the compiler translates the loop into an iterator-basedwhileloop.
9. Architectural Trade-offs & Complexity Analysis
When choosing or implementing an iteration strategy, it is critical to evaluate the algorithmic complexities of each approach.
10. Industry Reality & Interview Insights
The Iterator Pattern is a favorite topic in Low-Level Design (LLD) interviews at top-tier companies like Coinbase, Uber, and Confluent.
Key Interview Trap: Concurrent Modification
One of the most common follow-up questions in an LLD interview is: "What happens if the collection is modified (elements added/removed) while an iterator is actively traversing it?"
There are two primary industry strategies to handle this:
-
Fail-Fast Iterators:
- Mechanism: The collection maintains a structural modification count (
modCount). When the iterator is created, it stores the expected modification count (expectedModCount). On everynext()orhasNext()call, it compares these two values. If they differ, it immediately throws aConcurrentModificationException. - Use Case: Standard Java collections (e.g.,
ArrayList,HashMap).
- Mechanism: The collection maintains a structural modification count (
-
Fail-Safe (or Weakly Consistent) Iterators:
- Mechanism: The iterator operates on a cloned copy of the collection or uses copy-on-write data structures. Modifications to the original collection do not affect the active traversal.
- Use Case: Concurrent collections (e.g.,
CopyOnWriteArrayList,ConcurrentHashMap).
11. Summary and Key Takeaways
- Decoupled Architecture: The Iterator Pattern separates traversal algorithms from the underlying collection data structures.
- Single Responsibility: It moves the traversal state out of the collection class, keeping your collections clean and focused on storage.
- Open/Closed Principle Compliant: You can introduce new traversal strategies (e.g.,
MarchPlaylistIterator) by simply implementing thePlaylistIteratorinterface, without modifying existing collection code. - Uniform Interface: Clients can traverse arrays, lists, trees, or custom graphs using the exact same
hasNext()andnext()API. - State Isolation: Multiple iterators can traverse the same collection simultaneously because each iterator instance maintains its own independent traversal state (e.g., current index pointer).
- Production Standard: Standard library implementations (like Java's
IterableandIterator) form the backbone of modern collection frameworks and loop constructs.