Iteradores en Java — práctica con borrados seguros, una lista enlazada propia y una playlist de ida y vuelta (y el segundo for-each que no imprimía nada)
Mi primera clase Iterable funcionó a la primera. La metí en un for-each, salió la lista entera y me sentí un genio durante casi un minuto. Después quise comparar cada elemento con todos los demás y anidé dos for-each sobre la misma colección. El de fuera dio una vuelta… y el de dentro no imprimió nada. Ni error, ni excepción: silencio. Tardé una tarde entera en darme cuenta de que había guardado el iterador en un atributo de la clase, así que los dos bucles compartían el mismo cursor, y el de dentro lo dejaba siempre al final.
Esa es la gracia de este tema: un iterador es un cursor, y lo que pasa con él es muy fácil de entender… en cuanto lo ves moverse. Por eso hoy practicamos con tres programas en los que el cursor lo vas a ver, y a romper, tú mismo.
Si ya has leído la teoría de iteradores, este artículo es para aplicarla: borrar de una lista sin que salte la excepción, construir una colección propia que funcione en un for-each y recorrer una playlist en los dos sentidos. Son programas de dificultad media. Cada uno trae el código comentado, la salida y las dudas que suelen surgir.
Tabla de Contenidos
Antes de empezar
Conviene tener claro:
- Que el
for-eaches unIteratordisfrazado: pide un iterador alIterabley repitehasNext()ynext(). - Que para borrar mientras recorres hay que hacerlo con
it.remove()(una vez por cadanext()), no con elremovede la lista. - Que un
Iterabledevuelve un iterador nuevo en cada llamada aiterator(), y que cada iterador lleva su propio cursor. - Que
ListIteratorpermite ir hacia atrás y modificar la lista conset()yadd().
Si alguno te suena dudoso, repasa la teoría antes de seguir.
Los tres programas son independientes. Crea una carpeta para cada uno y compílalos por separado. Los programas 1 y 3 comparten la misma clase Cancion; el 2 no la necesita.
Programa 1: borrar con Iterator sin excepción
Una playlist con cinco canciones, de las cuales tres son demasiado cortas (menos de dos minutos y medio). Queremos quitarlas de tres maneras distintas y ver cuál se porta bien.
La idea
Vamos a hacer lo mismo tres veces: borrar las canciones de menos de 150 segundos. Primero con un for-each y el remove de la lista (para ver la excepción y, sobre todo, cómo queda la lista después). Luego con Iterator.remove(), la forma segura. Y, por último, con el atajo removeIf.
En la primera parte capturamos la ConcurrentModificationException con un try/catch para poder seguir. No lo harías en código real: es solo para enseñarte el estado en el que queda la lista.
Antes de ejecutar, piensa cuántas canciones borrará la primera versión. ¿Las tres? ¿Una? Después lo comprobamos.
El código
Cancion.java
public record Cancion(String titulo, int segundos) {
@Override
public String toString() {
return titulo + " (" + segundos / 60 + ":" + String.format("%02d", segundos % 60) + ")";
}
}
Borrar.java
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;
public class Borrar {
static List<Cancion> cargar() {
return new ArrayList<>(List.of(
new Cancion("Amanecer", 215), new Cancion("Intro", 45),
new Cancion("Carretera", 180), new Cancion("Faro", 135),
new Cancion("Silencio", 20)));
}
public static void main(String[] args) {
// 1. for-each + remove(): peligro
List<Cancion> a = cargar();
try {
for (Cancion c : a) {
if (c.segundos() < 150) {
a.remove(c);
}
}
} catch (ConcurrentModificationException e) {
System.out.println("1. for-each: " + e.getClass().getSimpleName());
}
System.out.println(" Se quedó a medias: " + a);
// 2. Iterator.remove(): la forma segura
List<Cancion> b = cargar();
Iterator<Cancion> it = b.iterator();
int borradas = 0;
while (it.hasNext()) {
if (it.next().segundos() < 150) {
it.remove();
borradas++;
}
}
System.out.println("2. Iterator: borradas " + borradas + " -> " + b);
// 3. El atajo: removeIf
List<Cancion> c = cargar();
c.removeIf(x -> x.segundos() < 150);
System.out.println("3. removeIf: " + c);
}
}
Salida
1. for-each: ConcurrentModificationException Se quedó a medias: [Amanecer (3:35), Carretera (3:00), Faro (2:15), Silencio (0:20)] 2. Iterator: borradas 3 -> [Amanecer (3:35), Carretera (3:00)] 3. removeIf: [Amanecer (3:35), Carretera (3:00)]
Compruébalo a mano:
- **
for-each+remove():** el recorrido llega aIntro(0:45), la borra cona.remove(c)y, en el siguientenext(), salta la excepción. La lista se queda a medias:Introse fue, peroFaro(2:15) ySilencio(0:20) siguen ahí, cuando también debían borrarse. - **
Iterator.remove():** borra las tres (Intro,FaroySilencio) y deja soloAmaneceryCarretera.borradascuenta 3. - **
removeIf:** mismo resultado, en una línea.
x -> x.segundos() < 150 es una lambda: una función pequeña que removeIf aplica a cada canción para decidir si se borra. No hace falta que la domines ahora; piensa en ella como «la condición, escrita en corto».
Preguntas que te puedes hacer
**¿Por qué la excepción no salta nada más borrar, sino en el siguiente next()?** Porque quien comprueba el contador de modificaciones es el iterador, y solo lo hace cuando le pides el siguiente elemento. El borrado en sí (a.remove(c)) lo hace la lista, que no sabe nada de iteradores.
**¿Por qué it.remove() no provoca la excepción?** Porque es el propio iterador quien borra, y al hacerlo actualiza su cuenta de modificaciones. La lista y el iterador vuelven a estar de acuerdo.
¿Por qué no basta con capturar la excepción y seguir? Porque el estado en el que queda la lista es impredecible: aquí se quedó a medias, con dos canciones cortas sin borrar. La excepción no es un fallo que se pueda ignorar, es un aviso de que el recorrido ya no es fiable.
**¿Y si recorro con un for de índices?** Funciona, pero cuidado: al borrar el elemento i, el siguiente pasa a ocupar la posición i y el i++ te lo salta. Se arregla recorriendo de atrás hacia delante. Con un iterador, te ahorras ese problema.
Llamar a it.remove() sin haber llamado antes a it.next() en esa vuelta, o dos veces seguidas. Salta IllegalStateException. Cada remove() necesita su next().
Pruébalo tú
- Cambia la condición para borrar las canciones de más de 200 segundos en lugar de las cortas, con
Iterator.remove(). - Modifica el programa para que, además de borrarlas, imprima el título de cada canción borrada (
"Borrada: " + c.titulo()). Pista: necesitas guardar la canción que devuelvenext()en una variable.
Programa 2: mi propia colección Iterable
Una lista enlazada hecha a mano, sin ArrayList: nodos que se apuntan unos a otros. Y lo importante: que funcione en un for-each como cualquier otra colección.
La idea
Una lista enlazada guarda sus elementos en nodos: cada nodo lleva un dato y una flecha al siguiente. La lista conoce solo el primer nodo (y, por comodidad, el último), y para llegar a cualquier otro hay que ir siguiendo flechas. Eso es justo lo que hace un iterador: su cursor es «el nodo que toca devolver».
Haremos la clase genérica (ListaEnlazada<T>) para que valga con cualquier tipo, y le daremos un iterador propio, como clase interna. Después la probamos con for-each, con dos for-each anidados (cada uno con su cursor), con un iterador agotado y con un método que acepta cualquier Iterable.
Dibuja en un papel tres cajas con flechas (FP2 → IC2 → AEDV) y mueve el dedo como lo haría el cursor. Cada vez que el dedo avanza, es un next().
El código
ListaEnlazada.java
import java.util.Iterator;
import java.util.NoSuchElementException;
public class ListaEnlazada<T> implements Iterable<T> {
private class Nodo {
T dato;
Nodo siguiente;
Nodo(T dato) {
this.dato = dato;
}
}
private Nodo primero;
private Nodo ultimo;
private int cuantos;
public void anadir(T dato) {
Nodo nodo = new Nodo(dato);
if (primero == null) {
primero = nodo;
} else {
ultimo.siguiente = nodo;
}
ultimo = nodo;
cuantos++;
}
public int tamano() {
return cuantos;
}
@Override
public Iterator<T> iterator() {
return new IteradorLista();
}
private class IteradorLista implements Iterator<T> {
private Nodo actual = primero; // cada iterador empieza en el primero
@Override
public boolean hasNext() {
return actual != null;
}
@Override
public T next() {
if (actual == null) {
throw new NoSuchElementException();
}
T dato = actual.dato;
actual = actual.siguiente;
return dato;
}
}
}
Enlazada.java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.NoSuchElementException;
public class Enlazada {
static int contar(Iterable<?> coleccion) {
int n = 0;
for (Object o : coleccion) {
n++;
}
return n;
}
public static void main(String[] args) {
ListaEnlazada<String> asignaturas = new ListaEnlazada<>();
asignaturas.anadir("FP2");
asignaturas.anadir("IC2");
asignaturas.anadir("AEDV");
System.out.println("Con for-each:");
for (String a : asignaturas) {
System.out.println(" " + a);
}
System.out.println("Dos for-each anidados (cada uno con su cursor):");
for (String a : asignaturas) {
for (String b : asignaturas) {
if (!a.equals(b)) {
System.out.println(" " + a + " + " + b);
}
}
}
Iterator<String> it = asignaturas.iterator();
for (int i = 0; i < asignaturas.tamano(); i++) {
it.next();
}
try {
it.next();
} catch (NoSuchElementException e) {
System.out.println("Iterador agotado: " + e.getClass().getSimpleName());
}
ArrayList<Integer> numeros = new ArrayList<>();
numeros.add(7);
numeros.add(8);
System.out.println("contar(mi lista) = " + contar(asignaturas));
System.out.println("contar(ArrayList) = " + contar(numeros));
}
}
Salida
Con for-each: FP2 IC2 AEDV Dos for-each anidados (cada uno con su cursor): FP2 + IC2 FP2 + AEDV IC2 + FP2 IC2 + AEDV AEDV + FP2 AEDV + IC2 Iterador agotado: NoSuchElementException contar(mi lista) = 3 contar(ArrayList) = 2
Compruébalo a mano:
- **
for-each:** las tres asignaturas, en el orden en que se añadieron. - **Dos
for-eachanidados:** son 6 parejas (3 × 3 menos las 3 en las queaybson la misma). Cadafor-eachpide su propio iterador, y los dos recorren la lista a la vez sin estorbarse. - Iterador agotado: tras tres
next(), el cursor está ennully el cuarto lanzaNoSuchElementException. - **
contar:** el mismo método sirve para mi lista (3) y para unArrayList(2), porque los dos sonIterable.
Iterable<?> significa «un Iterable de lo que sea». Es la forma de escribir un método que acepta cualquier colección recorrible sin importar el tipo de sus elementos.
Preguntas que te puedes hacer
**¿Por qué IteradorLista es una clase interna y no static?** Porque necesita ver el campo primero de la lista que la creó (private Nodo actual = primero;). Una clase interna (sin static) pertenece a un objeto concreto y accede a sus campos; una static no.
**¿Por qué dos for-each anidados funcionan aquí y a mí no me funcionó?** Porque iterator() devuelve un new IteradorLista() cada vez. Si hubiera guardado un único iterador en un atributo, los dos bucles compartirían cursor, y el de dentro lo dejaría siempre agotado. (Es justo lo que cuenta la anécdota del principio.)
**¿Por qué next() comprueba actual == null si ya existe hasNext()?** Porque nadie te obliga a llamar a hasNext() antes. El contrato dice que next() debe lanzar NoSuchElementException cuando no quedan elementos, y eso es lo que hace.
**¿Y remove()?** No lo hemos implementado. Como en Iterator es una operación opcional (por defecto lanza UnsupportedOperationException), nuestro iterador es de solo lectura. Para soportarlo habría que guardar también el nodo anterior y reenlazar, como en el programa de Python Tutor.
Olvidar implements Iterable<T> en la clase. Si solo escribes un método iterator() pero no declaras que implementas Iterable, el for-each no compila: lo comprobarás en los ejercicios.
Pruébalo tú
- Añade a
ListaEnlazadaun métodoexiste(T dato)que use unfor-eachpara decir si el dato está en la lista. - Haz que el iterador soporte
remove(): guarda el nodo anterior y reenlaza. Empieza por el caso fácil (borrar un nodo que no sea el primero).
Programa 3: ListIterator en ambos sentidos
La playlist vuelve. Esta vez la recorremos de principio a fin y de fin a principio con el mismo iterador, y la modificamos sobre la marcha con set() y add().
La idea
ListIterator tiene un cursor que se mueve entre dos elementos y puede ir hacia delante (next) y hacia atrás (previous). Vamos a probar tres cosas: ir y volver mostrando las posiciones con nextIndex() y previousIndex(); modificar con set() (sustituir la canción devuelta) y add() (insertar una nueva); y reutilizar el mismo iterador, que al terminar está al final, para recorrerlo a la inversa.
add() inserta antes del elemento que devolvería next(), es decir, justo después de la canción que acabas de pasar. Por eso el siguiente next() sigue devolviendo la canción de siempre y no la nueva.
El código
Usa la misma Cancion.java del programa 1.
Ambos.java
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class Ambos {
public static void main(String[] args) {
List<Cancion> lista = new ArrayList<>(List.of(
new Cancion("Amanecer", 215), new Cancion("Carretera", 180),
new Cancion("Tormenta", 240), new Cancion("Faro", 135)));
// 1. Ida y vuelta con el mismo iterador
ListIterator<Cancion> it = lista.listIterator();
System.out.println("Hacia delante:");
while (it.hasNext()) {
int i = it.nextIndex();
System.out.println(" " + i + ": " + it.next());
}
System.out.println("Hacia atrás:");
while (it.hasPrevious()) {
int i = it.previousIndex();
System.out.println(" " + i + ": " + it.previous());
}
// 2. set() y add() mientras recorremos
it = lista.listIterator();
while (it.hasNext()) {
Cancion c = it.next();
if (c.titulo().equals("Carretera")) {
it.add(new Cancion("Interludio", 30));
} else if (c.titulo().equals("Tormenta")) {
it.set(new Cancion("Tormenta (remix)", 250));
}
}
System.out.println("Resultado: " + lista);
// 3. El mismo iterador, ahora desde el final
System.out.print("Del final al principio:");
while (it.hasPrevious()) {
System.out.print(" " + it.previous().titulo() + ";");
}
System.out.println();
}
}
Salida
Hacia delante: 0: Amanecer (3:35) 1: Carretera (3:00) 2: Tormenta (4:00) 3: Faro (2:15) Hacia atrás: 3: Faro (2:15) 2: Tormenta (4:00) 1: Carretera (3:00) 0: Amanecer (3:35) Resultado: [Amanecer (3:35), Carretera (3:00), Interludio (0:30), Tormenta (remix) (4:10), Faro (2:15)] Del final al principio: Faro; Tormenta (remix); Interludio; Carretera; Amanecer;
Compruébalo a mano:
- Hacia delante: las posiciones 0 a 3, con
nextIndex()antes de cadanext(). - Hacia atrás: de la 3 a la 0. Aquí se usa
previousIndex()antes de cadaprevious(): devuelve la posición del elemento queprevious()va a entregar. - Resultado:
Interludio(0:30) se ha insertado justo después deCarretera, yTormentaha sido sustituida porTormenta (remix)(4:10). El resto queda igual. - Del final al principio: el iterador de la segunda parte terminó con el cursor al final, así que
hasPrevious()estrueyprevious()entrega las canciones desde la última.
Preguntas que te puedes hacer
**¿Por qué tras un add() el programa no vuelve a visitar la canción nueva?** Porque add() coloca el cursor detrás del elemento insertado. El siguiente next() devuelve Tormenta, no Interludio. Por eso no hay riesgo de bucle infinito.
**¿A qué canción afecta set()?** A la última que devolvió next() o previous(). Por eso va justo después de la llamada que la devolvió. Aquí, Tormenta es la devuelta por next() cuando el if la reconoce.
**¿Por qué nextIndex() y previousIndex() van antes de next() y previous()? Porque indican la posición de lo que va a** devolverse. Si los llamas después, te dan la posición del siguiente elemento, no la del que acabas de recibir.
**¿Puedo usar add() o set() con un Iterator normal?** No: solo ListIterator los tiene. El Iterator simple permite como mucho remove().
Llamar a set() o a remove() justo después de un add(), o dos set() seguidos tras un add(). Tras un add() no hay «última devuelta» y salta IllegalStateException.
Pruébalo tú
- Recorre la playlist hacia atrás desde el final (
lista.listIterator(lista.size())) y borra conremove()las canciones de menos de 150 segundos. - Inserta un
Interludioantes de cada canción en lugar de después. Pista: ¿qué tienes que hacer conprevious()?
Errores típicos en esta práctica
- **Borrar con
lista.remove(...)dentro de unfor-each**:ConcurrentModificationExceptiony lista a medias. Usait.remove(). - **Llamar a
it.remove()sinnext()previo** (o dos veces seguidas):IllegalStateException. - **Olvidar
implements Iterable<T>** al escribir una colección propia: elfor-eachno compila. - Guardar el iterador en un atributo y devolver siempre el mismo desde
iterator(): el segundo recorrido no encuentra nada. - Confundir la posición del cursor con la del elemento:
nextIndex()es la del que va a salir connext(), no la del que acaba de salir. - **Usar
add()y esperar que el siguientenext()devuelva lo añadido**: el cursor ya está detrás.
Míralo paso a paso en Python Tutor
Python Tutor ejecuta el código línea a línea y dibuja variables, objetos y referencias. Elige Java. Aquí tienes el borrado de un nodo en una lista enlazada, que es lo que hace remove() por dentro: no hay ningún «hueco» que tapar, solo una flecha que se desvía.
public class Tutor {
static class Nodo {
String dato;
Nodo siguiente;
Nodo(String dato, Nodo siguiente) {
this.dato = dato;
this.siguiente = siguiente;
}
}
public static void main(String[] args) {
Nodo lista = new Nodo("FP2", new Nodo("IC2", new Nodo("AEDV", null)));
Nodo anterior = null;
Nodo actual = lista;
while (actual != null) {
if (actual.dato.equals("IC2")) {
anterior.siguiente = actual.siguiente; // saltamos el nodo: así "borra" un iterador
} else {
anterior = actual;
}
actual = actual.siguiente;
}
for (Nodo n = lista; n != null; n = n.siguiente) {
System.out.println(n.dato);
}
}
}
Salida esperada:
FP2 AEDV
Qué observar paso a paso:
- Antes del
while, verás tres cajasNodoencadenadas por flechas:FP2→IC2→AEDV.listaapunta a la primera,actualtambién, yanterioresnull. - En la primera vuelta,
FP2no esIC2, así que entra elelse:anteriorpasa a apuntar aFP2yactualavanza aIC2. - En la segunda vuelta se cumple el
if. Fíjate en la flechasiguientedeFP2: **salta por encima deIC2** y apunta directamente aAEDV. La cajaIC2sigue en el dibujo, pero ya nada de la lista llega hasta ella. actual = actual.siguientetodavía puede avanzar desdeIC2(conserva su flecha aAEDV). Por eso el recorrido no se pierde. Al final, elforimprime soloFP2yAEDV.
Resumen
- El **
for-each** pide un iterador a unIterabley repitehasNext()/next(). - Para borrar mientras recorres, usa
it.remove()(una vez pornext()); conlista.remove()obtienesConcurrentModificationExceptiony una lista a medias. removeIfes el atajo para borrar por condición.- Una colección propia necesita
implements Iterable<T>y uniterator()que devuelva un iterador nuevo con su propio cursor. - Una clase interna (sin
static) es la forma natural de escribir ese iterador, porque ve los campos de la colección. ListIteratorañadeprevious(),nextIndex(),previousIndex(),set()yadd(), y su cursor siempre está entre dos elementos.
Checklist: ¿lo has practicado?
- [ ] He compilado y ejecutado los tres programas.
- [ ] He comprobado a mano al menos una salida de cada programa.
- [ ] He visto la
ConcurrentModificationExceptiony el estado en el que deja la lista. - [ ] He recorrido mi
ListaEnlazadacon dosfor-eachanidados. - [ ] He insertado y sustituido canciones con un
ListIterator. - [ ] He mirado el programa de Python Tutor y he visto cómo una flecha se salta un nodo.
Y ahora, a por los ejercicios: ejercicios de iteradores, con tres niveles, tres fragmentos para depurar y un chuletario al final. ¡Nos vemos allí!
Siguiente paso: ejerciciosIteradores en Java — ejercicios con playlists, títulos repetidos y un rango con paso (y el «Faro» que nunca se iba)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →
