Introduction
Les fonctions map, filter, reduce sont des opérations très utiles sur les Streams qui permettent de transformer simplement leurs éléments.
L’objectif de cet article est de vous montrer comment fonctionnent ces 3 opérations fondamentales. On ne rentrera pas dans les détails mais on utilisera des exemples concrets pour que vous puissiez comprendre leur principe et les appliquer dans des cas simples.
Sans plus attendre, passons aux cas d’usages ! 🙂
Map
L’opération map sur une stream permet de convertir les données de notre stream.
Exemple:
On a une classe de 8 élèves et on a les notes qu’ils ont eu au dernier devoirs. On aimerait tout d’abord pouvoir trier ces notes par ordre croissant. Les notes sont sous la forme de chaînes de caractères.
Si on a une liste de notes sous forme de Strings et qu’on veut la transformer en une liste d’entiers.
Avec un code sans map:
@Test
void shouldConvertMarksToOrderedList() {
List<String> initialMarks = List.of("8", "18", "20", "4", "12", "6", "11", "13");
List<Integer> expectedOrderedMarks = List.of(4, 6, 8, 11, 12, 13, 18, 20);
List<Integer> actualOrderedMarks = new ArrayList<>();
initialMarks.forEach(element -> actualOrderedMarks.add(Integer.valueOf(element)));
Collections.sort(actualOrderedMarks);
assertThat(actualOrderedMarks).isEqualTo(expectedOrderedMarks);
}
Avec l’opération Map:
@Test
void shouldConvertMarksToOrderedList() {
List<String> initialMarks = List.of("8", "18", "20", "4", "12", "6", "11", "13");
List<Integer> expectedOrderedMarks = List.of(4, 6, 8, 11, 12, 13, 18, 20);
List<Integer> actualOrderedMarks = initialMarks.stream()
.map(Integer::valueOf)
.sorted()
.collect(Collectors.toList());
assertThat(actualOrderedMarks).isEqualTo(expectedOrderedMarks);
}
En passant par les streams, on voit très facilement que les opérations de cast, tri et de collection sont liées entre elles.
On a un gain en lisibilité même s’il n’est pas flagrant au vu de la simplicité de l’exemple 🙂
Filter
Comme son nom l’indique, cette opération va nous permettre de filtrer sur une liste d’éléments.
Exemple:
On va rajouter comme règle dans notre exercice: on veut récupérer uniquement les notes au-dessus de 10.
Code sans map ni filter
@Test
void shouldConvertMarksAboveTenToOrderedList() {
List<String> initialMarks = List.of("8", "18", "20", "4", "12", "6", "11", "13");
List<Integer> expectedOrderedMarks = List.of(11, 12, 13, 18, 20);
List<Integer> actualOrderedMarks = new ArrayList<>();
initialMarks.forEach(element -> {
int currentMark = (Integer.parseInt(element));
if(currentMark >= 10){
actualOrderedMarks.add(currentMark);
}
});
Collections.sort(actualOrderedMarks);
assertThat(actualOrderedMarks).isEqualTo(expectedOrderedMarks);
}
Avec map et filter:
@Test
void shouldConvertMarksAboveTenToOrderedList() {
List<String> initialMarks = List.of("8", "18", "20", "4", "12", "6", "11", "13");
List<Integer> expectedOrderedMarks = List.of(11, 12, 13, 18, 20);
List<Integer> actualOrderedMarks = initialMarks.stream()
.map(Integer::valueOf)
.filter(mark -> mark >= 10)
.sorted()
.collect(Collectors.toList());
assertThat(actualOrderedMarks).isEqualTo(expectedOrderedMarks);
}
On commence à voir un gain assez net en lisibilité car on suit l’enchaînement des opérations sans code technique.
Reduce
L’opération Reduce est un peu moins intuitive que ce qu’on a vu jusqu’ici. Elle va permettre de “réduire” une liste d’éléments en un seul résultat.
Elle prend 2 pramètres:
- L’identité: c’est à dire la valeur initale de ma réduction
- Une fonction d’accumulation (BinaryOperator) qui va définir comment les éléments vont être accumulés.
Exemple:
J’aimerais avoir la moyenne de toutes notes supérieures à 10:
Je vais me servir du reduce pour avoir la somme de toutes mes notes.
La somme commence à zero puis je vais ajouter chacune de mes notes à la somme partielle des notes:
List<Integer> marksAboveTen = List.of(11,12,13,18,20)
double marksSum = marksAboveTen.stream()
.reduce(0, Integer::sum);
Si on détaille ce qu’il se passe:
Au tout début je fais
0 (l’identité) + 11 (ma première note) => mon accumulation partielle vaut 11
Je fais ensuite
11 (accumulation partielle) + 12 (ma seconde note) => mon accumulation partielle vaut 23
Ensuite on aura
23 (accumulation partielle) + 13 (la troisième note)
et ainsi de suite.
Voici une proposition d’implémentation sans utiliser de Stream:
@Test
void shouldGetAverageOfMarksAboveTen() {
List<String> initialMarks = List.of("8", "18", "20", "4", "12", "6", "11", "13");
double expectedAverage = 14.8;
List<Integer> actualOrderedMarks = new ArrayList<>();
initialMarks.forEach(element -> {
int currentMark = (Integer.parseInt(element));
if(currentMark >= 10){
actualOrderedMarks.add(currentMark);
}
});
Collections.sort(actualOrderedMarks);
AtomicInteger sum = new AtomicInteger();
actualOrderedMarks.forEach(sum::addAndGet);
double average = sum.doubleValue() / actualOrderedMarks.size();
assertThat(average).isEqualTo(expectedAverage);
}
Avec Reduce:
@Test
void shouldGetAverageOfMarksAboveTen() {
List<String> initialMarks = List.of("8", "18", "20", "4", "12", "6", "11", "13");
double expectedAverage = 14.8;
List<Integer> marksAboveTen = initialMarks.stream()
.mapToInt(Integer::valueOf)
.boxed()
.filter(x -> x >= 10)
.collect(Collectors.toList());
double sum = marksAboveTen.stream()
.reduce(0, Integer::sum);
double average = sum / marksAboveTen.size();
assertThat(average).isEqualTo(expectedAverage);
}
Bonus: Pour cet exemple, une façon plus claire existe sans reduce 😉
@Test
void shouldGetAverageOfMarksAboveTen() {
List<String> initialMarks = List.of("8", "18", "20", "4", "12", "6", "11", "13");
double expectedAverage = 14.8;
double average = initialMarks.stream()
.mapToDouble(Double::valueOf)
.filter(x -> x >= 10)
.average()
.orElseThrow(IllegalArgumentException::new);
assertThat(average).isEqualTo(expectedAverage);
}
Conclusion
On a vu quelques exemples d’utilisation de ces fonctions et on voit que plus on a d’opérations à faire sur nos éléments, plus on a un gain de lisibilité à utiliser ces opérations (on le voit surtout dans l’exemple avec reduce). J’espère qu’avec cet article, vous aurez compris comment fonctionne chacune de ces méthodes et que vous pourrez vous aussi, l’utiliser au quotidien 🙂
Pour aller plus loin de nombreux articles existent:
Filter: https://www.baeldung.com/java-stream-filter-lambda
Map: https://www.baeldung.com/java-maps-streams
Reduce: https://mkyong.com/java8/java-8-stream-reduce-examples/