Guide des collecteurs de Java 8

Guide des collectionneurs de Java 8

1. Vue d'ensemble

Dans ce didacticiel, nous allons passer en revue les collecteurs de Java 8, qui sont utilisés à la dernière étape du traitement d'unStream.

Si vous voulez en savoir plus sur l'APIStream elle-même, vérifiezthis article.

2. La méthodeStream.collect()

Stream.collect() est l’une des méthodes de terminal deStream API de Java 8. Il permet d'effectuer des opérations de repli mutables (reconditionner des éléments dans certaines structures de données et appliquer une logique supplémentaire, les concaténer, etc.) sur des éléments de données détenus dans une instanceStream.

La stratégie pour cette opération est fournie via l'implémentation de l'interfaceCollector.

3. Collectors

Toutes les implémentations prédéfinies se trouvent dans la classeCollectors. Il est courant d’utiliser l’importation statique suivante avec eux pour optimiser la lisibilité:

import static java.util.stream.Collectors.*;

ou simplement des collecteurs d'importation de votre choix:

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;

Dans les exemples suivants, nous allons réutiliser la liste suivante:

List givenList = Arrays.asList("a", "bb", "ccc", "dd");

3.1. Collectors.toList()

Le collecteurToList peut être utilisé pour collecter tous les élémentsStream dans une instanceList. La chose importante à retenir est le fait que nous ne pouvons pas supposer une implémentation particulière deListavec cette méthode. Si vous voulez avoir plus de contrôle sur cela, utilisez plutôttoCollection.

Créons une instanceStream représentant une séquence d’éléments et rassemblons-les dans une instanceList:

List result = givenList.stream()
  .collect(toList());

3.2. Collectors.toSet()

Le collecteurToSet peut être utilisé pour collecter tous les élémentsStream dans une instanceSet. La chose importante à retenir est le fait que nous ne pouvons pas supposer une implémentation particulière deSetavec cette méthode. Si nous voulons avoir plus de contrôle sur cela, nous pouvons utilisertoCollection à la place.

Créons une instanceStream représentant une séquence d’éléments et rassemblons-les dans une instanceSet:

Set result = givenList.stream()
  .collect(toSet());

UnSet ne contient pas d'éléments en double. Si notre collection contient des éléments égaux les uns aux autres, ils n'apparaissent qu'une seule fois dans lesSet résultants:

List listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb");
Set result = listWithDuplicates.stream().collect(toSet());
assertThat(result).hasSize(4);

3.3. Collectors.toCollection()

Comme vous l'avez probablement déjà remarqué, lorsque vous utilisez les collecteurstoSet and toList, vous ne pouvez faire aucune hypothèse sur leurs implémentations. Si vous souhaitez utiliser une implémentation personnalisée, vous devrez utiliser le collecteurtoCollection avec une collection fournie de votre choix.

Créons une instanceStream représentant une séquence d’éléments et rassemblons-les dans une instanceLinkedList:

List result = givenList.stream()
  .collect(toCollection(LinkedList::new))

Notez que cela ne fonctionnera pas avec des collections immuables. Dans ce cas, vous devrez soit écrire une implémentation personnalisée deCollector, soit utilisercollectingAndThen.

3.4. Collectors.toMap()

Le collecteurToMap peut être utilisé pour collecter les élémentsStream dans une instanceMap. Pour ce faire, nous devons fournir deux fonctions:

  • keyMapper

  • valueMapper

keyMapper sera utilisé pour extraire une cléMap d'un élémentStream, etvalueMapper sera utilisé pour extraire une valeur associée à une clé donnée.

Regroupons ces éléments dans unMap qui stocke les chaînes sous forme de clés et leurs longueurs sous forme de valeurs:

Map result = givenList.stream()
  .collect(toMap(Function.identity(), String::length))

Function.identity() est juste un raccourci pour définir une fonction qui accepte et renvoie la même valeur.

Que se passe-t-il si notre collection contient des éléments en double? Contrairement àtoSet,toMap ne filtre pas silencieusement les doublons. C’est compréhensible - comment devrait-il déterminer quelle valeur choisir pour cette clé?

List listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb");
assertThatThrownBy(() -> {
    listWithDuplicates.stream().collect(toMap(Function.identity(), String::length));
}).isInstanceOf(IllegalStateException.class);

Notez quetoMap n'évalue même pas si les valeurs sont également égales. S'il voit des clés en double, il lance immédiatement unIllegalStateException.

Dans de tels cas avec collision de clés, nous devrions utilisertoMap avec une autre signature:

Map result = givenList.stream()
  .collect(toMap(Function.identity(), String::length, (item, identicalItem) -> item));

Le troisième argument ici est unBinaryOperator, où nous pouvons spécifier comment nous voulons que les collisions soient gérées. Dans ce cas, nous choisirons simplement l'une de ces deux valeurs en collision car nous savons que les mêmes chaînes auront toujours la même longueur.

3.5. Collectors.c_ollectingAndThen () _

CollectingAndThen est un collecteur spécial qui permet d'effectuer une autre action sur un résultat juste après la fin de la collecte.

Recueillons les élémentsStream en une instanceList, puis convertissons le résultat en une instanceImmutableList:

List result = givenList.stream()
  .collect(collectingAndThen(toList(), ImmutableList::copyOf))

3.6. Collectors.j_oining () _

Le collecteurJoining peut être utilisé pour joindre des élémentsStream<String>.

Nous pouvons les rejoindre en faisant:

String result = givenList.stream()
  .collect(joining());

qui se traduira par:

"abbcccdd"

Vous pouvez également spécifier des séparateurs, des préfixes et des postfixes personnalisés:

String result = givenList.stream()
  .collect(joining(" "));

qui se traduira par:

"a bb ccc dd"

ou vous pouvez écrire:

String result = givenList.stream()
  .collect(joining(" ", "PRE-", "-POST"));

qui se traduira par:

"PRE-a bb ccc dd-POST"

3.7. Collectors.c_ounting () _

Counting est un simple collecteur qui permet de compter simplement tous les élémentsStream.

Maintenant nous pouvons écrire:

Long result = givenList.stream()
  .collect(counting());

3.8. Collectors.s_ummarizingDouble/Long/Int()_

SummarizingDouble/Long/Int est un collecteur qui renvoie une classe spéciale contenant des informations statistiques sur les données numériques dans unStream d'éléments extraits.

Nous pouvons obtenir des informations sur les longueurs de chaîne en faisant:

DoubleSummaryStatistics result = givenList.stream()
  .collect(summarizingDouble(String::length));

Dans ce cas, ce qui suit sera vrai:

assertThat(result.getAverage()).isEqualTo(2);
assertThat(result.getCount()).isEqualTo(4);
assertThat(result.getMax()).isEqualTo(3);
assertThat(result.getMin()).isEqualTo(1);
assertThat(result.getSum()).isEqualTo(8);

3.9. Collectors.averagingDouble/Long/Int()

AveragingDouble/Long/Int est un collecteur qui renvoie simplement une moyenne des éléments extraits.

Nous pouvons obtenir une longueur de chaîne moyenne en faisant:

Double result = givenList.stream()
  .collect(averagingDouble(String::length));

3.10. Collectors.s_ummingDouble/Long/Int()_

SummingDouble/Long/Int est un collecteur qui renvoie simplement une somme d'éléments extraits.

Nous pouvons obtenir la somme de toutes les longueurs de chaîne en faisant:

Double result = givenList.stream()
  .collect(summingDouble(String::length));

3.11. Collectors.maxBy()/minBy()

MaxBy/MinBy collectors return the biggest/the smallest element of a Stream according to a provided Comparator instance.

Nous pouvons choisir le plus gros élément en faisant:

Optional result = givenList.stream()
  .collect(maxBy(Comparator.naturalOrder()));

Notez que la valeur retournée est encapsulée dans une instanceOptional. Cela oblige les utilisateurs à repenser le cas du coin de collection vide.

3.12. Collectors.groupingBy()

Le collecteurGroupingBy est utilisé pour regrouper des objets par une propriété et stocker les résultats dans une instanceMap.

Nous pouvons les regrouper par longueur de chaîne et stocker les résultats du regroupement dans des instancesSet:

Map> result = givenList.stream()
  .collect(groupingBy(String::length, toSet()));

Cela se traduira par ce qui suit:

assertThat(result)
  .containsEntry(1, newHashSet("a"))
  .containsEntry(2, newHashSet("bb", "dd"))
  .containsEntry(3, newHashSet("ccc"));

Notez que le deuxième argument de la méthodegroupingBy est unCollector et vous êtes libre d'utiliser n'importe quelCollector de votre choix.

3.13. Collectors.partitioningBy()

PartitioningBy est un cas spécialisé degroupingBy qui accepte une instancePredicate et collecte les élémentsStream dans une instanceMap qui stocke les valeursBoolean sous forme de clés et les collections comme valeurs. Sous la clé «true», vous pouvez trouver une collection d'éléments correspondant auxPredicate donnés, et sous la clé «false», vous pouvez trouver une collection d'éléments ne correspondant pas auxPredicate donnés.

Tu peux écrire:

Map> result = givenList.stream()
  .collect(partitioningBy(s -> s.length() > 2))

Ce qui donne une carte contenant:

{false=["a", "bb", "dd"], true=["ccc"]}

3.14. Collectors.teeing()

Trouvons les nombres maximum et minimum d'unStream donné en utilisant les collecteurs que nous avons appris jusqu'à présent:

List numbers = Arrays.asList(42, 4, 2, 24);
Optional min = numbers.stream().collect(minBy(Integer::compareTo));
Optional max = numbers.stream().collect(maxBy(Integer::compareTo));
// do something useful with min and max

Ici, nous utilisons deux collectionneurs différents, puis combinons le résultat de ces deux pour créer quelque chose de significatif. Avant Java 12, afin de couvrir de tels cas d'utilisation, nous devions opérer deux fois sur lesStream donnés, stocker les résultats intermédiaires dans des variables temporaires, puis combiner ces résultats par la suite.

Heureusement, Java 12 offre un collecteur intégré qui gère ces étapes en notre nom: il suffit de fournir les deux collecteurs et la fonction de combinaison.

Puisque ce nouveau collecteurtees le flux donné vers deux directions différentes, il est appeléteeing:

numbers.stream().collect(teeing(
  minBy(Integer::compareTo), // The first collector
  maxBy(Integer::compareTo), // The second collector
  (min, max) -> // Receives the result from those collectors and combines them
));

Cet exemple est disponible sur GitHub dans le projetcore-java-12.

4. Collectionneurs personnalisés

Si vous souhaitez écrire votre implémentation Collector, vous devez implémenter l'interface Collector et spécifier ses trois paramètres génériques:

public interface Collector {...}
  1. T - le type d'objets qui seront disponibles pour la collecte,

  2. A - le type d'un objet accumulateur mutable,

  3. R - le type d'un résultat final.

Écrivons un exemple de collecteur pour collecter des éléments dans une instanceImmutableSet. Nous commençons par spécifier les bons types:

private class ImmutableSetCollector
  implements Collector, ImmutableSet> {...}

Puisque nous avons besoin d'une collection modifiable pour la gestion des opérations de collecte internes, nous ne pouvons pas utiliserImmutableSet pour cela; nous devons utiliser une autre collection mutable ou toute autre classe qui pourrait temporairement accumuler des objets pour nous. Dans ce cas, nous allons continuer avec unImmutableSet.Builder et maintenant nous devons implémenter 5 méthodes:

  • Fournisseur >supplier ()

  • BiConsumer , T>accumulator ()

  • BinaryOperator >combiner ()

  • Fonction , ImmutableSet >finisher ()

  • Définir characteristics ()

La méthodeThe supplier() renvoie une instanceSupplier qui génère une instance d'accumulateur vide, donc, dans ce cas, nous pouvons simplement écrire:

@Override
public Supplier> supplier() {
    return ImmutableSet::builder;
}

La méthodeThe accumulator() renvoie une fonction qui est utilisée pour ajouter un nouvel élément à un objetaccumulator existant, alors utilisons simplement la méthodeadd deBuilder.

@Override
public BiConsumer, T> accumulator() {
    return ImmutableSet.Builder::add;
}

La méthodeThe combiner() renvoie une fonction utilisée pour fusionner deux accumulateurs ensemble:

@Override
public BinaryOperator> combiner() {
    return (left, right) -> left.addAll(right.build());
}

La méthodeThe finisher() renvoie une fonction qui est utilisée pour convertir un accumulateur en type de résultat final, donc dans ce cas, nous utiliserons simplement la méthodebuild deBuilder:

@Override
public Function, ImmutableSet> finisher() {
    return ImmutableSet.Builder::build;
}

La méthodeThe characteristics() est utilisée pour fournir à Stream des informations supplémentaires qui seront utilisées pour les optimisations internes. Dans ce cas, on ne fait pas attention à l'ordre des éléments dans unSet pour que l'on utiliseCharacteristics.UNORDERED. Pour obtenir plus d’informations à ce sujet, consultez JavaDoc deCharacteristics.

@Override public Set characteristics() {
    return Sets.immutableEnumSet(Characteristics.UNORDERED);
}

Voici l'implémentation complète avec l'utilisation:

public class ImmutableSetCollector
  implements Collector, ImmutableSet> {

@Override
public Supplier> supplier() {
    return ImmutableSet::builder;
}

@Override
public BiConsumer, T> accumulator() {
    return ImmutableSet.Builder::add;
}

@Override
public BinaryOperator> combiner() {
    return (left, right) -> left.addAll(right.build());
}

@Override
public Function, ImmutableSet> finisher() {
    return ImmutableSet.Builder::build;
}

@Override
public Set characteristics() {
    return Sets.immutableEnumSet(Characteristics.UNORDERED);
}

public static  ImmutableSetCollector toImmutableSet() {
    return new ImmutableSetCollector<>();
}

et ici en action:

List givenList = Arrays.asList("a", "bb", "ccc", "dddd");

ImmutableSet result = givenList.stream()
  .collect(toImmutableSet());

5. Conclusion

Dans cet article, nous avons exploré en profondeur lesCollectors de Java 8 et montré comment en implémenter un. Assurez-vous decheck one of my projects which enhances the capabilities of parallel processing in Java.

Tous les exemples de code sont disponibles sur lesGitHub. Vous pouvez lire des articles plus intéressantson my site.