CodeGym /Cours /JAVA 25 SELF /Méthodes forEach, peek : effets secondaires

Méthodes forEach, peek : effets secondaires

JAVA 25 SELF
Niveau 30, Leçon 3
Disponible

1. Méthode forEach : action finale sur les éléments

Avant de plonger dans les détails, rappelons encore une fois ce que sont les opérations terminales et intermédiaires d’un flux.

  • Opérations intermédiaires (par exemple, filter, map, distinct, peek) — renvoient un nouveau flux et, en général, n’exécutent rien avant l’appel d’une opération terminale.
  • Opérations terminales (par exemple, forEach, collect, count, anyMatch) — déclenchent le traitement des éléments du flux et renvoient un résultat (ou ne renvoient rien, comme forEach).

Après une opération terminale, le flux est considéré comme fermé, et on ne peut plus lui appliquer d’autres opérations. C’est comme essayer de finir une glace déjà mangée — impossible : le flux est déjà « utilisé ».

Voyons maintenant deux méthodes importantes pour gérer les effets secondaires : forEach et peek.

Que fait forEach ?

forEach est une opération terminale de flux qui exécute l’action donnée pour chaque élément du flux. Elle est généralement utilisée pour l’affichage, l’écriture dans les logs, le calcul de statistiques et autres effets secondaires (side effects).

Signature de la méthode :

void forEach(Consumer<? super T> action)

Consumer<T> est une interface fonctionnelle qui prend un argument et ne renvoie rien (par exemple, System.out::println).

Exemple : afficher tous les éléments de la liste

Supposons que nous ayons une liste de noms d’utilisateurs :

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

Afficher tous les utilisateurs à l’écran avec l’API Stream est très simple :

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

Résultat :

Anna
Boris
Alex
Alina
Dmitry

On peut aussi utiliser une expression lambda :

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

Résultat :

Utilisateur: Anna
Utilisateur: Boris
Utilisateur: Alex
Utilisateur: Alina
Utilisateur: Dmitry

Important : forEach termine le flux

Après l’appel à forEach, le flux « se ferme ». Il est impossible de continuer la chaîne :

users.stream()
     .filter(name -> name.startsWith("A"))
     .forEach(System.out::println)
     .map(String::toUpperCase); // Erreur! Le flux est déjà fermé.

Toute tentative d’appeler quelque chose après forEach conduira à une erreur de compilation : une opération terminale renvoie void et non un nouveau flux.

2. Méthode peek : on observe sans intervenir

peek est une opération intermédiaire. Elle permet d’exécuter une action pour chaque élément à une étape donnée du traitement, sans modifier l’élément lui-même et sans terminer le flux.

Signature de la méthode :

Stream<T> peek(Consumer<? super T> action)
  • peek renvoie un nouveau flux dans lequel l’action action sera effectuée pour chaque élément.
  • Généralement utilisée pour le débogage, la journalisation ou le suivi de l’état du flux.

Exemple : journaliser après filtrage

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("A passé le filtre: " + name))
    .map(String::length)
    .collect(Collectors.toList());

Résultat dans la console :

A passé le filtre: Anna
A passé le filtre: Alex
A passé le filtre: Alina

Contenu de nameLengths :

[4, 4, 5]

Quand utiliser peek de façon appropriée ?

  • Pour déboguer la chaîne d’opérations : voir ce qui se passe à chaque étape.
  • Pour collecter des statistiques (par exemple, compter le nombre d’éléments).
  • Pour journaliser des données à des étapes intermédiaires.

Important : peek ne doit pas être utilisée pour modifier les éléments du flux. Pour les transformations, utilisez map. peek, c’est « jeter un coup d’œil », pas « intervenir ».

3. forEach vs peek : quelle est la différence ?

Méthode Type d’opération Quand l’appliquer Peut-on continuer la chaîne ? Meilleur emploi
forEach
Terminale À la toute fin du traitement du flux Non Actions finales (affichage, journalisation, écriture en BD)
peek
Intermédiaire Au milieu de la chaîne d’opérations Oui Débogage, journalisation intermédiaire, comptage

Exemple : différence d’utilisation

// Exemple avec forEach
users.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::toUpperCase)
    .forEach(System.out::println); // Ici, le flux se termine

// Exemple avec peek
users.stream()
    .filter(name -> name.startsWith("A"))
    .peek(name -> System.out.println("Filtre passé: " + name))
    .map(String::toUpperCase)
    .collect(Collectors.toList()); // On peut continuer la chaîne

À retenir

  • forEach est un point de non-retour : après, on ne peut plus rien faire avec le flux.
  • peek ne garantit pas l’exécution des actions s’il n’y a pas d’opération terminale. Si vous n’écrivez qu’une chaîne d’opérations intermédiaires, il ne se passera rien.

4. Points subtils : forEach n’est pas toujours le meilleur choix !

Pourquoi éviter d’utiliser forEach pour modifier des collections ?

Beaucoup de débutants essaient, avec forEach, de modifier les éléments d’une collection ou la collection elle-même (par exemple, supprimer des éléments). Mais c’est une mauvaise pratique : les flux ne sont pas conçus pour modifier les collections sources.

Exemple de mauvais usage :

List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Alex"));
names.stream().forEach(name -> {
    if (name.startsWith("A")) {
        names.remove(name); // Peut entraîner ConcurrentModificationException!
    }
});

Résultat : erreur à l’exécution — on ne peut pas modifier la collection pendant l’itération du flux (ConcurrentModificationException).

À quoi sert malgré tout forEach ?

  • Pour l’affichage à l’écran (par exemple, impression d’un rapport).
  • Pour la journalisation.
  • Pour appeler des services externes (p. ex. envoi d’e-mails).
  • Pour collecter des statistiques (par exemple, incrémenter un compteur).

5. Encore sur peek : uniquement pour le débogage !

Il est très tentant d’utiliser peek pour modifier des éléments, par exemple augmenter l’âge d’un utilisateur :

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

Pourquoi est-ce mauvais ?

  • Cela rompt le caractère déclaratif et la propreté de l’API Stream.
  • Ce code devient difficile à maintenir et à tester.
  • Des effets secondaires dans une opération intermédiaire peuvent mener à des bogues subtils.

Il vaut mieux utiliser map pour transformer les données :

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

Schéma : différence entre forEach et peek

users.stream()
    .filter(...)         // opération intermédiaire
    .peek(...)           // opération intermédiaire, "on observe"
    .map(...)            // opération intermédiaire
    .forEach(...)        // opération terminale, "on agit"

Explication :
— Tout ce qui précède forEach peut être combiné, réordonné, ajouté.
— Après forEach, le flux est fermé.

6. Erreurs courantes avec forEach et peek

Erreur n° 1 : utiliser peek pour modifier des données. peek est destinée à l’observation, pas à la modification des éléments du flux. Pour les transformations, utilisez map.

Erreur n° 2 : s’attendre à ce que peek s’exécute toujours. peek ne s’exécute que s’il y a une opération terminale après elle (collect, forEach, count, etc.). Sans opération terminale, il ne se passera rien.

Erreur n° 3 : essayer de continuer le flux après forEach. forEach est une opération terminale. Après elle, on ne peut plus appeler d’autres méthodes du flux.

Erreur n° 4 : modifier la collection à l’intérieur de forEach. Modifier la collection source (supprimer ou ajouter des éléments) pendant l’itération via forEach mène droit à une ConcurrentModificationException.

Erreur n° 5 : utiliser forEach au lieu de collect pour agréger le résultat. Si vous souhaitez rassembler des éléments dans une nouvelle collection, utilisez collect(Collectors.toList()), et non forEach avec un ajout manuel. Cela rompt le caractère déclaratif et peut conduire à des erreurs dans des scénarios multithread.

1
Mission
JAVA 25 SELF, niveau 30, leçon 3
Bloqué
Appel des élèves au tableau 👩‍🏫
Appel des élèves au tableau 👩‍🏫
1
Mission
JAVA 25 SELF, niveau 30, leçon 3
Bloqué
Débogage du flux de données dans la maison intelligente 🏠
Débogage du flux de données dans la maison intelligente 🏠
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION