Patrón Iterator
Un objeto que recorre una colección elemento a elemento sin mostrar cómo está guardada: hasNext() y next(). Es lo que usa el for-each, y permite varias formas de recorrer la misma colección.
nivel básicoTambién: iterador, cursor, for-each, Iterable
Visualízalo paso a paso
Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.
Iterator
Escribe los elementos de la colección y elige el recorrido. Con dos iteradores se ve que cada uno recuerda su propia posición.
- por recorrer
Paso 1
El for-each pide un iterador a la colección: un objeto aparte que empieza en la posición 0.
1public Iterator<String> iterator() {
2 return new Iterator<>() {
3 private int i = 0; // i = 0, salida = —
4 public boolean hasNext() { return i < n; }
5 public String next() {
6 return canciones[i++];
7 }
8 };
9}
10
11for (String c : lista) System.out.println(c);Variables
- i
- 0
- salida
- —
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
Una lista se recorre por índice; un conjunto o un árbol, no. Si cada colección obligara a recorrerla a su manera, el código que la usa tendría que saber cómo está guardada por dentro, y cambiar un ArrayList por un TreeSet rompería todos los bucles.
El patrón Iterator saca el recorrido a un objeto aparte, el iterador, con dos operaciones: hasNext() (¿queda algo?) y next() (dame el siguiente y avanza). Cada colección sabe crear su iterador, y quien la recorre solo usa esas dos operaciones. Como el iterador guarda por dónde va, puede haber varios recorridos a la vez sobre la misma colección.
En Java, una clase que implementa Iterable (con su método iterator()) puede usarse en un for-each: el compilador convierte el bucle en llamadas a hasNext() y next(). Python, JavaScript, C# y PHP tienen lo mismo, y además generadores (yield) para escribir iteradores sin clase.
Cuándo usarlo
- Escribes una colección propia y quieres que se pueda recorrer con for-each sin enseñar su estructura interna.
- Necesitas varias formas de recorrer lo mismo (al revés, por columnas, en profundidad o en anchura, filtrando).
- Los elementos se calculan sobre la marcha y no caben (o no hace falta tenerlos) en memoria: rangos, ficheros enormes, resultados de una base de datos.
Cuándo no
- Si ya usas una colección de la biblioteca: ya trae su iterador.
- Si solo necesitas un recorrido simple por índice dentro de la propia clase.
Participantes
- Iterador. La interfaz
IteratorconhasNext()ynext(). - Iterador concreto. Guarda la posición del recorrido y sabe avanzar por la estructura de una colección concreta.
- Agregado. La interfaz
Iterable, coniterator(), que crea un iterador nuevo cada vez. - Colección concreta. La clase (
Playlist) que implementaIterabley crea su iterador; puede ofrecer otros recorridos (alReves()).
Diagrama de clases
Ver el diagrama en PlantUML
1@startuml
2interface Iterable {
3 +iterator() : Iterator
4}
5interface Iterator {
6 +hasNext() : boolean
7 +next() : String
8}
9class Playlist {
10 -canciones : String[]
11 -n : int
12 +anadir(c : String) : void
13 +iterator() : Iterator
14 +alReves() : Iterable
15}
16class IteradorPlaylist {
17 -i : int
18 +hasNext() : boolean
19 +next() : String
20}
21Iterable <|.. Playlist
22Iterator <|.. IteradorPlaylist
23Playlist ..> IteradorPlaylist : crea
24@endumlPuedes copiarlo en el editor de diagramas UML y modificarlo.
El código
Una lista de canciones recorrible
Playlist guarda las canciones en un array, pero el main solo usa for-each y hasNext/next. alReves() ofrece otro recorrido sin tocar la colección.
1import java.util.*;
2
3/** Una colección propia: guarda las canciones en un array, pero quien la recorre no lo sabe. */
4class Playlist implements Iterable<String> {
5 private final String[] canciones = new String[10];
6 private int n = 0;
7
8 void anadir(String c) { canciones[n++] = c; }
9
10 /** El iterador: recuerda por dónde va y sabe si queda algo. */
11 public Iterator<String> iterator() {
12 return new Iterator<>() {
13 private int i = 0;
14 public boolean hasNext() { return i < n; }
15 public String next() {
16 if (!hasNext()) throw new NoSuchElementException();
17 return canciones[i++];
18 }
19 };
20 }
21
22 /** Otra forma de recorrerla, sin cambiar la colección: de la última a la primera. */
23 Iterable<String> alReves() {
24 return () -> new Iterator<>() {
25 private int i = n - 1;
26 public boolean hasNext() { return i >= 0; }
27 public String next() {
28 if (!hasNext()) throw new NoSuchElementException();
29 return canciones[i--];
30 }
31 };
32 }
33}
34
35public class Main {
36 public static void main(String[] args) {
37 Playlist lista = new Playlist();
38 lista.anadir("Tren de medianoche");
39 lista.anadir("Andén 3");
40 lista.anadir("Última estación");
41 for (String c : lista) System.out.println("> " + c); // el for-each usa iterator()
42 System.out.println("Al revés:");
43 for (String c : lista.alReves()) System.out.println("< " + c);
44 Iterator<String> it = lista.iterator(); // también se puede usar a mano
45 System.out.println("Primera: " + it.next() + "; segunda: " + it.next() + "; ¿quedan más? " + (it.hasNext() ? "sí" : "no"));
46 }
47}class Playlist:
"""Una colección propia: guarda las canciones en una lista, pero quien la recorre no lo sabe."""
def __init__(self):
self._canciones = []
def anadir(self, c):
self._canciones.append(c)
def __iter__(self):
"""El for usa __iter__: devuelve un iterador que recuerda por dónde va."""
return IteradorPlaylist(self._canciones)
def al_reves(self):
"""Otra forma de recorrerla: un generador, el iterador escrito sin clase."""
for i in range(len(self._canciones) - 1, -1, -1):
yield self._canciones[i]
class IteradorPlaylist:
def __init__(self, canciones):
self._canciones, self._i = canciones, 0
def __iter__(self):
return self
def __next__(self):
if self._i >= len(self._canciones):
raise StopIteration # así avisa Python de que no queda nada
c = self._canciones[self._i]
self._i += 1
return c
lista = Playlist()
lista.anadir("Tren de medianoche")
lista.anadir("Andén 3")
lista.anadir("Última estación")
for c in lista:
print(f"> {c}")
print("Al revés:")
for c in lista.al_reves():
print(f"< {c}")
it = iter(lista) # también se puede usar a mano
primera, segunda = next(it), next(it)
quedan = next(it, None) is not None # Python no tiene hasNext: next con un valor por defecto
print(f"Primera: {primera}; segunda: {segunda}; ¿quedan más? {'sí' if quedan else 'no'}")/** Una colección propia: guarda las canciones en un array, pero quien la recorre no lo sabe. */
class Playlist {
#canciones = [];
anadir(c) { this.#canciones.push(c); }
/** El for…of usa [Symbol.iterator]: devuelve un objeto con next() que recuerda por dónde va. */
[Symbol.iterator]() {
const canciones = this.#canciones;
let i = 0;
return {
next: () => (i < canciones.length ? { value: canciones[i++], done: false } : { value: undefined, done: true }),
};
}
/** Otra forma de recorrerla: un generador, el iterador escrito sin clase. */
*alReves() {
for (let i = this.#canciones.length - 1; i >= 0; i--) yield this.#canciones[i];
}
}
const lista = new Playlist();
lista.anadir("Tren de medianoche");
lista.anadir("Andén 3");
lista.anadir("Última estación");
for (const c of lista) console.log(`> ${c}`);
console.log("Al revés:");
for (const c of lista.alReves()) console.log(`< ${c}`);
const it = lista[Symbol.iterator](); // también se puede usar a mano
const primera = it.next().value;
const segunda = it.next().value;
const quedan = !it.next().done; // JavaScript no tiene hasNext: next() dice si ha terminado
console.log(`Primera: ${primera}; segunda: ${segunda}; ¿quedan más? ${quedan ? "sí" : "no"}`);using System;
using System.Collections;
using System.Collections.Generic;
// Una colección propia: guarda las canciones en un array, pero quien la recorre no lo sabe.
class Playlist : IEnumerable<string> {
private readonly string[] canciones = new string[10];
private int n = 0;
public void Anadir(string c) => canciones[n++] = c;
// El foreach usa GetEnumerator. Con yield return, el compilador escribe la clase del iterador.
public IEnumerator<string> GetEnumerator() {
for (int i = 0; i < n; i++) yield return canciones[i];
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// Otra forma de recorrerla, sin cambiar la colección: de la última a la primera.
public IEnumerable<string> AlReves() {
for (int i = n - 1; i >= 0; i--) yield return canciones[i];
}
}
class Program {
static void Main() {
var lista = new Playlist();
lista.Anadir("Tren de medianoche");
lista.Anadir("Andén 3");
lista.Anadir("Última estación");
foreach (var c in lista) Console.WriteLine(quot;> {c}");
Console.WriteLine("Al revés:");
foreach (var c in lista.AlReves()) Console.WriteLine(quot;< {c}");
using var it = lista.GetEnumerator(); // también se puede usar a mano
it.MoveNext(); string primera = it.Current;
it.MoveNext(); string segunda = it.Current;
bool quedan = it.MoveNext(); // en .NET, MoveNext avanza y dice si había algo
Console.WriteLine(quot;Primera: {primera}; segunda: {segunda}; ¿quedan más? {(quedan ? "sí" : "no")}");
}
}<?php
/** Una colección propia: guarda las canciones en un array, pero quien la recorre no lo sabe. */
class Playlist implements IteratorAggregate {
private array $canciones = [];
public function anadir(string $c): void { $this->canciones[] = $c; }
/** El foreach usa getIterator. Un generador (yield) es el iterador escrito sin clase. */
public function getIterator(): Generator {
foreach ($this->canciones as $c) yield $c;
}
/** Otra forma de recorrerla, sin cambiar la colección: de la última a la primera. */
public function alReves(): Generator {
for ($i = count($this->canciones) - 1; $i >= 0; $i--) yield $this->canciones[$i];
}
}
$lista = new Playlist();
$lista->anadir("Tren de medianoche");
$lista->anadir("Andén 3");
$lista->anadir("Última estación");
foreach ($lista as $c) echo "> $c\n";
echo "Al revés:\n";
foreach ($lista->alReves() as $c) echo "< $c\n";
$it = $lista->getIterator(); // también se puede usar a mano
$primera = $it->current(); $it->next();
$segunda = $it->current(); $it->next();
$quedan = $it->valid(); // valid() es el hasNext de PHP
echo "Primera: $primera; segunda: $segunda; ¿quedan más? ", $quedan ? "sí" : "no", "\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
> Tren de medianoche > Andén 3 > Última estación Al revés: < Última estación < Andén 3 < Tren de medianoche Primera: Tren de medianoche; segunda: Andén 3; ¿quedan más? sí
Borrar mientras se recorre
El error de ConcurrentModificationException y cómo lo resuelve el propio iterador.
1// Borrar mientras se recorre con for-each lanza ConcurrentModificationException:
2// el iterador de la lista detecta que alguien la ha cambiado por debajo
3for (String c : canciones)
4 if (c.startsWith("A")) canciones.remove(c); // ✗
5
6// El propio iterador sí sabe borrar el elemento por el que va
7Iterator<String> it = canciones.iterator();
8while (it.hasNext())
9 if (it.next().startsWith("A")) it.remove(); // ✓
10
11canciones.removeIf(c -> c.startsWith("A")); // ✓ lo mismo, más corto
12
13// Los streams también recorren con un iterador por dentro, sin decir cómo está guardado
14canciones.stream().filter(c -> c.length() > 10).forEach(System.out::println);# Borrar de una lista mientras se recorre no da error... pero se salta elementos
for c in canciones:
if c.startswith("A"):
canciones.remove(c) # ✗ el siguiente pasa a ocupar su sitio y no se mira
# Lo correcto: crear una lista nueva con los que se quedan
canciones = [c for c in canciones if not c.startswith("A")] # ✓
# Con un diccionario sí da error: RuntimeError: dictionary changed size during iteration// splice dentro de forEach se salta elementos: los índices cambian por debajo
canciones.forEach((c, i) => { if (c.startsWith("A")) canciones.splice(i, 1); }); // ✗
// Lo correcto: filter crea un array nuevo con los que se quedan
canciones = canciones.filter((c) => !c.startsWith("A")); // ✓
// Map y Set sí permiten borrar mientras se recorren con for…of
for (const [clave] of mapa) if (clave.startsWith("A")) mapa.delete(clave);// Modificar una lista dentro de un foreach lanza InvalidOperationException
foreach (var c in canciones)
if (c.StartsWith("A")) canciones.Remove(c); // ✗
canciones.RemoveAll(c => c.StartsWith("A")); // ✓
// O recorrer al revés por índice, para que borrar no mueva lo que queda por mirar
for (int i = canciones.Count - 1; i >= 0; i--)
if (canciones[i].StartsWith("A")) canciones.RemoveAt(i);<?php
// foreach recorre una copia del array: borrar dentro no da error, pero no lo ves en el bucle
foreach ($canciones as $i => $c) {
if (str_starts_with($c, "A")) unset($canciones[$i]);
}
// Lo habitual: array_filter devuelve un array nuevo con los que se quedan
$canciones = array_values(array_filter($canciones, fn ($c) => !str_starts_with($c, "A")));En la práctica
- Todas las colecciones de Java, los
for-each, losStreamy elResultSetde JDBC (que se recorre connext()). - Los generadores de Python (
yield) y de JavaScript (function*),for…ofy el protocoloSymbol.iterator. IEnumerableyyield returnen C#;Iterator,IteratorAggregatey los generadores en PHP.- La paginación de una API: un iterador pide la página siguiente solo cuando se acaba la actual.
Errores típicos
- Modificar la colección mientras se recorre con for-each: lanza
ConcurrentModificationException. UsaIterator.remove()oremoveIf. - Llamar a
next()dos veces en la misma vuelta del bucle (una en la condición y otra dentro): te saltas elementos. - Que
next()no lanceNoSuchElementExceptioncuando no quedan elementos y devuelva basura. - Que
hasNext()cambie el estado: debe poder llamarse varias veces seguidas con el mismo resultado.
Ejercicios
Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.
1. Recorrer un tablero de tres maneras
Tablero ya se puede recorrer por filas con for-each. Añade los recorridos porColumnas() y enSerpiente(), que devuelven un Iterable con su propio iterador. El main solo usa for-each: no sabe que dentro hay una matriz.
- Primero, las filas del tablero (números separados por espacios); después, una orden por línea:
filas,columnasoserpiente. Si las filas no tienen todas la misma longitud:El tablero tiene que ser rectangular. - Por columnas: de arriba abajo, columna a columna. En serpiente: la fila 0 de izquierda a derecha, la 1 de derecha a izquierda, la 2 otra vez de izquierda a derecha…
Ejemplo
1 2 3 4 5 6 7 8 9 10 11 12 filas columnas serpiente
Por filas: 1 2 3 4 5 6 7 8 9 10 11 12 Por columnas: 1 5 9 2 6 10 3 7 11 4 8 12 En serpiente: 1 2 3 4 8 7 6 5 9 10 11 12
Ver la solución explicada
1import java.util.*;
2
3/** Un tablero de números. Por defecto se recorre por filas; también por columnas y en serpiente. */
4class Tablero implements Iterable<Integer> {
5 private final int[][] celdas;
6 private final int filas, columnas;
7
8 Tablero(int[][] celdas) {
9 this.celdas = celdas;
10 filas = celdas.length;
11 columnas = celdas[0].length;
12 }
13
14 /** Por filas: de izquierda a derecha y de arriba abajo. */
15 public Iterator<Integer> iterator() {
16 return new Iterator<>() {
17 private int k = 0; // posición en el recorrido: fila k / columnas, columna k % columnas
18 public boolean hasNext() { return k < filas * columnas; }
19 public Integer next() {
20 if (!hasNext()) throw new NoSuchElementException();
21 int v = celdas[k / columnas][k % columnas];
22 k++;
23 return v;
24 }
25 };
26 }
27
28 /** Por columnas: de arriba abajo y de izquierda a derecha. */
29 Iterable<Integer> porColumnas() {
30 return () -> new Iterator<>() {
31 private int k = 0;
32 public boolean hasNext() { return k < filas * columnas; }
33 public Integer next() {
34 if (!hasNext()) throw new NoSuchElementException();
35 int v = celdas[k % filas][k / filas];
36 k++;
37 return v;
38 }
39 };
40 }
41
42 /** En serpiente: la fila 0 de izquierda a derecha, la 1 de derecha a izquierda, y así. */
43 Iterable<Integer> enSerpiente() {
44 return () -> new Iterator<>() {
45 private int k = 0;
46 public boolean hasNext() { return k < filas * columnas; }
47 public Integer next() {
48 if (!hasNext()) throw new NoSuchElementException();
49 int f = k / columnas, c = k % columnas;
50 if (f % 2 == 1) c = columnas - 1 - c;
51 k++;
52 return celdas[f][c];
53 }
54 };
55 }
56}
57
58public class Main {
59 static String unir(Iterable<Integer> valores) {
60 StringJoiner sj = new StringJoiner(" ");
61 for (int v : valores) sj.add(String.valueOf(v)); // solo for-each: no se sabe cómo está guardado
62 return sj.toString();
63 }
64
65 public static void main(String[] args) {
66 Scanner sc = new Scanner(System.in);
67 List<int[]> filas = new ArrayList<>();
68 List<String> ordenes = new ArrayList<>();
69 while (sc.hasNextLine()) {
70 String linea = sc.nextLine().trim();
71 if (linea.isEmpty()) continue;
72 if (linea.matches("-?\\d+(\\s+-?\\d+)*")) filas.add(Arrays.stream(linea.split("\\s+")).mapToInt(Integer::parseInt).toArray());
73 else ordenes.add(linea);
74 }
75 if (filas.isEmpty() || filas.stream().anyMatch(f -> f.length != filas.get(0).length)) {
76 System.out.println("El tablero tiene que ser rectangular");
77 return;
78 }
79 Tablero t = new Tablero(filas.toArray(new int[0][]));
80 for (String o : ordenes) {
81 switch (o) {
82 case "filas" -> System.out.println("Por filas: " + unir(t));
83 case "columnas" -> System.out.println("Por columnas: " + unir(t.porColumnas()));
84 case "serpiente" -> System.out.println("En serpiente: " + unir(t.enSerpiente()));
85 default -> System.out.println("Recorrido desconocido: " + o);
86 }
87 }
88 }
89}Cada recorrido es un iterador distinto sobre los mismos datos; la colección no cambia.
El código que recorre (unir) es el mismo para los tres: solo necesita algo Iterable.
2. Rangos y filtros sin listas
Escribe Rango, que genera los enteros de un intervalo con un paso (positivo o negativo) sin guardarlos en ninguna lista, y Filtrado, un iterador que envuelve a otro y solo deja pasar los elementos que cumplen una condición.
- Órdenes:
rango desde hasta paso,pares desde hasta,primos desde hasta,multiplos k desde hastayprimero desde hasta paso(el primer elemento del rango). Los resultados van separados por espacios, o(vacío). - El rango incluye
hastasi se llega justo a él. Un paso 0 lanzaIllegalArgumentExceptionconPaso no válido.next()sin elementos lanzaNoSuchElementException(elmainescribeSin elementos).
Ejemplo
rango 1 10 3 rango 10 1 -3 rango 0 20 5 rango 1 5 -1
1 4 7 10 10 7 4 1 0 5 10 15 20 (vacío)
Ver la solución explicada
1import java.util.*;
2import java.util.function.Predicate;
3
4/** Los enteros de desde a hasta (incluido), con un paso que puede ser negativo. */
5class Rango implements Iterable<Integer> {
6 private final int desde, hasta, paso;
7
8 Rango(int desde, int hasta, int paso) {
9 if (paso == 0) throw new IllegalArgumentException("Paso no válido");
10 this.desde = desde;
11 this.hasta = hasta;
12 this.paso = paso;
13 }
14
15 public Iterator<Integer> iterator() {
16 return new Iterator<>() {
17 private int actual = desde;
18 public boolean hasNext() { return paso > 0 ? actual <= hasta : actual >= hasta; }
19 public Integer next() {
20 if (!hasNext()) throw new NoSuchElementException();
21 int v = actual;
22 actual += paso;
23 return v;
24 }
25 };
26 }
27}
28
29/** Envuelve otro iterador y solo deja pasar los elementos que cumplen la condición. */
30class Filtrado<T> implements Iterator<T> {
31 private final Iterator<T> dentro;
32 private final Predicate<T> condicion;
33 private T siguiente;
34 private boolean preparado = false;
35
36 Filtrado(Iterator<T> dentro, Predicate<T> condicion) {
37 this.dentro = dentro;
38 this.condicion = condicion;
39 }
40
41 /** Busca por adelantado el siguiente que cumple la condición (sin consumirlo dos veces). */
42 public boolean hasNext() {
43 while (!preparado && dentro.hasNext()) {
44 T x = dentro.next();
45 if (condicion.test(x)) {
46 siguiente = x;
47 preparado = true;
48 }
49 }
50 return preparado;
51 }
52
53 public T next() {
54 if (!hasNext()) throw new NoSuchElementException();
55 preparado = false;
56 return siguiente;
57 }
58}
59
60public class Main {
61 static boolean esPrimo(int n) {
62 if (n < 2) return false;
63 for (int d = 2; d * d <= n; d++) if (n % d == 0) return false;
64 return true;
65 }
66
67 static String unir(Iterator<Integer> it) {
68 StringJoiner sj = new StringJoiner(" ");
69 while (it.hasNext()) sj.add(String.valueOf(it.next()));
70 return sj.length() == 0 ? "(vacío)" : sj.toString();
71 }
72
73 public static void main(String[] args) {
74 Scanner sc = new Scanner(System.in);
75 while (sc.hasNextLine()) {
76 String linea = sc.nextLine().trim();
77 if (linea.isEmpty()) continue;
78 String[] p = linea.split("\\s+");
79 try {
80 int[] n = new int[p.length - 1];
81 for (int i = 1; i < p.length; i++) n[i - 1] = Integer.parseInt(p[i]);
82 String r = switch (p[0] + "/" + n.length) {
83 case "rango/3" -> unir(new Rango(n[0], n[1], n[2]).iterator());
84 case "pares/2" -> unir(new Filtrado<>(new Rango(n[0], n[1], 1).iterator(), x -> x % 2 == 0));
85 case "primos/2" -> unir(new Filtrado<>(new Rango(n[0], n[1], 1).iterator(), Main::esPrimo));
86 case "multiplos/3" -> unir(new Filtrado<>(new Rango(n[1], n[2], 1).iterator(), x -> x % n[0] == 0));
87 case "primero/3" -> String.valueOf(new Rango(n[0], n[1], n[2]).iterator().next());
88 default -> "Orden no válida: " + linea;
89 };
90 System.out.println(r);
91 } catch (NumberFormatException e) {
92 System.out.println("Orden no válida: " + linea);
93 } catch (IllegalArgumentException e) {
94 System.out.println(e.getMessage());
95 } catch (NoSuchElementException e) {
96 System.out.println("Sin elementos");
97 } catch (ArithmeticException e) {
98 System.out.println("No hay múltiplos de 0");
99 }
100 }
101 }
102}Ningún número se guarda en una lista: cada uno se calcula cuando se pide. Así funcionan IntStream.range o los generadores de Python.
Filtrado es un iterador decorador: envuelve a cualquier otro, y los filtros se pueden encadenar.
Test
Test: Iterator
0/5 respondidas · 0 aciertosElige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.
1.¿Qué dos operaciones tiene un Iterator de Java?
2.¿Qué tiene que implementar una clase para poder usarse en un for-each?
3.¿Qué pasa si borras elementos de un ArrayList dentro de un for-each sobre él?
4.¿Qué ventaja tiene un iterador que calcula los elementos al pedirlos?
5.¿Puede haber dos recorridos a la vez sobre la misma colección?