1. Il problema dell'approccio imperativo
Iniziamo da un compito classico. Supponiamo di avere un elenco di utenti:
List<User> users = ...; // già popolata
Poniamo di dover ottenere l’elenco dei nomi di tutti gli utenti maggiorenni (da 18 anni in su). Come lo facevamo prima?
List<String> names = new ArrayList<>();
for (User user : users) {
if (user.getAge() >= 18) {
names.add(user.getName());
}
}
A prima vista tutto è semplice, ma nota i passaggi «tecnici»: dichiarare una nuova lista per il risultato, iterare tutti gli elementi, verificare la condizione, aggiungere il risultato.
Se il compito si complica — ad esempio, bisogna ottenere un elenco in ordine alfabetico delle e‑mail uniche di tutti gli utenti i cui nomi iniziano con la lettera «A» — il codice cresce rapidamente: filtraggio, estrazione dei campi, rimozione dei duplicati, ordinamento, eventuali conversioni del formato e‑mail, ecc. Il risultato è che, invece di una logica breve e chiara, ottieni una montagna di boilerplate difficile da leggere e mantenere.
Gli svantaggi di questo approccio, in due parole: molto codice ripetitivo, alta probabilità di errore (dimenticare l’ordinamento o gestire male i duplicati) e difficoltà nel combinare le operazioni. Questo si chiama stile imperativo: indichi al computer passo dopo passo come eseguire il compito, invece di dire cosa vuoi ottenere.
2. Stream API — stile dichiarativo
Con l’uscita di Java 8 è arrivato lo Stream API — uno strumento che consente di lavorare con le collezioni nello stile «cosa va fatto», non «come farlo esattamente». Questo approccio si chiama dichiarativo.
Che cos’è, in sostanza, uno Stream?
Non è una collezione a parte, bensì un flusso di dati: una sequenza di elementi che passano attraverso una catena di operazioni — filtraggio, trasformazione, ordinamento, raccolta del risultato, ecc. Lo stream non conserva nulla: «trascina» i dati lungo una pipeline. Le operazioni si possono combinare quasi come mattoncini LEGO: componi la catena e avviala.
Esempio:
List<String> names = users.stream() // creiamo uno stream dalla lista di utenti
.filter(u -> u.getAge() >= 18) // teniamo solo chi ha almeno 18 anni
.map(User::getName) // trasformiamo User → String (prendiamo il nome)
.collect(Collectors.toList()); // raccogliamo il risultato in una lista
E questo è tutto: con una riga abbiamo descritto l’intero compito: «prendi gli utenti, filtra per età, estrai i nomi, raccogli in una lista». Non c’è bisogno di scrivere a mano i cicli, creare variabili temporanee e assicurarsi di non dimenticare nulla.
Lo Stream API funziona come una catena di montaggio: definisci le fasi di elaborazione e i dati le attraversano da soli. Elegante, compatto e molto più leggibile.
3. Vantaggi dello Stream API
- Sinteticità e leggibilità. Confronta i due approcci — imperativo e dichiarativo — sullo stesso compito. Si vede subito quale sia più facile da leggere e mantenere.
- Composizione delle operazioni senza sforzo. Le operazioni si «infilano» facilmente in una catena: filter, map, sorted, collect — tutto in un’unica sequenza coerente.
- Meno errori. Lo Stream API elimina la routine: non devi gestire a mano collezioni intermedie e cicli — minore è la probabilità di dimenticare un passaggio o commettere un errore di indicizzazione.
- Possibilità di parallelismo. È facile scalare l’elaborazione su più core semplicemente chiamando parallelStream() al posto di stream(). Dettagli — più avanti.
- Stile di programmazione moderno. Le idee della programmazione funzionale (composizione di funzioni, assenza di cicli espliciti) aumentano l’espressività e sono apprezzate dal mercato.
Breve storia dello Stream API
Lo Stream API è apparso in Java 8 (2014) ed è stato un salto di qualità per il linguaggio. Prima, qualunque operazione non banale sulle collezioni richiedeva molto codice ausiliario, mentre altre piattaforme offrivano già approcci dichiarativi (map, filter, reduce, ecc.).
Da allora lo Stream API è lo standard de‑facto per l’elaborazione delle collezioni in Java. Se vuoi scrivere codice Java moderno — è imprescindibile.
4. Ambiti di applicazione dello Stream API
- Filtraggio: selezionare solo gli elementi necessari (ad esempio, tutti gli utenti con più di 18 anni).
- Trasformazione: estrarre un campo o creare un nuovo oggetto (ad esempio, l’elenco dei nomi degli utenti).
- Aggregazione: calcolare somma, media, conteggio, massimo/minimo.
- Ordinamento: ordinare gli elementi in base al criterio desiderato.
- Raggruppamento: suddividere gli elementi per categorie.
- Raccolta del risultato: raccogliere in lista, insieme, mappa, stringa, ecc.
Esempi di attività:
- Ottenere l’elenco delle e‑mail di tutti gli utenti il cui nome inizia per «A».
- Calcolare l’età media degli utenti.
- Trovare il primo utente con una determinata e‑mail.
- Raccogliere tutte le città di residenza uniche in un unico set.
5. Dettagli utili
Come funziona uno stream
[User1] --\
[User2] ---|--> [ filter ] --> [ map ] --> [ collect ] --> List<String>
[User3] --/
1. filter — lascia passare solo gli utenti che soddisfano la condizione.
2. map — trasforma l’utente in, ad esempio, un’e‑mail.
3. collect — raccoglie le e‑mail nella collezione desiderata.
Sintassi: come creare uno stream
- Da una collezione: list.stream() — stream degli elementi della collezione.
- Da un array: Arrays.stream(array)
- Da singoli valori: Stream.of("a", "b", "c")
6. Errori tipici nel passaggio allo Stream API
Errore n. 1: tentare di modificare la collezione all’interno dello stream. Lo Stream API non è pensato per modificare la collezione originale (ad esempio, è sconsigliato chiamare list.remove() nel corpo di forEach). Per le rimozioni usa, per esempio, removeIf sulla collezione prima o dopo l’elaborazione.
Errore n. 2: confondere collezione e stream. Uno stream non è una collezione! Lo stream scorre gli elementi una sola volta, dopodiché si chiude. Se devi elaborare di nuovo i dati — crea un nuovo stream().
Errore n. 3: catene troppo complesse. Non trasformare l’espressione in un «mostro» con decine di operazioni. Se la catena diventa lunga — suddividila in più passaggi con variabili intermedie per la leggibilità e il debug.
GO TO FULL VERSION