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 IterableArrayList, 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 Iterator loop anyway, but is shorter and less error-prone.
  • Use Iterator.remove() (or ListIterator.set()/add()) whenever you need to modify a collection while iterating over it. Never call the collection’s own add/remove inside 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 ConcurrentModificationException as 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 like CopyOnWriteArrayList or explicit synchronization.
  • Always guard a call to next() with a corresponding hasNext() check to avoid NoSuchElementException.
  • When implementing your own Iterable, store all traversal state (like a cursor) inside the returned Iterator object, not in the outer collection, so multiple independent iterators can run over the same instance safely.
  • Reach for ListIterator only when you need backward traversal or in-place replacement/insertion; use the plain Iterator otherwise 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 Iterator lets you traverse a collection one element at a time using hasNext() and next().
  • The for-each loop is syntactic sugar for an Iterator loop — 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; use removeIf() for simple predicate-based removal.
  • ListIterator extends Iterator with backward traversal, set(), and add(), and is available on any List.
  • Any custom class can support the for-each loop by implementing Iterable<E> and returning its own Iterator<E>.
  • next() throws NoSuchElementException when called with no elements remaining — always guard it with hasNext().