CodeGym /Corsi /JAVA 25 SELF /Collettori avanzati

Collettori avanzati

JAVA 25 SELF
Livello 31, Lezione 4
Disponibile

1. Introduzione

Quando lavorate con le collezioni tramite Stream API, spesso non basta raggruppare gli elementi: serve anche fare subito qualcosa con essi, per esempio trasformarli, filtrarli o raccoglierli in un altro tipo di collezione. Per questo in Java esistono i collettori downstream — collettori annidati che si applicano a ciascun gruppo o porzione di dati.

Che cos'è un collettore downstream?

È un collettore che si applica al risultato di un raggruppamento o di una partizione. Ad esempio, potete raggruppare gli studenti per corso con groupingBy e, all'interno di ciascun gruppo, raccogliere solo i nomi oppure solo gli studenti eccellenti (per esempio con una soglia di 4.5 di GPA).

Esempi

mapping

Consente di trasformare gli elementi del gruppo prima della raccolta.

Map<Integer, List<String>> namesByCourse = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.mapping(Student::getName, Collectors.toList())
    ));
  • Raggruppiamo gli studenti per corso.
  • Per ogni gruppo raccogliamo solo i nomi (non gli oggetti completi).

filtering

Consente di filtrare gli elementi all'interno del gruppo (per esempio, mantenere GPA >= 4.5).

Map<Integer, List<Student>> honorsByCourse = students.stream()
    .collect(Collectors.groupingBy(
        Student::getCourse,
        Collectors.filtering(s -> s.getGpa() >= 4.5, Collectors.toList())
    ));

In ciascun gruppo manteniamo solo gli studenti eccellenti.

flatMapping

Consente di «appiattire» le collezioni annidate all'interno dei gruppi.

Map<String, Set<String>> tagsByAuthor = books.stream()
    .collect(Collectors.groupingBy(
        Book::getAuthor,
        Collectors.flatMapping(
            book -> book.getTags().stream(),
            Collectors.toSet()
        )
    ));

Per ogni autore raccogliamo i tag unici di tutti i suoi libri.

partitioningBy con downstream

Funziona in modo simile a groupingBy, ma divide in due gruppi in base a una condizione booleana.

Map<Boolean, List<String>> namesByPassed = students.stream()
    .collect(Collectors.partitioningBy(
        s -> s.getGpa() >= 3.0,
        Collectors.mapping(Student::getName, Collectors.toList())
    ));

Dividiamo gli studenti in promossi e non promossi e, all'interno di ciascun gruppo, solo i nomi.

2. teeing: aggregazione simultanea con due collettori

A volte occorre calcolare contemporaneamente più aggregati sullo stream: ad esempio sia la somma sia la media, oppure il minimo e il massimo. Per questo, in Java 12+, è stato introdotto il collettore teeing.

Come funziona teeing?

Si passano due collettori e una funzione che ne combina i risultati.

Sintassi:

Collectors.teeing(collector1, collector2, (result1, result2) -> ...)

Esempi

Minimo + massimo

Optional<MinMax> minMax = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.minBy(Integer::compareTo),
        Collectors.maxBy(Integer::compareTo),
        (min, max) -> min.isPresent() && max.isPresent() ? new MinMax(min.get(), max.get()) : null
    ));

Troviamo contemporaneamente il minimo e il massimo.

Somma + media

Result result = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.summingInt(Integer::intValue),
        Collectors.averagingInt(Integer::intValue),
        (sum, avg) -> new Result(sum, avg)
    ));

Otteniamo un oggetto con somma e valore medio.

Esempio: report sugli stipendi

SalaryStats stats = employees.stream()
    .collect(Collectors.teeing(
        Collectors.summingInt(Employee::getSalary),
        Collectors.averagingInt(Employee::getSalary),
        SalaryStats::new
    ));

In SalaryStats conserviamo sia la somma sia la media.

3. toUnmodifiableList/Set/Map e collectingAndThen per il «congelamento»

Nelle versioni moderne di Java sono state introdotte collezioni che non possono essere modificate dopo la creazione — immutabili (unmodifiable). È comodo per le API in cui è importante che il risultato non possa essere cambiato per sbaglio.

toUnmodifiableList/Set/Map

  • Restituiscono una collezione immutabile.
  • Qualsiasi tentativo di aggiungere/rimuovere un elemento genererà UnsupportedOperationException.

Esempi:

List<String> names = students.stream()
    .map(Student::getName)
    .collect(Collectors.toUnmodifiableList());
Map<Integer, Student> byId = students.stream()
    .collect(Collectors.toUnmodifiableMap(Student::getId, Function.identity()));

collectingAndThen

Consente di applicare una funzione al risultato del collector — ad esempio, «congelare» la collezione.

List<String> names = students.stream()
    .map(Student::getName)
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        Collections::unmodifiableList
    ));

Prima raccogliamo in una lista normale, poi la rendiamo immutabile.

Esempio con Set:

Set<String> tags = books.stream()
    .flatMap(book -> book.getTags().stream())
    .collect(Collectors.collectingAndThen(
        Collectors.toSet(),
        Set::copyOf // Java 10+
    ));

4. Casi di pipeline

Report e statistiche per segmenti

Con i collettori avanzati si possono costruire report e statistiche complessi «in una riga».

Esempio: stipendio medio per reparto

Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingInt(Employee::getSalary)
    ));

Esempio: i 3 prodotti più costosi per categoria

Map<String, List<Product>> top3ByCategory = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.collectingAndThen(
            Collectors.toList(),
            list -> list.stream()
                        .sorted(Comparator.comparing(Product::getPrice).reversed())
                        .limit(3)
                        .toList()
        )
    ));

Risultato immutabile come contratto dell'API

Se il vostro metodo restituisce una collezione che non può essere modificata, ciò protegge da errori accidentali e rende l'API più affidabile.

public List<String> getTags() {
    return tags.stream()
        .collect(Collectors.toUnmodifiableList());
}

L'utente non potrà fare getTags().add("nuovo tag") — verrà lanciata un'eccezione.

Esempio: report con più aggregati (teeing)

public SalaryReport getSalaryReport(List<Employee> employees) {
    return employees.stream()
        .collect(Collectors.teeing(
            Collectors.averagingInt(Employee::getSalary),
            Collectors.summingInt(Employee::getSalary),
            SalaryReport::new
        ));
}

In SalaryReport conserviamo sia la retribuzione media sia quella totale.

5. Errori tipici e particolarità

Errore n. 1: ci si dimentica dell'immutabilità. Se restituite una lista normale, qualcuno potrebbe modificarla. Usate toUnmodifiableList/Set/Map oppure collectingAndThen per «congelare» il risultato.

Errore n. 2: collettore downstream errato. Se occorre trasformare gli elementi all'interno del gruppo — usate mapping; se occorre filtrare — filtering; se occorre «appiattire» collezioni annidate — flatMapping.

Errore n. 3: UnsupportedOperationException. Si verifica quando si tenta di modificare una collezione raccolta tramite toUnmodifiableList/Set/Map o «congelata» tramite collectingAndThen.

Errore n. 4: perdita di unicità/collisioni di chiavi. toUnmodifiableSet richiede elementi unici e toUnmodifiableMap — chiavi uniche; altrimenti si otterrà un'eccezione durante la raccolta.

1
Compito
JAVA 25 SELF, livello 31, lezione 4
Bloccato
Selezione dei candidati all'Accademia dei Grandi Risultati 📜
Selezione dei candidati all'Accademia dei Grandi Risultati 📜
1
Compito
JAVA 25 SELF, livello 31, lezione 4
Bloccato
Analisi rapida del portafoglio finanziario 💰
Analisi rapida del portafoglio finanziario 💰
1
Sondaggio/quiz
Raggruppamento e aggregazione, livello 31, lezione 4
Non disponibile
Raggruppamento e aggregazione
Stream API: raggruppamento e aggregazione
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION