1. Créer un flux
Pour utiliser l’API Stream, il faut d’abord obtenir un flux à partir d’une collection ou d’un tableau.
Exemples de création d’un flux
// À partir d’une liste
List<String> names = List.of("Anna", "Boris", "Alex", "Alina");
Stream<String> stream = names.stream();
// À partir d’un tableau
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);
// À partir de valeurs individuelles
Stream<String> letters = Stream.of("A", "B", "C");
En bref :
- list.stream() — pour les collections
- Arrays.stream(array) — pour les tableaux
- Stream.of(...) — pour des valeurs individuelles
Exemple dans le contexte de notre application
Supposons que nous ayons une liste d’utilisateurs :
List<String> users = List.of("Ivan", "Anna", "Petr", "Alexey");
Stream<String> userStream = users.stream();
Opérations intermédiaires et terminales
Point important : les opérations dans l’API Stream se divisent en deux types.
- Opérations intermédiaires (par exemple, filter, map, distinct) — elles décrivent des étapes de traitement. Elles renvoient un nouveau flux, mais ne déclenchent rien par elles‑mêmes.
- Opérations terminales (par exemple, collect, forEach, count) — elles déclenchent le pipeline et produisent un résultat.
Un flux fonctionne « paresseusement » : tant qu’aucune opération terminale n’est appelée, aucun calcul n’a lieu. C’est pourquoi on termine souvent la chaîne par collect(...) — c’est le point où le flux redevient une collection ou un autre résultat.
2. Opération filter: filtrer des éléments selon une condition
filter est une opération intermédiaire qui ne laisse passer que les éléments correspondant à une condition donnée.
Signature
Stream<T> filter(Predicate<? super T> predicate);
Predicate est une interface fonctionnelle qui prend un élément et renvoie true (garder) ou false (écarter).
Exemple : ne garder que les nombres pairs
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // [2, 4, 6]
Que se passe‑t‑il ?
- n -> n % 2 == 0 — une expression lambda qui vérifie si le nombre est divisible par 2 sans reste.
- filter ne conserve que les nombres pairs.
Exemple : filtrer les prénoms commençant par « A »
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");
List<String> aNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(aNames); // [Anna, Alex, Alina]
Point important : filter ne modifie pas la collection — il crée un nouveau flux qui ne contient que les éléments requis.
3. Opération map: transformer un élément en autre chose
map est une opération de transformation. Elle prend chaque élément du flux, applique une fonction et renvoie un nouvel élément.
Signature
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
Function est une interface qui prend un élément et renvoie quelque chose (éventuellement d’un autre type).
Exemple : obtenir la longueur des chaînes
List<String> names = List.of("Anna", "Boris", "Alex");
List<Integer> nameLengths = names.stream()
.map(name -> name.length())
.collect(Collectors.toList());
System.out.println(nameLengths); // [4, 5, 4]
Que se passe‑t‑il ?
- map transforme une chaîne en sa longueur (name -> name.length()).
- On obtient au final un flux de nombres.
Exemple : passer les chaînes en majuscules
List<String> names = List.of("Anna", "Boris", "Alex");
List<String> upperNames = names.stream()
.map(name -> name.toUpperCase())
.collect(Collectors.toList());
System.out.println(upperNames); // [ANNA, BORIS, ALEX]
4. Opération collect: rassembler le résultat dans une collection
collect est une opération terminale, c’est‑à‑dire qu’elle achève le travail du flux et rassemble le résultat dans une collection ou un autre conteneur.
Signature
<R, A> R collect(Collector<? super T, A, R> collector)
Pas d’inquiétude face à cette signature impressionnante ! Dans 99 % des cas, vous utiliserez des collecteurs prêts à l’emploi de la classe Collectors.
Collectors est une classe utilitaire proposant un ensemble de « collecteurs ». Elle indique au flux sous quelle forme rassembler le résultat : liste, ensemble, chaîne, etc.
Exemples :
- Collectors.toList() — vers une List
- Collectors.toSet() — vers un Set
- Collectors.joining(", ") — en une chaîne séparée par des virgules
Autrement dit, Collectors est comme un jeu de boîtes de formes différentes dans lesquelles vous emballez les éléments du flux.
Exemple : rassembler le résultat dans une List
List<String> filtered = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
Exemple : rassembler le résultat dans un Set
Set<String> uniqueNames = names.stream()
.map(String::toLowerCase)
.collect(Collectors.toSet());
Exemple : rassembler les chaînes séparées par des virgules
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result); // Anna, Boris, Alex
5. Chaînage d’opérations: filtrage + transformation + collecte du résultat
La plus grande force de l’API Stream — c’est la possibilité d’enchaîner les opérations les unes après les autres.
Exemple : obtenir la longueur des prénoms commençant par « A »
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");
List<Integer> aNameLengths = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::length)
.collect(Collectors.toList());
System.out.println(aNameLengths); // [4, 4, 5]
Étape par étape :
- .stream() — créer un flux à partir de la liste.
- .filter(name -> name.startsWith("A")) — ne garder que les prénoms commençant par "A".
- .map(String::length) — transformer chaque prénom en sa longueur.
- .collect(Collectors.toList()) — rassembler le résultat dans une liste.
Code impératif équivalent
Voici à quoi ressemblerait la même chose « à l’ancienne » :
List<Integer> result = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) {
result.add(name.length());
}
}
Comparez : avec l’API Stream — une seule ligne, on lit « ce que l’on fait », et non « comment on le fait ».
6. Pratique: quelques exercices courts
Entraînons‑nous ! Tous les exemples peuvent être exécutés dans un seul fichier — il suffit de changer les données.
Exercice 1: ne garder que les nombres impairs et les mettre au carré
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);
List<Integer> oddSquares = numbers.stream()
.filter(n -> n % 2 != 0)
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(oddSquares); // [1, 9, 25, 49]
Exercice 2: à partir d’une liste de chaînes, obtenir la liste de leurs premières lettres
List<String> names = List.of("Anna", "Boris", "Alex");
List<Character> initials = names.stream()
.map(name -> name.charAt(0))
.collect(Collectors.toList());
System.out.println(initials); // [A, B, A]
Exercice 3: filtrer les chaînes de longueur supérieure à 3 et les rassembler dans un Set
List<String> words = List.of("cat", "dog", "elephant", "ant", "bear");
Set<String> longWords = words.stream()
.filter(word -> word.length() > 3)
.collect(Collectors.toSet());
System.out.println(longWords); // [bear, elephant]
7. Erreurs typiques lors de l’utilisation de filter, map, collect
Erreur n° 1 : vous avez oublié collect — aucun résultat !
L’API Stream est paresseuse comme un chat sur le rebord de la fenêtre : tant que vous n’appelez pas une opération terminale (par exemple, collect ou forEach), il ne se passera rien. Si vous écrivez seulement users.stream().filter(...).map(...); — rien ne sera exécuté.
Erreur n° 2 : filter et map inversés
Parfois, les débutants font d’abord map, puis filter. Par exemple, names.stream().map(String::length).filter(len -> len > 3) produira des nombres, pas des chaînes. Si vous avez besoin des chaînes de longueur supérieure à 3, filtrez d’abord, puis transformez.
Erreur n° 3 : oublier l’immuabilité
Les opérations de l’API Stream ne modifient pas la collection d’origine ! Elles renvoient un nouveau résultat. Après List<String> upper = names.stream().map(String::toUpperCase).collect(Collectors.toList()); — la collection names restera inchangée.
Erreur n° 4 : tenter d’utiliser une liste externe modifiable
À éviter :
List<String> result = new ArrayList<>();
names.stream().filter(...).forEach(name -> result.add(name));
Mieux vaut utiliser collect — c’est plus sûr et plus court.
Erreur n° 5 : NullPointerException
Si la collection peut contenir des éléments null, l’appel de name.startsWith("A") sur null provoquera une erreur. Ajoutez un filtre sur null lorsque c’est possible :
.filter(name -> name != null && name.startsWith("A"))
GO TO FULL VERSION