Behavioral

Iterator Design Pattern

Provide a way to access elements of a collection sequentially

The Iterator Pattern: Traversing Collections Without Exposing Guts

Ever used a for-each loop in Java? Ever called next() on a list? Congratulations, you’ve been using the Iterator pattern all along.

The idea: provide a way to access elements of a collection sequentially without exposing the collection’s internal structure.

The Problem: Exposing Internal Structure

You’ve built a custom book collection:

class BookCollection {
    private Book[] books;
    private int index = 0;
    
    public BookCollection(int size) {
        books = new Book[size];
    }
    
    public void addBook(Book book) {
        books[index++] = book;
    }
    
    public Book[] getBooks() {
        return books;  // Exposing internal array!
    }
}

Client code:

BookCollection collection = new BookCollection(10);
// Add books...

// Client needs to know it's an array
Book[] books = collection.getBooks();
for (int i = 0; i < books.length; i++) {
    if (books[i] != null) {  // Client handles nulls
        System.out.println(books[i].getTitle());
    }
}

Problems:

Exposes internals: Client knows the collection uses an array. What if you change to a LinkedList? All client code breaks.

Tight coupling: Client code depends on implementation details.

Inconsistent iteration: Different collections have different iteration logic. Arrays use indices, LinkedLists use nodes.

No encapsulation: Clients can modify the internal array.

What if collections could provide a standard way to iterate without revealing their structure?

The Iterator Solution

Provide an iterator object that handles traversal:

// Iterator interface
interface Iterator {
    boolean hasNext();
    Object next();
}

// Aggregate interface
interface Aggregate {
    Iterator createIterator();
}

// Concrete iterator
class BookIterator implements Iterator {
    private Book[] books;
    private int position = 0;
    
    public BookIterator(Book[] books) {
        this.books = books;
    }
    
    public boolean hasNext() {
        return position < books.length && books[position] != null;
    }
    
    public Object next() {
        Book book = books[position];
        position++;
        return book;
    }
}

// Concrete aggregate
class BookCollection implements Aggregate {
    private Book[] books;
    private int index = 0;
    
    public BookCollection(int size) {
        books = new Book[size];
    }
    
    public void addBook(Book book) {
        books[index++] = book;
    }
    
    public Iterator createIterator() {
        return new BookIterator(books);
    }
}

Usage:

BookCollection collection = new BookCollection(10);
collection.addBook(new Book("1984"));
collection.addBook(new Book("Brave New World"));
collection.addBook(new Book("Fahrenheit 451"));

// Client doesn't know about internal structure
Iterator iterator = collection.createIterator();
while (iterator.hasNext()) {
    Book book = (Book) iterator.next();
    System.out.println(book.getTitle());
}

The client doesn’t know or care whether it’s an array, list, tree, or hash table. The iterator handles traversal.

The Components

1. Iterator Interface

Defines traversal interface:

interface Iterator<T> {
    boolean hasNext();
    T next();
    void remove();  // Optional
}

2. Concrete Iterator

Implements traversal for a specific collection:

class BookIterator implements Iterator<Book> {
    private Book[] books;
    private int position;
    
    public boolean hasNext() { /* ... */ }
    public Book next() { /* ... */ }
}

3. Aggregate Interface

Defines method to create iterator:

interface Aggregate<T> {
    Iterator<T> createIterator();
}

4. Concrete Aggregate

Creates and returns appropriate iterator:

class BookCollection implements Aggregate<Book> {
    public Iterator<Book> createIterator() {
        return new BookIterator(books);
    }
}

Java’s Built-In Iterator

Java provides the Iterator interface:

import java.util.Iterator;

class BookCollection implements Iterable<Book> {
    private List<Book> books = new ArrayList<>();
    
    public void addBook(Book book) {
        books.add(book);
    }
    
    @Override
    public Iterator<Book> iterator() {
        return books.iterator();  // Delegate to ArrayList's iterator
    }
}

Usage:

BookCollection collection = new BookCollection();
collection.addBook(new Book("1984"));

// Enhanced for loop (syntactic sugar for iterator)
for (Book book : collection) {
    System.out.println(book.getTitle());
}

// Or explicitly
Iterator<Book> iterator = collection.iterator();
while (iterator.hasNext()) {
    Book book = iterator.next();
    System.out.println(book.getTitle());
}

Implementing Iterable<T> enables the enhanced for loop syntax.

Real-World Example: Custom Binary Tree Iterator

class TreeNode {
    int value;
    TreeNode left;
    TreeNode right;
    
    public TreeNode(int value) {
        this.value = value;
    }
}

class BinaryTree implements Iterable<Integer> {
    private TreeNode root;
    
    public BinaryTree(TreeNode root) {
        this.root = root;
    }
    
    @Override
    public Iterator<Integer> iterator() {
        return new InOrderIterator(root);
    }
    
    // Inner class: In-order iterator
    private class InOrderIterator implements Iterator<Integer> {
        private Stack<TreeNode> stack = new Stack<>();
        
        public InOrderIterator(TreeNode root) {
            pushLeft(root);
        }
        
        private void pushLeft(TreeNode node) {
            while (node != null) {
                stack.push(node);
                node = node.left;
            }
        }
        
        @Override
        public boolean hasNext() {
            return !stack.isEmpty();
        }
        
        @Override
        public Integer next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            
            TreeNode node = stack.pop();
            pushLeft(node.right);
            return node.value;
        }
    }
}

Usage:

//       5
//      / \
//     3   7
//    / \
//   2   4
TreeNode root = new TreeNode(5);
root.left = new TreeNode(3);
root.right = new TreeNode(7);
root.left.left = new TreeNode(2);
root.left.right = new TreeNode(4);

BinaryTree tree = new BinaryTree(root);

// In-order traversal: 2, 3, 4, 5, 7
for (int value : tree) {
    System.out.println(value);
}

The iterator hides the complexity of tree traversal. Client just calls next().

Multiple Iterator Types

One collection can have multiple iterators:

class BinaryTree {
    private TreeNode root;
    
    public Iterator<Integer> inOrderIterator() {
        return new InOrderIterator(root);
    }
    
    public Iterator<Integer> preOrderIterator() {
        return new PreOrderIterator(root);
    }
    
    public Iterator<Integer> postOrderIterator() {
        return new PostOrderIterator(root);
    }
    
    public Iterator<Integer> levelOrderIterator() {
        return new LevelOrderIterator(root);
    }
}

Usage:

BinaryTree tree = new BinaryTree(root);

Iterator<Integer> inOrder = tree.inOrderIterator();
Iterator<Integer> preOrder = tree.preOrderIterator();

// Different traversal orders from same tree

Another Example: Range Iterator

class Range implements Iterable<Integer> {
    private int start;
    private int end;
    private int step;
    
    public Range(int start, int end, int step) {
        this.start = start;
        this.end = end;
        this.step = step;
    }
    
    @Override
    public Iterator<Integer> iterator() {
        return new RangeIterator();
    }
    
    private class RangeIterator implements Iterator<Integer> {
        private int current = start;
        
        @Override
        public boolean hasNext() {
            return current < end;
        }
        
        @Override
        public Integer next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            int value = current;
            current += step;
            return value;
        }
    }
}

Usage:

// Python-like range
Range range = new Range(0, 10, 2);  // 0, 2, 4, 6, 8

for (int i : range) {
    System.out.println(i);
}

Internal vs External Iterators

External Iterator (What we’ve been using)

Client controls iteration:

Iterator<Book> iterator = collection.iterator();
while (iterator.hasNext()) {
    Book book = iterator.next();
    // Client controls when to call next()
}

Pros: More control, can stop/start/skip

Cons: More code, easier to misuse

Internal Iterator

Collection controls iteration:

interface Aggregate<T> {
    void forEach(Consumer<T> action);
}

class BookCollection implements Aggregate<Book> {
    private List<Book> books;
    
    public void forEach(Consumer<Book> action) {
        for (Book book : books) {
            action.accept(book);
        }
    }
}

Usage:

collection.forEach(book -> System.out.println(book.getTitle()));

Pros: Simpler client code, harder to misuse

Cons: Less control, harder to break out early

Java’s Stream API uses internal iteration:

collection.stream()
    .filter(book -> book.getYear() > 2000)
    .forEach(book -> System.out.println(book.getTitle()));

Concurrent Modification

What happens if the collection changes during iteration?

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    list.remove(item);  // ConcurrentModificationException!
}

Java’s iterators are fail-fast: they throw ConcurrentModificationException if the collection is modified during iteration (except through the iterator’s own remove() method).

Safe way:

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    iterator.remove();  // Safe
}

Or use CopyOnWriteArrayList for concurrent access.

When to Use Iterator

Use Iterator when:

  • You need to traverse a collection without exposing its structure
  • You want to provide multiple traversal methods
  • You want a uniform interface for different collection types
  • You need to traverse the same collection in different ways

Real scenarios:

  • Collections (List, Set, Map)
  • Tree traversals (in-order, pre-order, post-order)
  • Graph traversals (BFS, DFS)
  • Custom data structures
  • File system navigation
  • Database result sets

Iterator vs Visitor

Iterator: Focuses on traversal. Client processes each element.

Visitor: Focuses on operations. Visitor performs operations on elements.

Iterator:

for (Element e : collection) {
    // Client code processes element
}

Visitor:

collection.accept(visitor);
// Visitor processes each element

Common Pitfalls

Pitfall 1: Concurrent Modification

// BAD
for (Book book : collection) {
    if (someCondition) {
        collection.remove(book);  // Exception!
    }
}

// GOOD
Iterator<Book> iterator = collection.iterator();
while (iterator.hasNext()) {
    Book book = iterator.next();
    if (someCondition) {
        iterator.remove();  // Safe
    }
}

Pitfall 2: Multiple Active Iterators

// Careful with multiple iterators on same mutable collection
Iterator<Book> it1 = collection.iterator();
Iterator<Book> it2 = collection.iterator();

it1.next();
collection.add(new Book());  // Invalidates both iterators
it2.next();  // May throw exception

Pitfall 3: Forgetting hasNext()

// BAD
Iterator<Book> iterator = collection.iterator();
Book book = iterator.next();  // What if collection is empty?

// GOOD
if (iterator.hasNext()) {
    Book book = iterator.next();
}

Iterator and SOLID Principles

Single Responsibility Principle

Collection manages data. Iterator manages traversal. Separate responsibilities.

Open/Closed Principle

Add new iterator types without modifying collection.

Liskov Substitution Principle

Any iterator can replace another anywhere Iterator interface is expected.

Dependency Inversion Principle

Client depends on Iterator interface, not concrete iterators.

The Mental Model

Think of Iterator like:

A tour guide: You’re visiting a museum. The guide (iterator) leads you from exhibit to exhibit. You don’t need to know the museum’s layout. Just follow the guide and call next().

Reading a book: You read page by page. hasNext() checks if there are more pages. next() turns the page. You don’t care how pages are physically stored.

Netflix autoplay: Watching episodes. hasNext() checks if there’s another episode. next() loads it. You don’t know how episodes are stored on servers.

Performance Considerations

Iterators typically have:

  • O(1) space overhead (just tracking position)
  • O(1) time per next() call for simple structures
  • O(log n) or O(n) for complex structures like trees

The overhead is usually negligible compared to the benefits.

Testing with Iterator

@Test
public void testIteratorTraversesAllElements() {
    BookCollection collection = new BookCollection();
    collection.addBook(new Book("A"));
    collection.addBook(new Book("B"));
    
    Iterator<Book> iterator = collection.iterator();
    
    assertTrue(iterator.hasNext());
    assertEquals("A", iterator.next().getTitle());
    assertTrue(iterator.hasNext());
    assertEquals("B", iterator.next().getTitle());
    assertFalse(iterator.hasNext());
}

@Test
public void testIteratorRemove() {
    List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
    Iterator<String> iterator = list.iterator();
    
    iterator.next();
    iterator.remove();
    
    assertEquals(2, list.size());
    assertEquals("B", list.get(0));
}

Real-World Usage

Iterator is everywhere in Java:

// Collections
List<String> list = new ArrayList<>();
Iterator<String> it = list.iterator();

// Enhanced for loop (uses iterator internally)
for (String s : list) { }

// Streams (internal iteration)
list.stream().forEach(System.out::println);

// Database results
ResultSet rs = statement.executeQuery("SELECT * FROM users");
while (rs.next()) {  // Iterator pattern
    String name = rs.getString("name");
}

Final Thoughts

The Iterator pattern is about separation of concerns. Collections handle data storage. Iterators handle traversal. Neither needs to know about the other’s internals.

It’s not about complexity. It’s about:

  • Hiding implementation details
  • Providing uniform traversal interface
  • Supporting multiple traversal types
  • Enabling the enhanced for loop

The key insight: clients shouldn’t know or care how collections store data. They just want to visit each element.

Remember:

  • Iterator handles traversal logic
  • Collection creates appropriate iterator
  • Client uses standard interface
  • Multiple iterator types possible

Next time you’re exposing a collection’s internals just to let clients traverse it, stop. Think Iterator. Provide a standard traversal interface.

Keep iterating.