El patrón Iterator proporciona una forma uniforme de recorrer los elementos de una colección sin exponer su representación interna. Permite encapsular algoritmos de recorrido y ofrecer distintas vistas sobre estructuras complejas manteniendo un contrato sencillo para el cliente.
Es fundamental en bibliotecas de colecciones, motores de bases de datos en memoria y APIs que necesitan recorrer datos heterogéneos de manera consistente, fomentando el principio de responsabilidad única al separar la estructura del recorrido.
Las colecciones personalizadas suelen almacenar datos en estructuras complejas (listas enlazadas, grafos, tablas dispersas). Permitir que el cliente acceda directamente a los detalles internos viola el encapsulamiento y dificulta cambiar la implementación. Sin Iterator, cada consumidor debería conocer la estructura concreta para recorrer los elementos, generando acoplamiento fuerte y código duplicado.
El patrón resuelve el problema introduciendo un objeto iterador que mantiene el estado del recorrido. El cliente pide el siguiente elemento sin importar cómo esté organizada la colección subyacente, y la colección controla cuáles iteradores ofrece.
La intención es proporcionar una interfaz estándar para acceder secuencialmente a los elementos de un agregado sin exponer su representación. Es pertinente cuando:
El patrón motiva a separar la responsabilidad de almacenar datos de la responsabilidad de recorrerlos, habilitando colecciones versátiles y fáciles de evolucionar.
Los elementos principales son:
hasNext()
y next()
.La estructura puede ampliarse con iteradores paralelos, iteradores seguros para concurrencia o iteradores que proyectan los datos a otro dominio.
Existen dos enfoques clásicos:
next()
. Facilita integrar el recorrido en bucles personalizados.Iterator se asocia principalmente al enfoque externo, aunque muchas bibliotecas combinan ambos estilos para ofrecer flexibilidad.
Un marketplace desea ofrecer un recorrido personalizado sobre su catálogo: según el perfil del cliente se necesitan filtros por categoría, disponibilidad e historial de compras. Además, se requiere exportar informes que recorran el catálogo en otro orden sin duplicar lógica.
Iterator permite encapsular estrategias de recorrido dentro de iteradores especializados. El catálogo expone métodos para crear iteradores filtrados o estadísticos; clientes distintos consumen la colección sin conocer cómo se almacenan los productos.
El siguiente ejemplo ilustra un catálogo que ofrece iteradores filtrados y la posibilidad de congelar la vista para recorridos consistentes:
package tutorial.iterator;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
public interface FiltroProducto {
boolean aceptar(Producto producto);
}
class Producto {
private final String sku;
private final String categoria;
private final double precio;
private final boolean disponible;
Producto(String sku, String categoria, double precio, boolean disponible) {
this.sku = sku;
this.categoria = categoria;
this.precio = precio;
this.disponible = disponible;
}
public String getSku() {
return sku;
}
public String getCategoria() {
return categoria;
}
public double getPrecio() {
return precio;
}
public boolean isDisponible() {
return disponible;
}
@Override
public String toString() {
return "Producto{" +
"sku='" + sku + '\'' +
", categoria='" + categoria + '\'' +
", precio=" + precio +
", disponible=" + disponible +
'}';
}
}
class CatalogoProductos implements Iterable<Producto> {
private final List<Producto> productos = new ArrayList<>();
public void agregar(Producto producto) {
productos.add(Objects.requireNonNull(producto));
}
@Override
public Iterator<Producto> iterator() {
return snapshot(productos).iterator();
}
public Iterator<Producto> iteradorFiltrado(FiltroProducto filtro) {
return new IteradorFiltrado(snapshot(productos), filtro);
}
private List<Producto> snapshot(List<Producto> origen) {
return Collections.unmodifiableList(new ArrayList<>(origen));
}
}
class IteradorFiltrado implements Iterator<Producto> {
private final List<Producto> productos;
private final FiltroProducto filtro;
private int indiceActual = 0;
private Producto siguienteValido;
IteradorFiltrado(List<Producto> productos, FiltroProducto filtro) {
this.productos = productos;
this.filtro = filtro;
avanzar();
}
private void avanzar() {
siguienteValido = null;
while (indiceActual < productos.size()) {
Producto candidato = productos.get(indiceActual++);
if (filtro.aceptar(candidato)) {
siguienteValido = candidato;
break;
}
}
}
@Override
public boolean hasNext() {
return siguienteValido != null;
}
@Override
public Producto next() {
if (siguienteValido == null) {
throw new java.util.NoSuchElementException("No hay más productos");
}
Producto actual = siguienteValido;
avanzar();
return actual;
}
@Override
public void remove() {
throw new UnsupportedOperationException("Iterador de solo lectura");
}
}
class RecomendadorProductos {
private final CatalogoProductos catalogo;
RecomendadorProductos(CatalogoProductos catalogo) {
this.catalogo = catalogo;
}
public void mostrarSugerencias(FiltroProducto filtro) {
Iterator<Producto> iterator = catalogo.iteradorFiltrado(filtro);
System.out.println("Sugerencias calculadas en " + Instant.now());
while (iterator.hasNext()) {
System.out.println(" - " + iterator.next());
}
}
}
class AplicacionIterator {
public static void main(String[] args) {
CatalogoProductos catalogo = new CatalogoProductos();
catalogo.agregar(new Producto("SKU-100", "libros", 19.99, true));
catalogo.agregar(new Producto("SKU-200", "hogar", 89.50, true));
catalogo.agregar(new Producto("SKU-300", "libros", 24.90, false));
catalogo.agregar(new Producto("SKU-400", "tecnologia", 399.00, true));
RecomendadorProductos recomendador = new RecomendadorProductos(catalogo);
recomendador.mostrarSugerencias(producto ->
"libros".equals(producto.getCategoria()) && producto.isDisponible());
}
}
El catálogo conserva su representación interna privada y genera una copia inmutable para cada iterador, evitando efectos colaterales durante el recorrido. IteradorFiltrado
calcula el siguiente elemento válido perezosamente, lo que permite combinar filtros sin crear colecciones intermedias. El recomendador consume el iterador sin conocer la estructura interna.
La documentación oficial de java.util.Iterator describe el contrato utilizado por las colecciones del JDK, incluyendo operaciones opcionales como remove()
. Interfaces como Iterable
, Spliterator
y las secuencias de streams modernizan el patrón, permitiendo procesamientos secuenciales o paralelos de forma fluida.
Iterator admite variantes según el dominio:
Un riesgo frecuente es permitir que las modificaciones estructurales de la colección invalide iteradores activos sin notificación, provocando excepciones en tiempo de ejecución. También se debe evitar que los iteradores expongan detalles internos (referencias mutables) que los clientes podrían usar para alterar la colección sin pasar por las reglas de negocio.
Otra mala práctica es abusar de iteradores que realizan trabajo pesado en next()
, degradando el rendimiento percibido y dificultando el control del tiempo de respuesta.
Iterator colabora con Composite para recorrer jerarquías de objetos sin exponer su estructura. Puede combinarse con Factory Method al crear iteradores especializados y con Strategy cuando el criterio de recorrido se selecciona dinámicamente. Además, suele trabajar junto a Memento para guardar la posición del recorrido y reanudarlo más tarde.