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:

  1. Tight Coupling: The LibraryClient is now tightly coupled to the internal implementation of BookCollection. It knows that the collection uses an ArrayList.
  2. Violates Encapsulation: The BookCollection has to expose its internal list, breaking its encapsulation and allowing the client to potentially modify it in unintended ways.
  3. Violates OCP: What if you decide to change the internal data structure from an ArrayList to a Book[] array, or a LinkedList, or a HashSet 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

  1. Iterator: The interface that defines the operations for traversal, most commonly hasNext() (to check if there are more elements) and next() (to retrieve the next element).
  2. Concrete Iterator: A class that implements the Iterator interface. It keeps track of the current position while traversing a specific Concrete Aggregate.
  3. Aggregate: The interface for the collection. It declares a factory method for creating an Iterator object (e.g., createIterator()).
  4. Concrete Aggregate: A class that implements the Aggregate interface. Its createIterator() method returns a new instance of a Concrete 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 the java.lang.Iterable interface are the direct implementation of this pattern. The for-each loop (for (Book book : bookCollection)) is pure syntactic sugar that, behind the scenes, gets an iterator from the collection and uses it in a while 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.