The Iterator Design Pattern is a behavioral pattern that provides a standard way to access elements of a collection sequentially without exposing its internal structure.
It’s particularly useful in situations where:
When faced with this need, developers often write custom for
loops or expose the underlying data structures (like ArrayList
or LinkedList
) directly. For example, a Playlist
class might expose its songs
array and let the client iterate however it wants.
But this approach makes the client tightly coupled to the collection’s internal structure, and it violates encapsulation. If the internal storage changes, the client code breaks. It also becomes difficult to add new traversal logic or support lazy iteration.
The Iterator Pattern solves this by abstracting the iteration logic into a dedicated object — the iterator. Collections provide an iterator via a method like createIterator()
, and the client uses it to access elements one by one.
This makes it easy to change the collection’s structure, add new traversal styles, and preserve clean, encapsulated designs.
Let’s walk through a real-world example to see how we can apply the Iterator Pattern to build a more maintainable, extensible, and standardized approach to traversing collections.
Imagine you're building a music streaming app that allows users to create and manage playlists. Each playlist stores a list of songs and provides features like:
Let’s say you start with a simple implementation:
And your client code might look like this:
By exposing the internal list of songs (getSongs()
), you allow clients to directly modify the collection. This can lead to unintended side effects, like removing songs from the list, reordering them, or injecting nulls.
The client assumes that the playlist uses a List<String>
. If you ever decide to change the internal storage (e.g., to a LinkedList
, array, or even a streaming buffer), the client code breaks.
The client is stuck with the default iteration order. Supporting multiple traversal styles (e.g., reverse, shuffled, filtered) requires rewriting the loop logic every time — violating the Single Responsibility and Open/Closed principles.
As more features are added (e.g., previewing songs, playing only favorites), the logic for traversing songs gets duplicated across multiple classes.
We need a clean, standardized way to:
This is exactly where the Iterator Pattern helps.
The Iterator Pattern provides a standard way to traverse elements in a collection without exposing its internal structure.
Instead of letting clients access the underlying data (like an ArrayList
), the collection provides an iterator object that offers sequential access to its elements through a common interface (e.g., hasNext()
, next()
).
Defines the contract for traversing a collection. Declares methods for traversing elements like:
hasNext()
— checks if there are more elementsnext()
— returns the next element in the sequenceIterator
interface for a specific collection.IterableCollection
interface.Let’s now implement the Iterator Pattern to decouple the way we traverse songs in a playlist from how the playlist is internally structured.
We’ll build:
Playlist
that stores songsPlaylistIterator
that can move through the playlistIterator
InterfaceThis interface defines the standard operations needed to iterate over a collection.
IterableCollection
InterfaceThis represents any collection that can produce an iterator.
Playlist
This class will hold a list of songs and return an iterator to traverse them.
PlaylistIterator
This class knows how to traverse a Playlist
one song at a time.
The client can now iterate through a playlist without knowing how it's implemented internally.
hasNext()
/next()
pattern