1. Conversion List → Set et inversement
Rappelons pourquoi nous avons besoin de transformer des collections. Dans les cas réels, il nous faut souvent :
- Obtenir des éléments uniques à partir d’une liste (par exemple, liste d’e-mails → ensemble d’adresses uniques).
- Construire une map (Map), par exemple, à partir d’une liste de prénoms obtenir une map « prénom → longueur du prénom ».
- Concaténer des éléments en une seule chaîne (par exemple, pour un affichage soigné).
Autrefois, il fallait écrire beaucoup de code avec des boucles, des conditions et des collections temporaires. Avec Stream API, tout est devenu plus simple et… plus élégant !
Exemple : obtenir l’ensemble des prénoms uniques à partir d’une liste
Supposons que nous ayons une liste de prénoms (quelqu’un a peut-être saisi son nom deux fois – ça arrive !) :
List<String> names = List.of("Anna", "Sergey", "Anna", "Mariya", "Ivan", "Sergey");
Notre objectif est d’obtenir une collection où chaque prénom n’apparaît qu’une seule fois, c’est-à-dire un ensemble (Set). Avec Stream API, cela se fait littéralement en une seule ligne :
Set<String> uniqueNames = names.stream()
.collect(Collectors.toSet());
System.out.println(uniqueNames);
Sortie :
[Mariya, Ivan, Anna, Sergey]
(L’ordre dans un Set n’est pas garanti – ne vous étonnez pas si l’ordre diffère chez vous.)
Et dans l’autre sens : Set → List ?
Parfois, il faut faire l’inverse : transformer un ensemble en liste (par exemple, pour trier ou accéder par index) :
List<String> namesList = uniqueNames.stream()
.collect(Collectors.toList());
System.out.println(namesList);
2. Conversion en Map : Collectors.toMap()
Exemple : à partir d’une liste de prénoms obtenir une Map « prénom → longueur du prénom »
Parfois, on veut être non seulement développeur, mais aussi cartographe – construire des cartes ! Essayons :
List<String> names = List.of("Anna", "Sergey", "Mariya", "Ivan");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name, // clé — le prénom lui-même
name -> name.length() // valeur — longueur du prénom
));
System.out.println(nameToLength);
Sortie :
{Mariya=5, Ivan=4, Anna=4, Sergey=6}
Point important : doublons de clés
Si la liste d’origine contient des prénoms identiques, alors en tentant de les collecter dans une Map, une erreur IllegalStateException : Duplicate key se produira. Java n’aime pas quand vous essayez d’associer deux valeurs à la même clé.
Comment gérer les doublons ?
Vous pouvez indiquer quoi faire en cas de collision de clés – par exemple, conserver la première valeur ou la dernière :
List<String> names = List.of("Anna", "Sergey", "Anna", "Mariya", "Ivan", "Sergey");
Map<String, Integer> nameToLength = names.stream()
.collect(Collectors.toMap(
name -> name,
name -> name.length(),
(oldValue, newValue) -> oldValue // conserver la première valeur
));
System.out.println(nameToLength);
Désormais, le programme ne plantera pas, et seule la première occurrence de chaque prénom sera présente dans la Map.
Exemple : Map avec des objets
Complexifions un peu : nous avons une liste d’utilisateurs, et nous voulons construire une Map « prénom → utilisateur » :
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
// Exemple de liste d'utilisateurs
List<User> users = List.of(
new User("Anna", 25),
new User("Sergey", 30),
new User("Mariya", 22)
);
Map<String, User> nameToUser = users.stream()
.collect(Collectors.toMap(
user -> user.name,
user -> user
));
System.out.println(nameToUser);
Sortie :
{Mariya=Mariya (22), Anna=Anna (25), Sergey=Sergey (30)}
3. Concaténer en une chaîne : Collectors.joining()
Parfois, on veut non seulement collecter une collection, mais produire une jolie chaîne pour l’affichage utilisateur ou les logs. Par exemple, concaténer tous les prénoms avec une virgule :
List<String> names = List.of("Anna", "Sergey", "Mariya", "Ivan");
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result);
Sortie :
Anna, Sergey, Mariya, Ivan
On peut ajouter un préfixe et un suffixe
String result = names.stream()
.collect(Collectors.joining(", ", "Liste: [", "]"));
System.out.println(result);
Sortie :
Liste: [Anna, Sergey, Mariya, Ivan]
4. Opérations terminales : forEach, collect, count, anyMatch, allMatch, noneMatch
Méthode forEach
Nous connaissons déjà bien forEach : cette opération exécute une action pour chaque élément du flux.
names.stream().forEach(name -> System.out.println("Salut, " + name + "!"));
Méthode collect
Rassemble les éléments dans une collection, une chaîne ou une autre structure. L’opération la plus fréquente – collecter dans une List ou un Set à l’aide de Collectors.toList() et Collectors.toSet().
Méthode count
Compte le nombre d’éléments dans le flux.
long count = names.stream()
.filter(name -> name.length() > 4)
.count();
System.out.println("Prénoms de plus de 4 lettres: " + count);
Méthodes anyMatch, allMatch, noneMatch
Vérifient si une condition est vraie pour au moins un élément (anyMatch), pour tous (allMatch) ou pour aucun (noneMatch).
boolean hasShortName = names.stream()
.anyMatch(name -> name.length() < 4);
System.out.println("Y a-t-il un prénom court ? " + hasShortName);
boolean allLong = names.stream()
.allMatch(name -> name.length() > 3);
System.out.println("Tous les prénoms font plus de 3 lettres ? " + allLong);
boolean noneIvan = names.stream()
.noneMatch(name -> name.equals("Ivan"));
System.out.println("N'y a-t-il pas Ivan ? " + noneIvan);
Sortie :
Y a-t-il un prénom court ? false
Tous les prénoms font plus de 3 lettres ? true
N'y a-t-il pas Ivan ? false
5. Opérations terminales et intermédiaires : consolidons les notions
Opérations intermédiaires (filter, map, distinct, sorted, limit, skip, peek) — renvoient un nouveau Stream, on peut chaîner.
Opérations terminales (forEach, collect, count, anyMatch, allMatch, noneMatch, reduce, findFirst, findAny) — clôturent le flux, plus aucun résultat ensuite !
Exemple de chaîne :
List<String> result = users.stream()
.filter(user -> user.age > 20)
.map(user -> user.name.toUpperCase())
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(result);
Sortie :
[ANNA, IVAN, MARIYA, SERGEY]
6. Erreurs courantes lors de la transformation des collections avec Stream
Erreur n° 1 : absence de gestion des doublons de clés dans toMap
Si la collection d’origine contient des clés dupliquées et que vous utilisez Collectors.toMap() sans gestion explicite, le programme lèvera une exception. Dans ces cas, indiquez toujours une fonction de fusion :
// Conserver la dernière valeur
.toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
Erreur n° 2 : utiliser forEach au lieu de collect
Les débutants essaient parfois de « collecter » une collection avec forEach, par exemple :
List<String> list = new ArrayList<>();
names.stream().forEach(name -> list.add(name)); // Ça fonctionne, mais ce n'est pas l'approche Stream !
Il vaut mieux utiliser collect(Collectors.toList()) : c’est plus sûr et plus propre.
Erreur n° 3 : tentative de réutiliser un flux
Un flux ne peut être utilisé qu’une seule fois. Après une opération terminale (par exemple, collect, forEach), tenter de continuer avec le même Stream conduit à une IllegalStateException.
Erreur n° 4 : violation du principe « sans effets de bord »
Les opérations intermédiaires doivent être « pures » (sans modifier des variables externes). Évitez de changer quoi que ce soit hors du flux dans map ou filter.
Erreur n° 5 : ignorer l’ordre dans Set et Map
Si l’ordre des éléments est important, utilisez des collections adaptées — par exemple, LinkedHashSet, TreeMap — et indiquez le collecteur adéquat.
GO TO FULL VERSION