Streams : reduce, findFirst et findAny
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
Mapou une chaîne, tapeCollectors.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, utilisefindFirst(): 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
-
Prends l'opération qui dit ce que tu veux.
anyMatchplutôt quefilter().findFirst().isPresent(),count()plutôt quetoList().size(). -
Ne fais jamais
.get()sur unOptionalsans savoir qu'il est rempli. PréfèreorElseThrow,orElseoumap. -
Pour construire une liste, une chaîne ou une
Map, utilisetoList()oucollect, jamaisreduce. -
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(); // ✅ -
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. -
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, uneMapou un regroupement.findFirst()renvoie unOptional: décide toujours quoi faire s'il est vide.anyMatch/allMatch/noneMatchpour un oui / non, et attention àallMatchsur une liste vide.reducereste rare : surtout pour additionner desBigDecimal, avec une valeur de départ neutre.