Back to Blog
Java

Java Iterator: Usage, Implementation, and Common Pitfalls

java iterator: Understand the Java Iterator interface: its methods, how for-each uses it, safe removal, custom iterators, and fail-fast behavior.

JavaIteratorCollectionIterableFor-Each Loop
Diagram of Java Iterator traversing a collection with next and hasNext indicators

The java iterator is the mechanism behind every for-each loop in Java. It is defined by the java.util.Iterator interface, which provides a uniform way to traverse collection elements without exposing the underlying data structure.

The Iterator Interface and Its Methods

The java.util.Iterator interface defines three core methods: hasNext(), next(), and remove(). The first two are required; remove() has a default implementation that throws UnsupportedOperationException in Java 8 and later.

public interface Iterator<E> { boolean hasNext(); E next(); default void remove() { throw new UnsupportedOperationException("remove"); } }

hasNext() returns true if there is another element in the collection. next() returns the next element and advances the cursor. Calling next() when hasNext() returns false throws NoSuchElementException. This two-step pattern is the reason iterators are safe to use in loops without manual index tracking.

The remove() method removes the last element returned by next() from the underlying collection. It can be called only once per next() call; calling it before next() or twice in a row throws IllegalStateException.

How the For-Each Loop Uses Iterator

The enhanced for loop, introduced in Java 5, relies on the Iterable interface. Any class that implements Iterable can be used in a for-each loop. The compiler converts the loop into an iterator-based traversal.

List<String> names = Arrays.asList("Alice", "Bob", "Charlie"); for (String name : names) { System.out.println(name); }

The bytecode for this loop is roughly equivalent to:

for (Iterator<String> it = names.iterator(); it.hasNext(); ) { String name = it.next(); System.out.println(name); }

This means the for-each loop cannot modify the collection while iterating, except through the iterator's own remove() method. If you need to remove elements during traversal, you must use an explicit iterator.

Removing Elements Safely with Iterator.remove()

The Iterator.remove() method is the only safe way to remove an element from a collection while iterating without causing a ConcurrentModificationException. For example, filtering a list:

List<Integer> numbers = new ArrayList<>(); numbers.add(1); numbers.add(2); numbers.add(3); numbers.add(4); Iterator<Integer> it = numbers.iterator(); while (it.hasNext()) { Integer num = it.next(); if (num % 2 == 0) { it.remove(); } }

After this loop, numbers contains [1, 3]. The remove() method updates the collection's modification count, so subsequent hasNext() and next() calls remain consistent. Using list.remove(num) inside the loop would instead throw ConcurrentModificationException because the list's structural modification is not reflected in the iterator's expected mod count.

Implementing a Custom Iterator

Sometimes you need to iterate over a custom data structure. Implementing Iterator requires careful handling of the cursor and the removal semantics. Consider a simple 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 RangeIterator(); } private class RangeIterator implements Iterator<Integer> { private int current = start; @Override public boolean hasNext() { return current <= end; } @Override public Integer next() { if (!hasNext()) { throw new NoSuchElementException(); } return current++; } } }

This iterator is read-only; remove() is not overridden, so it uses the default throwing implementation. For a mutable collection, you would need to track the last returned element and support removal. The key is to keep the cursor state consistent with the underlying data structure.

Iterator vs. Iterable vs. ListIterator

Iterable is the interface that provides an iterator() method. It is what enables the for-each loop. Iterator is the actual traversal object. ListIterator extends Iterator and adds bidirectional traversal, index access, and element replacement. It is available only on List implementations.

InterfacePurposeKey Methods
Iterable<T>Returns an Iteratoriterator()
Iterator<T>Forward-only traversalhasNext(), next(), remove()
ListIterator<T>Bidirectional traversal on listshasPrevious(), previous(), add(), set()

Choosing between them depends on the collection type and the traversal requirements. If you need to move backward or modify elements, ListIterator is the appropriate tool.

Fail-Fast Behavior and ConcurrentModificationException

Most Java collection iterators are fail-fast: they detect structural modification during iteration and throw ConcurrentModificationException as soon as possible. This is not a guarantee of correctness but a best-effort check to catch bugs early. The iterator stores a modCount value; if the collection's modCount changes, the iterator throws.

List<String> list = new ArrayList<>(List.of("a", "b", "c")); for (String s : list) { if (s.equals("b")) { list.remove(s); // throws ConcurrentModificationException } }

The exception occurs because list.remove(s) changes the modCount without updating the iterator's expected value. Using Iterator.remove() avoids this because it updates both the collection and the iterator's state. This behavior is important in multi-threaded contexts: if one thread modifies the collection while another iterates, the iterator will fail fast rather than produce undefined results.

Performance and Memory Considerations

Iterators themselves are lightweight objects, typically holding a reference to the collection and a cursor index. The cost of hasNext() and next() is usually O(1) for array-based lists but may be O(n) for linked structures if the iterator must traverse links. The for-each loop adds no significant overhead compared to manual indexing for ArrayList, but for LinkedList it is the recommended way to traverse because random access via get(index) is O(n) per call.

Memory-wise, an iterator does not copy the collection; it operates on the original data. However, if you create a new iterator for each loop, there is a small allocation cost. In most applications this is negligible, but in tight loops over very large collections, reusing an iterator is not possible because iterators are stateful and single-use.

When Not to Use an Iterator

Iterators are not always the best choice. For random access patterns, direct indexing with get(index) is faster for ArrayList because it avoids iterator overhead. For bulk operations like filtering, the Stream API with filter() and collect() often produces clearer code. Also, if you need to traverse multiple collections simultaneously or need to rewind, a ListIterator or a different approach may be more appropriate.

The decision should be based on the collection type, the operation you need, and whether you require modification during traversal. An iterator is the right tool when you need sequential access with the ability to remove elements safely, or when you are implementing a custom collection that should support the for-each loop.

java iterator: Practical Usage and Code Examples | RYUSLOG DEV