Streams : reduce, findFirst et findAny

Publié le · Mis à jour le

L'idée en une phrase

Un stream est une chaîne d'étapes (filter, map…) qui se termine toujours par une opération terminale : c'est elle qui produit le résultat, que ce soit une liste, un nombre, un élément ou un booléen. Cette fiche t'aide à choisir la bonne.

Une image pour comprendre : la chaîne de tri du courrier

Imagine un centre de tri postal.

  • Les lettres arrivent sur un tapis : c'est la source (ta liste).
  • Des postes de travail trient, écartent ou tamponnent les lettres : ce sont les opérations intermédiaires (filter, map, sorted).
  • Au bout du tapis, il y a un seul poste final, qui décide de ce qu'on fait des lettres : les ranger dans un sac, les compter, prendre la première qui correspond…

Ce poste final, c'est l'opération terminale. Et tant que personne n'est assis au bout du tapis, le tapis ne démarre pas : sans opération terminale, aucune étape du stream n'est exécutée.

commandes.stream()
        .filter(c -> c.montant().compareTo(BigDecimal.ZERO) > 0)
        .map(Commande::client);
// ❌ rien ne s'exécute : il manque l'opération terminale

Les données des exemples

enum Statut { EN_COURS, PAYEE, ANNULEE }

record Commande(String client, BigDecimal montant, Statut statut) {}

List<Commande> commandes = List.of(
        new Commande("Alice", new BigDecimal("120.00"), Statut.PAYEE),
        new Commande("Bruno", new BigDecimal("45.50"), Statut.EN_COURS),
        new Commande("Alice", new BigDecimal("80.00"), Statut.PAYEE),
        new Commande("Chloé", new BigDecimal("300.00"), Statut.ANNULEE));

Je veux… j'écris

Le tableau à garder sous la main :

Je veux… J'écris
une liste .toList()
une chaîne « A, B, C » .collect(Collectors.joining(", "))
regrouper par catégorie .collect(Collectors.groupingBy(...))
le premier élément qui correspond .findFirst()
savoir si au moins un élément correspond .anyMatch(...)
compter .count()
le plus grand / le plus petit .max(...) / .min(...)
une somme d'entiers .mapToInt(...).sum()
une somme de BigDecimal .reduce(BigDecimal.ZERO, BigDecimal::add)

La suite de la fiche détaille chaque cas.

Récupérer une liste : toList()

List<String> clients = commandes.stream()
        .map(Commande::client)
        .toList(); // [Alice, Bruno, Alice, Chloé]

⚠️ La liste renvoyée par toList() (Java 16+) ne peut pas être modifiée :

clients.add("David"); // 💥 UnsupportedOperationException

C'est voulu et c'est généralement ce qu'on veut : une liste qu'on ne modifie pas par erreur. Si tu as vraiment besoin d'ajouter des éléments ensuite, crée une nouvelle liste explicitement :

List<String> modifiable = new ArrayList<>(clients); // ✅ l'intention est claire

Construire autre chose avec collect

collect sert à ranger les éléments dans un conteneur : une chaîne, une Map, un Set… Java fournit les recettes toutes prêtes dans la classe Collectors.

Une chaîne de caractères :

String liste = commandes.stream()
        .map(Commande::client)
        .distinct()
        .collect(Collectors.joining(", ")); // "Alice, Bruno, Chloé"

Regrouper par catégorie :

Map<Statut, List<Commande>> parStatut = commandes.stream()
        .collect(Collectors.groupingBy(Commande::statut));
// PAYEE → [Alice 120, Alice 80], EN_COURS → [Bruno], ANNULEE → [Chloé]

Compter par catégorie :

Map<String, Long> nombreParClient = commandes.stream()
        .collect(Collectors.groupingBy(Commande::client, Collectors.counting()));
// {Alice=2, Bruno=1, Chloé=1}

Astuce : quand tu ne sais pas comment obtenir une Map ou une chaîne, tape Collectors. dans ton IDE et parcours la liste proposée. La recette existe presque toujours.

Chercher un élément : findFirst()

Optional<Commande> premiereEnCours = commandes.stream()
        .filter(c -> c.statut() == Statut.EN_COURS)
        .findFirst(); // Optional[Commande de Bruno]

findFirst() s'arrête dès qu'il a trouvé : il ne parcourt pas le reste de la liste.

Il renvoie un Optional, une boîte qui contient l'élément… ou qui est vide si rien ne correspond. Il faut donc décider quoi faire dans le cas vide :

// ✅ une erreur claire si l'élément doit exister
Commande c = premiereEnCours
        .orElseThrow(() -> new IllegalStateException("Aucune commande en cours"));

// ✅ une valeur par défaut
String client = premiereEnCours.map(Commande::client).orElse("aucun");

// ❌ plante avec NoSuchElementException si la boîte est vide
Commande c = premiereEnCours.get();

Note : tu croiseras aussi findAny(). Il renvoie « un élément quelconque » et n'a d'intérêt qu'avec des streams parallèles. Dans ton code, utilise findFirst() : il renvoie toujours le même résultat.

Tester une condition : anyMatch, allMatch, noneMatch

Quand tu veux juste une réponse oui / non, inutile de chercher un élément puis de vérifier s'il existe :

boolean aUneAnnulation = commandes.stream()
        .anyMatch(c -> c.statut() == Statut.ANNULEE);   // true : au moins une

boolean toutesPayees = commandes.stream()
        .allMatch(c -> c.statut() == Statut.PAYEE);     // false : pas toutes

boolean aucuneGratuite = commandes.stream()
        .noneMatch(c -> c.montant().signum() == 0);     // true : aucune
// ❌ détour inutile
boolean existe = commandes.stream().filter(...).findFirst().isPresent();
// ✅ direct
boolean existe = commandes.stream().anyMatch(...);

⚠️ Piège : sur une liste vide, allMatch renvoie true (« aucun élément ne contredit la condition »). Si une liste vide doit être un cas d'erreur, vérifie-la à part.

Compter, calculer, comparer

Compter :

long nombrePayees = commandes.stream()
        .filter(c -> c.statut() == Statut.PAYEE)
        .count(); // 2

Le plus grand ou le plus petit :

Optional<Commande> plusGrosse = commandes.stream()
        .max(Comparator.comparing(Commande::montant)); // la commande de Chloé

Comme pour findFirst, le résultat est un Optional : la liste pourrait être vide.

Une somme d'entiers :

int totalArticles = paniers.stream()
        .mapToInt(Panier::nombreArticles)
        .sum(); // 0 si la liste est vide

Une somme de montants en BigDecimal :

BigDecimal totalPaye = commandes.stream()
        .filter(c -> c.statut() == Statut.PAYEE)
        .map(Commande::montant)
        .reduce(BigDecimal.ZERO, BigDecimal::add); // 200.00

reduce : rarement, mais bien

reduce combine tous les éléments un par un pour obtenir une seule valeur. Pour la somme ci-dessus :

départ : 0
0      + 120.00 = 120.00
120.00 +  80.00 = 200.00

Dans la pratique, tu l'utiliseras surtout pour additionner des BigDecimal. Pour le reste, les méthodes dédiées (sum, count, max, toList, joining) sont plus lisibles et font moins d'erreurs.

Si tu l'utilises, une règle : la valeur de départ doit être « neutre ». 0 pour une addition, 1 pour une multiplication, BigDecimal.ZERO pour des montants. Si tu veux ajouter des frais de port de 5 €, ajoute-les après, pas comme valeur de départ :

BigDecimal total = sousTotal.add(new BigDecimal("5.00")); // ✅ clair et sans surprise

Les bonnes habitudes

  1. Prends l'opération qui dit ce que tu veux. anyMatch plutôt que filter().findFirst().isPresent(), count() plutôt que toList().size().

  2. Ne fais jamais .get() sur un Optional sans savoir qu'il est rempli. Préfère orElseThrow, orElse ou map.

  3. Pour construire une liste, une chaîne ou une Map, utilise toList() ou collect, jamais reduce.

  4. Pas d'effet de bord dans un stream. Ne remplis pas une liste extérieure avec forEach : laisse l'opération terminale construire le résultat.

    List<String> noms = new ArrayList<>();
    commandes.stream().forEach(c -> noms.add(c.client())); // ❌
    List<String> noms = commandes.stream().map(Commande::client).toList(); // ✅
    
  5. N'utilise pas parallelStream(). Sur les listes d'une application web classique, il n'apporte rien et peut produire des résultats faux. Si tu penses en avoir besoin, parles-en d'abord à l'équipe.

  6. Un stream ne sert qu'une fois. Après l'opération terminale, il est consommé : pour un deuxième calcul, repars de commandes.stream().

À retenir

  • Sans opération terminale, rien ne s'exécute.
  • toList() pour une liste (non modifiable), collect(Collectors...) pour une chaîne, une Map ou un regroupement.
  • findFirst() renvoie un Optional : décide toujours quoi faire s'il est vide.
  • anyMatch / allMatch / noneMatch pour un oui / non, et attention à allMatch sur une liste vide.
  • reduce reste rare : surtout pour additionner des BigDecimal, avec une valeur de départ neutre.

Avis sur l'article

Pas encore de note : soyez le premier à donner votre avis.

Les avis sont publiés après validation par le modérateur.

Votre note