Apuntes DAM

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

  1. Iterador. La interfaz Iterator con hasNext() y next().
  2. Iterador concreto. Guarda la posición del recorrido y sabe avanzar por la estructura de una colección concreta.
  3. Agregado. La interfaz Iterable, con iterator(), que crea un iterador nuevo cada vez.
  4. Colección concreta. La clase (Playlist) que implementa Iterable y crea su iterador; puede ofrecer otros recorridos (alReves()).

Diagrama de clases

crea«interface»Iterable+iterator() : Iterator«interface»Iterator+hasNext() : boolean+next() : StringPlaylist-canciones : String[]-n : int+anadir(c : String) : void+iterator() : Iterator+alReves() : IterableIteradorPlaylist-i : int+hasNext() : boolean+next() : String
Arrastra las clases para colocarlas a tu gusto.
Ver el diagrama en PlantUML
text
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@enduml

Puedes 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.

Java
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}

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.

Java
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);

En la práctica

  • Todas las colecciones de Java, los for-each, los Stream y el ResultSet de JDBC (que se recorre con next()).
  • Los generadores de Python (yield) y de JavaScript (function*), for…of y el protocolo Symbol.iterator.
  • IEnumerable y yield return en C#; Iterator, IteratorAggregate y 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. Usa Iterator.remove() o removeIf.
  • 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 lance NoSuchElementException cuando 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, columnas o serpiente. 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…
JavaRecorrer un tablero de tres manerasFácil

Ejemplo

Entrada (lo que se escribe por teclado)
1 2 3 4
5 6 7 8
9 10 11 12
filas
columnas
serpiente
Salida esperada
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
Test oculto #3
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
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 hasta y primero desde hasta paso (el primer elemento del rango). Los resultados van separados por espacios, o (vacío).
  • El rango incluye hasta si se llega justo a él. Un paso 0 lanza IllegalArgumentException con Paso no válido. next() sin elementos lanza NoSuchElementException (el main escribe Sin elementos).
JavaRangos y filtros sin listasMedio

Ejemplo

Entrada (lo que se escribe por teclado)
rango 1 10 3
rango 10 1 -3
rango 0 20 5
rango 1 5 -1
Salida esperada
1 4 7 10
10 7 4 1
0 5 10 15 20
(vacío)
Test oculto #3
Test oculto #4
0/4 tests pasados · pulsa un test para ver su entrada y su salida esperada
Ver la solución explicada
java
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 aciertos

Elige 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. 1.¿Qué dos operaciones tiene un Iterator de Java?

  2. 2.¿Qué tiene que implementar una clase para poder usarse en un for-each?

  3. 3.¿Qué pasa si borras elementos de un ArrayList dentro de un for-each sobre él?

  4. 4.¿Qué ventaja tiene un iterador que calcula los elementos al pedirlos?

  5. 5.¿Puede haber dos recorridos a la vez sobre la misma colección?

Relacionado