backend

8 min read

Iterator in Java: How to Loop Through Collections

Master the Iterator in Java with real code examples, learn when to use it over a for-each loop, and stop hitting ConcurrentModificationException for good.

Iterator in Java: How to Loop Through Collections thumbnail

Published By: Nelson Djalo | Date: April 8, 2026

Every Java developer has written a for-each loop to walk through a list. What most don't realize is that under the hood, Java is quietly creating an Iterator object and calling hasNext() and next() for you. Once you understand the Iterator directly, you can do things a for-each loop simply cannot - like safely removing elements mid-loop, building your own traversable data structures, or walking a list backwards.

This post breaks down the Iterator interface, shows where it shines, where it fails, and how to write your own.

Table of Contents

What is an Iterator in Java

An Iterator is an interface in the java.util package that gives you a standard way to traverse any collection - one element at a time. It doesn't care whether you're walking an ArrayList, a HashSet, or a custom data structure. The same three methods work everywhere.

Any class that implements Iterable (which includes every Collection in Java) can hand you an Iterator via its iterator() method. That's the contract that makes Java collections so consistent.

List<String> names = List.of("Ada", "Linus", "Grace");
Iterator<String> it = names.iterator();

while (it.hasNext()) {
    String name = it.next();
    System.out.println(name);
}

If you're still getting comfortable with collections, our Java for Beginners course covers the fundamentals from scratch.

The Three Iterator Methods

The Iterator<E> interface defines three methods. That's it.

  • hasNext() returns true if there's another element to visit.
  • next() returns the next element and advances the cursor.
  • remove() deletes the last element returned by next() from the underlying collection.
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5));
Iterator<Integer> it = numbers.iterator();

while (it.hasNext()) {
    int n = it.next();
    if (n % 2 == 0) {
        it.remove(); // safely removes even numbers
    }
}
System.out.println(numbers); // [1, 3, 5]

Calling next() when hasNext() is false throws NoSuchElementException. Calling remove() before next() throws IllegalStateException. Respect the contract.

Iterator vs For-Each Loop

The for-each loop (for (String s : list)) is syntactic sugar over the Iterator. The compiler generates the exact same hasNext() and next() calls. So why use an Iterator directly?

Use a for-each loop when:

  • You just want to read every element
  • You don't need the index
  • You don't need to modify the collection

Use an Iterator when:

  • You need to remove elements while looping
  • You need fine-grained control (e.g. skipping, peeking)
  • You're building a custom data structure
// For-each - clean but read-only
for (String s : names) {
    System.out.println(s);
}

// Iterator - required if you want to remove
Iterator<String> it = names.iterator();
while (it.hasNext()) {
    if (it.next().startsWith("A")) {
        it.remove();
    }
}

The for-each loop cannot call remove(). If you try to modify the collection inside one, you'll hit a ConcurrentModificationException. More on that in a second.

For a deeper look at Java's loop constructs, check out our For Loop in Java breakdown.

Iterator with ArrayList, HashSet, and HashMap

Every major collection supports Iterator. Here's how it looks across the big three.

ArrayList - ordered, indexed, allows duplicates.

List<String> langs = new ArrayList<>(List.of("Java", "Python", "Go"));
Iterator<String> it = langs.iterator();
while (it.hasNext()) {
    System.out.println(it.next());
}

HashSet - unordered, no duplicates. Iteration order is not guaranteed.

Set<Integer> ids = new HashSet<>(Set.of(10, 20, 30));
Iterator<Integer> it = ids.iterator();
while (it.hasNext()) {
    System.out.println(it.next());
}

HashMap - you iterate over keys, values, or entries. A Map isn't a Collection, so you grab an iterable view first.

Map<String, Integer> scores = new HashMap<>();
scores.put("Ada", 95);
scores.put("Linus", 88);

Iterator<Map.Entry<String, Integer>> it = scores.entrySet().iterator();
while (it.hasNext()) {
    Map.Entry<String, Integer> entry = it.next();
    System.out.println(entry.getKey() + " - " + entry.getValue());
}

ListIterator: Bidirectional Iteration

Lists get a more powerful cousin called ListIterator. It extends Iterator and adds the ability to walk backwards, add elements, and replace elements in place.

Extra methods worth knowing:

  • hasPrevious() and previous() - walk backwards
  • add(E e) - insert an element at the cursor
  • set(E e) - replace the last element returned by next() or previous()
  • nextIndex() and previousIndex() - get the cursor position
List<String> tasks = new ArrayList<>(List.of("write", "review", "ship"));
ListIterator<String> it = tasks.listIterator();

while (it.hasNext()) {
    String task = it.next();
    if (task.equals("review")) {
        it.set("code review"); // replace in place
        it.add("test");        // insert after current
    }
}
System.out.println(tasks); // [write, code review, test, ship]

ListIterator is only available on List implementations. You won't find it on Set or Map.

ConcurrentModificationException and How to Avoid It

This is the bug that trips up every junior developer at least once.

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

Under the hood, the ArrayList tracks a modCount. When you modify the list outside the Iterator, the next call to next() notices the mismatch and throws. The fix is simple - use the Iterator's own remove() method.

Iterator<String> it = items.iterator();
while (it.hasNext()) {
    if (it.next().equals("b")) {
        it.remove(); // safe
    }
}

Or, if you're on Java 8+, use removeIf():

items.removeIf(s -> s.equals("b"));

Cleaner, shorter, and does the right thing.

Writing a Custom Iterator

You can make any class iterable by implementing Iterable<T> and returning your own Iterator<T>. Here's a range iterator that spits out numbers 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<>() {
            private int current = start;

            @Override
            public boolean hasNext() {
                return current < end;
            }

            @Override
            public Integer next() {
                if (!hasNext()) throw new NoSuchElementException();
                return current++;
            }
        };
    }
}

// Usage
for (int i : new Range(1, 5)) {
    System.out.println(i); // 1, 2, 3, 4
}

Once you implement Iterable, you get the for-each loop for free. That's the real power of the interface.

Fail-Fast vs Fail-Safe Iterators

Java iterators come in two flavors.

Fail-fast iterators throw ConcurrentModificationException the moment they detect structural modification from outside the Iterator. ArrayList, HashMap, and HashSet all use fail-fast iterators. They're fast because they don't copy anything - they just check a counter.

Fail-safe iterators work on a snapshot or copy of the collection, so modifications during iteration don't blow up. They're found in concurrent collections like CopyOnWriteArrayList and ConcurrentHashMap. The trade-off is that you might not see the latest state.

List<String> safe = new CopyOnWriteArrayList<>(List.of("a", "b", "c"));
for (String s : safe) {
    safe.add("d"); // no exception, but "d" isn't seen in this loop
}

Rule of thumb - use fail-safe collections when multiple threads are reading and writing. For single-threaded code, stick with fail-fast.

Streams: The Modern Alternative

Since Java 8, streams have become the go-to for most iteration tasks. A stream is a higher-level abstraction that handles iteration for you, supports method chaining, and can run in parallel.

List<String> names = List.of("Ada", "Linus", "Grace", "Alan");

names.stream()
     .filter(n -> n.startsWith("A"))
     .map(String::toUpperCase)
     .forEach(System.out::println);

Streams are great for transformation pipelines, but Iterator still wins when you need imperative control or when you're implementing a data structure from scratch. For comparison and sorting logic inside those pipelines, see Comparable vs Comparator.

Want to go deeper on collections, streams, and the rest of the language? The Java Master Class covers everything from basics to advanced.

FAQ

What's the difference between Iterator and Iterable in Java? Iterable is an interface with one method - iterator() - that returns an Iterator. Any class that implements Iterable can be used in a for-each loop. Iterator is the actual object that walks through the elements.

Can I use Iterator with a Map? Not directly. A Map isn't a Collection. You call keySet(), values(), or entrySet() first, and then get an Iterator from that view.

Why does remove() on an Iterator throw IllegalStateException? Because you called remove() without calling next() first, or you called remove() twice in a row. The Iterator needs to know which element to delete, and that's the one most recently returned by next().

Is Iterator slower than a for loop? For ArrayList, an indexed for loop is marginally faster because it skips the Iterator object creation. For LinkedList, the Iterator is dramatically faster because it avoids the O(n) lookup on every get(i) call. Always prefer Iterator or for-each unless you've profiled a hot path.

What happens if I call next() past the end? It throws NoSuchElementException. Always guard with hasNext().

Wrapping Up

The Iterator interface is one of those quiet Java fundamentals that unlocks cleaner code once you actually use it. Reach for it when you need to remove elements, walk in both directions, or build your own iterable types - and drop down to streams when you just want to transform data.

Ready to sharpen your Java from the ground up? Start with our Java for Beginners course and build a real foundation.

Your Career Transformation Starts Now

Join thousands of developers mastering in-demand skills with Amigoscode. Try it free today.