Menu

Stream in Java: filter, map, collect e reduce spiegati

Come elaborare le collezioni con la Stream API di Java (filter, map, sorted, collect, count e reduce) costruendo pipeline leggibili invece di cicli scritti a mano.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Pipeline invece di cicli

Uno stream è una pipeline per elaborare una sequenza di valori. Parti da una sorgente, di solito una collezione, concateni operazioni come "tieni solo questi", "trasforma ciascuno", "ordinali", e alla fine raccogli il risultato. Invece di scrivere un ciclo con una lista temporanea e un if all'interno, descrivi ciò che vuoi e lasci che sia lo stream a scorrere gli elementi.

Ora che conosci le lambda, gli stream vanno al loro posto: ogni operazione riceve una lambda (o un method reference) che descrive il lavoro da fare su un elemento.

Leggilo dall'alto in basso: prendi i nomi, tieni quelli lunghi, mettili in maiuscolo, raccoglili in una lista. La lista originale names non viene mai modificata: uno stream produce un nuovo risultato e lascia intatta la sorgente.

Anatomia di uno stream

Ogni pipeline ha tre parti:

  • Una sorgente: list.stream(), Arrays.stream(array) o Stream.of(a, b, c).
  • Zero o più operazioni intermedie: filter, map, sorted, distinct, limit. Ognuna restituisce un altro stream, quindi puoi concatenarle.
  • Esattamente un'operazione terminale: collect, count, forEach, reduce, findFirst. È lei ad avviare il lavoro e a produrre un risultato (o un effetto collaterale).

La distinzione è importante per una regola che all'inizio sorprende tutti.

Le operazioni intermedie sono lazy

Le operazioni intermedie da sole non fanno niente. Si limitano a registrare ciò che deve succedere. La pipeline viene eseguita solo quando un'operazione terminale chiede un risultato.

Eseguilo: la lambda di map non viene mai eseguita. Aggiungi un'operazione terminale e l'intera catena prende vita:

Questa pigrizia non è un difetto da combattere: permette allo stream di unire le operazioni e di saltare il lavoro che non serve. Però significa che una pipeline senza operazione terminale non fa niente, un bug comune del tipo "perché non succede niente?".

filter e map: tenere e trasformare

Queste due operazioni fanno quasi tutto il lavoro. filter riceve un Predicate (una lambda che restituisce boolean) e tiene gli elementi che superano il test. map riceve una Function e sostituisce ogni elemento con il risultato della funzione.

Nota che map può cambiare il tipo degli elementi: qui uno stream di String diventa uno stream di Integer. String::length è un method reference, una scorciatoia per la lambda w -> w.length().

Le operazioni terminali producono un risultato

Dopo aver dato forma allo stream, un'operazione terminale lo trasforma in qualcosa di concreto:

Operazioni terminali comuni: collect (raccoglie in una lista, un set o una mappa), count, forEach, anyMatch / allMatch / noneMatch, findFirst, min / max e reduce. Dopo l'esecuzione di un'operazione terminale lo stream è consumato: non puoi riutilizzarlo. Chiama di nuovo list.stream() per una pipeline nuova.

Raccogliere i risultati

collect con Collectors è lo strumento principale per costruire un risultato. Il più comune è Collectors.toList():

Collectors offre anche toSet(), joining(...), groupingBy(...) e counting(). Da Java 16 in poi puoi sostituire collect(Collectors.toList()) con il più breve .toList() (che restituisce una lista non modificabile):

List<String> result = names.stream().map(String::toUpperCase).toList();

sorted, distinct e limit

Queste operazioni intermedie rimodellano lo stream prima della raccolta:

sorted() senza argomenti usa l'ordinamento naturale; passa un Comparator (qui una lambda) per un ordine personalizzato. Comparator.reverseOrder() è il modo più pulito per ordinare in modo decrescente.

reduce: ridurre uno stream a un solo valore

Quando devi combinare tutti gli elementi in un unico risultato (una somma, un prodotto, la stringa più lunga) reduce è lo strumento generale. Gli passi un valore iniziale e una funzione che unisce due valori:

Per semplici somme e medie, gli stream specializzati sono più chiari: nums.stream().mapToInt(Integer::intValue).sum(). Usa reduce quando non esiste un aggregatore già pronto.

Gli stream non sostituiscono ogni ciclo

Gli stream danno il meglio quando trasformi una collezione in un risultato. Non sono automaticamente più veloci di un ciclo, e sono scomodi quando devi modificare uno stato esterno o uscire prima in modi complicati. Una buona regola: se la pipeline si legge come una frase chiara, usa uno stream; se ti serve un contatore condiviso o un index, un semplice ciclo for è onesto e va benissimo.

Ricorda anche che uno stream si usa una sola volta. Operarci sopra due volte lancia un'eccezione:

Stream<String> s = names.stream();
s.forEach(System.out::println);
s.count();   // IllegalStateException: stream has already been operated upon

Prossima pagina: Optional

Diverse operazioni terminali degli stream (findFirst, min, max, reduce senza valore iniziale) potrebbero non trovare niente, quindi non restituiscono un valore semplice. Restituiscono un Optional, il contenitore di Java per "forse un valore, forse niente", che finalmente ti offre un'alternativa pulita alla restituzione di null. È l'argomento della prossima pagina.

Domande frequenti

Cos'è uno stream in Java?

Uno stream è una pipeline per elaborare una sequenza di elementi, di solito presi da una collezione, attraverso una catena di operazioni come filter, map e sorted, che termina con un'operazione terminale come collect o count. Non memorizza dati e non modifica la sorgente: descrive un calcolo. Ne crei uno con list.stream() e leggi la catena dall'alto in basso come una ricetta.

Come si riconverte uno stream in una lista in Java?

Chiudi la pipeline con un collector terminale: list.stream().filter(...).collect(Collectors.toList()). Da Java 16 in poi puoi usare il più breve .toList(), che restituisce una lista non modificabile. Senza un'operazione terminale non viene eseguito niente, perché le operazioni intermedie come filter e map sono lazy.

Quando conviene usare uno stream invece di un ciclo for?

Usa uno stream quando trasformi o filtri una collezione per ottenere un risultato: la pipeline si legge come una frase chiara ("prendi i nomi, tieni quelli lunghi, mettili in maiuscolo, raccoglili in una lista"). Resta su un semplice ciclo for quando devi modificare uno stato esterno, uscire prima in modi complicati o quando la logica è più semplice come sequenza di passi imperativi. Gli stream servono alla chiarezza, non alla velocità pura.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA