1. Méthode reduce: réduction universelle
En programmation, il faut souvent « réduire » une collection à une valeur finale unique: calculer une somme, trouver un produit, concaténer des chaînes, calculer une métrique agrégée ou rassembler des éléments dans une nouvelle structure. Autrefois, on le faisait manuellement via des boucles et une variable accumulateur. Aujourd’hui, l’API Stream propose des moyens élégants — les méthodes universelles reduce et collect, qui permettent d’écrire du code compact et déclaratif.
- reduce — réduit un flux à une valeur finale unique (somme, produit, concaténation, etc.).
- collect — transforme un flux en collection, chaîne, map ou structure arbitraire.
Voyons cela pas à pas.
À quoi sert reduce ?
reduce — une méthode terminale qui « réduit » les éléments d’un flux à une seule valeur à l’aide d’une fonction d’accumulation. Conceptuellement, cela ressemble à un parcours de la collection avec une accumulation pas à pas du résultat.
Signatures de la méthode reduce
Dans l’API Stream, il existe trois variantes principales de reduce():
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
- accumulator — fonction prenant la valeur accumulée courante et l’élément suivant, et renvoyant un nouveau résultat.
- identity — valeur initiale de l’accumulateur (par exemple, 0 pour la somme, 1 pour le produit).
- combiner — utilisé dans les flux parallèles pour fusionner des résultats intermédiaires.
Exemples d’utilisation de reduce
Exemple 1: Somme des nombres
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// reduce sans identity — le résultat est un Optional
Optional<Integer> sum1 = numbers.stream()
.reduce((a, b) -> a + b);
System.out.println(sum1.orElse(0)); // 15
// reduce avec identity — le résultat existe toujours
int sum2 = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum2); // 15
Exemple 2: Produit de tous les nombres
int product = numbers.stream()
.reduce(1, (a, b) -> a * b);
System.out.println(product); // 120
Exemple 3: Concaténation de chaînes
List<String> words = List.of("Java", "Stream", "API");
String phrase = words.stream()
.reduce("", (a, b) -> a + " " + b);
System.out.println(phrase.trim()); // Java Stream API
Exemple 4: Recherche de l’élément maximum
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
max.ifPresent(System.out::println); // 5
Exemple 5: Somme des longueurs de toutes les chaînes
List<String> texts = List.of("chat", "chien", "éléphant");
int totalLength = texts.stream()
.map(String::length)
.reduce(0, Integer::sum);
System.out.println(totalLength); // 14
Comment fonctionne reduce
La logique de reduce est équivalente à la boucle suivante:
T result = identity;
for (T element : collection) {
result = accumulator.apply(result, element);
}
return result;
Si identity n’est pas fourni, la valeur de départ est le premier élément du flux, et la méthode renvoie un Optional (qui peut être vide si le flux est vide).
2. Méthode collect: transformation universelle
collect — une méthode terminale qui transforme un flux en collection, chaîne, map ou toute autre structure. Pour cela, on utilise des « collecteurs » (Collector) qui décrivent le processus d’assemblage. Le plus souvent, on utilise ceux prêts à l’emploi de la classe Collectors.
Collecteurs les plus populaires
- Collectors.toList() — rassemble les éléments dans une List.
- Collectors.toSet() — rassemble les éléments dans un Set.
- Collectors.toMap() — rassemble les éléments dans une Map.
- Collectors.joining() — concatène des chaînes en une seule.
- Collectors.groupingBy() — regroupe les éléments selon un critère.
- Collectors.counting() — compte le nombre d’éléments.
- Collectors.summarizingInt() — collecte des statistiques sur les nombres (somme, moyenne, min/max).
Exemples d’utilisation de collect
Exemple 1: Collecte dans une liste
List<String> names = List.of("Anya", "Boris", "Vasya", "Anya");
List<String> uniqueNames = names.stream()
.distinct()
.collect(Collectors.toList());
System.out.println(uniqueNames); // [Anya, Boris, Vasya]
Exemple 2: Collecte dans un ensemble (Set)
Set<String> nameSet = names.stream()
.collect(Collectors.toSet());
System.out.println(nameSet); // [Anya, Boris, Vasya] (l'ordre n'est pas garanti)
Exemple 3: Collecte dans une chaîne
String csv = names.stream()
.collect(Collectors.joining(", "));
System.out.println(csv); // Anya, Boris, Vasya, Anya
Exemple 4: Collecte dans une Map
Supposons que nous ayons une classe :
public class Employee {
private String name;
private String department;
public Employee(String name, String department) {
this.name = name;
this.department = department;
}
public String getName() { return name; }
public String getDepartment() { return department; }
}
Construisons une map « nom → département » :
List<Employee> employees = List.of(
new Employee("Anya", "IT"),
new Employee("Boris", "HR"),
new Employee("Vasya", "IT")
);
Map<String, String> nameToDept = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getDepartment,
(oldValue, newValue) -> newValue // gestion des doublons de noms
));
System.out.println(nameToDept); // {Anya=IT, Boris=HR, Vasya=IT}
Exemple 5: Rassembler les éléments uniques dans un Set
Set<String> unique = names.stream()
.collect(Collectors.toSet());
System.out.println(unique);
Exemple 6: Collecter des statistiques sur des nombres
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
IntSummaryStatistics stats = numbers.stream()
.collect(Collectors.summarizingInt(Integer::intValue));
System.out.println(stats.getSum()); // 15
System.out.println(stats.getAverage()); // 3.0
System.out.println(stats.getMax()); // 5
System.out.println(stats.getMin()); // 1
3. Comparaison: quand utiliser reduce et quand — collect
reduce — lorsque vous devez obtenir une valeur finale unique à l’aide d’une opération binaire: somme, produit, maximum, concaténation.
collect — lorsque vous devez rassembler des éléments dans une collection/map/chaîne ou effectuer une agrégation plus complexe avec un Collector. Pour ces tâches, collect est généralement plus puissant et plus efficace.
Tableau: reduce vs collect
| Tâche | À utiliser | Exemple |
|---|---|---|
| Somme des nombres | |
|
| Produit | |
|
| Collecte dans List | |
|
| Collecte dans Map | |
|
| Regroupement | |
|
| Concaténation de chaînes | reduce / collect | reduce("", String::concat) ou Collectors.joining() |
4. Exercices pratiques
Exercice 1: Trouver la somme des longueurs de toutes les chaînes de la liste
List<String> words = List.of("chat", "chien", "éléphant");
int totalLength = words.stream()
.mapToInt(String::length)
.sum(); // ou via reduce: .reduce(0, Integer::sum)
System.out.println(totalLength); // 14
Exercice 2: Rassembler les éléments uniques dans un Set
List<String> fruits = List.of("pomme", "poire", "pomme", "orange");
Set<String> uniqueFruits = fruits.stream()
.collect(Collectors.toSet());
System.out.println(uniqueFruits); // [pomme, poire, orange]
Exercice 3: Construire une Map à partir d’une liste d’objets
List<Employee> employees = List.of(
new Employee("Anya", "IT"),
new Employee("Boris", "HR"),
new Employee("Vasya", "IT")
);
Map<String, String> nameToDept = employees.stream()
.collect(Collectors.toMap(
Employee::getName,
Employee::getDepartment,
(oldValue, newValue) -> newValue // en cas de noms identiques
));
System.out.println(nameToDept);
Exercice 4: Rassembler tous les noms séparés par une virgule
String allNames = employees.stream()
.map(Employee::getName)
.collect(Collectors.joining(", "));
System.out.println(allNames); // Anya, Boris, Vasya
5. Particularités d’implémentation et subtilités
Optional et reduce
Si vous utilisez reduce sans identity, le résultat est un Optional. C’est sûr: si le flux est vide, le résultat l’est aussi. N’oubliez pas de le traiter correctement: ifPresent(...), orElse(...), orElseThrow(...).
Optional<Integer> max = numbers.stream().reduce(Integer::max);
max.ifPresent(System.out::println);
Collecteurs personnalisés: si vous voulez aller plus loin
Vous pouvez écrire votre propre Collector si les standard ne suffisent pas. Mais pour 99 % des cas, ceux de Collectors suffisent largement.
Collecteurs et streams parallèles
Les collecteurs prêts à l’emploi de Collectors sont conçus pour fonctionner correctement avec parallelStream(). N’ajoutez pas des éléments manuellement à une collection mutable partagée dans un forEach sur un flux parallèle — vous risquez des conditions de concurrence (data races).
6. Erreurs courantes avec reduce et collect
Erreur n° 1: Vous ne vérifiez pas Optional après reduce. Si le flux est vide, reduce sans identity renvoie un Optional vide. Appeler get() entraînera une NoSuchElementException. Utilisez ifPresent, orElse ou orElseThrow.
Erreur n° 2: Vous essayez de rassembler une collection via reduce. C’est possible, mais collect est mieux adapté et plus rapide pour cela:
// Inefficace !
List<String> list = stream.reduce(
new ArrayList<>(),
(acc, elem) -> { acc.add(elem); return acc; },
(acc1, acc2) -> { acc1.addAll(acc2); return acc1; }
);
// Mieux ainsi :
List<String> list2 = stream.collect(Collectors.toList());
Erreur n° 3: Vous ne traitez pas les doublons de clés dans toMap. Si des clés en double apparaissent, une exception sera levée. Ajoutez le troisième argument à toMap pour résoudre les conflits.
Erreur n° 4: Vous utilisez des collections mutables dans des streams parallèles sans synchronisation. Dans collect, utilisez les collecteurs standard — ils fonctionnent correctement en mode parallèle. N’effectuez pas de list.add() dans un forEach sur un parallelStream().
Erreur n° 5: Vous confondez reduce et collect pour des tâches complexes. reduce — pour les agrégations simples (somme, maximum). collect — pour la collecte dans des collections, le regroupement, la construction de Map et l’agrégation complexe.
GO TO FULL VERSION