Java Iterators
An Iterator is an object that lets you walk through the elements of a collection one at a time, without exposing how that collection is stored internally. It is the standard way to loop over any type that implements Iterable — ArrayList, HashSet, LinkedList, and more — and it is also the exact mechanism the compiler uses under the hood whenever you write a for-each loop. Understanding iterators well means understanding how the for-each loop actually works, how to safely remove elements while looping, and how to build your own iterable types.
Overview: How Iterators Work
Every collection class in the Java Collections Framework that supports the for-each loop implements the java.lang.Iterable<E> interface, whose single method — Iterator<E> iterator() — returns a fresh Iterator positioned just before the first element. The Iterator<E> interface itself defines hasNext(), next(), and remove(). Internally, the iterator returned by a class like ArrayList keeps a small integer cursor field that tracks the index of the element next() should return next. Calling next() reads the value at cursor, increments cursor by one, and returns the value.
This is exactly what the compiler generates when you write a for-each loop such as for (String s : list) { ... }. Behind the scenes, javac rewrites it into a call to list.iterator() followed by a while (it.hasNext()) loop that calls it.next() on every pass. The for-each loop is not a separate looping mechanism at all — it is purely syntactic sugar over the exact Iterator protocol described above. That single fact explains several behaviors that otherwise seem mysterious, including why you cannot safely call list.remove(x) from inside a for-each loop over list.
Most built-in iterators (ArrayList, HashMap, HashSet, etc.) are fail-fast. Every mutable collection keeps an internal counter called modCount, which increments each time the collection is structurally modified (an element added or removed). When an iterator is created, it records the current value as expectedModCount. Every subsequent call to next() or remove() compares the two counters; if the collection was structurally changed by anything other than that same iterator’s own remove() (or a ListIterator‘s add()), the counters will not match and the iterator throws a ConcurrentModificationException. This is a deliberate safety mechanism, not a bug — it stops your code from silently reading a corrupted, partially-shifted collection while iterating over it.
Java also provides a more powerful sub-interface, ListIterator<E>, available only on classes that implement List (such as ArrayList and LinkedList). Unlike a plain Iterator, a ListIterator can move backward as well as forward, report its current index, replace the last-returned element with set(E e), and insert new elements mid-traversal with add(E e).
Before Iterator existed, early Java collections (Vector, Hashtable) used an older interface called Enumeration, which supports only hasMoreElements() and nextElement() and has no remove(). You will rarely need it in modern code. On the newer end, Java 8 introduced Spliterator, a more advanced traversal-and-partitioning interface used internally by the Stream API to support parallel processing; you rarely call it directly, but it is what powers list.stream().
Syntax
The general pattern for using an iterator explicitly looks like this:
Iterator<Type> it = collection.iterator();
while (it.hasNext()) {
Type element = it.next();
// use element
// optionally: it.remove();
}
| Method | Description |
|---|---|
boolean hasNext() |
Returns true if there is at least one more element to visit. |
E next() |
Returns the next element and advances the cursor. Throws NoSuchElementException if no elements remain. |
void remove() |
Removes the element most recently returned by next() from the underlying collection. Must be called at most once per call to next(), or it throws IllegalStateException. It is an optional operation — iterators over immutable collections throw UnsupportedOperationException. |
default void forEachRemaining(Consumer<? super E> action) |
Applies action to every remaining element (Java 8+). |
For any List, calling list.listIterator() returns a ListIterator<E> with these additional methods:
| Method | Description |
|---|---|
boolean hasPrevious() |
True if there is an element before the cursor. |
E previous() |
Returns the previous element and moves the cursor backward. |
int nextIndex() |
Index that the next call to next() would return. |
int previousIndex() |
Index that the next call to previous() would return. |
void set(E e) |
Replaces the last element returned by next() or previous(). |
void add(E e) |
Inserts a new element immediately before the implicit cursor position. |
Examples
Example 1: Basic Iteration
The most common use of an iterator is a simple read-only traversal:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> fruits = new ArrayList<>();
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Cherry");
Iterator<String> it = fruits.iterator();
while (it.hasNext()) {
String fruit = it.next();
System.out.println(fruit);
}
}
}
Output:
Apple
Banana
Cherry
This is exactly equivalent to writing for (String fruit : fruits) { System.out.println(fruit); } — the compiler generates the same bytecode either way. Using the explicit form is useful whenever you need access to the Iterator object itself, for example to call remove().
Example 2: Removing Elements Safely
To delete elements while looping, call Iterator.remove() instead of the collection’s own remove() method:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
Iterator<Integer> it = numbers.iterator();
while (it.hasNext()) {
int n = it.next();
if (n % 2 == 0) {
it.remove();
}
}
System.out.println(numbers);
}
}
Output:
[1, 3, 5, 7, 9]
The iterator’s remove() deletes the element it just returned, and it keeps its own expectedModCount in sync with the list’s modCount, so no ConcurrentModificationException is thrown. This is the only safe way to remove elements one-by-one while iterating without collecting them into a separate list first.
Example 3: ListIterator — Traversing in Both Directions
ListIterator adds backward traversal, index reporting, and in-place replacement:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ListIterator;
public class Main {
public static void main(String[] args) {
List<String> colors = new ArrayList<>(Arrays.asList("Red", "Green", "Blue"));
ListIterator<String> lit = colors.listIterator();
while (lit.hasNext()) {
int idx = lit.nextIndex();
String color = lit.next();
if (color.equals("Green")) {
lit.set("Lime");
}
System.out.println(idx + ": " + color);
}
System.out.println("Forward pass result: " + colors);
while (lit.hasPrevious()) {
String color = lit.previous();
System.out.println("Backward: " + color);
}
}
}
Output:
0: Red
1: Green
2: Blue
Forward pass result: [Red, Lime, Blue]
Backward: Blue
Backward: Lime
Backward: Red
Notice that lit.set("Lime") replaces "Green" in the underlying list, but the local variable color printed on that line still holds the value returned by next() before the replacement. When the same ListIterator then walks backward with previous(), it reflects the list’s current state — it sees "Lime", not "Green", because the underlying array was already updated.
Under the Hood: Writing Your Own Iterable
Because Iterable and Iterator are just ordinary interfaces, any class you write can support the for-each loop by implementing Iterable<E> and returning an object that implements Iterator<E>. This is exactly how ArrayList, HashSet, and every other built-in collection support the for-each loop internally — there is no special compiler magic beyond recognizing the Iterable interface.
import java.util.Iterator;
import java.util.NoSuchElementException;
public class Main {
static class Range implements Iterable<Integer> {
private final int start;
private final int end;
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++;
}
};
}
}
public static void main(String[] args) {
Range range = new Range(1, 6);
for (int n : range) {
System.out.print(n + " ");
}
System.out.println();
}
}
Output:
1 2 3 4 5
Each call to range.iterator() returns a brand-new anonymous Iterator object with its own private current field, so two independent for-each loops over the same Range would not interfere with each other. Also notice that next() defensively checks hasNext() itself and throws NoSuchElementException if called too many times — this matches the documented contract of Iterator.next() and is exactly the exception a caller should expect to catch. This custom iterator is not fail-fast, because there is no backing collection to structurally modify; fail-fast behavior is something ArrayList, HashMap, and similar classes add deliberately on top of the basic interface.
Common Mistakes
Mistake 1: Modifying a Collection Directly While Iterating
Calling the collection’s own remove() from inside a for-each loop (or an explicit Iterator loop that ignores the iterator) throws ConcurrentModificationException:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>(Arrays.asList("Ann", "Bob", "Cara", "Dan"));
try {
for (String name : names) {
if (name.equals("Bob")) {
names.remove(name);
}
}
} catch (ConcurrentModificationException e) {
System.out.println("Caught: " + e.getClass().getSimpleName());
}
System.out.println(names);
}
}
Output:
Caught: ConcurrentModificationException
[Ann, Cara, Dan]
The for-each loop is using an Iterator behind the scenes, but names.remove(name) bypasses it and changes modCount directly. The very next call to the hidden iterator’s next() detects the mismatch and throws. The fix is to drive the removal through the iterator itself:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> names = new ArrayList<>(Arrays.asList("Ann", "Bob", "Cara", "Dan"));
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
if (name.equals("Bob")) {
it.remove();
}
}
System.out.println(names);
}
}
Output:
[Ann, Cara, Dan]
No exception is thrown because it.remove() updates the iterator’s expectedModCount at the same time it updates the list.
Mistake 2: Calling next() Without Checking hasNext()
It is easy to assume there is always "one more" element, especially when consuming elements in pairs:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
public class Main {
public static void main(String[] args) {
List<Integer> nums = new ArrayList<>(Arrays.asList(10, 20, 30));
Iterator<Integer> it = nums.iterator();
try {
while (it.hasNext()) {
int first = it.next();
int second = it.next();
System.out.println(first + " + " + second);
}
} catch (NoSuchElementException e) {
System.out.println("Caught: " + e.getClass().getSimpleName());
}
}
}
Output:
10 + 20
Caught: NoSuchElementException
The loop condition only guarantees there is one more element, not two. With an odd number of items the second it.next() call in the final iteration has nothing left to return. The fix is to check hasNext() before every single call to next():
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> nums = new ArrayList<>(Arrays.asList(10, 20, 30));
Iterator<Integer> it = nums.iterator();
while (it.hasNext()) {
int first = it.next();
if (!it.hasNext()) {
break;
}
int second = it.next();
System.out.println(first + " + " + second);
}
}
}
Output:
10 + 20
The leftover 30 has no partner, so the guarded version simply stops instead of crashing.
Best Practices
- Prefer the for-each loop for simple read-only traversal — it compiles to an
Iteratorloop anyway, but is shorter and less error-prone. - Use
Iterator.remove()(orListIterator.set()/add()) whenever you need to modify a collection while iterating over it. Never call the collection’s ownadd/removeinside a loop over that same collection. - Since Java 8, prefer
collection.removeIf(predicate)over a manual iterator loop when you are only removing elements that match a condition — it is shorter and just as safe. - Don’t treat
ConcurrentModificationExceptionas a thread-safety guarantee — it is a best-effort debugging aid for single-threaded misuse, not a lock. For true concurrent access use a class likeCopyOnWriteArrayListor explicit synchronization. - Always guard a call to
next()with a correspondinghasNext()check to avoidNoSuchElementException. - When implementing your own
Iterable, store all traversal state (like a cursor) inside the returnedIteratorobject, not in the outer collection, so multiple independent iterators can run over the same instance safely. - Reach for
ListIteratoronly when you need backward traversal or in-place replacement/insertion; use the plainIteratorotherwise for simplicity.
Practice Exercises
Exercise 1: Write a program that removes every element divisible by 3 from a List<Integer> containing the numbers 1 through 15, using an explicit Iterator and remove() (do not use removeIf). Print the resulting list.
Exercise 2: Implement a class EvenNumbers that implements Iterable<Integer> and whose iterator yields the first n positive even numbers (2, 4, 6, …). Use it in a for-each loop with n = 5. Expected output: 2 4 6 8 10
Exercise 3: Given a List<String> of names that may contain the value "Unknown", use a ListIterator to replace every "Unknown" with "N/A" while walking forward, then reuse the same ListIterator to print the list in reverse order.
Summary
- An
Iteratorlets you traverse a collection one element at a time usinghasNext()andnext(). - The for-each loop is syntactic sugar for an
Iteratorloop — there is no separate looping mechanism underneath it. - Most built-in iterators are fail-fast: structurally modifying the collection outside the iterator during iteration throws
ConcurrentModificationException. - Use
Iterator.remove()to delete elements safely while iterating; useremoveIf()for simple predicate-based removal. ListIteratorextendsIteratorwith backward traversal,set(), andadd(), and is available on anyList.- Any custom class can support the for-each loop by implementing
Iterable<E>and returning its ownIterator<E>. next()throwsNoSuchElementExceptionwhen called with no elements remaining — always guard it withhasNext().
