Design PatternsBehavioral
Iterator Pattern
Traverse the elements of a collection sequentially without exposing its underlying representation.
Design Patterns
Intent
Iterator extracts traversal logic out of a collection into a separate object. Clients loop over a list, a tree, a paginated API or an infinite sequence using the same hasNext() / next() protocol — without knowing how the elements are stored.
Java builds this pattern into the language: any class that implements Iterable<T> works in an enhanced for loop.
Structure
classDiagram
direction LR
class Iterable~T~ {
<<interface>>
+iterator() Iterator~T~
}
class Iterator~T~ {
<<interface>>
+hasNext() boolean
+next() T
}
class Range {
-int from
-int to
+iterator() Iterator
}
class RangeIterator {
-int current
}
Iterable <|.. Range
Iterator <|.. RangeIterator
Range ..> RangeIterator : creates
Example: custom iterables
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
// 1) A numeric range with a step — works in for-each loops
record Range(int from, int to, int step) implements Iterable<Integer> {
public Iterator<Integer> iterator() {
return new Iterator<>() {
private int current = from;
public boolean hasNext() { return current < to; }
public Integer next() {
if (!hasNext()) throw new NoSuchElementException();
int value = current;
current += step;
return value;
}
};
}
}
// 2) A binary tree with an in-order iterator (uses an explicit stack, no recursion)
record TreeNode(int value, TreeNode left, TreeNode right) {}
class InOrder implements Iterator<Integer> {
private final Deque<TreeNode> stack = new ArrayDeque<>();
InOrder(TreeNode root) { pushLeft(root); }
private void pushLeft(TreeNode n) { for (; n != null; n = n.left()) stack.push(n); }
public boolean hasNext() { return !stack.isEmpty(); }
public Integer next() {
TreeNode n = stack.pop();
pushLeft(n.right());
return n.value();
}
}
// 3) A lazily paginated "API" — the client never sees page boundaries
class PagedIterator implements Iterator<String> {
private final IntFunction<List<String>> fetchPage;
private Iterator<String> page = Collections.emptyIterator();
private int nextPage = 0;
private boolean exhausted;
PagedIterator(IntFunction<List<String>> fetchPage) { this.fetchPage = fetchPage; }
public boolean hasNext() {
while (!page.hasNext() && !exhausted) {
List<String> items = fetchPage.apply(nextPage++);
System.out.println(" (fetched page " + (nextPage - 1) + ": " + items.size() + " items)");
exhausted = items.isEmpty();
page = items.iterator();
}
return page.hasNext();
}
public String next() { if (!hasNext()) throw new NoSuchElementException(); return page.next(); }
}
class Main {
public static void main(String[] args) {
for (int i : new Range(0, 20, 5)) System.out.print(i + " ");
System.out.println();
TreeNode tree = new TreeNode(8, new TreeNode(3, new TreeNode(1, null, null), new TreeNode(6, null, null)),
new TreeNode(10, null, new TreeNode(14, null, null)));
Iterator<Integer> it = new InOrder(tree);
while (it.hasNext()) System.out.print(it.next() + " ");
System.out.println();
List<String> users = List.of("ada", "bob", "cy", "dee", "eve");
Iterator<String> paged = new PagedIterator(p -> users.subList(Math.min(p * 2, users.size()), Math.min(p * 2 + 2, users.size())));
// Adapt any Iterator to a Stream
Stream<String> stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(paged, Spliterator.ORDERED), false);
System.out.println(stream.map(String::toUpperCase).limit(3).toList());
}
}Output
Notice that the paginated iterator fetched only the pages needed for limit(3) — iterators are naturally lazy.
Fail-fast vs fail-safe
| Fail-fast | Weakly consistent / fail-safe | |
|---|---|---|
| Collections | ArrayList, HashMap, HashSet |
ConcurrentHashMap, CopyOnWriteArrayList |
| Concurrent modification | Throws ConcurrentModificationException |
No exception — may or may not reflect changes |
| How | Checks a modCount on every next() |
Iterates over a snapshot or tolerates changes |
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
for (String s : list) {
if (s.equals("b")) list.remove(s); // ConcurrentModificationException
}
list.removeIf(s -> s.equals("b")); // correct — or use iterator.remove()
Where you’ll see it
java.util.Iterator,ListIterator(bidirectional),Spliterator(splittable, for parallel streams)Scanner,BufferedReader.lines(),DirectoryStream,ResultSet(a cursor)- Spring Data’s
Slice/PageandStream<T>repository methods; Spring Batch’sItemReader - Java Streams are “internal iterators” — you pass the operation in, the library drives the loop
Finished this lesson?
Mark it complete to track your progress through Design Patterns.