Differences between Collectors.toMap() and Collectors.groupingBy() to collect into a Map

前端 未结 4 1539
傲寒
傲寒 2021-02-01 16:10

I want to create a Map from a List of Points and have inside the map all entries from the list mapped with the same parentId such as

相关标签:
4条回答
  • 2021-02-01 16:39

    The following code does the stuff. Collectors.toList() is the default one, so you can skip it, but in case you want to have Map<Long, Set<Point>> Collectors.toSet() would be needed.

    Map<Long, List<Point>> map = pointList.stream()
                    .collect(Collectors.groupingBy(Point::getParentId, Collectors.toList()));
    
    0 讨论(0)
  • 2021-02-01 16:43

    TLDR :

    To collect into a Map that contains a single value by key (Map<MyKey,MyObject>), use Collectors.toMap().
    To collect into a Map that contains multiple values by key (Map<MyKey, List<MyObject>>), use Collectors.groupingBy().


    Collectors.toMap()

    By writing :

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

    The returned object will have the Map<Long,Point> type.
    Look at the Collectors.toMap() function that you are using :

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

    It returns a Collector with as result Map<K,U> where K and U are the type of return of the two functions passed to the method. In your case, Point::getParentId is a Long and c refers to a Point. Whereas the Map<Long,Point> returned when collect() is applied on.

    And this behavior is rather expected as Collectors.toMap() javadoc states :

    returns a Collector that accumulates elements into a Map whose keys and values are the result of applying the provided mapping functions to the input elements.

    But if the mapped keys contains duplicates (according to Object.equals(Object)), an IllegalStateException is thrown
    It will be probably your case as you will group the Points according to a specific property : parentId.

    If the mapped keys may have duplicates, you could use the toMap(Function, Function, BinaryOperator) overload but it will not really solve your problem as it will not group elements with the same parentId. It will just provide a way to not have two elements with the same parentId.


    Collectors.groupingBy()

    To achieve your requirement, you should use Collectors.groupingBy() which the behavior and the method declaration suits much better to your need :

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

    It is specified as :

    Returns a Collector implementing a "group by" operation on input elements of type T, grouping elements according to a classification function, and returning the results in a Map.

    The method takes a Function.
    In your case, the Function parameter is Point (the type of Stream) and you return Point.getParentId() as you want to group elements by parentId values.

    So you could write :

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

    Or with a method reference :

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

    Collectors.groupingBy() : go further

    Indeed the groupingBy() collector goes further than the actual example. The Collectors.groupingBy(Function<? super T, ? extends K> classifier) method is finally just a convenient method to store the values of the collected Map in a List.
    To store values of the Map in another thing than a List or to store the result of a specific computation , groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream) should interest you.

    For example :

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

    So beyond the asked question, you should consider groupingBy() as a flexible way to choose values that you want to store into the collected Map, what definitively toMap() is not.

    0 讨论(0)
  • 2021-02-01 16:50

    It is quite often true that a map from object.field to Collection of objects that share this field is better stored in a Multimap (Guava has a nice implementation for a multimap). If you don't NEED the multimap to be mutable (which should be the desired case), you can use

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

    If you must use a mutable map, you can either implement a collector (as demonstrated here: https://blog.jayway.com/2014/09/29/java-8-collector-for-gauvas-linkedhashmultimap/) or use a for loop (or forEach) to populate an empty, mutable multimap.

    A multimap gives you additional functionality that you normally need when you use a map from field to collection of objects sharing a field (like the count of total objects).

    A mutable multimap also makes it easier to add and remove elements to the map (without being concerned with the edge cases).

    0 讨论(0)
  • 2021-02-01 16:55

    Collectors.groupingBy is exactly what you want, it creates a Map from your input collection, creating an Entry using the Function you provide for it's key, and a List of Points with your associated key as it's value.

    Map<Long, List<Point>> pointByParentId = chargePoints.stream()
        .collect(Collectors.groupingBy(Point::getParentId));
    
    0 讨论(0)
提交回复
热议问题