W3docs

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 pattern

remove() 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 ConcurrentModificationException

Fail-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 STOP

forEach 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 1

Tres cosas que hay que hacer bien:

  1. next() debe lanzar NoSuchElementException cuando se agota. No devuelvas null ni ningún valor centinela.
  2. El iterador debe ser una instancia nueva en cada llamada a iterator(). Llamar for (... : it) dos veces sobre el mismo iterable debería empezar desde el principio en ambas ocasiones.
  3. remove() es opcional. No lo implementes a menos que realmente puedas — el cuerpo default que 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.

java— editable, runs on the server

Lo que se extrae de la ejecución:

  • El bucle for-each imprimió cada elemento; entre bastidores le pidió al ArrayList un iterador y lo recorrió con hasNext/next.
  • Iterator.remove eliminó las cadenas vacías durante la iteración sin lanzar ConcurrentModificationException. Esa es la única técnica correcta de eliminación en el bucle con un Iterator simple.
  • forEachRemaining es 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ó ConcurrentModificationException en la siguiente llamada a next(). La excepción es intencional: hace que el error sea evidente.
  • El Countdown personalizado muestra el contrato mínimo necesario para escribir un iterable funcional. hasNext informa limpiamente; next lanza cuando se agota; sin remove (hereda el default).

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.

Práctica

Práctica
Dentro de un bucle `for-each` sobre `List<String> list`, llamas a `list.remove(name)` para eliminar las entradas que coinciden. La primera eliminación funciona; la siguiente iteración lanza una excepción. ¿Cuál es la solución correcta?
Dentro de un bucle `for-each` sobre `List<String> list`, llamas a `list.remove(name)` para eliminar las entradas que coinciden. La primera eliminación funciona; la siguiente iteración lanza una excepción. ¿Cuál es la solución correcta?
Was this page helpful?