Java Iterators
Walk through Java collections with the Iterator interface — hasNext, next, remove — and the Iterable contract.
Java Iterators
Every time you write for (T x : collection) in Java, you're calling on a hidden pair: the Iterable<T> interface that lets the collection be looped over, and the Iterator<T> it hands the loop. The for-each loop is sugar; the Iterator is the engine. Understanding what it does — and what its three methods are allowed to throw — is the difference between "my list traversal works most of the time" and "I know exactly when it will break."
This chapter is about plain Iterator<E>. The richer ListIterator<E>, which can walk backwards and modify during iteration, gets its own chapter next.
The two interfaces
Iterable<T> is the contract for "things you can iterate":
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) { ... }
default Spliterator<T> spliterator() { ... }
}Iterator<T> is the cursor:
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() { throw new UnsupportedOperationException(); }
default void forEachRemaining(Consumer<? super E> action) { ... }
}Every Collection<E> extends Iterable<E> — that's why a for-each loop works on a List, a Set, a Queue. (A Map is not Iterable; you iterate its entrySet(), keySet(), or values().) The for-each loop:
for (String name : names) { System.out.println(name); }is compiled to:
for (Iterator<String> it = names.iterator(); it.hasNext(); ) {
String name = it.next();
System.out.println(name);
}Once you've seen that desugaring, the rest of Iterator's rules make sense.
The three methods, and what they throw
hasNext() returns true if next() would succeed. Idempotent — calling it twice in a row is fine. Never throws (in well-behaved implementations).
next() advances the cursor and returns the element. Throws NoSuchElementException if there is no next element. This is the only iterator method that throws an exception by design when you misuse it. Always guard with hasNext() if there's any chance the collection is empty:
while (it.hasNext()) { use(it.next()); } // safe patternremove() removes the element most recently returned by next(). It's a default method that throws UnsupportedOperationException unless the iterator implements it. ArrayList, HashMap.keySet().iterator(), and friends all support it. Iterators returned from List.of(...), Collections.unmodifiableList(...), and stream .iterator() do not. You also can't call remove() twice in a row without an intervening next() — that throws IllegalStateException.
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
if (name.isEmpty()) it.remove(); // legal, fail-safe removal
}it.remove() is the only safe way to remove from a collection while iterating with a plain Iterator. The collection's own remove(...) would invalidate the iterator and throw ConcurrentModificationException on the next call.
Fail-fast iteration
Most JDK collection iterators are fail-fast: they record the collection's modification count when the iterator is created, check it on every hasNext/next call, and throw ConcurrentModificationException if it has changed by anyone other than the iterator itself.
List<String> names = new ArrayList<>(List.of("a", "b", "c"));
Iterator<String> it = names.iterator();
names.add("d"); // direct mutation, not via iterator
it.next(); // throws ConcurrentModificationExceptionFail-fast is a best effort diagnostic, not a thread-safety guarantee. It catches the common bug ("oh, I modified the list inside the loop and now my iterator is confused") cleanly and early. It does not protect against concurrent modification from another thread — for that you need a concurrent collection (CopyOnWriteArrayList, ConcurrentHashMap) whose iterators are weakly consistent instead: they walk a snapshot and never throw.
forEachRemaining and forEach
Two default methods make iteration shorter when you don't need the cursor:
list.forEach(System.out::println); // every element
Iterator<String> it = list.iterator();
while (it.hasNext() && !it.next().equals("STOP")) { }
it.forEachRemaining(System.out::println); // everything past STOPforEach is on Iterable; forEachRemaining is on Iterator. Both are sequential. Don't use them when you also need to remove — they hide the cursor, and remove requires it.
Writing your own Iterator
You'll write one when you implement a custom collection-like type. The contract is small, but every part of it matters:
class Countdown implements Iterable<Integer> {
private final int from;
Countdown(int from) { this.from = from; }
@Override public Iterator<Integer> iterator() {
return new Iterator<>() {
int n = from;
@Override public boolean hasNext() { return n > 0; }
@Override public Integer next() {
if (n <= 0) throw new NoSuchElementException();
return n--;
}
};
}
}
for (int x : new Countdown(3)) System.out.println(x); // 3 2 1Three things to get right:
next()must throwNoSuchElementExceptionwhen exhausted. Don't returnnullor some sentinel.- The iterator should be a fresh instance every call to
iterator(). Callingfor (... : it)twice on the same iterable should both start from the beginning. remove()is opt-in. Don't implement it unless you actually can — thedefaultbody that throws is fine.
A worked example: iteration, removal, fail-fast, custom iterable
The program below walks an ArrayList three ways (for-each, explicit iterator, forEachRemaining), removes elements safely with Iterator.remove, demonstrates the fail-fast exception when you bypass the iterator, and finishes with a small custom Iterable<T>.
What to take from the run:
- The for-each loop printed every element; behind the scenes it asked the
ArrayListfor an iterator and walked it withhasNext/next. Iterator.removedeleted the empty strings during iteration without aConcurrentModificationException. That's the only correct in-loop deletion technique with a plainIterator.forEachRemainingis a tidy way to drain whatever the iterator hasn't yielded yet — useful right after a partial walk.- Mutating the list directly while another iterator is live threw
ConcurrentModificationExceptionon the nextnext()call. The exception is intentional: it makes the bug loud. - The custom
Countdownshows the minimum contract you need to write a working iterable.hasNextreports cleanly;nextthrows when exhausted; noremove(it inherits the default).
What's next
A plain Iterator can walk forward and remove. That's enough for sets, maps, and queues — they don't have positions in any other sense. Lists do, and they get a richer cursor: ListIterator<E> can move forward and backward, report indices, and add or set elements during the walk. That's the next chapter.
Practice
Inside a `for-each` loop over `List<String> list`, you call `list.remove(name)` to drop matching entries. The first removal works; the next iteration throws. What's the right fix?