Iteradores en Java — ejercicios con playlists, títulos repetidos y un rango con paso (y el «Faro» que nunca se iba)
Me pidieron quitar los títulos repetidos de una lista y lo hice con un for de índices: si el elemento i ya estaba antes, remove(i) y a seguir. Con los datos de ejemplo funcionaba de maravilla. Con los datos reales, donde un mismo título salía dos veces seguidas, siempre se quedaba uno colado. Java no se quejó en ningún momento. El problema era mío: al borrar el elemento i, el siguiente pasa a ocupar su sitio y mi i++ lo saltaba sin mirarlo.
Los iteradores existen, entre otras cosas, para que eso no pase. Hoy practicamos con tres programas de dificultad creciente, tres fragmentos para depurar (uno de ellos, un cursor que se pasa de largo) y un chuletario para llevarte.
Estos ejercicios usan la teoría de iteradores y repasan lo de la práctica. Sin ellas puedes hacerlos, pero te costará más.
Tabla de Contenidos
Cómo usar este artículo
- Lee el enunciado de un nivel y no mires la solución todavía.
- Escribe tu versión, compílala y compara tu salida con la esperada.
- Si te atascas, mira solo la 🔵 pista y vuelve a intentarlo.
- Cuando termines, compara con la solución y fíjate en por qué está hecha así, no solo en que dé la misma salida.
Ningún ejercicio usa Scanner. Los datos van fijos en el código para que tu salida sea idéntica a la esperada y puedas comprobarla de un vistazo.
Nivel 1 · Básico: recorrer una playlist con Iterator
Enunciado. Usa este record para representar las canciones (un record es una clase de datos con constructor y getters ya hechos; titulo() y segundos() son sus métodos de acceso):
public record Cancion(String titulo, int segundos) {
@Override
public String toString() {
return titulo + " (" + segundos / 60 + ":" + String.format("%02d", segundos % 60) + ")";
}
}
En una clase N1 con main, crea una lista con estas cinco canciones:
- Amanecer, 215 segundos
- Carretera, 180 segundos
- Tormenta, 240 segundos
- Faro, 135 segundos
- Marea, 200 segundos
Recórrela con un **Iterator escrito a mano** (nada de for-each) y, para cada canción, imprime su número de orden y la propia canción. Al terminar, muestra la duración total de la playlist con el formato m:ss y cuál es la canción más larga.
Salida esperada:
1. Amanecer (3:35) 2. Carretera (3:00) 3. Tormenta (4:00) 4. Faro (2:15) 5. Marea (3:20) Duración total: 16:10 La más larga: Tormenta (4:00)
Dentro del while (it.hasNext()), una sola llamada a next() y guarda el resultado en una variable. Con ella puedes imprimirla, sumar sus segundos y comparar con la más larga hasta ahora. Para la duración total, total / 60 son los minutos y total % 60 los segundos (con String.format("%02d", …) te salen siempre dos cifras).
Nivel 2 · Intermedio: títulos sin repetir
Enunciado. Tienes esta lista de títulos, con repeticiones:
Faro, Marea, Faro, Amanecer, Marea, Faro
En N2, déjala con solo la primera aparición de cada título, manteniendo el orden, y cuenta cuántos has quitado. Tiene que ser con un Iterator y su remove(). Usa un HashSet<String> auxiliar (un conjunto: no guarda repetidos, y su método add devuelve false si el elemento ya estaba).
Imprime la lista antes, la lista después y el número de títulos quitados.
Salida esperada:
Antes: [Faro, Marea, Faro, Amanecer, Marea, Faro] Después: [Faro, Marea, Amanecer] Quitados: 3
Por cada título que te da next(), prueba a añadirlo al conjunto. Si add devuelve false, es que ya lo habías visto: it.remove() y cuenta uno más. Recuerda crear la lista con new ArrayList<>(List.of(...)), porque List.of da una lista que no se puede modificar.
Nivel 3 · Desafío: un rango con paso
Enunciado. Crea una clase Rango que sea Iterable<Integer> y que genere los números desde inicio hasta fin (este no incluido) de paso en paso. El paso puede ser negativo (cuenta atrás). Debe poder usarse directamente en un for-each:
for (int n : new Rango(0, 10, 3)) { ... } // 0 3 6 9
Si el paso es 0, el constructor lanza IllegalArgumentException("el paso no puede ser 0"). Cada llamada a iterator() debe dar un recorrido independiente, y next() debe lanzar NoSuchElementException cuando se acaben los números.
Para probar tu clase, usa este main (clase N3):
import java.util.List;
public class N3 {
public static void main(String[] args) {
System.out.print("De 0 a 10 de 3 en 3:");
for (int n : new Rango(0, 10, 3)) {
System.out.print(" " + n);
}
System.out.println();
System.out.print("De 10 a 0 de 4 en 4:");
for (int n : new Rango(10, 0, -4)) {
System.out.print(" " + n);
}
System.out.println();
System.out.print("De 5 a 5:");
for (int n : new Rango(5, 5, 1)) {
System.out.print(" " + n);
}
System.out.println(" (vacío)");
List<Cancion> lista = List.of(
new Cancion("Amanecer", 215), new Cancion("Carretera", 180),
new Cancion("Tormenta", 240), new Cancion("Faro", 135),
new Cancion("Marea", 200));
System.out.println("Posiciones pares de la playlist:");
for (int i : new Rango(0, lista.size(), 2)) {
System.out.println(" " + i + ": " + lista.get(i));
}
try {
new Rango(0, 5, 0);
} catch (IllegalArgumentException e) {
System.out.println("Error: " + e.getMessage());
}
}
}
Salida esperada:
De 0 a 10 de 3 en 3: 0 3 6 9 De 10 a 0 de 4 en 4: 10 6 2 De 5 a 5: (vacío) Posiciones pares de la playlist: 0: Amanecer (3:35) 2: Tormenta (4:00) 4: Marea (3:20) Error: el paso no puede ser 0
El iterador necesita un único dato propio, el número actual, que empieza en inicio. Para saber si quedan números, mira el signo del paso: con paso positivo sigues mientras actual < fin; con paso negativo, mientras actual > fin. Y next() devuelve el valor actual antes de sumarle el paso.
¿Qué falla aquí?
Tres fragmentos con un fallo cada uno. Antes de leer la explicación, haz dos cosas: predice si compila y, si compila, qué imprime (o qué pasa al ejecutarlo). Los tres usan el mismo record Cancion del nivel 1.
Fragmento 1
F1.java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class F1 {
public static void main(String[] args) {
List<Cancion> lista = new ArrayList<>(List.of(
new Cancion("Amanecer", 215), new Cancion("Carretera", 180),
new Cancion("Faro", 135)));
Iterator<Cancion> it = lista.iterator();
while (it.hasNext()) {
System.out.println(it.next().titulo() + " dura " + it.next().segundos() + " s");
}
}
}
Piénsalo antes de seguir: ¿compila? ¿qué imprime? Quería ver cada título con su duración.
Explicación. Compila y arranca bien, pero lo que imprime es falso y, además, acaba mal:
Amanecer dura 180 s Exception in thread "main" java.util.NoSuchElementException at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1052) at F1.main(F1.java:13)
En cada vuelta hay dos llamadas a next(), y cada una avanza el cursor. En la primera vuelta, la primera llamada devuelve Amanecer y la segunda devuelve Carretera: el programa mezcla el título de una canción con la duración de la siguiente (y 180 s no son los de Amanecer, sino los de Carretera). En la segunda vuelta, hasNext() ve que queda Faro, pero la primera llamada a next() ya se lo lleva, y la segunda se encuentra el final de la lista: NoSuchElementException.
Llamar a next() varias veces dentro de la misma vuelta del bucle. hasNext() solo garantiza un elemento más, y cada next() consume uno. Con un número impar de elementos, además, salta la excepción.
Arreglo. Una sola llamada a next(), guardada en una variable, y ya puedes usarla las veces que quieras:
Iterator<Cancion> it = lista.iterator();
while (it.hasNext()) {
Cancion c = it.next(); // una sola llamada a next() por vuelta
System.out.println(c.titulo() + " dura " + c.segundos() + " s");
}
Amanecer dura 215 s Carretera dura 180 s Faro dura 135 s
Regla de oro del iterador: **un next() por cada hasNext()**. Si necesitas el elemento más de una vez, guárdalo en una variable local.
Fragmento 2
F2.java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class F2 {
public static void main(String[] args) {
List<Cancion> lista = new ArrayList<>(List.of(
new Cancion("Amanecer", 215), new Cancion("Carretera", 180),
new Cancion("Faro", 135), new Cancion("Marea", 200)));
Iterator<Cancion> it = lista.iterator();
while (it.hasNext()) {
Cancion c = it.next();
System.out.println("Reviso " + c.titulo());
if (c.segundos() < 150) {
lista.remove(c);
}
}
System.out.println("Quedan " + lista.size() + ": " + lista);
}
}
¿Compila? ¿qué imprime? Quiero revisar todas las canciones y borrar las de menos de 150 segundos. Fíjate en cuál de ellas se borra y qué pasa con la que va detrás.
Explicación. Compila y no lanza ninguna excepción. Y sin embargo falta algo en la salida:
Reviso Amanecer Reviso Carretera Reviso Faro Quedan 3: [Amanecer (3:35), Carretera (3:00), Marea (3:20)]
Marea nunca se revisa. Se borra Faro con lista.remove(c) (por la lista, no por el iterador) siendo la penúltima canción: la lista pasa de 4 a 3 elementos y el cursor del iterador vale justo 3. Entonces hasNext(), que solo compara «¿el cursor es distinto del tamaño?», responde false y el bucle termina sin llamar a next() otra vez. Y es next() quien comprueba si la lista ha cambiado, así que **no salta ninguna ConcurrentModificationException**. El fallo pasa de puntillas: el programa «funciona» y se salta una canción.
Modificar la lista por fuera del iterador. A veces explota con una excepción, y a veces, como aquí, deja un resultado equivocado sin avisar. Es peor lo segundo.
Arreglo. El borrado se hace a través del iterador:
Iterator<Cancion> it = lista.iterator();
while (it.hasNext()) {
Cancion c = it.next();
System.out.println("Reviso " + c.titulo());
if (c.segundos() < 150) {
it.remove(); // borra a través del iterador
}
}
Reviso Amanecer Reviso Carretera Reviso Faro Reviso Marea Quedan 3: [Amanecer (3:35), Carretera (3:00), Marea (3:20)]
Con it.remove() ya se revisan las cuatro canciones y el resultado es el mismo que antes en cuanto a lo borrado. La diferencia es que ahora el recorrido es correcto, y no por casualidad.
Fragmento 3
Playlist.java
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Playlist {
private final List<Cancion> canciones = new ArrayList<>();
public void anadir(Cancion c) {
canciones.add(c);
}
public Iterator<Cancion> iterator() {
return canciones.iterator();
}
}
F3.java
public class F3 {
public static void main(String[] args) {
Playlist lista = new Playlist();
lista.anadir(new Cancion("Amanecer", 215));
lista.anadir(new Cancion("Faro", 135));
for (Cancion c : lista) {
System.out.println(c);
}
}
}
*¿Compila? ¿qué imprime? La clase tiene un método iterator(), ¿no? Entonces debería valer para un for-each.*
Explicación. No compila:
F3.java:7: error: for-each not applicable to expression type
for (Cancion c : lista) {
^
required: array or java.lang.Iterable
found: Playlist
1 error
El for-each no busca un método llamado iterator(): exige que el tipo sea Iterable (o un array). Tener el método no basta, hay que declarar que cumples el contrato con implements Iterable<Cancion>. Java comprueba el tipo, no la forma.
Escribir el método iterator() pero olvidar implements Iterable<T> en la cabecera de la clase. El mensaje habla de «array or java.lang.Iterable», y es justo eso lo que falta.
Arreglo. Declara la interfaz (y marca el método con @Override, que te avisa si te equivocas en la firma):
public class Playlist implements Iterable<Cancion> {
private final List<Cancion> canciones = new ArrayList<>();
public void anadir(Cancion c) {
canciones.add(c);
}
@Override
public Iterator<Cancion> iterator() {
return canciones.iterator();
}
}
Amanecer (3:35) Faro (2:15)
Pon siempre @Override en iterator(), hasNext() y next(). Si te equivocas en una letra, el compilador protesta en el acto en lugar de dejarte con un método que nadie llama.
Soluciones
Mira esta sección solo cuando hayas intentado el ejercicio. Si tu código es distinto pero da la misma salida y cumple los contratos, también es una solución válida.
Solución del Nivel 1
N1.java
import java.util.Iterator;
import java.util.List;
public class N1 {
public static void main(String[] args) {
List<Cancion> lista = List.of(
new Cancion("Amanecer", 215), new Cancion("Carretera", 180),
new Cancion("Tormenta", 240), new Cancion("Faro", 135),
new Cancion("Marea", 200));
Iterator<Cancion> it = lista.iterator();
int numero = 0;
int total = 0;
Cancion larga = null;
while (it.hasNext()) {
Cancion c = it.next();
numero++;
total += c.segundos();
System.out.println(numero + ". " + c);
if (larga == null || c.segundos() > larga.segundos()) {
larga = c;
}
}
System.out.println("Duración total: " + total / 60 + ":" + String.format("%02d", total % 60));
System.out.println("La más larga: " + larga);
}
}
Por qué está hecho así:
- Un único
it.next()por vuelta, guardado enc. Con esa variable imprimimos, sumamos los segundos y comparamos. larga == null || c.segundos() > larga.segundos(): en la primera vuelta no hay «la más larga hasta ahora», así que la primera canción ocupa ese puesto; a partir de ahí, solo cambia si la nueva es más larga.numero++antes de imprimir, para que la numeración empiece en 1.- Los segundos del total se calculan con
total % 60y se imprimen con%02d, para que 10 sea10y 5 sea05.
Solución del Nivel 2
N2.java
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
public class N2 {
public static void main(String[] args) {
List<String> titulos = new ArrayList<>(List.of(
"Faro", "Marea", "Faro", "Amanecer", "Marea", "Faro"));
System.out.println("Antes: " + titulos);
Set<String> vistos = new HashSet<>();
Iterator<String> it = titulos.iterator();
int quitados = 0;
while (it.hasNext()) {
String t = it.next();
if (!vistos.add(t)) { // add() devuelve false si ya estaba
it.remove();
quitados++;
}
}
System.out.println("Después: " + titulos);
System.out.println("Quitados: " + quitados);
}
}
Por qué está hecho así:
vistos.add(t)hace dos cosas a la vez: añade el título y devuelvefalsesi ya estaba. El!convierte «ya estaba» en «bórralo».it.remove()borra la última canción que devolviónext(), así que el siguiente elemento no se salta aunque haya repeticiones seguidas (justo el fallo del bucle con índices de la anécdota).quitadoscuenta los borrados: son 3 porqueFaroaparece 3 veces (quita 2) yMarea2 (quita 1).- La lista es
new ArrayList<>(List.of(...)). ConList.ofa secas,it.remove()lanzaríaUnsupportedOperationException.
Solución del Nivel 3
Rango.java
import java.util.Iterator;
import java.util.NoSuchElementException;
public class Rango implements Iterable<Integer> {
private final int inicio;
private final int fin;
private final int paso;
public Rango(int inicio, int fin, int paso) {
if (paso == 0) {
throw new IllegalArgumentException("el paso no puede ser 0");
}
this.inicio = inicio;
this.fin = fin;
this.paso = paso;
}
@Override
public Iterator<Integer> iterator() {
return new Iterator<>() {
private int actual = inicio;
@Override
public boolean hasNext() {
return paso > 0 ? actual < fin : actual > fin;
}
@Override
public Integer next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
int valor = actual;
actual += paso;
return valor;
}
};
}
}
Por qué está hecho así:
- El constructor valida
paso == 0y lanzaIllegalArgumentException: es mejor fallar al crear el rango que quedarse en un bucle infinito después. hasNext()comprueba el signo del paso con un operador ternario:paso > 0 ? actual < fin : actual > fin. Coninicio == finninguna de las dos condiciones se cumple, y el rango sale vacío.- Aquí el iterador es una clase anónima (
new Iterator<>() { ... }): hace lo mismo que la clase interna con nombre de la teoría, pero escrita en el sitio porque solo se usa una vez.actualse inicializa eninicio, así que cada llamada aiterator()empieza de cero. next()compruebahasNext()y lanzaNoSuchElementExceptioncuando no quedan números: es lo que promete el contrato deIterator.- El
for (int n : new Rango(...))funciona porqueRangoesIterable<Integer>, y Java convierte cadaIntegerenintautomáticamente. - En la prueba de la playlist,
new Rango(0, lista.size(), 2)genera los índices 0, 2 y 4: así se reutiliza el rango para saltar de dos en dos.
Míralo paso a paso en Python Tutor
El fallo del fragmento 1 es perfecto para verlo en vivo, porque ocurre en una sola línea. Entra en pythontutor.com/java.html, elige Java y pega este programa. Es el mismo fallo, con un iterador casero que lanza la excepción a la vista:
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() {
if (posicion >= datos.length) {
throw new IllegalStateException("no quedan canciones");
}
return datos[posicion++];
}
}
public static void main(String[] args) {
Cursor it = new Cursor(new String[]{"Amanecer", "Carretera", "Faro"});
while (it.hasNext()) {
String titulo = it.next();
String siguiente = it.next(); // ¡segunda llamada en la misma vuelta!
System.out.println(titulo + " / " + siguiente);
}
}
}
Salida esperada:
Amanecer / Carretera Exception in thread "main" java.lang.IllegalStateException: no quedan canciones at Tutor$Cursor.next(Tutor.java:12) at Tutor.main(Tutor.java:22)
Qué observar, paso a paso:
- En la primera vuelta,
it.hasNext()estruey la variableposiciondel cursor vale 0. Trastitulo = it.next(), vale 1 ytituloes"Amanecer". - La segunda llamada,
siguiente = it.next(), dejaposicionen 2 ysiguientees"Carretera": la vuelta ha consumido dos títulos. Se imprimeAmanecer / Carretera. - En la segunda vuelta,
hasNext()sigue siendotrue(2 < 3), pero la primera llamada anext()ya dejaposicionen 3. Cuando entras en la segunda llamada, elifcomprueba queposicion >= datos.lengthy es cierto. - Se ejecuta el
throw: el programa se detiene en esa línea ysiguientenunca llega a tener valor. Compara con el fragmento 1: es el mismo recorrido, solo que aquí ves el cursor a cada paso.
Chuletario: Iteradores
Lo esencial en 30 segundos
- **
Iterator**: el cursor.hasNext()mira,next()avanza y devuelve,remove()borra el último devuelto. - **
Iterable**: algo que sabe dar un iterador coniterator(). Elfor-eachsolo lo exige a él. - **Un
next()por vuelta**, guardado en una variable. - **Borrar mientras recorres =
it.remove()**, nuncalista.remove(). - **Un
Iterablepropio devuelve un iterador nuevo** en cada llamada aiterator(). - **
ListIterator**: solo en listas; añadeprevious(),set()yadd().
Sintaxis
| Quiero… | Código | Resultado |
|---|---|---|
| Recorrer sin índices | for (Cancion c : lista) { … } | Funciona con todo Iterable |
| Pedir un iterador | Iterator<Cancion> it = lista.iterator(); | Cursor nuevo, al principio |
| Recorrer a mano | while (it.hasNext()) { Cancion c = it.next(); … } | Un next() por vuelta |
| Borrar mientras recorro | it.remove(); | Tras next(), una vez por next() |
| Borrar con una condición | lista.removeIf(c -> c.segundos() < 150); | Atajo |
| Hacer iterable mi clase | class A implements Iterable<T> { … } | Y un método iterator() |
| Iterador propio | return new IteradorA(); | Clase interna con su cursor |
| Ir hacia atrás | ListIterator<T> it = lista.listIterator(); it.previous(); | Solo para listas |
| Sustituir o insertar | it.set(x); / it.add(x); | Actúa sobre la posición del cursor |
| Aceptar cualquier colección | static int contar(Iterable<?> c) | Vale para lista, conjunto… |
Plantillas de los ejercicios
Recorrido manual con acumulador (Nivel 1):
Iterator<Cancion> it = lista.iterator();
int total = 0;
while (it.hasNext()) {
Cancion c = it.next(); // un solo next() por vuelta
total += c.segundos();
}
Quitar con un conjunto auxiliar (Nivel 2):
Set<String> vistos = new HashSet<>();
Iterator<String> it = titulos.iterator();
while (it.hasNext()) {
if (!vistos.add(it.next())) { // add() es false si ya estaba
it.remove();
}
}
Iterable con iterador anónimo (Nivel 3):
public class Rango implements Iterable<Integer> {
@Override
public Iterator<Integer> iterator() {
return new Iterator<>() {
private int actual = inicio; // cursor propio
@Override
public boolean hasNext() { return paso > 0 ? actual < fin : actual > fin; }
@Override
public Integer next() {
if (!hasNext()) throw new NoSuchElementException();
int valor = actual;
actual += paso;
return valor;
}
};
}
}
Errores que más se repiten
| Síntoma | Causa probable |
|---|---|
ConcurrentModificationException | Modificas la lista (remove/add) fuera del iterador mientras la recorres |
| Un elemento se queda sin visitar tras borrar, sin excepción | Borrado con lista.remove() en lugar de it.remove() (pasa con el penúltimo) |
NoSuchElementException | next() sin hasNext(), o dos next() por vuelta |
IllegalStateException al borrar | remove() antes del primer next(), o dos seguidos |
UnsupportedOperationException al borrar | Lista inmutable (List.of) o iterador sin remove() |
| «for-each not applicable to expression type» | La clase no declara implements Iterable<T> |
El segundo for-each no recorre nada | iterator() devuelve siempre el mismo iterador |
Antes de entregar, pregúntate
- ¿Hago **un solo
next()** por vuelta y guardo el resultado en una variable? - ¿Borro con **
it.remove()** y no con elremovede la lista? - ¿Mi clase declara **
implements Iterable<T>y devuelve un iterador nuevo** en cadaiterator()? - ¿Mi
next()lanza **NoSuchElementException** cuando no quedan elementos?
Si quieres repasar el tema desde la base, vuelve a la teoría de iteradores.
Sigue aprendiendoTodos los artículos de Java¿Te ha servido este artículo? Así puedes apoyar SergioLearns →
