CodeGym /Cursos /JAVA 25 SELF /Collector y Spliterator personalizados

Collector y Spliterator personalizados

JAVA 25 SELF
Nivel 33 , Lección 4
Disponible

1. Collectors personalizados: cuándo y cómo escribir los tuyos

En Java, la Stream API utiliza la interfaz Collector para transformar un stream en una colección o un agregado. Normalmente usas los colectores listos de la clase Collectors (toList(), toMap(), groupingBy() y otros), pero a veces necesitas algo especial — y entonces puedes escribir tu propio Collector.

Collector — es un objeto que describe cómo recolectar el resultado final a partir de los elementos del stream. Define cuatro (en realidad cinco) componentes clave:

  • supplier — crea un nuevo contenedor para recolectar elementos (por ejemplo, una nueva lista o un mapa).
  • accumulator — añade el siguiente elemento al contenedor.
  • combiner — combina dos contenedores (¡importante para streams paralelos!).
  • finisher — convierte el contenedor en el resultado final (por ejemplo, lo hace inmutable o lo transforma a otro tipo).
  • characteristics — conjunto de flags que describen las propiedades del collector (p. ej., si admite paralelismo, si cambia el tipo de resultado, etc.).

Firma:

Collector<T, A, R>
  • T — tipo de los elementos del stream,
  • A — tipo del acumulador intermedio,
  • R — tipo del resultado.

2. Ejemplo: Collector para MultiMap (Map<K, List<V>>)

Supongamos que quieres recolectar un stream de pares Pair<K, V> en Map<K, List<V>> (una multimapa), donde a cada clave le corresponde una lista de valores.

Ejemplo de implementación:

public static <K, V> Collector<Pair<K, V>, ?, Map<K, List<V>>> toMultiMap() {
    return Collector.of(
        HashMap::new, // supplier
        (map, pair) -> map.computeIfAbsent(pair.key(), k -> new ArrayList<>()).add(pair.value()), // accumulator
        (map1, map2) -> { // combiner
            map2.forEach((k, vList) -> map1.merge(k, vList, (l1, l2) -> { l1.addAll(l2); return l1; }));
            return map1;
        },
        Function.identity(), // finisher
        Collector.Characteristics.UNORDERED
    );
}

Uso:

List<Pair<String, Integer>> pairs = List.of(
    new Pair<>("a", 1), new Pair<>("b", 2), new Pair<>("a", 3)
);

Map<String, List<Integer>> multiMap = pairs.stream().collect(toMultiMap());
// multiMap: {a=[1, 3], b=[2]}

3. Ejemplo: Collector para top N elementos

Supongamos que quieres recolectar un stream en una lista de los N elementos mayores (por ejemplo, top-5 en orden descendente).

Implementación:

public static <T> Collector<T, ?, List<T>> topN(int n, Comparator<? super T> comparator) {
    return Collector.of(
        () -> new PriorityQueue<>(n, comparator), // supplier
        (pq, t) -> {
            pq.offer(t);
            if (pq.size() > n) pq.poll(); // eliminamos el menor
        },
        (pq1, pq2) -> {
            pq2.forEach(t -> {
                pq1.offer(t);
                if (pq1.size() > n) pq1.poll();
            });
            return pq1;
        },
        pq -> {
            List<T> result = new ArrayList<>(pq);
            result.sort(comparator.reversed()); // en orden descendente
            return result;
        },
        Collector.Characteristics.UNORDERED
    );
}

Uso:

List<Integer> top3 = Stream.of(5, 1, 9, 3, 7, 2).collect(topN(3, Comparator.naturalOrder()));
// top3: [9, 7, 5]

4. Cuándo NO conviene escribir tu propio Collector

  • Si puedes expresar la tarea mediante una combinación de colectores estándar y operaciones downstream (groupingBy, mapping, flatMapping, collectingAndThen y otros), es mejor usarlos.
  • Un Collector propio solo es necesario para escenarios realmente no estándar (estructura de datos especial, agregación compleja, top N, multimapas, etc.).
  • No escribas un Collector por el mero hecho de hacerlo — complica el mantenimiento y las pruebas.

Ejemplo:

// En lugar de tu propio Collector para Map<K, Set<V>>:
.collect(Collectors.groupingBy(
    Pair::key,
    Collectors.mapping(Pair::value, Collectors.toSet())
))

5. Spliterator personalizado: para qué y cómo

Spliterator — es una interfaz especial para el recorrido y la división de colecciones (u otras fuentes de datos) de forma eficiente, especialmente para el procesamiento paralelo. A diferencia de un iterador normal, un Spliterator puede «dividir» (split) la colección en partes independientes para su procesamiento en paralelo.

Métodos clave:

  • tryAdvance(Consumer<? super T> action) — procesar el siguiente elemento.
  • trySplit() — intentar dividir la colección en dos partes (devuelve un nuevo Spliterator para una de las partes).
  • estimateSize() — estimación de la cantidad de elementos restantes.
  • characteristics() — máscara de bits de características (ORDERED, SIZED, SUBSIZED y otros).

trySplit: estrategias de partición

Partición equilibrada — es importante para streams paralelos: trySplit debe devolver partes de tamaño aproximadamente igual para equilibrar la carga entre hilos.

Si no hay nada que dividir (por ejemplo, pocos elementos) — devuelve null.

Ejemplo: Spliterator para leer un archivo por porciones
Supongamos que tienes un archivo grande y quieres procesarlo de 1000 líneas cada vez (por porciones), para no mantenerlo todo en memoria.

public class ChunkedLineSpliterator implements Spliterator<List<String>> {
    private final BufferedReader reader;
    private final int chunkSize;

    public ChunkedLineSpliterator(BufferedReader reader, int chunkSize) {
        this.reader = reader;
        this.chunkSize = chunkSize;
    }

    @Override
    public boolean tryAdvance(Consumer<? super List<String>> action) {
        List<String> chunk = new ArrayList<>(chunkSize);
        try {
            String line;
            for (int i = 0; i < chunkSize && (line = reader.readLine()) != null; i++) {
                chunk.add(line);
            }
            if (chunk.isEmpty()) return false;
            action.accept(chunk);
            return true;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public Spliterator<List<String>> trySplit() {
        // Para la lectura en streaming desde archivo la partición no tiene sentido — devolvemos null
        return null;
    }

    @Override
    public long estimateSize() {
        return Long.MAX_VALUE; // desconocido de antemano
    }

    @Override
    public int characteristics() {
        return ORDERED | NONNULL;
    }
}

Uso:

try (BufferedReader reader = Files.newBufferedReader(Path.of("big.txt"))) {
    StreamSupport.stream(new ChunkedLineSpliterator(reader, 1000), false)
        .forEach(chunk -> processChunk(chunk));
}

Características de Spliterator

  • ORDERED — los elementos siguen un orden determinado (por ejemplo, una lista).
  • SIZED — se conoce el número exacto de elementos.
  • SUBSIZED — todos los Spliterator obtenidos mediante trySplit también son SIZED.
  • IMMUTABLE — la fuente no cambia durante el recorrido.
  • CONCURRENT — la fuente admite modificación paralela segura.
  • DISTINCT, SORTED, NONNULL — propiedades adicionales.

Importante: especificar correctamente las características influye en la optimización de los streams.

6. Ejemplos

  • Lectura del archivo por porciones (chunks) — permite procesar archivos grandes por partes, sin cargarlo todo en memoria.
  • Parsing sin asignaciones innecesarias — si analizas un flujo de bytes/caracteres y quieres minimizar la creación de objetos temporales, puedes implementar un Spliterator que entregue «ventanas» o «segmentos» del array de origen.

Ejemplo: Spliterator para analizar CSV por líneas

public class CsvLineSpliterator implements Spliterator<String[]> {
    private final BufferedReader reader;

    public CsvLineSpliterator(BufferedReader reader) {
        this.reader = reader;
    }

    @Override
    public boolean tryAdvance(Consumer<? super String[]> action) {
        try {
            String line = reader.readLine();
            if (line == null) return false;
            action.accept(line.split(","));
            return true;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public Spliterator<String[]> trySplit() {
        return null; // análisis secuencial
    }

    @Override
    public long estimateSize() {
        return Long.MAX_VALUE;
    }

    @Override
    public int characteristics() {
        return ORDERED | NONNULL;
    }
}

7. Integración con parallel() — cómo hacerlo de forma segura

  • Si tu Spliterator admite partición en paralelo (trySplit no devuelve null), y las características incluyen SIZED/SUBSIZED, entonces la Stream API podrá paralelizar el procesamiento de forma eficiente.
  • Para fuentes de streaming (archivos, sockets) normalmente no se admite la partición — usa streams secuenciales.
  • Para colecciones y arrays — implementa una partición equilibrada (por ejemplo, divide el array a la mitad).

Ejemplo: Spliterator para un array

public class ArraySpliterator<T> implements Spliterator<T> {
    private final T[] array;
    private int start, end;

    public ArraySpliterator(T[] array, int start, int end) {
        this.array = array;
        this.start = start;
        this.end = end;
    }

    @Override
    public boolean tryAdvance(Consumer<? super T> action) {
        if (start < end) {
            action.accept(array[start++]);
            return true;
        }
        return false;
    }

    @Override
    public Spliterator<T> trySplit() {
        int mid = (start + end) >>> 1;
        if (mid == start) return null;
        ArraySpliterator<T> split = new ArraySpliterator<>(array, start, mid);
        start = mid;
        return split;
    }

    @Override
    public long estimateSize() {
        return end - start;
    }

    @Override
    public int characteristics() {
        return ORDERED | SIZED | SUBSIZED | IMMUTABLE;
    }
}

Uso:

String[] arr = {"a", "b", "c", "d"};
StreamSupport.stream(new ArraySpliterator<>(arr, 0, arr.length), true)
    .forEach(System.out::println);
1
Cuestionario/control
Optimización del trabajo con colecciones, nivel 33, lección 4
No disponible
Optimización del trabajo con colecciones
Optimización del trabajo con colecciones
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION