1. Che cos'è l'elaborazione pigra?
L'elaborazione pigra, o «valutazione pigra» (lazy evaluation), è il principio secondo cui le operazioni sui dati vengono rimandate finché il risultato non serve davvero. Nel contesto delle Stream API significa: se scrivi una catena di trasformazioni su una collezione, Java non le esegue subito — aspetta finché non viene invocata un'operazione terminale. E solo allora viene valutata l'intera catena.
Perché serve? Primo, si risparmiano risorse: gli elementi che alla fine non servono semplicemente non vengono elaborati. Secondo, aumentano le prestazioni — si possono costruire catene lunghe senza creare molte collezioni intermedie. Infine, è disponibile la «valutazione a corto circuito»: non appena si trova il primo elemento adatto, l'elaborazione successiva si interrompe.
Un'analogia: immagina un cameriere pigro. Tu dici: «Porta il menù, poi il caffè e poi un dolce». Lui annuisce, ma non fa nulla… finché non aggiungi: «Adesso portalo davvero». Solo allora va a eseguire l'ordine — e potrebbe portare solo il caffè, se i dolci sono finiti. Più o meno allo stesso modo funziona l'elaborazione pigra negli stream.
2. Operazioni intermedie e terminali
Intermedie (intermediate):
- filter
- map
- sorted
- distinct
- peek (per il debug)
- e altre
Le operazioni intermedie restituiscono un nuovo Stream, ma non avviano i calcoli. Si limitano a «costruire il piano» di elaborazione.
Terminali (terminal):
- collect
- forEach
- reduce
- count
- findFirst, findAny
- anyMatch, allMatch, noneMatch
- e altre
Solo un'operazione terminale avvia l'esecuzione dell'intera catena.
Esempio: non accade nulla senza un'operazione terminale
List<String> names = List.of("Alisa", "Bob", "Vasya");
names.stream()
.filter(name -> {
System.out.println("Filtro " + name);
return name.startsWith("A");
});
// Nessun messaggio! Il codice sopra si limita a "costruire" la pipeline.
Ora aggiungiamo un'operazione terminale:
names.stream()
.filter(name -> {
System.out.println("Filtro " + name);
return name.startsWith("A");
})
.forEach(System.out::println);
// Ora vedremo l'output in console!
Risultato:
Filtro Alisa
Filtro Bob
Filtro Vasya
Alisa
3. Vantaggi dell'elaborazione pigra
Risparmio di risorse
L'elaborazione pigra permette di non sprecare tempo e memoria sugli elementi che non servono. Per esempio, se cerchi il primo oggetto adatto, l'elaborazione si fermerà alla prima corrispondenza.
List<String> names = List.of("Alisa", "Bob", "Vasya", "Anna");
String firstA = names.stream()
.filter(name -> {
System.out.println("Verifico: " + name);
return name.startsWith("A");
})
.findFirst()
.orElse("Non trovato");
System.out.println("Risultato: " + firstA);
Output:
Verifico: Alisa
Risultato: Alisa
Nota: gli altri elementi non vengono nemmeno verificati!
Catene lunghe senza collezioni intermedie
Si possono combinare molte operazioni (filter, map, sorted, ecc.) senza creare collezioni a ogni passaggio.
List<String> names = List.of("Alisa", "Bob", "Vasya", "Anna");
List<String> result = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.sorted()
.toList(); // Java 16+, prima — .collect(Collectors.toList())
Valutazione a corto circuito
Se basta sapere «se esiste un elemento adatto», gli altri non verranno verificati:
boolean hasLongName = names.stream()
.anyMatch(name -> {
System.out.println("Verifico: " + name);
return name.length() > 10;
});
// Se il primo elemento è lungo — gli altri non verranno verificati!
4. Esempi: come funziona l'elaborazione pigra
Esempio 1: non accade nulla senza un'operazione terminale
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
numbers.stream()
.filter(n -> {
System.out.println("Filtro " + n);
return n % 2 == 0;
});
// Nessun output!
Esempio 2: catena con un'operazione terminale
numbers.stream()
.filter(n -> {
System.out.println("Filtro " + n);
return n % 2 == 0;
})
.map(n -> {
System.out.println("Moltiplico " + n);
return n * 10;
})
.forEach(System.out::println);
Output:
Filtro 1
Filtro 2
Moltiplico 2
20
Filtro 3
Filtro 4
Moltiplico 4
40
Filtro 5
Nota importante: le operazioni vengono eseguite per elemento: prima filter, poi map, quindi forEach — per ogni elemento a turno. Non sono due passaggi separati «prima filtrare tutto, poi trasformare tutto».
Esempio 3: uso di peek per il debug
numbers.stream()
.filter(n -> n % 2 == 0)
.peek(n -> System.out.println("Passato il filtro: " + n))
.map(n -> n * 10)
.peek(n -> System.out.println("Dopo map: " + n))
.forEach(System.out::println);
5. Dettagli utili
Non usare gli stream con effetti collaterali
La pigrizia può giocare brutti scherzi se conti su un'esecuzione immediata. Le azioni collaterali dentro map, filter o peek (scrittura su file, modifica di stato esterno) possono essere eseguite in un ordine diverso, non per tutti gli elementi o non essere eseguite affatto senza un'operazione terminale.
Filtra il prima possibile
Posiziona filter il più vicino possibile all'inizio della catena, così da scartare prima gli elementi superflui e ridurre il lavoro successivo.
Serve solo il primo risultato? Usa i terminali appropriati
Se serve il primo elemento adatto, invoca findFirst o findAny. Questo permette allo stream di fermarsi subito dopo aver trovato il risultato.
Gli stream non servono per modificare la collezione di origine
Gli stream non sono pensati per aggiungere/rimuovere elementi dalla collezione originale. Per modificare la struttura della collezione usa altri meccanismi.
Visualizzazione del funzionamento degli stream pigri
List<String> words = List.of("cat", "dog", "elephant", "fox", "giraffe");
words.stream()
.filter(w -> w.length() > 3)
.map(String::toUpperCase)
.forEach(System.out::println);
Come avviene:
| Fase | cat | dog | elephant | fox | giraffe |
|---|---|---|---|---|---|
|
✗ | ✗ | ✓ | ✗ | ✓ |
|
— | — | ELEPHANT | — | GIRAFFE |
|
— | — | stampa | — | stampa |
Tabella: confronto tra approcci eager e lazy
| Approccio | Quando viene eseguita l'elaborazione? | Uso di memoria | Prestazioni |
|---|---|---|---|
| Eager (avido) | Subito alla chiamata | Può essere elevato | A volte lento |
| Lazy (pigro) | Solo quando necessario | Minimo | Di solito più veloce |
Approccio eager — per esempio, quando organizzi manualmente più passaggi sulla collezione creando liste intermedie.
Approccio lazy — sono gli stream: non si fa nulla finché non serve il risultato finale.
6. Errori tipici nell'uso degli stream pigri
Errore n. 1: aspettarsi un risultato immediato. I principianti pensano che le chiamate a filter o map vengano eseguite subito. Ma senza un terminale (per esempio, collect, forEach) non accadrà nulla — da qui «il debug non funziona», «non c'è output».
Errore n. 2: effetti collaterali nelle operazioni intermedie. Scrivere su file, modificare variabili esterne dentro map/filter/peek — è una cattiva pratica. A causa della pigrizia e delle ottimizzazioni, tali azioni possono non essere eseguite completamente, non nell'ordine previsto o non essere eseguite affatto.
Errore n. 3: dimenticare di chiamare un'operazione terminale. È stata scritta una catena di stream, ma manca la conclusione con collect, forEach, ecc. Risultato — «silenzio».
Errore n. 4: aspettarsi che tutti gli elementi vengano elaborati. Operazioni come findFirst o anyMatch interrompono la pipeline al primo risultato. Gli altri elementi non vengono elaborati — da qui la sorpresa «perché il mio println non ha funzionato per tutti?».
Errore n. 5: usare gli stream per modificare la collezione originaria. Gli stream non sono pensati per modificare le collezioni di origine (aggiunta/rimozione di elementi). Usa metodi specializzati delle collezioni o iteratori.
GO TO FULL VERSION