• 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=IteratorPatternTest

Real 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:

  1. Identify Collection Types: Look for areas where you have different types of collections that need to be traversed.

  2. Define Iterator Interface: Create a common interface for traversing all collection types.

  3. Implement Concrete Iterators: Build specific iterators for each collection type.

  4. Create Aggregate Interface: Define an interface for creating appropriate iterators.

  5. 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]