web-dev-qa-db-fra.com

Différences entre Collectors.toMap () et Collectors.groupingBy () à rassembler dans une carte

Je veux créer une Map à partir d'une List sur Points et avoir dans la carte toutes les entrées de la liste mappées avec le même parentId tel que Map<Long, List<Point>>.
J'ai utilisé Collectors.toMap() mais il ne compile pas:

Map<Long, List<Point>> pointByParentId = chargePoints.stream()
    .collect(Collectors.toMap(Point::getParentId, c -> c));
13
Tim Schwalbe

TLDR: 

Pour collecter une Map contenant une valeur unique par clé (Map<MyKey,MyObject>), utilisez Collectors.toMap() .
Pour rassembler dans une Map qui contient plusieurs valeurs par clé (Map<MyKey, List<MyObject>>), utilisez Collectors.groupingBy() .


Collectors.toMap ()

En écrivant : 

chargePoints.stream().collect(Collectors.toMap(Point::getParentId, c -> c));

L'objet retourné aura le type Map<Long,Point>.
Regardez la fonction Collectors.toMap() que vous utilisez:

Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper)

Il retourne une Collector avec comme résultat Map<K,U>K et U sont le type de retour des deux fonctions passées à la méthode . Dans votre cas, Point::getParentId est un long et c se réfère à un Point. Attendu que le Map<Long,Point> retourné lorsque collect() est appliqué sur.

Et ce comportement est plutôt attendu comme Collectors.toMap () javadoc déclare:

retourne une Collector qui accumule des éléments dans une Map dont les clés et les valeurs sont le résultat de l'application des fonctions de mappage fournies à l'entrée éléments.

Mais si les clés mappées contiennent des doublons (selon Object.equals(Object)), une IllegalStateException est levée
Ce sera probablement votre cas puisque vous regrouperez les Points selon une propriété spécifique: parentId

Si les clés mappées peuvent avoir des doublons, vous pouvez utiliser la surcharge toMap(Function, Function, BinaryOperator) mais cela ne résoudra pas vraiment votre problème car cela ne groupera pas les éléments avec la même parentId. Cela fournira juste un moyen de ne pas avoir deux éléments avec le même parentId


Collectors.groupingBy ()

Pour répondre à vos besoins, vous devez utiliser Collectors.groupingBy() dont le comportement et la déclaration de méthode conviennent mieux à votre besoin:

public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) 

Il est spécifié comme:

Retourne un collecteur implémentant une opération "groupe par" sur l'entrée éléments de type T, regroupement d’éléments selon une classification fonction et renvoyer les résultats dans une carte.

La méthode prend une Function.
Dans votre cas, le paramètre Function est Point (le type de Stream) et vous renvoyez Point.getParentId() si vous souhaitez regrouper les éléments par les valeurs parentId.

Pour que vous puissiez écrire:

Map<Long, List<Point>> pointByParentId = 
                       chargePoints.stream()
                                   .collect(Collectors.groupingBy( p -> p.getParentId())); 

Ou avec une référence de méthode: 

Map<Long, List<Point>> pointByParentId = 
                       chargePoints.stream()
                                   .collect(Collectors.groupingBy(Point::getParentId));

Collectors.groupingBy (): aller plus loin

En effet, le collecteur groupingBy() va plus loin que l'exemple actuel. La méthode Collectors.groupingBy(Function<? super T, ? extends K> classifier) n’est finalement qu’une méthode pratique pour stocker les valeurs de la Map collectée dans une List.
Pour stocker les valeurs de Map dans autre chose que List ou pour stocker le résultat d'un calcul spécifique, groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream) devrait vous intéresser. 

Par exemple : 

Map<Long, Set<Point>> pointByParentId = 
                       chargePoints.stream()
                                   .collect(Collectors.groupingBy(Point::getParentId, toSet()));

Donc, au-delà de la question posée, vous devriez considérer groupingBy() comme un moyen flexible de choisir les valeurs que vous souhaitez stocker dans la Map collectée, ce que toMap() ne correspond définitivement pas. 

39
davidxxx

Collectors.groupingBy est exactement ce que vous voulez, il crée une carte à partir de votre collection d'entrées, créant une entrée à l'aide de la Function que vous fournissez pour sa clé et une liste de points avec la valeur de votre clé associée.

Map<Long, List<Point>> pointByParentId = chargePoints.stream()
    .collect(Collectors.groupingBy(Point::getParentId));
3
Patrick

Le code suivant fait le truc. Collectors.toList() est la valeur par défaut, vous pouvez donc l'ignorer, mais au cas où vous souhaiteriez que Map<Long, Set<Point>>Collectors.toSet() soit nécessaire.

Map<Long, List<Point>> map = pointList.stream()
                .collect(Collectors.groupingBy(Point::getParentId, Collectors.toList()));
3
xenteros

Il est souvent vrai qu'une carte de object.field à Collection d'objets partageant ce champ est mieux stockée dans une carte multiple (Guava a une implémentation de Nice pour une carte multiple) . Si vous N'AVEZ PAS BESOIN de la carte multiple mutable (ce qui devrait être le cas désiré), vous pouvez utiliser

Multimaps.index(chargePoints, Point::getParentId);

Si vous devez utiliser une carte mutable, vous pouvez implémenter un collecteur (comme illustré ici: https://blog.jayway.com/2014/09/29/Java-8-collector-for-gauvas-linkedhashmultimap/ ) ou utilisez une boucle for (ou forEach) pour remplir une carte multimodale vide et modifiable.

Une carte multiple vous offre des fonctionnalités supplémentaires dont vous avez normalement besoin lorsque vous utilisez une carte d'un champ à une collection d'objets partageant un champ (comme le nombre total d'objets).

Une carte multimodale modifiable facilite également l'ajout et la suppression d'éléments à la carte (sans se préoccuper des cas Edge).

0
Roy Shahaf