22. Iterator (Iterador) - Patrón de Comportamiento

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.

22.1 Problema y contexto

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.

22.2 Intención y motivación

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:

  • Se desea soportar múltiples recorridos sobre la misma estructura (orden natural, orden filtrado, orden inverso).
  • La colección debe proteger su invariantes evitando que el cliente manipule directamente las referencias internas.
  • Se requiere que varios clientes recorran la misma colección en paralelo sin bloquearse mutuamente.
  • El diseño apunta a extender nuevos tipos de recorrido sin modificar el código cliente.

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.

22.3 Participantes y estructura

Los elementos principales son:

  • Iterator: interfaz que declara operaciones como hasNext() y next().
  • ConcreteIterator: implementaciones concretas que almacenan el estado del recorrido.
  • Aggregate: interfaz que declara un método para crear iteradores.
  • ConcreteAggregate: colecciones reales que devuelven iteradores específicos.
  • Cliente: utiliza el iterador sin conocer los detalles internos del agregado.

La estructura puede ampliarse con iteradores paralelos, iteradores seguros para concurrencia o iteradores que proyectan los datos a otro dominio.

22.4 Iteradores externos vs. internos

Existen dos enfoques clásicos:

  • Iterador externo: el cliente controla el avance llamando a next(). Facilita integrar el recorrido en bucles personalizados.
  • Iterador interno: la colección recibe una función y la ejecuta por cada elemento. Simplifica ciertos recorridos pero limita el control del cliente (por ejemplo, detenerse en medio).

Iterator se asocia principalmente al enfoque externo, aunque muchas bibliotecas combinan ambos estilos para ofrecer flexibilidad.

22.5 Escenario: recomendador de productos con filtros dinámicos

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.

22.6 Implementación en Java

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());
    }
}

22.7 Explicación del flujo

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.

22.8 Iterator en el ecosistema Java

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.

22.9 Variantes y extensiones

Iterator admite variantes según el dominio:

  • Iteradores bidireccionales: permiten avanzar y retroceder sobre la colección.
  • Iteradores con snapshot: capturan una vista inmutable para aislarse de modificaciones concurrentes.
  • Iteradores compuestos: recorren varias colecciones como si fueran una sola, muy utilizados en motores de consultas.
  • Iteradores perezosos: generan elementos bajo demanda, como ocurre en streams infinitos.

22.10 Riesgos y malas prácticas

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.

22.11 Buenas prácticas para aplicar Iterator

  • Definir contratos claros sobre concurrencia: especificar si el iterador es fail-fast, snapshot o tolerante.
  • Mantener la inmutabilidad de los elementos devueltos cuando se requiera preservar invariantes.
  • Ofrecer métodos auxiliares en la colección para crear iteradores especializados, evitando que el cliente componga filtros complejos por su cuenta.
  • Utilizar pruebas de rendimiento para validar que los iteradores personalizados no introducen sobrecarga innecesaria.

22.12 Relación con otros patrones

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.