The Collections Framework

Iterators & the Iterable Interface

15 min Lesson 9 of 14

Iterators & the Iterable Interface

Every time you write a for-each loop over a List or Set, something is working quietly behind the scenes: the Iterator protocol. Understanding it lets you traverse collections safely, remove elements mid-loop without errors, and even build your own iterable data structures.

The Iterator Interface

The java.util.Iterator<E> interface defines three methods:

  • boolean hasNext() — returns true if there are more elements to visit.
  • E next() — returns the next element and advances the cursor.
  • void remove() — removes the element most recently returned by next() from the underlying collection (optional operation).

You obtain an iterator from any collection through its iterator() method:

import java.util.ArrayList; import java.util.Iterator; import java.util.List; List<String> cities = new ArrayList<>(List.of("Cairo", "Riyadh", "Dubai", "Amman")); Iterator<String> it = cities.iterator(); while (it.hasNext()) { String city = it.next(); System.out.println(city); }

This is exactly what the for-each loop compiles down to. The compiler rewrites for (String c : cities) into the while (it.hasNext()) form above.

Removing Elements Safely During Iteration

A classic beginner mistake is calling list.remove() inside a for-each loop. This throws a ConcurrentModificationException because the collection detects that it was structurally changed while an iterator is active.

Never call collection.remove() inside a for-each loop. The for-each loop holds an implicit iterator; modifying the collection outside that iterator invalidates it and triggers ConcurrentModificationException.

The correct approach is to use Iterator.remove() instead:

List<String> cities = new ArrayList<>(List.of("Cairo", "Riyadh", "Dubai", "Amman")); Iterator<String> it = cities.iterator(); while (it.hasNext()) { String city = it.next(); if (city.startsWith("D")) { it.remove(); // safe: removes "Dubai" without invalidating the iterator } } System.out.println(cities); // [Cairo, Riyadh, Amman]

The rule is simple: always call next() before remove(). Calling remove() twice in a row without an intervening next() throws IllegalStateException.

Java 8+ alternative: collection.removeIf(predicate) is even cleaner for bulk removal and handles the iterator internally. Use it when you do not need per-element logic beyond a simple condition: cities.removeIf(c -> c.startsWith("D"));

The Iterable Interface

The java.lang.Iterable<T> interface has just one required method:

public interface Iterable<T> { Iterator<T> iterator(); }

Any class that implements Iterable can be used in a for-each loop. All standard collection interfaces (Collection, List, Set, Queue) extend Iterable, which is why they all support for-each.

Implementing Iterable on a Custom Class

Suppose you have a simple range type that represents a sequence of integers from start to end (exclusive). By implementing Iterable<Integer> you get for-each support for free:

import java.util.Iterator; import java.util.NoSuchElementException; public class IntRange implements Iterable<Integer> { private final int start; private final int end; public IntRange(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 is idiomatic Java:

for (int n : new IntRange(1, 6)) { System.out.print(n + " "); // 1 2 3 4 5 }
Each call to iterator() must return a fresh, independent iterator. If you return the same object twice, the second for-each loop starts mid-sequence or is already exhausted. The anonymous inner class above captures current as a local variable, so every iterator() call creates a new instance with current = start.

ListIterator — Bidirectional Traversal

java.util.ListIterator<E> extends Iterator and adds backwards traversal and in-place replacement, available on any List:

List<String> words = new ArrayList<>(List.of("hello", "world", "java")); ListIterator<String> lit = words.listIterator(); while (lit.hasNext()) { String w = lit.next(); lit.set(w.toUpperCase()); // replace in place } System.out.println(words); // [HELLO, WORLD, JAVA]

You can also traverse backwards with hasPrevious() and previous(), or insert elements with add().

When to Use an Iterator Explicitly

In day-to-day code, prefer the for-each loop or stream pipelines. Reach for an explicit iterator only when you need to:

  • Remove elements during traversal (use Iterator.remove() or removeIf()).
  • Replace elements in a List during traversal (use ListIterator.set()).
  • Interleave two iterators of the same collection in a single loop.
  • Implement a custom Iterable type.

Summary

Iterator<E> provides a uniform traversal protocol: hasNext(), next(), and the safe remove(). The Iterable<T> interface — whose single method returns an Iterator — is what unlocks the for-each loop for any type. When you need to delete while iterating, always go through the iterator's own remove() method, not the collection's. And when you design a custom container, implementing Iterable is the clean way to grant clients idiomatic traversal.