CodeGym /Corsi /JAVA 25 SELF /Collector e Spliterator personalizzati

Collector e Spliterator personalizzati

JAVA 25 SELF
Livello 33 , Lezione 4
Disponibile

1. Collector personalizzati: quando e come scriverli

Nello Java Stream API per trasformare uno stream in una collezione o in un aggregato si usa l'interfaccia Collector. Di solito usi i collector pronti della classe Collectors (toList(), toMap(), groupingBy() ecc.), ma a volte serve qualcosa di particolare — e allora puoi scrivere un Collector tutto tuo.

Collector — è un oggetto che descrive come raccogliere dagli elementi dello stream il risultato finale. Definisce quattro (in realtà cinque) componenti chiave:

  • supplier — crea un nuovo contenitore per raccogliere gli elementi (per esempio, una nuova lista o mappa).
  • accumulator — aggiunge l'elemento successivo al contenitore.
  • combiner — unisce due contenitori (importante per gli stream paralleli!).
  • finisher — trasforma il contenitore nel risultato finale (per esempio, lo rende immutabile o lo converte in un altro tipo).
  • characteristics — insieme di flag che descrivono le proprietà del collector (per esempio, se supporta il parallelismo, se cambia il tipo del risultato, ecc.).

Firma:

Collector<T, A, R>
  • T — tipo degli elementi dello stream,
  • A — tipo dell'accumulatore intermedio,
  • R — tipo del risultato.

2. Esempio: Collector per una MultiMap (Map<K, List<V>>)

Supponiamo di voler raccogliere uno stream di coppie Pair<K, V> in Map<K, List<V>> (multi‑mappa), dove a ogni chiave corrisponde una lista di valori.

Esempio di implementazione:

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

Utilizzo:

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. Esempio: Collector per i primi N elementi

Supponiamo che tu voglia raccogliere uno stream in una lista dei N elementi più grandi (per esempio, top‑5 in ordine decrescente).

Implementazione:

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(); // rimuoviamo il più piccolo
        },
        (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()); // in ordine decrescente
            return result;
        },
        Collector.Characteristics.UNORDERED
    );
}

Utilizzo:

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

4. Quando NON conviene scrivere un proprio Collector

  • Se il problema può essere espresso tramite una combinazione di collector standard e operazioni downstream (groupingBy, mapping, flatMapping, collectingAndThen ecc.), è meglio usare quelli.
  • Un Collector personalizzato serve solo per scenari davvero non standard (struttura dati particolare, aggregazione complessa, top‑N, multi‑mappe, ecc.).
  • Non scrivere un Collector solo per il gusto di farlo — complica manutenzione e test.

Esempio:

// Invece di un Collector personalizzato per Map<K, Set<V>>:
.collect(Collectors.groupingBy(
    Pair::key,
    Collectors.mapping(Pair::value, Collectors.toSet())
))

5. Spliterator personalizzato: perché e come

Spliterator — è un'interfaccia speciale per l'iterazione e la suddivisione efficiente di collezioni (o di altre sorgenti di dati) in parti, soprattutto per l'elaborazione parallela. A differenza di un iteratore normale, uno Spliterator può “dividere” (split) una collezione in pezzi indipendenti per l'elaborazione parallela.

Metodi chiave:

  • tryAdvance(Consumer<? super T> action) — elabora l'elemento successivo.
  • trySplit() — tenta di dividere la collezione in due parti (restituisce un nuovo Spliterator per una delle parti).
  • estimateSize() — stima del numero di elementi rimanenti.
  • characteristics() — maschera di bit delle caratteristiche (ORDERED, SIZED, SUBSIZED ecc.).

trySplit: strategie di suddivisione

Suddivisione bilanciata — è importante per gli stream paralleli: trySplit dovrebbe restituire parti di dimensioni circa uguali, così che i thread siano caricati in modo uniforme.

Se non c'è nulla da dividere (per esempio, pochi elementi) — restituiamo null.

Esempio: Spliterator per leggere un file a porzioni
Supponiamo di avere un file grande e di volerlo elaborare per 1000 righe alla volta (a porzioni), per non tenere tutto in 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() {
        // Per la lettura in streaming da file la suddivisione non ha senso — restituiamo null
        return null;
    }

    @Override
    public long estimateSize() {
        return Long.MAX_VALUE; // sconosciuto a priori
    }

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

Utilizzo:

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

Caratteristiche di Spliterator

  • ORDERED — gli elementi seguono un ordine definito (per esempio, una lista).
  • SIZED — è nota la quantità esatta di elementi.
  • SUBSIZED — tutti gli Spliterator ottenuti tramite trySplit sono anch'essi SIZED.
  • IMMUTABLE — la sorgente non cambia durante l'iterazione.
  • CONCURRENT — la sorgente supporta modifiche parallele sicure.
  • DISTINCT, SORTED, NONNULL — proprietà aggiuntive.

Importante: indicare correttamente le caratteristiche influisce sull'ottimizzazione degli stream.

6. Esempi

  • Lettura del file a porzioni (chunk) — consente di elaborare file grandi a pezzi, senza caricare tutto in memoria.
  • Parsing senza allocazioni superflue — se stai facendo il parsing di uno stream di byte/caratteri e vuoi minimizzare la creazione di oggetti temporanei, puoi implementare uno Spliterator che restituisce “finestre” o “slice” dell'array sorgente.

Esempio: Spliterator per il parsing di un CSV riga per riga

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; // parsing sequenziale
    }

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

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

7. Integrazione con parallel() — come farlo in modo sicuro

  • Se il tuo Spliterator supporta la suddivisione parallela (trySplit non restituisce null) e le caratteristiche includono SIZED/SUBSIZED, allora lo Stream API potrà parallelizzare l'elaborazione in modo efficace.
  • Per le sorgenti stream (file, socket) di solito la suddivisione non è supportata — usa stream sequenziali.
  • Per collezioni e array — implementa una suddivisione bilanciata (ad esempio, dividi l'array a metà).

Esempio: Spliterator per 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;
    }
}

Utilizzo:

String[] arr = {"a", "b", "c", "d"};
StreamSupport.stream(new ArraySpliterator<>(arr, 0, arr.length), true)
    .forEach(System.out::println);
1
Sondaggio/quiz
Ottimizzazione del lavoro con le collezioni, livello 33, lezione 4
Non disponibile
Ottimizzazione del lavoro con le collezioni
Ottimizzazione del lavoro con le collezioni
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION