The Iterator Pattern is a fundamental behavioral design pattern that provides a standardized way to access elements of a collection sequentially without exposing its underlying representation. This pattern is crucial for creating flexible, maintainable custom collections that can integrate seamlessly with Java's enhanced for-loops and streaming APIs.
This article explores how to implement the Iterator Pattern for custom collections, demonstrating both traditional and modern approaches in Java.
Understanding the Iterator Pattern
Problem: When you create a custom collection, you want clients to be able to traverse its elements without knowing:
- The collection's internal data structure
- The implementation details of storage
- How elements are organized internally
Solution: The Iterator Pattern provides a uniform interface for traversing different data structures while keeping the traversal logic separate from the collection itself.
Key Benefits:
- Encapsulation: Hides the collection's internal structure
- Flexibility: Multiple traversal algorithms can coexist
- Simplicity: Uniform interface across different collections
- Java Integration: Works with enhanced for-loops
Core Iterator Interfaces in Java
Java provides two key interfaces in java.util:
1. Iterator
public interface Iterator<E> { boolean hasNext(); E next(); default void remove() { /* optional removal */ } // Java 8+ default methods for additional functionality } 2. Iterable
public interface Iterable<T> { Iterator<T> iterator(); // Java 8+ default methods default void forEach(Consumer<? super T> action) { /* ... */ } default Spliterator<T> spliterator() { /* ... */ } } Basic Implementation: Custom Array Collection
Let's start with a simple custom array-based collection:
import java.util.Iterator; import java.util.NoSuchElementException; public class CustomArray<T> implements Iterable<T> { private T[] elements; private int size; private static final int DEFAULT_CAPACITY = 10; @SuppressWarnings("unchecked") public CustomArray() { this.elements = (T[]) new Object[DEFAULT_CAPACITY]; this.size = 0; } public void add(T element) { if (size == elements.length) { resize(); } elements[size++] = element; } public T get(int index) { if (index < 0 || index >= size) { throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); } return elements[index]; } public int size() { return size; } @SuppressWarnings("unchecked") private void resize() { T[] newArray = (T[]) new Object[elements.length * 2]; System.arraycopy(elements, 0, newArray, 0, size); elements = newArray; } // Implementing Iterable interface @Override public Iterator<T> iterator() { return new ArrayIterator(); } // Iterator implementation as inner class private class ArrayIterator implements Iterator<T> { private int currentIndex = 0; @Override public boolean hasNext() { return currentIndex < size; } @Override public T next() { if (!hasNext()) { throw new NoSuchElementException("No more elements in the collection"); } return elements[currentIndex++]; } @Override public void remove() { if (currentIndex == 0) { throw new IllegalStateException("next() must be called before remove()"); } int removeIndex = currentIndex - 1; // Shift elements to fill the gap System.arraycopy(elements, removeIndex + 1, elements, removeIndex, size - removeIndex - 1); elements[--size] = null; // Help garbage collection currentIndex--; } } } Usage Example:
public class ArrayExample { public static void main(String[] args) { CustomArray<String> array = new CustomArray<>(); array.add("Java"); array.add("Python"); array.add("JavaScript"); array.add("Go"); // Using enhanced for-loop (requires Iterable implementation) System.out.println("Using enhanced for-loop:"); for (String language : array) { System.out.println(language); } // Using explicit iterator System.out.println("\nUsing explicit iterator:"); Iterator<String> iterator = array.iterator(); while (iterator.hasNext()) { String language = iterator.next(); System.out.println(language); if (language.equals("Python")) { iterator.remove(); // Remove current element } } // Using forEach (Java 8+) System.out.println("\nUsing forEach:"); array.forEach(System.out::println); } } Advanced Implementation: Binary Search Tree Iterator
For more complex data structures, the iterator can implement sophisticated traversal algorithms:
import java.util.Iterator; import java.util.NoSuchElementException; import java.util.Stack; public class BinarySearchTree<T extends Comparable<T>> implements Iterable<T> { private Node root; private int size; private class Node { T data; Node left, right; Node(T data) { this.data = data; } } public void add(T data) { root = addRecursive(root, data); size++; } private Node addRecursive(Node current, T data) { if (current == null) { return new Node(data); } if (data.compareTo(current.data) < 0) { current.left = addRecursive(current.left, data); } else if (data.compareTo(current.data) > 0) { current.right = addRecursive(current.right, data); } return current; } public int size() { return size; } // In-order traversal iterator @Override public Iterator<T> iterator() { return new InOrderIterator(); } // Pre-order traversal iterator public Iterator<T> preOrderIterator() { return new PreOrderIterator(); } // In-order iterator using stack private class InOrderIterator implements Iterator<T> { private Stack<Node> stack = new Stack<>(); private Node current; public InOrderIterator() { current = root; pushLeft(current); } private void pushLeft(Node node) { while (node != null) { stack.push(node); node = node.left; } } @Override public boolean hasNext() { return !stack.isEmpty(); } @Override public T next() { if (!hasNext()) { throw new NoSuchElementException(); } Node node = stack.pop(); pushLeft(node.right); return node.data; } } // Pre-order iterator private class PreOrderIterator implements Iterator<T> { private Stack<Node> stack = new Stack<>(); public PreOrderIterator() { if (root != null) { stack.push(root); } } @Override public boolean hasNext() { return !stack.isEmpty(); } @Override public T next() { if (!hasNext()) { throw new NoSuchElementException(); } Node node = stack.pop(); if (node.right != null) stack.push(node.right); if (node.left != null) stack.push(node.left); return node.data; } } } Usage Example:
public class TreeExample { public static void main(String[] args) { BinarySearchTree<Integer> tree = new BinarySearchTree<>(); tree.add(50); tree.add(30); tree.add(70); tree.add(20); tree.add(40); tree.add(60); tree.add(80); System.out.println("In-order traversal:"); for (Integer value : tree) { System.out.print(value + " "); } // Output: 20 30 40 50 60 70 80 System.out.println("\n\nPre-order traversal:"); Iterator<Integer> preOrder = tree.preOrderIterator(); while (preOrder.hasNext()) { System.out.print(preOrder.next() + " "); } // Output: 50 30 20 40 70 60 80 } } Bidirectional Iterator: Custom Linked List
For collections that support bidirectional traversal:
import java.util.Iterator; import java.util.ListIterator; import java.util.NoSuchElementException; public class CustomLinkedList<T> implements Iterable<T> { private Node head, tail; private int size; private class Node { T data; Node next, prev; Node(T data) { this.data = data; } } public void addFirst(T data) { Node newNode = new Node(data); if (head == null) { head = tail = newNode; } else { newNode.next = head; head.prev = newNode; head = newNode; } size++; } public void addLast(T data) { Node newNode = new Node(data); if (tail == null) { head = tail = newNode; } else { tail.next = newNode; newNode.prev = tail; tail = newNode; } size++; } public int size() { return size; } @Override public Iterator<T> iterator() { return new ForwardIterator(); } public Iterator<T> descendingIterator() { return new BackwardIterator(); } public ListIterator<T> listIterator() { return new BidirectionalIterator(); } // Forward-only iterator private class ForwardIterator implements Iterator<T> { private Node current = head; @Override public boolean hasNext() { return current != null; } @Override public T next() { if (!hasNext()) { throw new NoSuchElementException(); } T data = current.data; current = current.next; return data; } } // Backward-only iterator private class BackwardIterator implements Iterator<T> { private Node current = tail; @Override public boolean hasNext() { return current != null; } @Override public T next() { if (!hasNext()) { throw new NoSuchElementException(); } T data = current.data; current = current.prev; return data; } } // Bidirectional iterator implementing ListIterator private class BidirectionalIterator implements ListIterator<T> { private Node nextNode = head; private Node lastReturned = null; private int nextIndex = 0; @Override public boolean hasNext() { return nextIndex < size; } @Override public T next() { if (!hasNext()) { throw new NoSuchElementException(); } lastReturned = nextNode; nextNode = nextNode.next; nextIndex++; return lastReturned.data; } @Override public boolean hasPrevious() { return nextIndex > 0; } @Override public T previous() { if (!hasPrevious()) { throw new NoSuchElementException(); } nextNode = (nextNode == null) ? tail : nextNode.prev; lastReturned = nextNode; nextIndex--; return lastReturned.data; } @Override public int nextIndex() { return nextIndex; } @Override public int previousIndex() { return nextIndex - 1; } @Override public void remove() { if (lastReturned == null) { throw new IllegalStateException(); } // Implementation of remove operation // (simplified for brevity) } @Override public void set(T t) { if (lastReturned == null) { throw new IllegalStateException(); } lastReturned.data = t; } @Override public void add(T t) { throw new UnsupportedOperationException("Add not supported"); } } } Usage Example:
public class LinkedListExample { public static void main(String[] args) { CustomLinkedList<String> list = new CustomLinkedList<>(); list.addLast("A"); list.addLast("B"); list.addLast("C"); list.addLast("D"); System.out.println("Forward traversal:"); for (String item : list) { System.out.print(item + " "); } System.out.println("\n\nBackward traversal:"); Iterator<String> descending = list.descendingIterator(); while (descending.hasNext()) { System.out.print(descending.next() + " "); } System.out.println("\n\nBidirectional traversal:"); ListIterator<String> listIterator = list.listIterator(); while (listIterator.hasNext()) { System.out.print(listIterator.next() + " "); } while (listIterator.hasPrevious()) { System.out.print(listIterator.previous() + " "); } } } Thread-Safe Iterator Implementation
For concurrent collections, iterators need special consideration:
import java.util.Iterator; import java.util.NoSuchElementException; import java.util.concurrent.locks.ReentrantReadWriteLock; public class ThreadSafeCollection<T> implements Iterable<T> { private Object[] elements; private int size; private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); public ThreadSafeCollection(int capacity) { this.elements = new Object[capacity]; this.size = 0; } public void add(T element) { lock.writeLock().lock(); try { if (size == elements.length) { throw new IllegalStateException("Collection is full"); } elements[size++] = element; } finally { lock.writeLock().unlock(); } } @Override public Iterator<T> iterator() { return new ThreadSafeIterator(); } private class ThreadSafeIterator implements Iterator<T> { private final Object[] snapshot; private int cursor = 0; @SuppressWarnings("unchecked") public ThreadSafeIterator() { lock.readLock().lock(); try { // Create a snapshot for consistent iteration snapshot = new Object[size]; System.arraycopy(elements, 0, snapshot, 0, size); } finally { lock.readLock().unlock(); } } @Override public boolean hasNext() { return cursor < snapshot.length; } @SuppressWarnings("unchecked") @Override public T next() { if (!hasNext()) { throw new NoSuchElementException(); } return (T) snapshot[cursor++]; } } } Filtering Iterator Pattern
Create specialized iterators that filter elements:
import java.util.Iterator; import java.util.NoSuchElementException; import java.util.function.Predicate; public class FilteredCollection<T> implements Iterable<T> { private T[] elements; private int size; @SuppressWarnings("unchecked") public FilteredCollection(int capacity) { this.elements = (T[]) new Object[capacity]; this.size = 0; } public void add(T element) { if (size < elements.length) { elements[size++] = element; } } @Override public Iterator<T> iterator() { return new ArrayIterator(); } public Iterator<T> filteredIterator(Predicate<T> filter) { return new FilteredIterator(filter); } private class ArrayIterator implements Iterator<T> { private int cursor = 0; @Override public boolean hasNext() { return cursor < size; } @Override public T next() { if (!hasNext()) { throw new NoSuchElementException(); } return elements[cursor++]; } } private class FilteredIterator implements Iterator<T> { private int cursor = 0; private final Predicate<T> filter; private T nextElement; public FilteredIterator(Predicate<T> filter) { this.filter = filter; findNext(); } private void findNext() { while (cursor < size) { T element = elements[cursor++]; if (filter.test(element)) { nextElement = element; return; } } nextElement = null; } @Override public boolean hasNext() { return nextElement != null; } @Override public T next() { if (!hasNext()) { throw new NoSuchElementException(); } T result = nextElement; findNext(); return result; } } } Usage Example:
public class FilteredExample { public static void main(String[] args) { FilteredCollection<Integer> numbers = new FilteredCollection<>(10); for (int i = 1; i <= 10; i++) { numbers.add(i); } System.out.println("Even numbers:"); Iterator<Integer> evenIterator = numbers.filteredIterator(n -> n % 2 == 0); while (evenIterator.hasNext()) { System.out.print(evenIterator.next() + " "); } System.out.println("\n\nNumbers greater than 5:"); Iterator<Integer> greaterThan5 = numbers.filteredIterator(n -> n > 5); while (greaterThan5.hasNext()) { System.out.print(greaterThan5.next() + " "); } } } Best Practices for Iterator Implementation
- Fail-Fast vs. Fail-Safe:
- Fail-Fast: Throw
ConcurrentModificationExceptionif collection is modified during iteration - Fail-Safe: Work on a snapshot of the collection
- Immutable Iterators: Consider making iterators immutable for thread safety
- Resource Management: Ensure iterators don't hold resources indefinitely
- Lazy Evaluation: For large collections, compute elements on-demand
- State Validation: Check for illegal state transitions in
remove()operations
// Example of fail-fast iterator private class FailFastIterator implements Iterator<T> { private int expectedModCount = modCount; private int cursor = 0; private void checkForComodification() { if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } @Override public boolean hasNext() { checkForComodification(); return cursor < size; } @Override public T next() { checkForComodification(); if (!hasNext()) { throw new NoSuchElementException(); } return elements[cursor++]; } } Conclusion
The Iterator Pattern is essential for creating professional-grade custom collections in Java. By implementing this pattern, you:
- Enable Seamless Integration: Your collections work with enhanced for-loops and streaming APIs
- Maintain Encapsulation: Hide internal data structure details from clients
- Support Multiple Traversal Strategies: Different iterators for different needs
- Improve Code Reusability: Standard interface across all collections
- Enhance Flexibility: Easy to add new traversal algorithms
Whether you're building simple arrays, complex trees, or specialized data structures, implementing the Iterator Pattern ensures your collections are robust, maintainable, and developer-friendly. The pattern's power lies in its simplicity and the standardization it brings to collection traversal in Java.