Iteradores en Java — teoría con una playlist, Iterator, Iterable y ListIterator (y el for-each que no me dejaba borrar)
Estaba preparando la playlist para un viaje en coche y quise quitar las canciones de menos de dos minutos y medio. Un for-each, un if con un remove dentro, y a otra cosa. Java me contestó esto:
¡Alto ahí!: ConcurrentModificationException Quedan: [Amanecer (3:35), Carretera (3:00), Marea (3:20)]
*»¿Alto ahí? ¡Pero si ha borrado Faro!»* Y es verdad: la canción desapareció de la lista y, aun así, Java se quejó. (En mi programa real no había try/catch, así que el programa se paró en seco; aquí lo capturo solo para poder enseñarte cómo quedó la lista.) La explicación está en algo que usas todos los días sin verlo: el for-each no es magia, es un iterador trabajando por detrás.
Hoy abrimos el capó. Verás qué son Iterator e Iterable y en qué se diferencian, por qué borrar mientras recorres exige hacerlo a través del iterador, cómo hacer que tu propia clase funcione en un for-each y qué añade ListIterator para ir y volver. Si has leído la teoría de Comparable y Cloneable, ya conoces el truco: son interfaces pequeñas con un contrato claro. Te vendrá bien repasar también las interfaces y las excepciones, porque aquí aparecen tres excepciones nuevas. Y cuando termines, la práctica de iteradores te espera con tres programas completos.
Tabla de Contenidos
Iterator: el cursor que recorre una colección
Una playlist es una lista de canciones. Para recorrerla, hasta ahora hacías esto:
for (Cancion c : lista) {
System.out.println(c);
}
Pero ese for-each es solo una forma cómoda de escribir otra cosa. Mira los dos programas juntos, el de siempre y el que el compilador entiende por debajo:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class DosFormas {
public static void main(String[] args) {
List<Cancion> lista = new ArrayList<>();
lista.add(new Cancion("Amanecer", 215));
lista.add(new Cancion("Carretera", 180));
lista.add(new Cancion("Tormenta", 240));
System.out.println("Con for-each:");
for (Cancion c : lista) {
System.out.println(" " + c);
}
System.out.println("Con Iterator:");
Iterator<Cancion> it = lista.iterator();
while (it.hasNext()) {
Cancion c = it.next();
System.out.println(" " + c);
}
}
}
Con for-each: Amanecer (3:35) Carretera (3:00) Tormenta (4:00) Con Iterator: Amanecer (3:35) Carretera (3:00) Tormenta (4:00)
Sale exactamente lo mismo. Lo que hace el for-each es pedirle a la lista un iterador (lista.iterator()) y repetir «¿hay siguiente? dame el siguiente». Ese iterador es un objeto de tipo Iterator, una interfaz con tres métodos:
public interface Iterator<E> {
boolean hasNext(); // ¿quedan elementos por visitar?
E next(); // avanza y devuelve el siguiente
void remove(); // opcional: borra el último que devolvió next()
}
| Método | Qué hace | Detalle |
|---|---|---|
hasNext() | Dice si quedan elementos | No avanza: puedes llamarlo mil veces seguidas |
next() | Avanza una posición y devuelve ese elemento | Lanza NoSuchElementException si ya no quedan |
remove() | Borra de la colección el último elemento que devolvió next() | Es opcional: algunas colecciones no lo permiten |
Cada vez que llamas a lista.iterator() te devuelven un iterador nuevo, con su propio cursor empezando desde el principio. Dos iteradores sobre la misma lista no se molestan entre sí (mientras no modifiques la lista, como verás).
hasNext() y next(): el compás del recorrido
Imagínate el iterador como un cursor que se coloca entre dos canciones, no encima de una. Al empezar está antes de la primera. next() salta por encima de una canción y te la devuelve; hasNext() solo mira si hay otra canción delante, sin moverse. Veámoslo con una playlist de dos canciones:
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
public class Agotado {
public static void main(String[] args) {
List<Cancion> lista = List.of(new Cancion("Faro", 135), new Cancion("Marea", 200));
Iterator<Cancion> it = lista.iterator();
System.out.println("¿Hay siguiente? " + it.hasNext());
System.out.println("next() -> " + it.next());
System.out.println("¿Hay siguiente? " + it.hasNext());
System.out.println("next() -> " + it.next());
System.out.println("¿Hay siguiente? " + it.hasNext());
try {
it.next();
} catch (NoSuchElementException e) {
System.out.println("Ya no quedan canciones: " + e.getClass().getSimpleName());
}
}
}
¿Hay siguiente? true next() -> Faro (2:15) ¿Hay siguiente? true next() -> Marea (3:20) ¿Hay siguiente? false Ya no quedan canciones: NoSuchElementException
Entre líneas:
- Mientras queden canciones,
hasNext()datrue. Tras la segunda llamada anext(), el cursor está al final y pasa afalse. - Si llamas a
next()con el cursor ya al final, Java no devuelvenullni se inventa nada: lanzaNoSuchElementException, una excepción no comprobada (no hace faltatry/catch, pero el programa se para si nadie la captura). - Por eso el patrón correcto siempre es
while (it.hasNext())y una sola llamada anext()dentro del bucle.
Llamar a next() sin comprobar antes hasNext(), o llamarlo dos veces en la misma vuelta del bucle. Con un número impar de elementos, la segunda llamada se encuentra el final y salta NoSuchElementException. Si necesitas el elemento varias veces, guárdalo en una variable.
Iterable: lo que necesita un for-each
Si Iterator es «el recorrido», Iterable es «algo que sabe darte un recorrido». Es otra interfaz, y mucho más pequeña todavía:
public interface Iterable<T> {
Iterator<T> iterator();
}
Un único método: dame un iterador. Y aquí está la clave de todo el tema: el for-each solo exige que lo que va a la derecha de los dos puntos sea un array o un Iterable. Cuando escribes
for (Cancion c : lista) { ... }
el compilador lo convierte en esto:
Iterator<Cancion> it = lista.iterator();
while (it.hasNext()) {
Cancion c = it.next();
...
}
Iterable<T> | Iterator<T> | |
|---|---|---|
| Qué es | Algo que se puede recorrer | El recorrido en sí (el cursor) |
| Método principal | iterator() | hasNext(), next(), remove() |
| Cuántos tiene una lista | Es la propia lista | Uno nuevo cada vez que lo pides |
¿Sirve para un for-each? | Sí | No (no se puede escribir for (x : it)) |
Todas las colecciones de Java (listas, conjuntos…) son Iterable, y por eso todas funcionan en un for-each. (Las veremos con calma en el tema de colecciones.) Los arrays también, aunque por una vía especial del compilador: no usan iterador, sino un bucle con índice.
Iterable y Iterator se parecen tanto en el nombre que es fácil mezclarlos. Truco para recordarlo: -able = «capaz de» (algo que se puede iterar); -or = «el que hace» (el que itera).
Borrar mientras recorres: remove() del Iterator
Volvamos al programa del principio. Esto es lo que hacía:
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.List;
public class Concurrente {
public static void main(String[] args) {
List<Cancion> lista = new ArrayList<>(List.of(
new Cancion("Amanecer", 215), new Cancion("Faro", 135),
new Cancion("Carretera", 180), new Cancion("Marea", 200)));
try {
for (Cancion c : lista) {
if (c.segundos() < 150) {
lista.remove(c); // modificamos la lista por fuera del iterador
}
}
} catch (ConcurrentModificationException e) {
System.out.println("¡Alto ahí!: " + e.getClass().getSimpleName());
}
System.out.println("Quedan: " + lista);
}
}
Cuando empieza el for-each, la lista le entrega un iterador. Por dentro, un ArrayList lleva la cuenta de cuántas veces se ha modificado su estructura (un contador llamado modCount), y el iterador se apunta ese contador al empezar. En cada next() comprueba que no haya cambiado. Yo llamé a lista.remove(c) por mi cuenta, el contador cambió y el iterador, al ver que su cuenta ya no coincidía con la de la lista, lanzó ConcurrentModificationException en el siguiente next(). No es que el borrado fallara: es que Java prefiere parar antes que seguir con un recorrido que ya no es de fiar.
La forma correcta de borrar es pedírselo al iterador: él sabe qué ha devuelto y, al borrar, actualiza su propia cuenta para no enfadarse.
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Quitar {
public static void main(String[] args) {
List<Cancion> lista = new ArrayList<>(List.of(
new Cancion("Amanecer", 215), new Cancion("Faro", 135),
new Cancion("Carretera", 180), new Cancion("Marea", 200)));
Iterator<Cancion> it = lista.iterator();
while (it.hasNext()) {
Cancion c = it.next();
if (c.segundos() < 150) {
it.remove(); // borra la última canción que devolvió next()
}
}
System.out.println("Quedan: " + lista);
Iterator<Cancion> otro = lista.iterator();
try {
otro.remove(); // ¡todavía no hemos llamado a next()!
} catch (IllegalStateException e) {
System.out.println("remove() sin next(): " + e.getClass().getSimpleName());
}
}
}
Quedan: [Amanecer (3:35), Carretera (3:00), Marea (3:20)] remove() sin next(): IllegalStateException
Las reglas de remove():
- Borra el elemento que devolvió **la última llamada a
next()**. Por eso va después de él, normalmente dentro de unif. - Solo se puede llamar **una vez por cada
next()**. Dosremove()seguidos, o uno antes del primernext(), lanzanIllegalStateException(la segunda parte del programa). - Es una operación opcional: las listas inmutables (como las de
List.of(...)) lanzanUnsupportedOperationExceptionsi intentas borrar. Por eso en el programa usonew ArrayList<>(List.of(...)), que sí se puede modificar.
Modificar la colección (con add o remove de la propia lista) mientras la recorres con un for-each o con un iterador. Lo correcto es it.remove(). Para añadir mientras recorres, necesitarás un ListIterator, que verás más abajo.
Si solo quieres borrar según una condición, hay un atajo que ya hace todo esto por dentro: lista.removeIf(c -> c.segundos() < 150). Lo verás funcionando en la práctica.
Crear tu propio Iterable
Aquí está la parte que más cuesta y la que más recompensa. Si tu clase guarda cosas, puedes hacer que funcione en un for-each con solo dos ingredientes: que **implemente Iterable** y que su método iterator() devuelva un objeto que sepa recorrerla. Una playlist hecha a mano con un array:
import java.util.Iterator;
import java.util.NoSuchElementException;
public class Playlist implements Iterable<Cancion> {
private final Cancion[] canciones = new Cancion[10];
private int total = 0;
public void anadir(Cancion c) {
canciones[total++] = c;
}
@Override
public Iterator<Cancion> iterator() {
return new IteradorPlaylist();
}
private class IteradorPlaylist implements Iterator<Cancion> {
private int posicion = 0; // el cursor de este recorrido
@Override
public boolean hasNext() {
return posicion < total;
}
@Override
public Cancion next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return canciones[posicion++];
}
}
}
Tres piezas:
implements Iterable<Cancion>: la etiqueta que le dice alfor-each«a mí se me puede recorrer».iterator()devuelve un objeto nuevo deIteradorPlaylist: cada recorrido tiene su propio cursor (posicion).IteradorPlaylistes una clase interna (declarada dentro dePlaylist, sinstatic): por eso puede vercancionesytotalde la playlist que la creó, sin que se las pases.
Fíjate también en que next() comprueba hasNext() y lanza NoSuchElementException si no quedan canciones: es lo que promete el contrato de la interfaz. Y ahora, lo bonito:
public class MiPlaylist {
public static void main(String[] args) {
Playlist lista = new Playlist();
lista.anadir(new Cancion("Amanecer", 215));
lista.anadir(new Cancion("Carretera", 180));
lista.anadir(new Cancion("Marea", 200));
for (Cancion c : lista) {
System.out.println(c);
}
}
}
Amanecer (3:35) Carretera (3:00) Marea (3:20)
Una clase hecha por mí, en un for-each como cualquier lista de Java. Yo no he escrito ningún bucle con índices fuera de la clase: quien usa Playlist no necesita saber que dentro hay un array.
Guardar el iterador en un atributo y devolver siempre ese mismo objeto desde iterator(). El segundo for-each encuentra el cursor ya al final y no recorre nada. Cada llamada a iterator() tiene que crear un iterador nuevo.
ListIterator: ir y volver
El Iterator solo avanza. Las listas ofrecen una versión mejorada, ListIterator, que se obtiene con lista.listIterator() y permite ir hacia atrás, saber en qué posición está el cursor y modificar la lista mientras recorres:
| Método | Qué hace |
|---|---|
hasPrevious() / previous() | Como hasNext() / next(), pero hacia atrás |
nextIndex() / previousIndex() | Posición del elemento que devolvería next() / previous() |
set(x) | Sustituye el último elemento devuelto por next() o previous() |
add(x) | Inserta x justo donde está el cursor |
Y su comportamiento se entiende mejor con el cursor «entre canciones» del que hablábamos:
import java.util.List;
import java.util.ListIterator;
public class IdaYVuelta {
public static void main(String[] args) {
List<Cancion> lista = List.of(
new Cancion("Amanecer", 215), new Cancion("Carretera", 180), new Cancion("Faro", 135));
ListIterator<Cancion> it = lista.listIterator();
System.out.println("next() -> " + it.next());
System.out.println("next() -> " + it.next());
System.out.println("previous() -> " + it.previous());
System.out.println("next() -> " + it.next());
System.out.println("nextIndex: " + it.nextIndex() + ", previousIndex: " + it.previousIndex());
}
}
next() -> Amanecer (3:35) next() -> Carretera (3:00) previous() -> Carretera (3:00) next() -> Carretera (3:00) nextIndex: 2, previousIndex: 1
Después de dos next(), el cursor está entre Carretera y Faro. previous() salta hacia atrás por encima de Carretera y la devuelve; el siguiente next() la vuelve a devolver, porque el cursor se quedó justo antes de ella. Por eso nextIndex() es 2 y previousIndex() es 1 al final. Los usos más interesantes (modificar y añadir) los tienes en la práctica.
ListIterator solo existe para listas. Otras colecciones (conjuntos, por ejemplo) tienen Iterator, porque en ellas «hacia atrás» no tiene sentido: no hay posiciones.
Errores típicos, de un vistazo
ConcurrentModificationException. Has modificado la colección por fuera del iterador mientras la recorrías. Usa it.remove().
NoSuchElementException. Llamaste a next() sin comprobar hasNext(), o dos veces en la misma vuelta.
IllegalStateException al borrar. remove() antes del primer next(), o dos remove() seguidos sin un next() entre medias.
Devolver siempre el mismo iterador desde iterator(). Cada recorrido necesita el suyo.
Confundir Iterable (la colección) con Iterator (el cursor). El for-each pide un Iterable.
Míralo paso a paso en Python Tutor
Un iterador es un objeto con una variable de posición, y esa posición es justo lo que cuesta imaginar. Python Tutor ejecuta el código línea a línea y dibuja variables, objetos y referencias. Elige Java, pega el programa y pulsa Visualize Execution. Aquí tienes un iterador «casero» con los mismos métodos que el de Java, para que puedas ver su cursor moverse y comprobar que dos iteradores sobre el mismo array no se pisan:
public class Tutor {
static class Cursor {
private final String[] datos;
private int posicion = 0;
Cursor(String[] datos) { this.datos = datos; }
boolean hasNext() { return posicion < datos.length; }
String next() { return datos[posicion++]; }
}
public static void main(String[] args) {
String[] titulos = {"Amanecer", "Carretera", "Faro"};
Cursor it = new Cursor(titulos);
Cursor otro = new Cursor(titulos);
System.out.println("it -> " + it.next());
System.out.println("it -> " + it.next());
System.out.println("otro -> " + otro.next());
while (it.hasNext()) {
System.out.println("it -> " + it.next());
}
}
}
Salida esperada:
it -> Amanecer it -> Carretera otro -> Amanecer it -> Faro
Qué observar paso a paso:
- Tras crear
ityotro, verás un array con tres títulos y dos objetosCursor. Los dos apuntan al mismo array y tienenposicionigual a 0. - Con las dos primeras llamadas a
it.next(), laposiciondeitpasa de 0 a 1 y luego a 2. Cadanext()devuelve el elemento que había antes de incrementar. - En
otro.next()fíjate en que solo cambiaposiciondeotro(pasa a 1). El cursor deitsigue en 2: cada iterador lleva su propia posición. - En el
while,it.hasNext()estrueporque 2 < 3. Tras el últimonext(),posicionvale 3 yhasNext()pasa afalse: así termina el bucle.
Resumen y tabla rápida
| Quiero… | Escribo | Detalle |
|---|---|---|
| Recorrer sin índices | for (Cancion c : lista) | Funciona con todo lo que sea Iterable |
| Recorrer a mano | Iterator<Cancion> it = lista.iterator(); | Con while (it.hasNext()) |
| Pasar al siguiente elemento | Cancion c = it.next(); | Una llamada por vuelta |
| Borrar mientras recorro | it.remove(); | Tras next(), una vez por next() |
| Borrar con una condición | lista.removeIf(condicion) | Atajo, ya lo hace por dentro |
Que mi clase funcione en un for-each | implements Iterable<T> | Y iterator() devuelve un iterador nuevo |
| Ir y volver en una lista | ListIterator<T> it = lista.listIterator(); | previous(), set(), add() |
Tres ideas para quedarte:
- **El
for-eaches unIteratordisfrazado:** pide un iterador alIterabley repitehasNext()/next(). - **Para borrar mientras recorres, hazlo con
it.remove()**, no con la lista: si no,ConcurrentModificationException. - **Un
Iterablefabrica iteradores nuevos:** cada recorrido lleva su propio cursor, y por eso tu clase propia puede usarse en unfor-each.
Y ahora, a practicar
Con esto tienes la teoría completa. En la práctica de iteradores borramos canciones sin excepción, construimos una lista enlazada propia que funciona en un for-each y recorremos una playlist en los dos sentidos con ListIterator. Y si quieres repasar antes lo anterior, vuelve a la teoría de Comparable y Cloneable.
¿Te ha servido este artículo? Así puedes apoyar SergioLearns →

2 comentarios