Iterator
Behavioral Design Patterns
Imagine you have a BookCollection
class that stores a list of books. Internally, you decide to use an ArrayList
. Your client code, wanting to print the names of all the books, might look like this:
public class LibraryClient {
public void printBooks(BookCollection collection) {
ArrayList<Book> books = collection.getBooks(); // Exposing the internal list!
for (int i = 0; i < books.size(); i++) {
System.out.println(books.get(i).getTitle());
}
}
}
This design has several major problems:
- Tight Coupling: The
LibraryClient
is now tightly coupled to the internal implementation ofBookCollection
. It knows that the collection uses anArrayList
. - Violates Encapsulation: The
BookCollection
has to expose its internal list, breaking its encapsulation and allowing the client to potentially modify it in unintended ways. - Violates OCP: What if you decide to change the internal data structure from an
ArrayList
to aBook[]
array, or aLinkedList
, or aHashSet
for better performance? You would have to find and change every single client that iterates over the collection.
The client shouldn't have to know how the collection stores its elements, only that it wants to go through them one by one.
A Standardized Way to Traverse
The Iterator is a behavioral design pattern that provides a way to access the elements of a collection sequentially without exposing its underlying representation.
The core idea is to extract the traversal logic from the collection and place it into a separate "iterator" object. This iterator object becomes the universal remote for traversing any collection. It provides a simple, standard interface (usually hasNext()
and next()
) that the client can use. The client asks the collection for its iterator, and then uses that iterator to walk through the elements, completely oblivious to whether the underlying structure is an array, a list, a set, or a complex tree.
The Participants
- Iterator: The interface that defines the operations for traversal, most commonly
hasNext()
(to check if there are more elements) andnext()
(to retrieve the next element). - Concrete Iterator: A class that implements the
Iterator
interface. It keeps track of the current position while traversing a specificConcrete Aggregate
. - Aggregate: The interface for the collection. It declares a factory method for creating an
Iterator
object (e.g.,createIterator()
). - Concrete Aggregate: A class that implements the
Aggregate
interface. ItscreateIterator()
method returns a new instance of aConcrete Iterator
that is tailored to its specific internal data structure.
Example: A Custom Book Iterator
To demonstrate the principle, let's build a custom iterator for our BookCollection
.
// The Iterator interface
public interface IBookIterator {
boolean hasNext();
Book next();
}
// The Aggregate interface
public interface IBookCollection {
IBookIterator createIterator();
}
// Concrete Aggregate (stores books in an array)
public class BookCollection implements IBookCollection {
private Book[] books;
// ... constructor, addBook method, etc.
@Override
public IBookIterator createIterator() {
return new BookIterator(this);
}
// Getter for the iterator to use
public Book[] getBooks() { return this.books; }
}
// Concrete Iterator for the BookCollection
public class BookIterator implements IBookIterator {
private BookCollection collection;
private int index = 0;
public BookIterator(BookCollection collection) {
this.collection = collection;
}
@Override
public boolean hasNext() {
return index < collection.getBooks().length;
}
@Override
public Book next() {
if (!hasNext()) {
// Or throw NoSuchElementException
return null;
}
Book nextBook = collection.getBooks()[index];
index++;
return nextBook;
}
}
// The client is now completely decoupled from the internal representation
public class LibraryClient {
public void printBooks(IBookCollection collection) {
IBookIterator iterator = collection.createIterator();
while (iterator.hasNext()) {
Book book = iterator.next();
System.out.println(book.getTitle());
}
}
}
Now, we can change BookCollection
to use a LinkedList
or any other structure. As long as it provides a corresponding iterator, the LibraryClient
code does not need to change at all.
Interview Focus: Analysis and Trade-offs
Benefits:
- Decoupling and SRP: It decouples your data structures from the algorithms that traverse them. The collection's responsibility is to store data; the iterator's responsibility is to traverse it.
- Simplifies Client Code: The client uses a simple, standard interface to traverse any collection.
- Multiple Traversals: You can have several independent iterators traversing the same collection at the same time, each keeping track of its own position.
- Polymorphic Traversal: The client code can treat all collections that provide an iterator in the same way.
Drawbacks:
- Complexity for Simple Collections: For a very simple collection with no plans to change, creating a separate iterator class can feel like unnecessary boilerplate. (However, as we'll see, this is rarely an issue in practice).
How Iterator Relates to Other Concepts
- A Core Language Feature: This is the most important point for an interview. The Iterator pattern is so fundamental that it's built directly into most modern languages. In Java, the
java.util.Iterator
interface and thejava.lang.Iterable
interface are the direct implementation of this pattern. Thefor-each
loop (for (Book book : bookCollection)
) is pure syntactic sugar that, behind the scenes, gets an iterator from the collection and uses it in awhile
loop, just like our example. - Factory Method: The
createIterator()
method on the aggregate class is a perfect example of the Factory Method pattern. It's a method for creating an object (the iterator), but it lets the concrete aggregate decide which specific iterator class to return. - Composite: The Iterator pattern is essential for working with the Composite pattern. You can create different kinds of iterators (e.g.,
DepthFirstIterator
,BreadthFirstIterator
) to provide different ways of traversing the complex tree structure of a composite object. - State: An iterator can be thought of as a state machine. Its internal state is the "current position" in the collection.