使用Collectors.toMap()和Collectors.groupingBy()收集到Map中的区别

44
我想从一个PointList中创建一个Map,并在地图内将列表中的所有条目与相同的父ID映射,例如:Map<Long,List<Point>>
我使用了Collectors.toMap(),但它无法编译:
Map<Long, List<Point>> pointByParentId = chargePoints.stream()
    .collect(Collectors.toMap(Point::getParentId, c -> c));

9
听起来你正在寻找Collectors.groupingBy - Louis Wasserman
4
我同意LouisWasserman对问题的解释,但你应该在问题中更清晰地表达。提供一些示例输入和输出,或甚至是一个最小完整可验证示例 - Robin Topper
4个回答

105

TLDR :

若要将单个值按键(Map<MyKey,MyObject>)收集到Map中,请使用Collectors.toMap()

若要将多个值按键(Map<MyKey, List<MyObject>>)收集到Map中,请使用Collectors.groupingBy()


Collectors.toMap()

写下以下代码:

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

返回的对象将具有Map<Long,Point>类型。
请查看您正在使用的Collectors.toMap()函数:
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper)

它返回一个Collector,其结果为Map<K,U>,其中KU是传递给该方法的两个函数的返回类型。 在您的情况下,Point::getParentId是一个Long类型,c是一个Point类型。 当应用于collect()时,返回Map<Long,Point>
而这种行为是预期的,因为Collectors.toMap() javadoc声明如下:

返回一个Collector,将元素累积到Map中,其键和值是将所提供的映射函数应用于输入元素的结果。

如果映射键包含重复项(根据Object.equals(Object)的结果),则会抛出IllegalStateException
这可能是您的情况,因为您将根据特定属性对Point进行分组:parentId
如果映射键可能存在重复项,则可以使用toMap(Function, Function, BinaryOperator)重载,但它不会真正解决您的问题,因为它不会将具有相同parentId的元素分组。它只提供了一种避免具有相同parentId的两个元素的方法。

Collectors.groupingBy()

要实现您的要求,应使用Collectors.groupingBy(),其行为和方法声明更适合您的需要:
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) 

这个方法的作用是:

返回一个Collector,该Collector实现了对类型为T的输入元素执行“group by”操作,根据分类函数对元素进行分组,并将结果返回到Map中。

该方法接受一个Function参数。
在您的情况下,Function参数是Point(Stream的type),您需要返回Point.getParentId(),以按parentId值对元素进行分组。

因此,您可以编写以下代码:

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

或者使用方法引用:

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

Collectors.groupingBy():更进一步

实际上,groupingBy()收集器比实际示例更进一步。 Collectors.groupingBy(Function<? super T, ? extends K> classifier) 方法最终只是将收集到的Map值存储在List中的便捷方法。
若要将Map的值存储在除List之外的其他地方或存储特定计算的结果,则应该使用groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream)

例如:

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

除了被问到的问题之外,你还应该考虑 groupingBy() 作为一种灵活的方式来选择你想要存储到收集的 Map 中的值,这绝对不是 toMap() 所能达到的。


2
@TimSchwalbe 在 [so] 上,我们不说谢谢,而是通过点赞和接受答案来表达感谢之意。 - xenteros
1
寻找关于toMap()的信息!打个招呼! - GhostCat

4

Collectors.groupingBy就是你想要的,它从输入的集合中创建一个Map,使用你提供的Function作为它的键来创建一个Entry,并将带有你关联键的点的列表作为它的值。

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

文档:https://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html#groupingBy-java.util.function.Function- - Matt C.

4
以下代码执行操作。Collectors.toList() 是默认值,所以你可以跳过它,但是如果你想要一个 Map>,就需要使用 Collectors.toSet()。
Map<Long, List<Point>> map = pointList.stream()
                .collect(Collectors.groupingBy(Point::getParentId, Collectors.toList()));

0

通常情况下,从对象.字段到共享该字段的对象集合的映射最好存储在Multimap中(Guava有一个不错的Multimap实现)。 如果您不需要让Multimap可变(这应该是期望的情况),您可以使用

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

如果您必须使用可变映射,您可以实现一个收集器(如此处所示:https://blog.jayway.com/2014/09/29/java-8-collector-for-gauvas-linkedhashmultimap/),或使用for循环(或forEach)来填充一个空的、可变的多重映射。

多重映射为您提供了额外的功能,通常在您使用从字段到共享字段对象集合的映射时需要这些功能(例如总对象计数)。

可变多重映射还使添加和删除元素更容易(无需考虑边缘情况)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接