Iteradores en Java
Recorre colecciones Java con la interfaz Iterator — hasNext, next, remove — y el contrato Iterable.
Cada vez que escribes for (T x : collection) en Java, estás invocando un par oculto: la interfaz Iterable<T>, que permite recorrer la colección, y el Iterator<T> que le entrega al bucle. El bucle for-each es azúcar sintáctico; el Iterator es el motor. Entender qué hace — y qué excepciones pueden lanzar sus tres métodos — es la diferencia entre "mi recorrido de lista funciona la mayoría de las veces" y "sé exactamente cuándo fallará."
Este capítulo trata sobre el Iterator<E> básico. El más rico ListIterator<E>, que puede recorrer hacia atrás y modificar durante la iteración, tiene su propio capítulo a continuación.
Las dos interfaces
Iterable<T> es el contrato para "cosas que se pueden iterar":
public interface Iterable<T> {
Iterator<T> iterator();
default void forEach(Consumer<? super T> action) { ... }
default Spliterator<T> spliterator() { ... }
}Iterator<T> es el cursor:
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() { throw new UnsupportedOperationException(); }
default void forEachRemaining(Consumer<? super E> action) { ... }
}Toda Collection<E> extiende Iterable<E> — por eso un bucle for-each funciona con un List, un Set, una Queue. (Un Map no es Iterable; se itera su entrySet(), keySet() o values() — ver cómo iterar un HashMap.) El bucle for-each:
for (String name : names) { System.out.println(name); }se compila a:
for (Iterator<String> it = names.iterator(); it.hasNext(); ) {
String name = it.next();
System.out.println(name);
}Una vez que has visto esa desazucarización, el resto de las reglas de Iterator tiene sentido.
Los tres métodos y lo que lanzan
hasNext() devuelve true si next() tendría éxito. Es idempotente — llamarlo dos veces seguidas está bien. Nunca lanza excepciones (en implementaciones bien escritas).
next() avanza el cursor y devuelve el elemento. Lanza NoSuchElementException si no hay siguiente elemento. Este es el único método del iterador que lanza una excepción por diseño cuando se usa incorrectamente. Siempre protege con hasNext() si existe alguna posibilidad de que la colección esté vacía:
while (it.hasNext()) { use(it.next()); } // safe patternremove() elimina el elemento que devolvió más recientemente next(). Es un método default que lanza UnsupportedOperationException a menos que el iterador lo implemente. ArrayList, HashMap.keySet().iterator() y similares lo soportan. Los iteradores devueltos por List.of(...), Collections.unmodifiableList(...) y el .iterator() de un stream no lo soportan. Tampoco puedes llamar a remove() dos veces seguidas sin un next() intermedio — eso lanza IllegalStateException.
Iterator<String> it = names.iterator();
while (it.hasNext()) {
String name = it.next();
if (name.isEmpty()) it.remove(); // legal, fail-safe removal
}it.remove() es la única forma segura de eliminar de una colección mientras se itera con un Iterator simple. El propio remove(...) de la colección invalidaría el iterador y lanzaría ConcurrentModificationException en la siguiente llamada.
Iteración fail-fast
La mayoría de los iteradores de colecciones del JDK son fail-fast: registran el recuento de modificaciones de la colección cuando se crea el iterador, lo comprueban en cada llamada a hasNext/next y lanzan ConcurrentModificationException si alguien distinto al propio iterador lo ha cambiado.
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 es un diagnóstico de mejor esfuerzo, no una garantía de seguridad de hilos. Detecta el error común ("modifiqué la lista dentro del bucle y ahora mi iterador está confundido") de forma limpia y temprana. No protege contra modificaciones concurrentes desde otro hilo — para eso necesitas una colección concurrente (CopyOnWriteArrayList, ConcurrentHashMap) cuyos iteradores son débilmente consistentes: recorren una instantánea y nunca lanzan excepciones.
forEachRemaining y forEach
Dos métodos default hacen la iteración más corta cuando no necesitas el 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 está en Iterable; forEachRemaining está en Iterator. Ambos son secuenciales. No los uses cuando también necesites hacer remove — ocultan el cursor, y remove lo requiere.
Escribir tu propio Iterator
Lo escribirás cuando implementes un tipo personalizado similar a una colección. El contrato es pequeño, pero cada parte importa:
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 1Tres cosas que hay que hacer bien:
next()debe lanzarNoSuchElementExceptioncuando se agota. No devuelvasnullni ningún valor centinela.- El iterador debe ser una instancia nueva en cada llamada a
iterator(). Llamarfor (... : it)dos veces sobre el mismo iterable debería empezar desde el principio en ambas ocasiones. remove()es opcional. No lo implementes a menos que realmente puedas — el cuerpodefaultque lanza la excepción está bien.
Un ejemplo completo: iteración, eliminación, fail-fast e iterable personalizado
El programa siguiente recorre un ArrayList de tres formas (for-each, iterador explícito, forEachRemaining), elimina elementos de forma segura con Iterator.remove, demuestra la excepción fail-fast cuando se evita el iterador y termina con un pequeño Iterable<T> personalizado.
Lo que se extrae de la ejecución:
- El bucle for-each imprimió cada elemento; entre bastidores le pidió al
ArrayListun iterador y lo recorrió conhasNext/next. Iterator.removeeliminó las cadenas vacías durante la iteración sin lanzarConcurrentModificationException. Esa es la única técnica correcta de eliminación en el bucle con unIteratorsimple.forEachRemaininges una forma ordenada de vaciar lo que el iterador aún no ha producido — útil justo después de un recorrido parcial.- Mutar la lista directamente mientras otro iterador estaba activo lanzó
ConcurrentModificationExceptionen la siguiente llamada anext(). La excepción es intencional: hace que el error sea evidente. - El
Countdownpersonalizado muestra el contrato mínimo necesario para escribir un iterable funcional.hasNextinforma limpiamente;nextlanza cuando se agota; sinremove(hereda eldefault).
Qué sigue
Un Iterator simple puede recorrer hacia adelante y eliminar. Eso es suficiente para sets, maps y queues — no tienen posiciones en ningún otro sentido. Las listas sí, y obtienen un cursor más rico: ListIterator<E> puede moverse hacia adelante y hacia atrás, informar índices, y hacer add o set de elementos durante el recorrido. Ese es el siguiente capítulo.