使用Collectors.groupingBy创建复杂对象

jpf*_*ire 3 java lambda mapreduce java-8

oracle的简化教程中,可以使用Stream.collect计算流中的平均年龄:

Averager averageCollect = roster.stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .map(Person::getAge)
    .collect(Averager::new, Averager::accept, Averager::combine);
Run Code Online (Sandbox Code Playgroud)

但是如果想要创建一个Map<Person.Sex, Averager>使用lambda + groupingBy而不是简单的平均值,如教程结尾所示:

Map<Person.Sex, Integer> totalAgeByGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(
                Person::getGender,                      
                Collectors.reducing(
                    0,
                    Person::getAge,
                    Integer::sum)));
Run Code Online (Sandbox Code Playgroud)

Stu*_*rks 9

是的,这有点微妙.要更改映射中的值,您必须更改groupingBy调用的下游收集器.在这种情况下,您必须应用嵌套的下游收集器.

流开始于人,我们希望Averagers作为地图的值.要从Person到Averager,我们首先需要将每个Person映射到它们的年龄(一个int),然后将整数提供给Averager.

我们首先对性别进行分组,因此需要处理与每个性别相对应的人员.下一步是使用mapping收集器作为下游收集器将人员映射到他们的年龄groupingBy.

现在你有了年龄,你想Averager为每个组创建实例.本Averager教程中的类已经有了收集器方法 - 它支持适用于传递给Stream.collect前面示例中的调用的供应商,累加器和组合器函数.相反的Stream.collect,虽然,我们要使用的Averager方法,形成了一个嵌套的下游收集器mapping,我们刚刚建立收集器.给定这些方法,创建收集器的便捷方法是使用Collector.of.

你可能会尝试这样的事情:

Map<Person.Sex, Averager> map =
    roster.stream()
          .collect(groupingBy(Person::getGender,
              mapping(Person::getAge,
                  Collector.of(Averager::new, Averager::accept, Averager::combine))));
Run Code Online (Sandbox Code Playgroud)

可是等等!这不起作用!你得到一个相当讨厌的编译失败,看起来像这样:

error: no suitable method found for of(Averager::new,Averager::accept,Averager::combine)
                      Collector.of(Averager::new, Averager::accept, Averager::combine))));
method Collector.<T#1,R#1>of(Supplier<R#1>,BiConsumer<R#1,T#1>,BinaryOperator<R#1>,Characteristics...) is not applicable
  (cannot infer type-variable(s) T#1,R#1
    (argument mismatch; bad return type in method reference
      void cannot be converted to R#1))
method Collector.<T#2,A,R#2>of(Supplier<A>,BiConsumer<A,T#2>,BinaryOperator<A>,Function<A,R#2>,Characteristics...) is not applicable
  (cannot infer type-variable(s) T#2,A,R#2
    (argument mismatch; bad return type in method reference
      void cannot be converted to A))
where T#1,R#1,T#2,A,R#2 are type-variables:
T#1 extends Object declared in method <T#1,R#1>of(Supplier<R#1>,BiConsumer<R#1,T#1>,BinaryOperator<R#1>,Characteristics...)
R#1 extends Object declared in method <T#1,R#1>of(Supplier<R#1>,BiConsumer<R#1,T#1>,BinaryOperator<R#1>,Characteristics...)
T#2 extends Object declared in method <T#2,A,R#2>of(Supplier<A>,BiConsumer<A,T#2>,BinaryOperator<A>,Function<A,R#2>,Characteristics...)
A extends Object declared in method <T#2,A,R#2>of(Supplier<A>,BiConsumer<A,T#2>,BinaryOperator<A>,Function<A,R#2>,Characteristics...)
R#2 extends Object declared in method <T#2,A,R#2>of(Supplier<A>,BiConsumer<A,T#2>,BinaryOperator<A>,Function<A,R#2>,Characteristics...)
Run Code Online (Sandbox Code Playgroud)

确认!实际上,一旦你超越了20行错误信息的恐吓因素,冷静下来,并阅读它想说的内容,它实际上非常清楚编译器试图做什么以及它是如何失败的.您还必须非常仔细地查看API.

本教程定义了在Averager方法中使用的三种方法Stream.collect,它们具有以下签名(为简洁起见省略了泛型):

collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner)
Run Code Online (Sandbox Code Playgroud)

请注意,该combiner方法是一个BiConsumer.但是,该Collector.of方法定义如下:

of(Supplier supplier, BiConsumer accumulator, BinaryOperator combiner, Collector.Characteristics... characteristics)
Run Code Online (Sandbox Code Playgroud)

(characteristics论证是varargs,它与我们无关,所以我们可以省略它.)

这里要注意的是组合器Collector.of是一个BinaryOperator而不是一个BiConsumer.该BinaryOperator版本与版本完全相同BiConsumer,但此外,它返回组合结果.要解决这个问题,我们只需更改combine()方法Averager而不是返回void,我们添加一个return this语句:

public Averager combine(Averager other) {
    total += other.total;
    count += other.count;
    return this;
}
Run Code Online (Sandbox Code Playgroud)

请注意,此combine()方法的版本仍适合作为第三个参数传递给Stream.collect.A BinaryOperatorBiConsumer需要的地方是兼容的; 简单地忽略返回值.

完成此更改后Averager.combine,此代码(与上面相同)应该有效:

Map<Person.Sex, Averager> map =
    roster.stream()
          .collect(groupingBy(Person::getGender,
              mapping(Person::getAge,
                  Collector.of(Averager::new, Averager::accept, Averager::combine))));
Run Code Online (Sandbox Code Playgroud)