Java 8中的stream.collect( ... groupingBy ( ... mapping( ... reducing )))方法中的reducing BinaryOperator用法

10

我使用groupingBymappingreducing尝试解决以下问题:Elegantly create map with object fields as key/value from object stream in Java 8。简单概括一下,目标是将年龄作为键,人的爱好作为Set值,生成一个映射。

我想到的其中一种解决方案(并不理想,但这不是重点)有奇怪的行为。

以以下列表作为输入:

List<Person> personList = Arrays.asList(
     new Person(/* name */ "A", /* age */ 23, /* hobbies */ asList("a")),
     new Person("BC", 24, asList("b", "c")),
     new Person("D", 23, asList("d")),
     new Person("E", 23, asList("e"))
);

以及以下解决方案:

Collector<List<String>, ?, Set<String>> listToSetReducer = Collectors.reducing(new HashSet<>(), HashSet::new, (strings, strings2) -> {
  strings.addAll(strings2);
  return strings;
});
Map<Integer, Set<String>> map = personList.stream()
                                          .collect(Collectors.groupingBy(o -> o.age, 
                                                                         Collectors.mapping(o -> o.hobbies, listToSetReducer)));
System.out.println("map = " + map);

我收到了:

map = {23=[a, b, c, d, e], 24=[a, b, c, d, e]}

显然不是我预期的结果。我更倾向于期望这个:

map = {23=[a, d, e], 24=[b, c]}

现在,如果我只是交换二元运算符(减少收集器)的(strings, strings2)的顺序为(strings2, strings),我就会得到预期的结果。那么我错过了什么?

我是否误解了reducing收集器?或者我错过了哪个文档片段,使得我的使用不像预期的那样工作?

Java版本为1.8.0_121,如果有影响的话。

1个回答

20

Reduction 不应修改传入的对象。在您的情况下,您正在修改传入的 HashSet,该集合应该是标识值并返回它,因此所有组的结果都将具有相同的 HashSet 实例,其中包含所有值。

您需要的是一个可变归约,可以通过 Collector.of(…) 实现,就像已经使用预构建收集器 Collectors.toList()Collectors.toSet() 等实现一样。

Map<Integer, Set<String>> map = personList.stream()
    .collect(Collectors.groupingBy(o -> o.age,
        Collector.of(HashSet::new, (s,p) -> s.addAll(p.hobbies), (s1,s2) -> {
            s1.addAll(s2);
            return s1;
        })));

我们需要自定义收集器的原因是Java 8没有flatMapping收集器,而Java 9将要引入它。有了这个,解决方案看起来像是:

Map<Integer, Set<String>> map = personList.stream()
    .collect(Collectors.groupingBy(o -> o.age,
        Collectors.flatMapping(p -> p.hobbies.stream(), Collectors.toSet())));

有趣...起初我甚至试图在Collectors中找到flatMapping(或者应用hobbies.stream()并与之一起工作)。好知道它即将推出。哦,亲爱的,我把组合器和运算符搞混了。所以,交换参数只是巧合地得到了正确的结果。感谢您的澄清! - Roland
2
在顺序上下文中,缩减函数将始终被评估为f(previous, next),其中previous将是第一次评估的身份值和后续评估的先前结果。因此,(a,b)->a将始终以身份值结束,而(a,b)->b将使用映射器函数创建的新集合。但在并行评估中,两个参数都可以是先前部分评估的结果,由于部分结果可能为空,因此任一参数都可能是身份值,因此使用第二个参数不是可靠的修复方法。 - Holger

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