- Shift Elevate
- Posts
- Iterator Pattern: Traverse Collections with Consistent Interface
Iterator Pattern: Traverse Collections with Consistent Interface
The Pain of Inconsistent Collection Access
Picture this: You're building a music playlist application that needs to support different types of collections: regular playlists, shuffled playlists, and recently played tracks. Your initial approach seems logical: create different methods for each collection type to access their elements.
But then requirements expand: "We need to support playlists with different sorting orders, filtered playlists, and playlists that combine multiple sources. Also, users want to be able to skip forward, go backward, and jump to specific positions in any playlist type."
Suddenly, you're creating different traversal methods for each collection type, making your code inconsistent and hard to maintain. Adding new collection types means creating new traversal logic, and clients need to know the specific implementation details of each collection. Sound familiar?
The Iterator pattern solves this by providing a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
Understanding the Iterator Pattern
The Iterator pattern provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation. It encapsulates the traversal logic and provides a consistent interface for iterating over different types of collections.
Think of it like a universal remote control for different types of media players: whether you're playing a CD, streaming music, or listening to a radio station, the remote provides the same buttons (play, pause, next, previous) even though the underlying mechanisms are completely different.
This pattern promotes Encapsulation, Consistency, and Flexibility while hiding the complexity of different collection implementations.

Iterator Pattern Components
Core Components
Iterator Interface: Defines the interface for accessing and traversing elements
Concrete Iterator: Implements the iterator interface and keeps track of the current position
Aggregate Interface: Defines the interface for creating an iterator object
Concrete Aggregate: Implements the aggregate interface and returns an appropriate iterator
Client: Uses the iterator to traverse the aggregate without knowing its concrete class
Complete Java Implementation
Let's build a music playlist system that demonstrates the Iterator pattern's power in providing consistent collection traversal.
Iterator Interface
public interface SongIterator {
boolean hasNext();
Song next();
Song current();
void reset();
}
public class Song {
private String title;
private String artist;
private String genre;
public Song(String title, String artist, String genre) {
this.title = title;
this.artist = artist;
this.genre = genre;
}
public String getTitle() { return title; }
public String getArtist() { return artist; }
public String getGenre() { return genre; }
@Override
public String toString() {
return String.format("%s by %s [%s]", title, artist, genre);
}
}
Aggregate Interface
public interface Playlist {
SongIterator createIterator();
void addSong(Song song);
void removeSong(Song song);
String getPlaylistName();
int getSongCount();
}
Concrete Iterator
public class ShuffledPlaylistIterator implements SongIterator {
private List<Song> songs;
private int currentIndex;
public ShuffledPlaylistIterator(List<Song> songs) {
this.songs = new ArrayList<>(songs);
shuffleSongs();
this.currentIndex = 0;
}
private void shuffleSongs() {
Collections.shuffle(songs);
}
@Override
public boolean hasNext() {
return currentIndex < songs.size();
}
@Override
public Song next() {
if (hasNext()) {
return songs.get(currentIndex++);
}
return null;
}
@Override
public Song current() {
if (currentIndex > 0 && currentIndex <= songs.size()) {
return songs.get(currentIndex - 1);
}
return null;
}
@Override
public void reset() {
shuffleSongs(); // Reshuffle on reset
currentIndex = 0;
}
}
public class FilteredPlaylistIterator implements SongIterator {
private List<Song> filteredSongs;
private int currentIndex;
public FilteredPlaylistIterator(List<Song> songs, String filterGenre) {
this.filteredSongs = songs.stream()
.filter(song -> song.getGenre().equalsIgnoreCase(filterGenre))
.collect(Collectors.toList());
this.currentIndex = 0;
}
@Override
public boolean hasNext() {
return currentIndex < filteredSongs.size();
}
@Override
public Song next() {
if (hasNext()) {
return filteredSongs.get(currentIndex++);
}
return null;
}
@Override
public Song current() {
if (currentIndex > 0 && currentIndex <= filteredSongs.size()) {
return filteredSongs.get(currentIndex - 1);
}
return null;
}
@Override
public void reset() {
currentIndex = 0;
}
}
Concrete Aggregate
public class ShuffledPlaylist implements Playlist {
private String name;
private List<Song> songs;
public ShuffledPlaylist(String name) {
this.name = name;
this.songs = new ArrayList<>();
}
@Override
public SongIterator createIterator() {
return new ShuffledPlaylistIterator(songs);
}
@Override
public void addSong(Song song) {
songs.add(song);
}
@Override
public void removeSong(Song song) {
songs.remove(song);
}
@Override
public String getPlaylistName() {
return name;
}
@Override
public int getSongCount() {
return songs.size();
}
}
public class FilteredPlaylist implements Playlist {
private String name;
private List<Song> songs;
private String filterGenre;
public FilteredPlaylist(String name, String filterGenre) {
this.name = name;
this.songs = new ArrayList<>();
this.filterGenre = filterGenre;
}
@Override
public SongIterator createIterator() {
return new FilteredPlaylistIterator(songs, filterGenre);
}
@Override
public void addSong(Song song) {
songs.add(song);
}
@Override
public void removeSong(Song song) {
songs.remove(song);
}
@Override
public String getPlaylistName() {
return name + " (Filtered: " + filterGenre + ")";
}
@Override
public int getSongCount() {
return (int) songs.stream()
.filter(song -> song.getGenre().equalsIgnoreCase(filterGenre))
.count();
}
}
Client
public class MusicPlayer {
private Playlist currentPlaylist;
private SongIterator iterator;
private int songsPlayed;
public void loadPlaylist(Playlist playlist) {
this.currentPlaylist = playlist;
this.iterator = playlist.createIterator();
this.songsPlayed = 0;
System.out.println("Loaded playlist: " + playlist.getPlaylistName());
System.out.println("Total songs: " + playlist.getSongCount());
}
public void playNext() {
if (iterator != null && iterator.hasNext()) {
Song song = iterator.next();
songsPlayed++;
System.out.println("Now playing: " + song);
System.out.println("Progress: " + songsPlayed + "/" + currentPlaylist.getSongCount());
} else {
System.out.println("End of playlist reached");
}
}
public void showCurrentSong() {
if (iterator != null) {
Song current = iterator.current();
if (current != null) {
System.out.println("Current song: " + current);
} else {
System.out.println("No song currently playing");
}
}
}
public void resetPlaylist() {
if (iterator != null) {
iterator.reset();
songsPlayed = 0;
System.out.println("Playlist reset to beginning");
}
}
public void showPlaylistInfo() {
if (currentPlaylist != null) {
System.out.println("Playlist: " + currentPlaylist.getPlaylistName());
System.out.println("Songs: " + currentPlaylist.getSongCount());
}
}
}
public class MusicPlayerDemo {
public static void main(String[] args) {
// Create sample songs
List<Song> sampleSongs = Arrays.asList(
new Song("Bohemian Rhapsody", "Queen", "Rock"),
new Song("Imagine", "John Lennon", "Pop"),
new Song("Hotel California", "Eagles", "Rock")
);
MusicPlayer player = new MusicPlayer();
System.out.println("=== Music Player Iterator Pattern Demo ===\n");
// Test Shuffled Playlist
System.out.println("1. Shuffled Playlist:");
System.out.println("====================");
ShuffledPlaylist shuffledPlaylist = new ShuffledPlaylist("Shuffled Mix");
sampleSongs.forEach(shuffledPlaylist::addSong);
player.loadPlaylist(shuffledPlaylist);
player.playNext();
player.playNext();
player.showCurrentSong();
// Test Filtered Playlist
System.out.println("\n2. Filtered Playlist (Rock only):");
System.out.println("=================================");
FilteredPlaylist rockPlaylist = new FilteredPlaylist("Rock Collection", "Rock");
sampleSongs.forEach(rockPlaylist::addSong);
player.loadPlaylist(rockPlaylist);
player.playNext();
player.playNext();
// Test reset functionality
System.out.println("\n3. Reset and Replay:");
System.out.println("===================");
player.resetPlaylist();
player.playNext();
}
}
Expected Output:
=== Music Player Iterator Pattern Demo ===
1. Shuffled Playlist:
====================
Loaded playlist: Shuffled Mix
Total songs: 3
Now playing: Hotel California by Eagles [Rock]
Progress: 1/3
Now playing: Imagine by John Lennon [Pop]
Progress: 2/3
Current song: Imagine by John Lennon [Pop]
2. Filtered Playlist (Rock only):
=================================
Loaded playlist: Rock Collection (Filtered: Rock)
Total songs: 2
Now playing: Bohemian Rhapsody by Queen [Rock]
Progress: 1/2
Now playing: Hotel California by Eagles [Rock]
Progress: 2/2
3. Reset and Replay:
===================
Playlist reset to beginning
Now playing: Bohemian Rhapsody by Queen [Rock]
Progress: 1/2
🚀 Get the Complete Implementation
The full code with advanced iterator implementations and collection management is available in our Design Patterns Repository.
# Clone and run the complete demo
git clone https://github.com/shift-elevate/design-patterns.git
cd design-patterns
mvn test -Dtest=IteratorPatternTestReal World Examples
The Iterator pattern is extensively used in real world applications:
1. Java Collections Framework
Java's Collections Framework uses the Iterator pattern extensively. The Iterator interface provides a consistent way to traverse different types of collections (ArrayList, LinkedList, HashSet, etc.) without exposing their internal structure.
2. Database Result Sets
Database APIs use the Iterator pattern for result sets, allowing applications to traverse query results without knowing the specific database implementation or result set structure.
3. File System Traversal
File system APIs use the Iterator pattern to traverse directories and files, providing a consistent interface regardless of the underlying file system implementation.
When to Use the Iterator Pattern
Understanding when to apply the Iterator pattern is crucial for making the right architectural decisions. Here's when it shines and when alternatives might be better:
✅ Ideal Scenarios:
You need to traverse different types of collections in a uniform way.
You want to hide the complexity of different collection implementations from clients.
You need to support multiple traversal algorithms for the same collection.
You want to provide a consistent interface for accessing collection elements.
❌ Skip It When:
The collection is simple and unlikely to change.
You need random access to collection elements.
The overhead of creating iterator objects is too high for your use case.
Next Steps: Apply Iterator Pattern in Your Project
Ready to implement the Iterator pattern in your own projects? Here's a structured approach to get you started:
Identify Collection Types: Look for areas where you have different types of collections that need to be traversed.
Define Iterator Interface: Create a common interface for traversing all collection types.
Implement Concrete Iterators: Build specific iterators for each collection type.
Create Aggregate Interface: Define an interface for creating appropriate iterators.
Test Consistent Traversal: Ensure all collection types can be traversed using the same interface.
The Iterator pattern transforms inconsistent collection access into uniform, maintainable traversal mechanisms. By providing a consistent iterator interface, you build systems that are easier to use and extend, regardless of the underlying collection implementation.
Found this helpful? Share it with a colleague who's struggling with inconsistent collection access. Got questions? We'd love to hear from you at [email protected]