Java Iterator Interface: Usage and Implementation
java iterator interface: Understand the Java Iterator interface: its methods, how to implement custom iterators, and common pitfalls like fail-fast behavior.
The Java Iterator interface is the foundation of collection traversal in the Java Collections Framework. It provides a uniform way to access elements sequentially without exposing the underlying data structure. Every Collection in Java returns an Iterator via the iterator() method, and the enhanced for loop compiles down to iterator usage. Understanding this interface is essential for writing robust collection-processing code, especially when you need to remove elements during traversal or implement your own iterable types.
The Iterator Interface and Its Contract
The Iterator<E> interface defines three abstract methods: boolean hasNext(), E next(), and default void remove(). The hasNext() method returns true if the iteration has more elements. next() returns the next element and advances the cursor. The remove() method removes the last element returned by next() from the underlying collection. Since Java 8, forEachRemaining(Consumer<? super E>) provides a default implementation that processes all remaining elements.
The contract is strict: calling next() when hasNext() returns false throws NoSuchElementException. Calling remove() before next() or after a second next() throws IllegalStateException. These rules ensure predictable behavior across all implementations.
Using Iterator with Standard Collections
Most developers use the enhanced for loop, which hides the iterator. However, direct iterator usage becomes necessary when you need to remove elements during traversal. For example, removing items from a List while iterating with a for loop can cause ConcurrentModificationException or skip elements. The iterator's remove() method is the safe way to delete the current element.
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie")); Iterator<String> it = names.iterator(); while (it.hasNext()) { String name = it.next(); if (name.startsWith("A")) { it.remove(); } }
This code removes all names starting with "A". The remove() method is the only safe way to modify the collection during iteration because it updates the iterator's internal state and the collection's modification count consistently.
Implementing a Custom Iterator
To create a custom iterator, you implement the Iterator interface. This is common when you have a custom data structure or want to provide a specialized traversal. For instance, consider a Range class that yields integers from start to end.
public class Range implements Iterable<Integer> { private final int start; private final int end; public Range(int start, int end) { this.start = start; this.end = end; } @Override public Iterator<Integer> iterator() { return new Iterator<Integer>() { private int current = start; @Override public boolean hasNext() { return current <= end; } @Override public Integer next() { if (!hasNext()) { throw new NoSuchElementException(); } return current++; } }; } }
The anonymous inner class captures the current variable. The iterator() method returns a fresh iterator each time, which is required for the Iterable contract. Note that the iterator is fail-fast in the sense that it does not support remove(); the default remove() throws UnsupportedOperationException. If you need removal, you must implement it explicitly and handle the underlying data structure accordingly.
The remove() Method and Its Limitations
The remove() method is optional. The default implementation throws UnsupportedOperationException, and many iterators, such as those from Set or Map views, do not support removal. When you implement a custom iterator, you must decide whether removal is meaningful. If you support it, you must track the state to ensure that remove() is called exactly once per next(). The typical pattern is to keep a lastReturned index and reset it after removal.
public class ListIterator<T> implements Iterator<T> { private final List<T> list; private int cursor = 0; private int lastReturned = -1; public ListIterator(List<T> list) { this.list = list; } @Override public boolean hasNext() { return cursor < list.size(); } @Override public T next() { if (!hasNext()) throw new NoSuchElementException(); lastReturned = cursor; return list.get(cursor++); } @Override public void remove() { if (lastReturned < 0) throw new IllegalStateException(); list.remove(lastReturned); cursor = lastReturned; lastReturned = -1; } }
This implementation adjusts the cursor after removal because the list shrinks. The lastReturned flag prevents double removal.
Fail-Fast Behavior and Concurrent Modification
Most iterators in the Java Collections Framework are fail-fast: if the collection is structurally modified after the iterator is created, the iterator throws ConcurrentModificationException on the next access. This is a safety mechanism to prevent undefined behavior, not a guarantee of atomicity. The iterator checks a modification count (modCount) stored in the collection. When you call next() or hasNext(), it compares the current modCount with the expected value. If they differ, it throws.
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3)); Iterator<Integer> it = numbers.iterator(); numbers.add(4); // structural modification while (it.hasNext()) { System.out.println(it.next()); // throws ConcurrentModificationException }
The exception is thrown on the first next() call because the iterator detects the change. This behavior is not guaranteed for all implementations; some concurrent collections, like CopyOnWriteArrayList, use fail-safe iterators that operate on a snapshot and do not throw. Understanding this distinction is critical when choosing a collection for concurrent access.
Performance and Operational Considerations
Iterators are lightweight objects, but their behavior affects performance. The enhanced for loop is syntactic sugar for iterator usage, so there is no performance penalty. However, the fail-fast check adds a small overhead on every next() call. In single-threaded scenarios, this overhead is negligible. When you need to remove elements, using the iterator's remove() is more efficient than collecting elements to remove and then calling removeAll() because it avoids a second pass and temporary collection.
For large collections, consider whether you need the iterator at all. If you only need to read elements, the enhanced for loop is clear and concise. If you need to filter and transform, Java streams may offer a more declarative approach, but they also have overhead. The iterator gives you fine-grained control, especially when you need to break early or interleave logic.
Common Pitfalls and Edge Cases
A common mistake is calling next() without checking hasNext(), which throws NoSuchElementException. Another is using remove() incorrectly, such as calling it twice in a row. Also, be careful with iterators over Map entries: the keySet(), values(), and entrySet() views return iterators that are fail-fast and support removal, but removal from the map during iteration must go through the iterator.
Another edge case is iterating over a collection that is modified by another thread. The fail-fast behavior is not a synchronization mechanism; you must still use external synchronization or a concurrent collection. The iterator is not thread-safe by itself.
When implementing Iterable, ensure that each call to iterator() returns a fresh iterator. Reusing a single iterator across multiple loops will cause unexpected behavior because the iterator's state is exhausted.