Nous avons déjà utilisé la méthode collect() à plusieurs reprises pour combiner les éléments renvoyés par Stream dans une ArrayList. Il s'agit d'une opération de réduction, utile pour convertir une collection en un autre type (généralement une collection mutable). La fonction collect(), si elle est utilisée en combinaison avec certaines méthodes de la classe d'outils Collectors, peut être très pratique, comme nous le présenterons dans cette section.
Continuons à utiliser la liste Person précédente comme exemple pour voir ce que la méthode collect() peut faire. Supposons que nous souhaitions rechercher toutes les personnes âgées de plus de 20 ans dans la liste d'origine. Voici la version implémentée en utilisant la mutabilité et la méthode forEach() :
Copiez le code comme suit :
List<Person> oldThan20 = new ArrayList<>(); people.stream()
.filter(personne -> personne.getAge() > 20)
.forEach(personne -> OldThan20.add(person)); System.out.println("Personnes de plus de 20 ans : " + OldThan20);
Nous utilisons la méthode filter() pour filtrer toutes les personnes de plus de 20 ans de la liste. Ensuite, dans la méthode forEach, nous ajoutons des éléments à une ArrayList préalablement initialisée. Jetons d'abord un coup d'œil à la sortie de ce code, puis reconstruisons-le plus tard.
Copiez le code comme suit :
Personnes de plus de 20 ans : [Sara - 21 ans, Jane - 21 ans, Greg - 35 ans]
Le résultat du programme est correct, mais il reste encore un petit problème. Premièrement, l’ajout d’éléments à une collection est une opération de bas niveau : elle est impérative et non déclarative. Si nous voulons transformer cette itération en concurrente, nous devons prendre en compte les problèmes de sécurité des threads - la variabilité rend la parallélisation difficile. Heureusement, ce problème peut être facilement résolu en utilisant la méthode collect(). Voyons comment cela est réalisé.
La méthode collect() accepte un Stream et les collecte dans un conteneur de résultats. Pour ce faire, il doit connaître trois choses :
+ Comment créer un conteneur de résultats (par exemple, en utilisant la méthode ArrayList::new) + Comment ajouter un seul élément au conteneur (par exemple, en utilisant la méthode ArrayList::add) + Comment fusionner un ensemble de résultats dans un autre (par exemple, en utilisant la méthode ArrayList: :addAll)
Le dernier élément n'est pas requis pour les opérations en série ; le code est conçu pour prendre en charge les opérations en série et en parallèle.
Nous fournissons ces opérations à la méthode collect et la laissons collecter le flux filtré.
Copiez le code comme suit :
Liste<Personne> plus âgéeThan20 =
personnes.stream()
.filter(personne -> personne.getAge() > 20)
.collect (ArrayList :: new, ArrayList :: add, ArrayList :: addAll);
System.out.println("Personnes âgées de plus de 20 ans : " + OldThan20);
Le résultat de ce code est le même que précédemment, mais il y a de nombreux avantages à l’écrire de cette façon.
Tout d'abord, notre méthode de programmation est plus ciblée et expressive, transmettant clairement l'objectif de collecter les résultats dans une ArrayList. Le premier paramètre de collect() est une usine ou un producteur, et le paramètre suivant est une opération utilisée pour collecter des éléments.
Deuxièmement, puisque nous n’effectuons pas de modifications explicites dans le code, nous pouvons facilement effectuer cette itération en parallèle. Nous laissons la bibliothèque sous-jacente gérer les modifications, et elle s'occupera des problèmes de coordination et de sécurité des threads, même si l'ArrayList elle-même n'est pas thread-safe - beau travail.
Si les conditions le permettent, la méthode collect() peut ajouter des éléments à différentes sous-listes en parallèle, puis les fusionner dans une grande liste de manière thread-safe (le dernier paramètre est utilisé pour l'opération de fusion).
Nous avons vu qu'il y a de nombreux avantages à utiliser la méthode collect() par rapport à l'ajout manuel d'éléments à une liste. Regardons une version surchargée de cette méthode - c'est plus simple et plus pratique - elle prend un Collector comme paramètre. Ce Collector est une interface qui inclut des producteurs, des additionneurs et des combinateurs. Dans les versions précédentes, ces opérations étaient transmises aux méthodes en tant que paramètres indépendants. L'utilisation de Collector est plus simple et peut être réutilisée. La classe d'outils Collectors fournit une méthode toList qui peut générer une implémentation de Collector pour ajouter des éléments à une ArrayList. Modifions le code précédent et utilisons la méthode collect().
Copiez le code comme suit :
Liste<Personne> plus âgéeThan20 =
personnes.stream()
.filter(personne -> personne.getAge() > 20)
.collect(Collectors.toList());
System.out.println("Personnes âgées de plus de 20 ans : " + OldThan20);
Une version concise de la méthode collect() de la classe d'outils Collectors est utilisée, mais elle peut être utilisée de plusieurs manières. Il existe plusieurs méthodes différentes dans la classe d'outils Collectors pour effectuer différentes opérations de collecte et d'addition. Par exemple, en plus de la méthode toList(), il existe également la méthode toSet(), qui peut être ajoutée à un Set, la méthode toMap(), qui peut être utilisée pour collecter dans un ensemble clé-valeur, et la méthode toList(), qui peut être utilisée pour collecter dans un ensemble clé-valeur, et méthode join(), qui peut être fusionnée en une chaîne. Nous pouvons également combiner des méthodes telles que mapping(), collectingAndThen(), minBy(), maxBy() et groupingBy() pour les utiliser.
Utilisons la méthode groupingBy() pour regrouper les personnes par âge.
Copiez le code comme suit :
Map<Integer, List<Person>> peopleByAge =
personnes.stream()
.collect(Collectors.groupingBy(Person::getAge));
System.out.println("Regroupé par âge : " + peopleByAge);
Appelez simplement la méthode collect() pour terminer le regroupement. groupingBy() accepte une expression lambda ou une référence de méthode - c'est ce qu'on appelle une fonction de classification - et renvoie la valeur d'un certain attribut de l'objet qui doit être regroupé. Selon la valeur renvoyée par notre fonction, les éléments du contexte appelant sont placés dans un certain groupe. Les résultats du regroupement peuvent être vus dans le résultat :
Copiez le code comme suit :
Regroupé par âge : {35=[Greg - 35], 20=[John - 20], 21=[Sara - 21, Jane - 21]}
Les gens ont été regroupés par âge.
Dans l'exemple précédent, nous avons regroupé les personnes par âge. Une variante de la méthode groupingBy() peut regrouper selon plusieurs conditions. La méthode simple groupingBy() utilise un classificateur pour collecter des éléments. Le collecteur général groupingBy() peut spécifier un collecteur pour chaque groupe. En d’autres termes, les éléments passeront par différents classificateurs et collections au cours du processus de collecte, comme nous le verrons ci-dessous.
En continuant avec l'exemple ci-dessus, cette fois, au lieu de regrouper par âge, nous récupérons simplement les noms des personnes et les trions par âge.
Copiez le code comme suit :
Map<Integer, List<String>> nameOfPeopleByAge =
personnes.stream()
.collecter(
groupingBy(Person::getAge, mapping(Person::getName, toList())));
System.out.println("Personnes regroupées par âge : " + nameOfPeopleByAge);
Cette version de groupingBy() accepte deux paramètres : le premier est l'âge, qui est la condition de regroupement, et le second est un collecteur, qui est le résultat renvoyé par la fonction mapping(). Ces méthodes proviennent toutes de la classe d'outils Collectors et sont importées statiquement dans ce code. La méthode mapping() accepte deux paramètres, l'un est l'attribut utilisé pour le mappage et l'autre est l'endroit où les objets doivent être collectés, comme une liste ou un ensemble. Jetons un coup d'œil au résultat du code ci-dessus :
Copiez le code comme suit :
Personnes regroupées par âge : {35=[Greg], 20=[John], 21=[Sara, Jane]}
Comme vous pouvez le constater, les noms des personnes ont été regroupés par âge.
Regardons à nouveau une opération de combinaison : regroupez par la première lettre du nom, puis sélectionnez la personne la plus âgée de chaque groupe.
Copiez le code comme suit :
Comparator<Person> byAge = Comparator.comparing(Person::getAge);
Map<Character, Facultatif<Person>> plus ancienPersonOfEachLetter =
personnes.stream()
.collect(groupingBy(personne -> personne.getName().charAt(0),
réduction (BinaryOperator.maxBy (byAge))));
System.out.println("Personne la plus âgée de chaque lettre :");
System.out.println(oldestPersonOfEachLetter);
Nous avons d’abord classé les noms par ordre alphabétique. Pour y parvenir, nous passons une expression lambda comme premier paramètre de groupingBy(). Cette expression lambda est utilisée pour renvoyer la première lettre du nom pour le regroupement. Le deuxième paramètre n'est plus mapping(), mais effectue une opération de réduction. Au sein de chaque groupe, il utilise la méthode maxBy() pour dériver l'élément le plus ancien de tous les éléments. La syntaxe semble un peu lourde en raison des nombreuses opérations qu'elle combine, mais le tout se lit comme ceci : regroupez par première lettre du nom, puis descendez jusqu'à l'aîné du groupe. Considérez le résultat de ce code, qui répertorie la personne la plus âgée dans un groupe de noms commençant par une lettre donnée.
Copiez le code comme suit :
Personne la plus âgée de chaque lettre :
{S=Facultatif[Sara - 21], G=Facultatif[Greg - 35], J=Facultatif[Jane - 21]}
Nous avons déjà expérimenté la puissance de la méthode collect() et de la classe utilitaire Collectors. Dans la documentation officielle de votre IDE ou JDK, prenez le temps d'étudier la classe d'outils Collectors et de vous familiariser avec les différentes méthodes qu'elle propose. Nous utiliserons ensuite des expressions lambda pour implémenter certains filtres.