CodeGym /Corsi /JAVA 25 SELF /Elaborazione pigra (lazy evaluation) nelle Stream API

Elaborazione pigra (lazy evaluation) nelle Stream API

JAVA 25 SELF
Livello 33 , Lezione 1
Disponibile

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
filter
map
ELEPHANT GIRAFFE
forEach
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.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION