Patrón Iterator en Java: recorrer colecciones con elegancia

📅 Actualizado en febrero 2026 ✍️ Ángel López ⏱️ 16 min de lectura ✓ Nivel intermedio ★ ★ ★ ★ ★ (5/5)

🔄 ¿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().
ConcreteIteratorClase 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.
ConcreteAggregateArrayList, 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

Java
import 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 personalizado
import 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 ListIterator
import 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 Stream
import 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-fastLanza ConcurrentModificationException si la colección se modifica estructuralmente durante la iteración (excepto mediante Iterator.remove()).ArrayList, HashMap, HashSet, LinkedList, TreeMap, TreeSet
Fail-safeTrabaja sobre una copia o usa mecanismos internos que toleran modificaciones concurrentes. No lanza excepciones.CopyOnWriteArrayList, ConcurrentHashMap, ConcurrentSkipListSet
Java — Fail-fast en acción
import 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

Java
Iterator<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.

El patrón Iterator es un patrón de diseño de comportamiento (GoF) que proporciona una forma de acceder secuencialmente a los elementos de una colección sin exponer su estructura interna. En Java está integrado directamente en el lenguaje mediante las interfaces java.util.Iterator y java.lang.Iterable.
Iterable es una interfaz que indica que un objeto puede ser recorrido y tiene el método iterator() que devuelve un Iterator. Iterator es la interfaz que realiza el recorrido con los métodos hasNext(), next() y remove(). Una clase implementa Iterable para que se pueda usar con for-each; Iterator es el mecanismo de recorrido en sí.
Un iterador fail-fast es aquel que lanza ConcurrentModificationException si la colección subyacente es modificada estructuralmente después de crear el iterador (por ejemplo, añadiendo o eliminando elementos). Los iteradores de ArrayList, HashMap y la mayoría de colecciones del paquete java.util son fail-fast.
Se debe crear un Iterator personalizado cuando se tiene una estructura de datos propia (un árbol, un grafo, un rango numérico) que no hereda de las colecciones estándar de Java. La clase debe implementar Iterable y devolver una instancia de Iterator que sepa recorrer la estructura interna.
Los Streams de Java 8+ utilizan internamente Spliterator, una evolución del Iterator diseñada para soportar tanto iteración secuencial como paralelismo. Cualquier objeto Iterable puede convertirse en Stream llamando a StreamSupport.stream(spliterator(), false), lo que conecta el patrón Iterator clásico con la programación funcional moderna.
Absolutamente. El patrón Iterator es uno de los más utilizados en programación moderna. En Java está integrado en el propio lenguaje (for-each, Iterable, Iterator, Streams), y su principio de separar el recorrido de la estructura se aplica en prácticamente todos los lenguajes de programación actuales.
Valora este artículo

💬 Foro de discusión

¿Tienes dudas sobre Patrón Iterator en Java: recorrer colecciones con elegancia? Comparte tu pregunta con la comunidad.

¿Tienes cuenta? o comenta como invitado ↓

Todavía no hay mensajes. ¡Sé el primero en participar!