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.
GO TO FULL VERSION