CodeGym /Corsi /JAVA 25 SELF /Metodi forEach, peek: effetti collaterali

Metodi forEach, peek: effetti collaterali

JAVA 25 SELF
Livello 30 , Lezione 3
Disponibile

1. Metodo forEach: azione finale sugli elementi

Prima di entrare nei dettagli, ricordiamo ancora una volta che cosa sono le operazioni terminali e intermedie di uno stream.

  • Operazioni intermedie (ad esempio, filter, map, distinct, peek) — restituiscono un nuovo stream e di solito non eseguono azioni fino alla chiamata di un’operazione terminale.
  • Operazioni terminali (ad esempio, forEach, collect, count, anyMatch) — avviano l’elaborazione degli elementi dello stream e restituiscono un risultato (oppure non restituiscono nulla, come forEach).

Dopo un’operazione terminale lo stream è considerato chiuso, e non è più possibile applicare altre operazioni. È come cercare di finire un gelato già mangiato — non si può: lo stream è già «usato».

Ora conosciamo due metodi importanti per lavorare con effetti collaterali: forEach e peek.

Che cosa fa forEach?

forEach è un’operazione terminale dello stream che esegue l’azione specificata per ogni elemento dello stream. Di solito è usata per stampare a video, scrivere nei log, calcolare statistiche e altri effetti collaterali (side effects).

Firma del metodo:

void forEach(Consumer<? super T> action)

Consumer<T> è un’interfaccia funzionale che accetta un argomento e non restituisce nulla (ad esempio, System.out::println).

Esempio: stampare tutti gli elementi della lista

Supponiamo di avere una lista di nomi utente:

List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");

Stampare tutti gli utenti a video con le Stream API è molto semplice:

users.stream().forEach(System.out::println);

Risultato:

Anna
Boris
Alex
Alina
Dmitry

Si può usare anche un’espressione lambda:

users.stream().forEach(name -> System.out.println("Utente: " + name));

Risultato:

Utente: Anna
Utente: Boris
Utente: Alex
Utente: Alina
Utente: Dmitry

Importante: forEach chiude lo stream

Dopo la chiamata a forEach lo stream si «chiude». Non si può continuare la catena:

users.stream()
     .filter(name -> name.startsWith("A"))
     .forEach(System.out::println)
     .map(String::toUpperCase); // Errore! Lo stream è già chiuso.

Un tentativo di invocare qualcosa dopo forEach porterà a un errore di compilazione: un’operazione terminale restituisce void, non un nuovo stream.

2. Metodo peek: sbirciare senza intervenire

peek è un’operazione intermedia. Consente di eseguire un’azione per ogni elemento in una fase specifica dell’elaborazione, senza modificare l’elemento stesso e senza chiudere lo stream.

Firma del metodo:

Stream<T> peek(Consumer<? super T> action)
  • peek restituisce un nuovo stream, in cui per ogni elemento verrà eseguita l’azione action.
  • Di solito è usato per il debug, il logging o il monitoraggio dello stato dello stream.

Esempio: logging dopo il filtro

List<String> users = List.of("Anna", "Boris", "Alex", "Alina", "Dmitry");

List<Integer> nameLengths = users.stream()
    .filter(name -> name.startsWith("A"))
    .peek(name -> System.out.println("Passato il filtro: " + name))
    .map(String::length)
    .collect(Collectors.toList());

Risultato in console:

Passato il filtro: Anna
Passato il filtro: Alex
Passato il filtro: Alina

Contenuto di nameLengths:

[4, 4, 5]

Dove è comodo usare peek?

  • Per fare debug della catena di operazioni: vedere cosa succede a ogni fase.
  • Per raccogliere statistiche (ad esempio, contare gli elementi).
  • Per fare logging dei dati nelle fasi intermedie.

Importante: peek non dovrebbe essere usato per modificare gli elementi dello stream. Per le trasformazioni c’è map. peek significa «sbirciare», non «intervenire».

3. forEach vs peek: qual è la differenza?

Metodo Tipo di operazione Quando si applica Si può continuare la catena? Per cosa è più adatto
forEach
Terminale Alla fine dell’elaborazione dello stream No Azioni finali (stampa, logging, scrittura nel DB)
peek
Intermedia Nel mezzo della catena di operazioni Debug, logging intermedio, conteggio

Esempio: differenza d’uso

// Esempio con forEach
users.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::toUpperCase)
    .forEach(System.out::println); // Qui lo stream termina

// Esempio con peek
users.stream()
    .filter(name -> name.startsWith("A"))
    .peek(name -> System.out.println("Filtro superato: " + name))
    .map(String::toUpperCase)
    .collect(Collectors.toList()); // Si può continuare la catena

Da ricordare

  • forEach è un punto di non ritorno: dopo di esso non si può più fare nulla con lo stream.
  • peek non garantisce l’esecuzione delle azioni se non viene chiamata un’operazione terminale. Se si scrive solo una catena di operazioni intermedie, non succederà nulla.

4. Aspetti non ovvi: forEach non è sempre la scelta migliore!

Perché non conviene usare forEach per modificare le collezioni?

Molti principianti provano a usare forEach per modificare gli elementi di una collezione o la collezione stessa (ad esempio, rimuovere elementi). Ma è una cattiva pratica: gli stream non sono pensati per modificare le collezioni di origine.

Esempio di uso scorretto:

List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Alex"));
names.stream().forEach(name -> {
    if (name.startsWith("A")) {
        names.remove(name); // Può portare a ConcurrentModificationException!
    }
});

Risultato: errore a runtime — non si può modificare la collezione durante l’iterazione dello stream (ConcurrentModificationException).

Per cosa usare comunque forEach?

  • Per la stampa a video (ad esempio, stampare un report).
  • Per il logging.
  • Per chiamare servizi esterni (ad esempio, invio di email).
  • Per raccogliere statistiche (ad esempio, incrementare un contatore).

5. Ancora su peek: solo per il debug!

La tentazione è usare peek per modificare gli elementi, per esempio aumentare l’età dell’utente:

users.stream()
    .peek(user -> user.setAge(user.getAge() + 1)) // Male!
    .collect(Collectors.toList());

Perché è sbagliato?

  • Viola la natura dichiarativa e la purezza delle Stream API.
  • Questo codice diventa difficile da mantenere e testare.
  • Gli effetti collaterali in un’operazione intermedia possono portare a bug non ovvi.

Meglio usare map per trasformare i dati:

List<User> olderUsers = users.stream()
    .map(user -> new User(user.getName(), user.getAge() + 1))
    .collect(Collectors.toList());

Schema: differenza tra forEach e peek

users.stream()
    .filter(...)         // operazione intermedia
    .peek(...)           // operazione intermedia, "sbirciamo"
    .map(...)            // operazione intermedia
    .forEach(...)        // operazione terminale, "eseguiamo un'azione"

Spiegazione:
— Tutto ciò che è prima di forEach si può combinare, riordinare, aggiungere.
— Dopo forEach lo stream è chiuso.

6. Errori tipici nell’uso di forEach e peek

Errore n. 1: usare peek per modificare i dati. peek è pensato solo per l’osservazione, non per modificare gli elementi dello stream. Per le trasformazioni usate map.

Errore n. 2: aspettarsi che peek venga sempre eseguito. peek viene eseguito solo se dopo c’è un’operazione terminale (collect, forEach, count, ecc.). Senza un’operazione terminale non succederà nulla.

Errore n. 3: tentare di continuare lo stream dopo forEach. forEach è un’operazione terminale. Dopo di essa non si possono chiamare altri metodi dello stream.

Errore n. 4: modificare la collezione all’interno di forEach. Modificare la collezione di origine (rimuovere o aggiungere elementi) durante l’iterazione tramite forEach porta direttamente a ConcurrentModificationException.

Errore n. 5: usare forEach invece di collect per raccogliere il risultato. Se volete raccogliere gli elementi in una nuova collezione, usate collect(Collectors.toList()), non forEach con aggiunta manuale. Ciò viola la dichiaratività e può portare a errori in scenari multithread.

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