Skip to content

Iterators: El GPS de las Colecciones

Introducción: ¿Quién tiene el turno?

¡Hola, equipo! 🖖

Imaginad que sois el DJ de una fiesta (o de vuestro propio setup de programación). Tenéis una lista de reproducción en Spotify. Cuando escucháis música, raramente decís: "Quiero escuchar la canción número 437 de la lista". No, lo que hacéis es darle a "Siguiente", "Siguiente", "Siguiente".

Hasta ahora, con los Arrays y los ArrayList, hemos abusado del bucle for con un índice i (lista.get(i)). Pero... ¿qué pasa con los Sets que no tienen orden ni índice? ¿O con las LinkedList donde buscar por índice es lentísimo?

Aquí entra en juego el Iterator (Iterador). Es un objeto "chivato" que sabe exactamente dónde está el dedo puesto en la colección y cuál es el siguiente elemento. Es la forma universal, estandarizada y segura de recorrer CUALQUIER colección en Java, desde una lista simple hasta un árbol complejo.

¡Vamos a aprender a movernos por los datos como Pedro por su casa! 🚶‍♂️➡️


Conceptos Fundamentales

Definición: Iterator

Es una interfaz (java.util.Iterator) que permite recorrer una colección elemento por elemento. * Abstracción: No le importa si la colección es un Array, una Lista Enlazada o un Grafo. Él solo sabe ir "al siguiente". * Seguridad: Es la única forma segura de eliminar elementos de una colección mientras la estás recorriendo.

El problema del ConcurrentModificationException

Si intentas borrar un elemento de una lista usando un bucle for-each normal (for (String s : lista)), Java lanzará una excepción y tu programa crasheará. Esto pasa porque la colección detecta que ha cambiado de tamaño mientras la leías. Solución: Usar iterator.remove().

Mapa Mental: El Ciclo de Vida del Iterador



Desarrollo y Ejemplos Prácticos

Vamos a ver los métodos de la interfaz Iterator<E>. Son pocos, pero poderosos.

Los 4 Fantásticos (Métodos del Iterator)

  1. boolean hasNext(): Pregunta si hay más elementos. Es el semáforo. Si dice true, puedes pasar. Si dice false, frenas.
  2. E next(): Devuelve el siguiente elemento y avanza el puntero. Si lo llamas cuando no hay más, lanza NoSuchElementException.
  3. void remove(): Elimina de la colección el último elemento devuelto por next(). Ojo: Solo se puede llamar una vez por cada llamada a next().
  4. void forEachRemaining(Consumer action): Ejecuta una acción para todos los elementos restantes.

1. Iterator en ArrayList (El Caso Clásico)

Vamos a ver cómo borrar elementos de forma segura. Imaginad una lista de notas; queremos borrar los suspensos.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class ArrayListIterator {
    public static void main(String[] args) {
        List<Integer> notas = new ArrayList<>();
        notas.add(10);
        notas.add(3); // Suspenso
        notas.add(8);
        notas.add(4); // Suspenso

        System.out.println("Notas originales: " + notas);

        // 1. Obtener el iterador
        Iterator<Integer> it = notas.iterator();

        // 2. Recorrer con el patrón estándar while + hasNext
        while (it.hasNext()) {
            Integer nota = it.next(); // Obtenemos el dato y avanzamos

            // Lógica de borrado SEGURA
            if (nota < 5) {
                // notas.remove(nota); // ¡ERROR! Esto lanzaría ConcurrentModificationException
                it.remove(); // ¡CORRECTO! El iterador gestiona el borrado
                System.out.println("Nota " + nota + " eliminada.");
            }
        }

        System.out.println("Notas aprobadas: " + notas);
    }
}

2. Iterator en LinkedList (Eficiencia Pura)

En una LinkedList, usar get(i) es un crimen contra el rendimiento (tiene que recorrer la lista desde el principio cada vez). El iterador mantiene la posición, por lo que es O(1) avanzar.

import java.util.LinkedList;
import java.util.Iterator;

public class LinkedListIterator {
    public static void main(String[] args) {
        LinkedList<String> ciudades = new LinkedList<>();
        ciudades.add("Madrid");
        ciudades.add("Barcelona");
        ciudades.add("Valencia");

        Iterator<String> it = ciudades.iterator();

        System.out.println("--- Recorriendo LinkedList eficientemente ---");

        while (it.hasNext()) {
            String ciudad = it.next();
            System.out.println("Visitando: " + ciudad);
        }

        // Bonus: ListIterator permite ir hacia atrás (solo en Listas)
        // ListIterator<String> listIt = ciudades.listIterator();
    }
}

3. Iterator en Deque (Doble Dirección)

Los Deque (ArrayDeque) pueden recorrerse en orden normal o inverso.

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;

public class DequeIterator {
    public static void main(String[] args) {
        Deque<String> historial = new ArrayDeque<>();
        historial.push("Home");
        historial.push("Perfil");
        historial.push("Configuración"); // Top de la pila

        System.out.println("--- Iterador Normal (De arriba a abajo) ---");
        Iterator<String> it = historial.iterator();
        while(it.hasNext()) {
            System.out.println(it.next());
        }

        System.out.println("--- Iterador Descendente (De abajo a arriba) ---");
        Iterator<String> reverseIt = historial.descendingIterator();

        // Ejemplo de forEachRemaining (Java 8)
        reverseIt.forEachRemaining(pagina -> {
            System.out.println("Visto hace rato: " + pagina);
        });
    }
}

4. Iterator en Queue (Respetando la Cola)

Iterar una cola nos permite ver quién está esperando sin sacarlos (poll) de la fila.

import java.util.LinkedList;
import java.util.Queue;
import java.util.Iterator;

public class QueueIterator {
    public static void main(String[] args) {
        Queue<String> colaImpresion = new LinkedList<>();
        colaImpresion.offer("Doc1.pdf");
        colaImpresion.offer("Foto.jpg");
        colaImpresion.offer("Tesis.docx");

        Iterator<String> it = colaImpresion.iterator();

        System.out.println("Documentos en espera (sin imprimir aún):");
        int posicion = 1;
        while(it.hasNext()) {
            System.out.println(posicion + ". " + it.next());
            posicion++;
        }
        // La cola sigue intacta
        System.out.println("Tamaño actual: " + colaImpresion.size());
    }
}

5. Iterator en HashSet (El Caos)

Aquí el iterador es vital porque no existe el acceso por índice get(i). Recuerda: el orden es impredecible.

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class HashSetIterator {
    public static void main(String[] args) {
        Set<String> codigosUnicos = new HashSet<>();
        codigosUnicos.add("A100");
        codigosUnicos.add("Z999");
        codigosUnicos.add("B200");

        Iterator<String> it = codigosUnicos.iterator();

        System.out.println("Recorriendo Set (Orden aleatorio):");
        while(it.hasNext()) {
            String codigo = it.next();
            System.out.println("Código procesado: " + codigo);
            // Si intentáramos sacar un patrón de orden aquí, fallaríamos.
        }
    }
}

6. Iterator en TreeSet (Orden y Concierto)

Aquí el iterador recorrerá los elementos siguiendo el orden natural (o el del Comparador).

import java.util.TreeSet;
import java.util.Iterator;

public class TreeSetIterator {
    public static void main(String[] args) {
        TreeSet<Integer> precios = new TreeSet<>();
        precios.add(50);
        precios.add(10);
        precios.add(100);

        Iterator<Integer> it = precios.iterator();

        System.out.println("Precios de menor a mayor:");
        while(it.hasNext()) {
            int p = it.next();
            System.out.print(p + "€ ");
        }
        System.out.println();

        // TreeSet también tiene descendingIterator()
        System.out.println("Precios de mayor a menor:");
        Iterator<Integer> itDesc = precios.descendingIterator();
        itDesc.forEachRemaining(p -> System.out.print(p + "€ "));
    }
}

7. Iterator en TreeMap (Entradas, Claves y Valores)

Maps no son Iterables

Un Map no extiende de Iterable, por lo que no tiene método .iterator() directo. Tienes que iterar sobre sus Claves (keySet), sus Valores (values) o sus Entradas (entrySet).

import java.util.TreeMap;
import java.util.Iterator;
import java.util.Map;

public class TreeMapIterator {
    public static void main(String[] args) {
        // Ordenado por clave (ID)
        TreeMap<Integer, String> empleados = new TreeMap<>();
        empleados.put(103, "Ana");
        empleados.put(101, "Carlos"); // Saldrá primero
        empleados.put(102, "Beto");

        // OPCIÓN RECOMENDADA: Iterar EntrySet (Clave + Valor)
        // entrySet() devuelve un Set<Map.Entry>, que SÍ es iterable
        Iterator<Map.Entry<Integer, String>> it = empleados.entrySet().iterator();

        System.out.println("--- Listado de Empleados (Por ID) ---");
        while(it.hasNext()) {
            Map.Entry<Integer, String> ficha = it.next();

            Integer id = ficha.getKey();
            String nombre = ficha.getValue();

            System.out.println("ID: " + id + " | Nombre: " + nombre);

            // Ejemplo: Despedir al empleado 102 mientras recorremos
            if (id == 102) {
                it.remove(); // Borra la entrada del mapa de forma segura
                System.out.println("--> Empleado " + nombre + " despedido.");
            }
        }

        System.out.println("Plantilla restante: " + empleados);
    }
}



Diagrama de Flujo: La lógica interna del while

Así es como piensa el código cada vez que hacéis un bucle con iterador.

graph TD
    Start((Inicio)) --> Check["it.hasNext()?"]

    Check -- true (Hay dato) --> Get["it.next()"]
    Get --> Process["Procesar Dato"]
    Process --> Logic["¿Borrar?"]

    Logic -- Sí --> Remove["it.remove()"]
    Remove --> Check

    Logic -- No --> Check

    Check -- false (Fin) --> End((Terminar))

    style Start fill:#f9f,stroke:#333
    style End fill:#f9f,stroke:#333
    style Remove fill:#ff9999,stroke:#333

Tabla Comparativa: Bucle For vs Iterator

Característica Bucle for-i Bucle for-each Iterator
Sintaxis Compleja list.get(i) Simple for(O o : c) Verbosa while(it.hasNext())
Acceso Por índice (Random Access) Secuencial Secuencial
Borrar Elementos ⚠️ Peligroso (desplaza índices) PROHIBIDO (Exception) SEGURO (it.remove())
Compatible con Solo Lists y Arrays Todas las Colecciones Todas las Colecciones

Ejercicios Reflexivos

  1. El cursor invisible: Cuando llamas a it.next(), el iterador te devuelve el elemento y "salta" al hueco entre ese elemento y el siguiente. Si llamas a it.remove(), ¿qué elemento borra? ¿El que acabas de saltar o el siguiente que vas a leer? (Respuesta: El que acabas de devolver con next()).
  2. Doble llamada: ¿Qué pasa si dentro del while llamas dos veces seguidas a it.next()? (Ej: System.out.println(it.next() + it.next())). ¿Es seguro?
  3. Legacy Code: En Java antiguo (antes de la versión 5), no existía el for-each. Todo se hacía con Iteradores. Si ves un código viejo, ¿sabrías refactorizarlo a un for-each moderno? (Siempre que no borre elementos, claro).

Aplicación en el Mundo Real

  1. Cursores de Base de Datos (JDBC): Cuando haces una consulta SQL (SELECT * FROM usuarios), Java te devuelve un ResultSet. Un ResultSet funciona exactamente igual que un iterador: tiene un método .next() que mueve el cursor fila por fila. Esto permite leer tablas de 1 millón de filas sin cargar el millón en la memoria RAM a la vez.
  2. Lectura de Ficheros Grandes: Al leer un archivo de texto gigante (logs del servidor), usamos Scanner o Stream. Ambos usan el patrón iterador para leer línea a línea en lugar de cargar todo el fichero de golpe.
  3. Motores de Videojuegos: Imagina una lista de enemigos en pantalla. En cada frame, tienes que moverlos. Usas un iterador. Si un enemigo muere (HP <= 0), usas it.remove() para sacarlo de la lista de renderizado sin romper el bucle del juego.

Para Saber Más