Back to Blog
Java

Java Iterator Interface: Usage and Implementation

Understand the Java Iterator interface: its methods, how to implement custom iterators, and common pitfalls like fail-fast behavior.

JavaIteratorCollectionIterableFail-FastConcurrentModificationException
Illustration of Java Iterator interface traversing a collection with 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 through its iterator() method, and the enhanced for loop uses that iterator for any Iterable. 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 boolean hasNext(), E next(), and a default remove() method. 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; the default implementation throws UnsupportedOperationException. 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. Direct iterator usage becomes useful 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". When an iterator supports remove(), that method is the safe way to modify the collection during traversal because it keeps the iterator state and the collection's modification count consistent.

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 class keeps its own current field and uses the start parameter, which is effectively final. The iterator() method returns a fresh iterator each time, which is required for the Iterable contract. This iterator does not support remove(); the default implementation throws UnsupportedOperationException. If you need removal, you must implement it explicitly and handle the underlying data structure accordingly.

The remove() Method and Its Limitations

Removal is optional. The default implementation throws UnsupportedOperationException, and iterators over unmodifiable collections 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 SimpleListIterator<T> implements Iterator<T> { private final List<T> list; private int cursor = 0; private int lastReturned = -1; public SimpleListIterator(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 subsequent traversal calls. 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 a method that reads or advances the iteration, such as next() or remove(), 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 }

On the first next() call, the iterator detects the change and throws. This behavior is not guaranteed for all implementations; some concurrent collections, such as CopyOnWriteArrayList, expose iterators over an immutable 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 whenever the target is an Iterable, so it avoids the boilerplate of writing the loop manually. The fail-fast check adds a small overhead on every next() call. In single-threaded scenarios, this overhead is usually negligible.

When you need to remove elements, using the iterator's remove() can avoid a second pass and a temporary collection. For bulk removals, however, a method such as removeIf or removeAll may be more readable or faster depending on the collection, so choose the approach that best expresses the operation.

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: in mutable Map implementations, the keySet(), values(), and entrySet() views return fail-fast iterators and support removal, but removal while traversing should go through the iterator's remove() or a bulk method such as removeIf, not through Map.remove().

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.

The Java Iterator Interface: Methods, Custom Iterators, and Fail-Fast Behavior | RYUSLOG DEV