Iterator

June 17, 2026 in patterns 6 minutes

A intermediate-level guide to Iterator: before-and-after java code and diagrams for a CS student.

The Leaky Collection Problem

Imagine you are building an application that manages a list of student names. To keep things simple, you decide to store these names in a standard array inside your NameList class. When you want to display those names, you write a loop that asks the list for its size and then grabs each name one by one using an index.

This approach works perfectly—until it doesn’t.

If you later realize that an array is inefficient for your needs and decide to switch to a different data structure (like a linked list or a tree), every single piece of code that uses getNameAt(index) will break. Your internal implementation has “leaked” into the rest of your program. The client (the part of the code using your list) is too tightly coupled to how the names are stored; it knows too much about the guts of the NameList.

class NameList {
    private String[] names;
    private int size;

    public NameList(String[] names) {
        this.names = names;
        this.size = names.length;
    }

    // Leaky implementation: client must know internal array structure
    // and manual index management to traverse.
    public int getSize() {
        return size;
    }

    public String getNameAt(int index) {
        return names[index];
    }
}

public class Main {
    public static void main(String[] args) {
        NameList list = new NameList(new String[]{"Alice", "Bob", "Charlie"});
        
        // Problem: Traversal logic is coupled to the index-based structure
        for (int i = 0; i < list.getSize(); i++) {
            System.out.println(list.getNameAt(i));
        }
    }
}

In the diagram below, notice how the Main class is forced to manage the logic of the loop and the index itself. It must know that NameList uses an integer-based indexing system to function.

  classDiagram
    class Main {
        +main(String[] args)
    }
    class NameList {
        -String[] names
        -int size
        +getSize() int
        +getNameAt(int index) String
    }
    Main --> NameList : calls getSize and getNameAt

Hiding the Traversal Logic

To fix this, we need to stop telling the collection how to loop through itself. Instead, we should ask the collection for a specialized object—an Iterator—that handles the traversal for us.

Think of it like a vending machine. You don’t need to know if the machine uses a spiral coil, a robotic arm, or gravity to move an item to the slot. You simply interact with a standard interface: you press a button (or call next()), and the machine provides the item. The internal mechanism is hidden from you, but the result is consistent.

By using an Iterator, we decouple the “what” (getting the next name) from the “how” (incrementing an integer index or moving a pointer in memory).

interface NameIterator {
    boolean hasNext();
    String next();
}

class NameList {
    private String[] names;

    public NameList(String[] names) {
        this.names = names;
    }

    // Encapsulation: Client only interacts with the Iterator interface.
    // The internal array structure is now hidden.
    public NameIterator createIterator() {
        return new ArrayNameIterator();
    }

    private class ArrayNameIterator implements NameIterator {
        private int index = 0;

        @Override
        public boolean hasNext() {
            return index < names.length;
        }

        @Override
        public String next() {
            return names[index++];
        }
    }
}

public class Main {
    public static void main(String[] args) {
        NameList list = new NameList(new String[]{"Alice", "Bob", "Charlie"});

        // Solution: Traversal is decoupled from the internal structure.
        NameIterator it = list.createIterator();
        while (it.hasNext()) {
            System.out.println(it.next());
        }
    }
}

In this refactored version, Main no longer knows that NameList uses an array. It only knows how to talk to the NameIterator interface. If we change the internal storage of NameList, we only have to update the private ArrayNameIterator class; the Main method remains untouched.

  classDiagram
    class NameIterator {
        <<interface>>
        +hasNext() boolean
        +next() String
    }
    class ArrayNameIterator {
        -int index
        +hasNext() boolean
        +next() String
    }
    class NameList {
        -String[] names
        +createIterator() NameIterator
    }
    ArrayNameIterator ..|> NameIterator : implements
    NameList --> NameIterator : creates

To understand how these objects work together during runtime, we can look at the sequence of messages sent between the client and the iterator.

  sequenceDiagram
    participant M as Main
    participant L as NameList
    participant I as ArrayNameIterator
    M ->> L: createIterator()
    L -->> M: I
    loop while it.hasNext() is true
        M ->> I: hasNext()
        I -->> M: true
        M ->> I: next()
        I ->> I: index++
        I -->> M: "Alice"
    end

The Hidden Mechanics

You might be wondering, “If I use a for-each loop in Java, am I still doing this?” The answer is yes. The for-each syntax is “syntactic sugar”—a shorthand way of writing code that the compiler expands into a standard Iterator loop behind the scenes.

When you write for (String name : list), the Java compiler actually converts it into something similar to the logic seen in our solution: it calls a method on the collection to get an iterator and then repeatedly calls hasNext() and next(). This is why modern collections like ArrayList or HashSet all feel so consistent when you loop through them.

Edge Cases and Safety

While Iterators are powerful, they come with one major rule: Do not modify the collection while you are iterating over it.

If you use an Iterator to walk through a list and, halfway through, you call a method on the original NameList to add or remove an item, the iterator’s internal state (like its current index) becomes out of sync with the actual structure of the data. In Java, this typically results in a ConcurrentModificationException. This is a “fail-fast” mechanism designed to prevent your program from behaving unpredictably due to corrupted traversal logic. If you must remove items during a loop, use the specific removal method provided by the Iterator itself rather than the collection.

When to Use It, and When Not To

The Iterator pattern is a cornerstone of clean, encapsulated code. However, it isn’t always necessary.

Use an Iterator when:

  • You want to hide the internal structure of a complex data structure.
  • You need to provide different ways to traverse the same data (e.g., forward, backward, or in a specific order).
  • You want your client code to be able to switch between different types of collections without changing its own logic.

Avoid an Iterator when:

  • The collection is extremely simple and its structure is unlikely to ever change. In those rare cases, the extra classes might add unnecessary complexity (though this is rarely a concern in modern software engineering).

Takeaways

  • An Iterator hides how a collection is stored so you only care about what is inside.
  • Using an Iterator prevents your code from breaking when you change a data structure.
  • Modern “for-each” loops are often just shorthand for using an Iterator.

Usage

Setup the collection for iteration

NameList list = new NameList(new String[]{"Alice", "Bob"});