🔄 ¿Qué es el patrón Iterator?
El patrón Iterator (Iterador) es un patrón de diseño de comportamiento catalogado por la Gang of Four (GoF) en su obra clásica Design Patterns (1994). Su propósito es proporcionar un mecanismo para acceder secuencialmente a los elementos de una colección sin exponer su representación interna. En otras palabras, el Iterator permite recorrer una estructura de datos — sea una lista, un árbol, un conjunto o cualquier agregado — de forma uniforme e independiente de cómo esté organizada internamente.
Lo que hace especialmente relevante a este patrón es que Java lo adoptó como parte fundamental del lenguaje. Las interfaces java.util.Iterator y java.lang.Iterable, el bucle for-each introducido en Java 5, y el sistema de Streams de Java 8 son todas manifestaciones directas del patrón Iterator. Comprender este patrón no es solo un ejercicio académico: es entender uno de los pilares del ecosistema Java.
💡 Clasificación GoF: Iterator es un patrón de comportamiento (behavioral). Se sitúa junto a otros patrones como Observer, Strategy y Command. Su objetivo es definir cómo los objetos interactúan y se comunican entre sí.
🎯 El problema que resuelve el Iterator
Imaginemos que tenemos una aplicación de gestión de empleados. Los empleados pueden almacenarse internamente en un ArrayList, en un LinkedList, en un TreeSet ordenado por nombre, o incluso en una estructura personalizada como un árbol jerárquico por departamentos. Sin el patrón Iterator, el código que necesita recorrer estos empleados tendría que conocer la estructura interna de cada contenedor:
Java — ❌ Sin Iterator: código acoplado a la estructura// Si es un ArrayList, recorremos por índice for (int i = 0; i < listaEmpleados.size(); i++) { Empleado emp = listaEmpleados.get(i); System.out.println(emp.getNombre()); } // Si cambiamos a un LinkedList, get(i) es O(n) → rendimiento terrible // Si cambiamos a un TreeSet, no existe get(i) → ¡el código ni compila! // Si cambiamos a un árbol jerárquico, necesitamos recursión → reescribir todo
El patrón Iterator resuelve este problema desacoplando el mecanismo de recorrido de la estructura de datos. El código cliente no necesita saber si los datos están en un array, una lista enlazada o un árbol: solo necesita un Iterator que exponga los métodos hasNext() y next().
Java — ✅ Con Iterator: código desacoplado// Este código funciona igual con ArrayList, LinkedList, TreeSet, // o cualquier colección que implemente Iterable Iterator<Empleado> it = coleccionEmpleados.iterator(); while (it.hasNext()) { Empleado emp = it.next(); System.out.println(emp.getNombre()); } // O con for-each (usa Iterator internamente): for (Empleado emp : coleccionEmpleados) { System.out.println(emp.getNombre()); }
📐 Estructura GoF del patrón Iterator
La estructura formal del patrón Iterator según la GoF involucra cuatro participantes que colaboran entre sí. Comprender estos roles es clave para implementar iteradores personalizados cuando las colecciones estándar no son suficientes.
| Participante | En Java | Responsabilidad |
|---|---|---|
| Iterator (interfaz) | java.util.Iterator<E> | Define los métodos para recorrer la colección: hasNext(), next(), remove(). |
| ConcreteIterator | Clase interna de ArrayList, HashSet, etc. | Implementa Iterator para una estructura concreta. Mantiene la posición actual del recorrido. |
| Aggregate (interfaz) | java.lang.Iterable<E> | Declara el método iterator() que crea y devuelve un Iterator. |
| ConcreteAggregate | ArrayList, HashSet, TreeMap, etc. | Implementa iterator() devolviendo su ConcreteIterator específico. |
El principio fundamental es que el Aggregate sabe cómo crear el Iterator adecuado para su estructura interna, pero el cliente solo interactúa con las interfaces Iterator e Iterable. Si cambiamos la implementación del agregado (por ejemplo, de ArrayList a TreeSet), el código cliente no necesita ningún cambio.
🔧 La interfaz java.util.Iterator
La interfaz java.util.Iterator<E> define el contrato mínimo que todo iterador debe cumplir. Desde Java 8 incluye también un método default adicional que facilita la programación funcional.
Java — Métodos de la interfaz Iterator<E>public interface Iterator<E> { // Devuelve true si quedan elementos por recorrer boolean hasNext(); // Devuelve el siguiente elemento y avanza el cursor E next(); // Lanza NoSuchElementException si no hay más elementos // Elimina el último elemento devuelto por next() [opcional] default void remove() { throw new UnsupportedOperationException("remove"); } // [Java 8+] Ejecuta una acción para cada elemento restante default void forEachRemaining(Consumer<? super E> accion) { Objects.requireNonNull(accion); while (hasNext()) { accion.accept(next()); } } }
Uso práctico con colecciones estándar
Javaimport java.util.*; public class EjemploIterator { public static void main(String[] args) { List<String> ciudades = new ArrayList<>( Arrays.asList("Madrid", "Barcelona", "Sevilla", "Valencia", "Bilbao")); // Recorrido con Iterator explícito Iterator<String> it = ciudades.iterator(); while (it.hasNext()) { String ciudad = it.next(); System.out.println(ciudad); // remove() es seguro dentro del bucle del Iterator if (ciudad.equals("Sevilla")) { it.remove(); // Elimina "Sevilla" de la lista } } System.out.println("Después de eliminar: " + ciudades); // Salida: [Madrid, Barcelona, Valencia, Bilbao] // [Java 8+] forEachRemaining con expresión lambda Iterator<String> it2 = ciudades.iterator(); it2.forEachRemaining(c -> System.out.println("Ciudad: " + c)); } }
⚠️ Precaución: Una vez que se llama a forEachRemaining(), el iterador queda agotado. Llamar a hasNext() después devolverá false. Para un nuevo recorrido, se debe obtener un iterador fresco con iterator().
♻️ La interfaz Iterable y el bucle for-each
La interfaz java.lang.Iterable<T>, introducida en Java 5, tiene un único método abstracto: iterator(). Cualquier clase que implemente Iterable puede usarse directamente en un bucle for-each (también llamado enhanced for), que es la forma más idiomática y segura de recorrer colecciones en Java.
Java — La interfaz Iterable<T>public interface Iterable<T> { // Crea y devuelve un Iterator para esta colección Iterator<T> iterator(); // [Java 8+] Ejecuta una acción para cada elemento default void forEach(Consumer<? super T> accion) { for (T elemento : this) { accion.accept(elemento); } } // [Java 8+] Crea un Spliterator para paralelismo default Spliterator<T> spliterator() { return Spliterators.spliteratorUnknownSize(iterator(), 0); } }
El for-each por debajo
El compilador Java transforma automáticamente el bucle for-each en un bucle con Iterator. Esto significa que ambas formas son exactamente equivalentes a nivel de bytecode:
Java — Equivalencia for-each ↔ Iterator// Lo que escribimos: for (String ciudad : ciudades) { System.out.println(ciudad); } // Lo que el compilador genera internamente: Iterator<String> it = ciudades.iterator(); while (it.hasNext()) { String ciudad = it.next(); System.out.println(ciudad); } // Ambas versiones producen exactamente el mismo bytecode
✅ Regla de oro: Se debe usar for-each siempre que sea posible. Solo se recurre al Iterator explícito cuando se necesita eliminar elementos durante el recorrido (método remove()), ya que el for-each no expone el iterador y no permite llamar a remove().
🛠️ Implementación de un Iterator personalizado
La verdadera potencia del patrón Iterator se manifiesta cuando creamos nuestras propias colecciones. Al implementar Iterable, nuestra clase se integra con todo el ecosistema Java: for-each, Streams, el Collections Framework y cualquier API que trabaje con iteradores.
Veamos un ejemplo completo: una clase Rango que representa un rango de números enteros y permite iterarlos sin almacenarlos todos en memoria.
Java — Rango.java: Iterator personalizadoimport java.util.Iterator; import java.util.NoSuchElementException; /** * Representa un rango de enteros [inicio, fin) con un paso configurable. * Implementa Iterable para integrarse con for-each y Streams. */ public class Rango implements Iterable<Integer> { private final int inicio; private final int fin; private final int paso; public Rango(int inicio, int fin) { this(inicio, fin, 1); } public Rango(int inicio, int fin, int paso) { if (paso == 0) throw new IllegalArgumentException("El paso no puede ser cero"); this.inicio = inicio; this.fin = fin; this.paso = paso; } @Override public Iterator<Integer> iterator() { return new RangoIterator(); } // Clase interna que implementa la lógica de recorrido private class RangoIterator implements Iterator<Integer> { private int actual = inicio; @Override public boolean hasNext() { return (paso > 0) ? actual < fin : actual > fin; } @Override public Integer next() { if (!hasNext()) { throw new NoSuchElementException("No hay más elementos en el rango"); } int valor = actual; actual += paso; return valor; } } // Método de conveniencia public int tamaño() { return Math.max(0, (int) Math.ceil((double)(fin - inicio) / paso)); } } // Uso: public class PruebaRango { public static void main(String[] args) { // Rango simple: 1, 2, 3, 4, 5 for (int n : new Rango(1, 6)) { System.out.print(n + " "); } // Salida: 1 2 3 4 5 // Rango con paso: 0, 5, 10, 15, 20 for (int n : new Rango(0, 25, 5)) { System.out.print(n + " "); } // Salida: 0 5 10 15 20 // Compatible con Streams (Java 8+) int suma = StreamSupport.stream(new Rango(1, 101).spliterator(), false) .mapToInt(Integer::intValue) .sum(); System.out.println("Suma 1..100: " + suma); // 5050 } }
💡 Ventaja clave: La clase Rango no almacena todos los números en memoria — los genera uno a uno bajo demanda a través del iterador. Un new Rango(1, 1_000_000_000) ocuparía solo unos pocos bytes, frente a los gigabytes que necesitaría un ArrayList con mil millones de enteros.
↔️ ListIterator: iteración bidireccional
Las listas (List) en Java ofrecen una versión ampliada del iterador llamada ListIterator<E>, que extiende Iterator añadiendo la capacidad de recorrer la lista en ambas direcciones y de modificar elementos durante el recorrido.
| Método | Descripción |
|---|---|
hasPrevious() | Devuelve true si hay un elemento anterior. |
previous() | Devuelve el elemento anterior y retrocede el cursor. |
nextIndex() | Devuelve el índice del siguiente elemento. |
previousIndex() | Devuelve el índice del elemento anterior. |
set(E e) | Reemplaza el último elemento devuelto por next() o previous(). |
add(E e) | Inserta un elemento en la posición actual del cursor. |
Java — Ejemplo de ListIteratorimport java.util.*; public class EjemploListIterator { public static void main(String[] args) { List<String> idiomas = new ArrayList<>( Arrays.asList("Java", "Python", "C++", "JavaScript")); // Obtener ListIterator empezando desde el final ListIterator<String> it = idiomas.listIterator(idiomas.size()); // Recorrer en orden inverso System.out.println("Recorrido inverso:"); while (it.hasPrevious()) { int indice = it.previousIndex(); String idioma = it.previous(); System.out.println(" [" + indice + "] " + idioma); } // Salida: [3] JavaScript, [2] C++, [1] Python, [0] Java // Recorrer hacia adelante y modificar while (it.hasNext()) { String idioma = it.next(); if (idioma.equals("C++")) { it.set("C++ (moderno)"); // Reemplazar } if (idioma.equals("Python")) { it.add("Kotlin"); // Insertar después de Python } } System.out.println("Lista modificada: " + idiomas); // Salida: [Java, Python, Kotlin, C++ (moderno), JavaScript] } }
⚡ Spliterator y la conexión con Streams
Java 8 introdujo Spliterator (Splittable Iterator), una evolución del patrón Iterator diseñada para soportar tanto la iteración secuencial como la procesamiento paralelo. Es la pieza que conecta el patrón Iterator clásico con el sistema de Streams.
A diferencia de Iterator, que solo puede avanzar elemento a elemento, un Spliterator puede dividirse (split) en dos partes para que cada una sea procesada por un hilo diferente. Esto es lo que permite que parallelStream() funcione.
Java — De Iterable a Streamimport java.util.stream.StreamSupport; // Cualquier Iterable puede convertirse en Stream Rango rango = new Rango(1, 1001); // Stream secuencial long pares = StreamSupport.stream(rango.spliterator(), false) .filter(n -> n % 2 == 0) .count(); System.out.println("Números pares del 1 al 1000: " + pares); // 500 // Stream paralelo (segundo parámetro = true) long suma = StreamSupport.stream(rango.spliterator(), true) .mapToLong(Integer::longValue) .sum(); System.out.println("Suma paralela: " + suma); // 500500 // Las colecciones estándar lo hacen más fácilmente: List<String> nombres = List.of("Ana", "Carlos", "Beatriz", "Diana"); nombres.stream() .sorted() .forEach(System.out::println); // Salida: Ana, Beatriz, Carlos, Diana
✅ Evolución del patrón: El camino evolutivo del Iterator en Java ha sido: Enumeration (Java 1.0) → Iterator (Java 2) → Iterable + for-each (Java 5) → Spliterator + Streams (Java 8). Cada paso añadió funcionalidad manteniendo la compatibilidad con los anteriores.
🛡️ Iteradores fail-fast vs fail-safe
Un concepto crucial al trabajar con iteradores en Java es la diferencia entre iteradores fail-fast y fail-safe. Esta distinción determina qué sucede cuando la colección se modifica mientras está siendo recorrida.
| Tipo | Comportamiento | Colecciones |
|---|---|---|
| Fail-fast | Lanza ConcurrentModificationException si la colección se modifica estructuralmente durante la iteración (excepto mediante Iterator.remove()). | ArrayList, HashMap, HashSet, LinkedList, TreeMap, TreeSet |
| Fail-safe | Trabaja sobre una copia o usa mecanismos internos que toleran modificaciones concurrentes. No lanza excepciones. | CopyOnWriteArrayList, ConcurrentHashMap, ConcurrentSkipListSet |
Java — Fail-fast en acciónimport java.util.*; public class EjemploFailFast { public static void main(String[] args) { List<String> frutas = new ArrayList<>( Arrays.asList("Manzana", "Naranja", "Plátano", "Uva")); // ❌ INCORRECTO: modificar la lista durante for-each try { for (String fruta : frutas) { if (fruta.equals("Naranja")) { frutas.remove(fruta); // ¡ConcurrentModificationException! } } } catch (ConcurrentModificationException e) { System.out.println("Error: " + e.getClass().getSimpleName()); } // ✅ CORRECTO: usar Iterator.remove() Iterator<String> it = frutas.iterator(); while (it.hasNext()) { if (it.next().equals("Naranja")) { it.remove(); // Seguro: el iterador gestiona la eliminación } } System.out.println("Lista limpia: " + frutas); // Salida: [Manzana, Plátano, Uva] // ✅ ALTERNATIVA [Java 8+]: removeIf (usa Iterator internamente) frutas.removeIf(f -> f.equals("Plátano")); System.out.println("Tras removeIf: " + frutas); // Salida: [Manzana, Uva] } }
✅ Buenas prácticas al usar iteradores
Después de años de evolución de Java, la comunidad ha establecido un conjunto de buenas prácticas claras para trabajar con iteradores. Seguirlas permite escribir código más seguro, legible y mantenible.
| ✅ Hacer | ❌ Evitar |
|---|---|
Usar for-each como opción predeterminada. | Usar for con índice en colecciones (lento en LinkedList). |
Usar Iterator.remove() para eliminar durante recorrido. | Llamar a coleccion.remove() dentro de un for-each. |
Implementar Iterable en colecciones propias. | Exponer la estructura interna con getters públicos. |
Usar removeIf() para filtrado con eliminación (Java 8+). | Construir una lista de «elementos a eliminar» y borrar después. |
| Usar Streams para transformaciones complejas. | Anidar bucles con iteradores para operaciones funcionales. |
| Documentar el orden de iteración de colecciones propias. | Asumir orden de iteración en HashSet o HashMap. |
🧩 Ejemplo integrador: catálogo de productos iterable
Este ejemplo integrador muestra una clase Catalogo que almacena productos internamente en un Map pero expone un iterador que los recorre ordenados por precio ascendente. Demuestra cómo el patrón Iterator desacopla la organización interna de la forma en que se recorren los elementos.
Java — Sistema de catálogo iterable (ejemplo integrador)import java.util.*; /** * Producto con nombre, categoría y precio. */ class Producto implements Comparable<Producto> { private final String nombre; private final String categoria; private final double precio; public Producto(String nombre, String categoria, double precio) { this.nombre = nombre; this.categoria = categoria; this.precio = precio; } public String getNombre() { return nombre; } public String getCategoria() { return categoria; } public double getPrecio() { return precio; } @Override public int compareTo(Producto otro) { return Double.compare(this.precio, otro.precio); } @Override public String toString() { return String.format("%s (%.2f€) [%s]", nombre, precio, categoria); } } /** * Catálogo de productos que almacena por código (Map) pero * itera ordenado por precio (Iterator personalizado). */ class Catalogo implements Iterable<Producto> { // Estructura interna: HashMap (búsqueda rápida por código) private final Map<String, Producto> productos = new HashMap<>(); public void agregar(String codigo, Producto producto) { productos.put(codigo, producto); } public Producto buscarPorCodigo(String codigo) { return productos.get(codigo); } public int tamaño() { return productos.size(); } // El iterador recorre los productos ordenados por precio @Override public Iterator<Producto> iterator() { List<Producto> ordenados = new ArrayList<>(productos.values()); Collections.sort(ordenados); // Ordena por precio (Comparable) return ordenados.iterator(); } // Iterador filtrado por categoría public Iterable<Producto> porCategoria(String categoria) { return () -> productos.values().stream() .filter(p -> p.getCategoria().equalsIgnoreCase(categoria)) .sorted() .iterator(); } } // Uso del catálogo public class PruebaCatalogo { public static void main(String[] args) { Catalogo catalogo = new Catalogo(); catalogo.agregar("LAP001", new Producto("Portátil Gamer", "Electrónica", 1299.99)); catalogo.agregar("LIB001", new Producto("Clean Code", "Libros", 35.50)); catalogo.agregar("AUR001", new Producto("Auriculares BT", "Electrónica", 79.90)); catalogo.agregar("LIB002", new Producto("Design Patterns", "Libros", 42.00)); catalogo.agregar("TEC001", new Producto("Teclado mecánico", "Periféricos", 89.95)); // for-each: recorre ordenado por precio (¡sin saber que internamente es un HashMap!) System.out.println("=== Catálogo completo (por precio) ==="); for (Producto p : catalogo) { System.out.println(" " + p); } // Salida: // Clean Code (35,50€) [Libros] // Design Patterns (42,00€) [Libros] // Auriculares BT (79,90€) [Electrónica] // Teclado mecánico (89,95€) [Periféricos] // Portátil Gamer (1299,99€) [Electrónica] // Iterador filtrado por categoría System.out.println("\n=== Solo electrónica ==="); for (Producto p : catalogo.porCategoria("Electrónica")) { System.out.println(" " + p); } // Compatible con Streams double total = StreamSupport.stream(catalogo.spliterator(), false) .mapToDouble(Producto::getPrecio) .sum(); System.out.printf("\nValor total del catálogo: %.2f€%n", total); } }
❌ Errores frecuentes con iteradores
Error 1: Modificar la colección durante for-each
Java// ❌ INCORRECTO: lanza ConcurrentModificationException for (String item : lista) { if (item.startsWith("X")) { lista.remove(item); // Modificación estructural durante iteración } } // ✅ CORRECTO opción A: Iterator explícito Iterator<String> it = lista.iterator(); while (it.hasNext()) { if (it.next().startsWith("X")) { it.remove(); } } // ✅ CORRECTO opción B: removeIf (Java 8+) lista.removeIf(item -> item.startsWith("X"));
Error 2: Llamar a next() sin verificar hasNext()
Java// ❌ INCORRECTO: NoSuchElementException si la lista está vacía Iterator<String> it = lista.iterator(); String primero = it.next(); // ¡Puede explotar! // ✅ CORRECTO: siempre verificar antes Iterator<String> it = lista.iterator(); if (it.hasNext()) { String primero = it.next(); }
Error 3: Reutilizar un iterador agotado
JavaIterator<String> it = lista.iterator(); while (it.hasNext()) { it.next(); } // Primer recorrido completo // ❌ INCORRECTO: el iterador ya está agotado, este bucle no ejecuta nada while (it.hasNext()) { System.out.println(it.next()); } // ✅ CORRECTO: obtener un nuevo iterador Iterator<String> it2 = lista.iterator(); // Fresco while (it2.hasNext()) { System.out.println(it2.next()); }
📝 Ejercicios prácticos resueltos
Ejercicio 1: Iterador de números Fibonacci
Crea una clase Fibonacci que implemente Iterable<Long> y genere los primeros N números de la secuencia de Fibonacci bajo demanda (sin almacenarlos todos). Debe poder usarse con for-each.
Ejercicio 2: Filtrar y eliminar con Iterator
Dada una lista de cadenas, utiliza un Iterator explícito para eliminar todas las cadenas que tengan menos de 4 caracteres. No utilices removeIf() ni Streams; el objetivo es practicar el uso directo de Iterator.remove().
Ejercicio 3: Colección circular iterable
Implementa una clase BufferCircular<E> que almacene hasta N elementos y, cuando se llene, sobrescriba los más antiguos. Debe implementar Iterable<E> para poder recorrerse con for-each en el orden de inserción (del más antiguo al más reciente).
❓ Preguntas frecuentes sobre Patrón Iterator en Java: recorrer colecciones con elegancia
Las dudas más comunes respondidas de forma clara y directa.
💬 Foro de discusión
¿Tienes dudas sobre Patrón Iterator en Java: recorrer colecciones con elegancia? Comparte tu pregunta con la comunidad.
Todavía no hay mensajes. ¡Sé el primero en participar!