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)
boolean hasNext(): Pregunta si hay más elementos. Es el semáforo. Si dicetrue, puedes pasar. Si dicefalse, frenas.E next(): Devuelve el siguiente elemento y avanza el puntero. Si lo llamas cuando no hay más, lanzaNoSuchElementException.void remove(): Elimina de la colección el último elemento devuelto pornext(). Ojo: Solo se puede llamar una vez por cada llamada anext().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
- 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 ait.remove(), ¿qué elemento borra? ¿El que acabas de saltar o el siguiente que vas a leer? (Respuesta: El que acabas de devolver connext()). - Doble llamada: ¿Qué pasa si dentro del
whilellamas dos veces seguidas ait.next()? (Ej:System.out.println(it.next() + it.next())). ¿Es seguro? - 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 unfor-eachmoderno? (Siempre que no borre elementos, claro).
Aplicación en el Mundo Real
- Cursores de Base de Datos (JDBC):
Cuando haces una consulta SQL (
SELECT * FROM usuarios), Java te devuelve unResultSet. UnResultSetfunciona 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. - Lectura de Ficheros Grandes:
Al leer un archivo de texto gigante (logs del servidor), usamos
ScanneroStream. Ambos usan el patrón iterador para leer línea a línea en lugar de cargar todo el fichero de golpe. - 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
-
Documentación Oficial de Iterator: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Iterator.html
-
GeeksForGeeks - Iterators in Java: Fuente original, buenos diagramas. https://www.geeksforgeeks.org/java/iterators-in-java/
-
Baeldung - Java Iterator vs Iterable: Diferencias técnicas importantes. https://www.baeldung.com/java-iterator-vs-iterable