聚合操作 ”部分,描述了以下操作流程,该流程计算了roster集合中所有男性成员的平均年龄:

  1. double average = roster
  2. .stream()
  3. .filter(p -> p.getGender() == Person.Sex.MALE)
  4. .mapToInt(Person::getAge)
  5. .average()
  6. .getAsDouble();

JDK包含许多终端操作(如 averagesumminmax,和 count),通过组合流的内容,返回一个值。这些操作称为归约操作。JDK还包含返回集合,而不是单个值的归约操作。许多归约运算执行特定任务,例如查找值的平均值或将元素分组为类别。但是,JDK为您提供了通用的归约操作 reducecollect,本节将对此进行详细说明。
本节涵盖以下主题:

您可以在示例ReductionExamples中找到本节中描述的代码摘录。

Stream.reduce方法

Stream.reduce方法是泛型的归约运算。考虑以下管道,该管道计算集合roster中男性成员的年龄总和。它使用 Stream.sum归约运算:

  1. Integer totalAge = roster
  2. .stream()
  3. .mapToInt(Person::getAge)
  4. .sum();

将其与以下管道进行比较,该管道使用Stream.reduce运算来计算相同的值:

  1. Integer totalAgeReduce = roster
  2. .stream()
  3. .map(Person::getAge)
  4. .reduce(
  5. 0,
  6. (a, b) -> a + b);

reduce本示例中的操作采用两个参数:

  • identity:如果流中没有元素,则identity元素既是reduce的初始值,也是默认结果。在此示例中,标识元素为0;。这是年龄总和的初始值,如果roster集合中不存在成员,则为默认值。
  • accumulator:累加器函数有两个参数:约简的部分结果(在此示例中,是到目前为止所有已处理整数的总和)和流的下一个元素(在此示例中,是整数)。它返回一个新的部分结果。在此示例中,accumulator函数是一个lambda表达式,该表达式将两个Integer值相加并返回一个Integer值:

    1. (a, b) -> a + b

    reduce操作始终返回新值。但是,累加器函数每次处理流的元素时也会返回一个新值。假设您想将流的元素简化为一个更复杂的对象,例如集合。这可能会妨碍您的应用程序的性能。如果您的reduce操作涉及将元素添加到集合中,则每次累加器函数处理一个元素时,它都会创建一个包含该元素的新集合,这效率很低。相反,对您而言,更新现有集合会更有效。您可以使用下一部分介绍的Stream.collect方法来执行此操作 。

    Stream.collect方法

    reduce方法(该方法在处理元素时总是会创建一个新值)不同,该 collect方法会修改或变异现有值。
    考虑如何查找流中的平均值。您需要两个数据:值的总数和这些值的总和。但是,与reduce方法和所有其他归约方法一样,collect方法仅返回一个值。您可以创建一个包含成员变量的新数据类型,该成员变量跟踪值的总数和这些值的总和,例如以下类 Averager

    1. class Averager implements IntConsumer
    2. {
    3. private int total = 0;
    4. private int count = 0;
    5. public double average() {
    6. return count > 0 ? ((double) total)/count : 0;
    7. }
    8. public void accept(int i) { total += i; count++; }
    9. public void combine(Averager other) {
    10. total += other.total;
    11. count += other.count;
    12. }
    13. }

    以下管道使用Averager类和collect方法来计算所有男性成员的平均年龄: ```java Averager averageCollect = roster.stream() .filter(p -> p.getGender() == Person.Sex.MALE) .map(Person::getAge) .collect(Averager::new, Averager::accept, Averager::combine);

System.out.println(“Average age of male members: “ + averageCollect.average());

  1. 本示例中的`collect`操作采用三个参数:
  2. - `supplier`:供应商具有工厂职能;它构造了新的实例。对于`collect`操作,它将创建结果容器的实例。在此示例中,它是`Averager`类的新实例。
  3. - `accumulator`:累加器功能将流元素合并到结果容器中。在此示例中,通过将`count`变量加1并将流元素的值添加到`total`成员变量来修改`Averager`结果容器,该流元素的值是代表男性成员年龄的整数。
  4. - `combiner`:组合器函数接受两个结果容器并合并其内容。在这个例子中,它通过将另一个`Averager`实例的`count`成员变量增加`count`变量,并将另一个`Averager`实例的`total`成员变量的值添加到`total`成员变量,来修改`Averager`结果容器。
  5. 请注意以下几点:
  6. - 供应商是一个lambda表达式(或方法引用),与诸如`reduce`操作中的identity元素之类的值相反。
  7. - 累加器和组合器函数不返回值。
  8. - 您可以对并行流使用`collect`操作;有关更多信息,请参见[并行](https://www.yuque.com/ican/canjava/lqrn2a)一节 。(如果使用并行流运行`collect`方法,则只要合并器函数创建新对象(例如本示例中的`Averager`对象),JDK都会创建一个新线程。因此,您不必担心同步。)
  9. 尽管JDK为您提供了计算流中元素平均值的`average`操作,但是如果需要从流中的元素中计算多个值,则可以使用`collect`操作和一个自定义类。<br />`collect`操作最适合集合。以下示例通过`collect`操作将男性成员的姓名放入集合中:
  10. ```java
  11. List<String> namesOfMaleMembersCollect = roster
  12. .stream()
  13. .filter(p -> p.getGender() == Person.Sex.MALE)
  14. .map(p -> p.getName())
  15. .collect(Collectors.toList());

此版本的collect操作采用Collector类型的参数 。此类封装了在collect需要三个参数(供应商,累加器和合并器功能)的操作中用作参数的函数。
Collectors类包含了许多有用的归约操作,如累加元件到集合并且根据不同的标准总结元件。这些归约运算返回Collector类的实例,因此您可以将它们用作collect运算的参数。
本示例使用的 Collectors.toList操作,将流元素累积到List的新实例中。与Collectors类中的大多数操作一样,toList运算符返回Collector的实例,而不是集合。
以下示例roster按性别对集合成员进行分组:

  1. Map<Person.Sex, List<Person>> byGender =
  2. roster
  3. .stream()
  4. .collect(
  5. Collectors.groupingBy(Person::getGender));

groupingBy操作返回一个映射,该映射的键是应用指定为其参数指定的lambda表达式而得到的值(称为分类函数)。在此示例中,返回的映射包含两个键Person.Sex.MALEPerson.Sex.FEMALE。键的对应值是包含流元素的List实例,这些流元素在由分类功能处理时对应于键值。例如,与key对应的值Person.Sex.MALE是包含所有男性成员的List实例。
以下示例检索roster集合中每个成员的名称,并按性别将其分组:

  1. Map<Person.Sex, List<String>> namesByGender =
  2. roster
  3. .stream()
  4. .collect(
  5. Collectors.groupingBy(
  6. Person::getGender,
  7. Collectors.mapping(
  8. Person::getName,
  9. Collectors.toList())));

此示例中的groupingBy操作采用两个参数:分类函数和Collector的实例。Collector参数称为下游收集器。这是一个收集器,Java运行时将其应用于另一个收集器的结果。因此,groupingBy操作使您可以将collect方法应用于groupingBy操作创建的List值。此示例应用了收集器mapping,该收集器将映射功能应用于Person::getName流的每个元素。因此,结果流仅包含成员名称。像此示例一样,包含一个或多个下游收集器的管道称为多级归约
以下示例检索每个性别成员的总年龄:

  1. Map<Person.Sex, Integer> totalAgeByGender =
  2. roster
  3. .stream()
  4. .collect(
  5. Collectors.groupingBy(
  6. Person::getGender,
  7. Collectors.reducing(
  8. 0,
  9. Person::getAge,
  10. Integer::sum)));

reducing操作采用三个参数:

  • identity:与Stream.reduce操作一样,如果流中没有元素,则identity元素既是reduce的初始值,也是默认结果。在此示例中,标识元素为0;。这是年龄总和的初始值,如果不存在成员,则为默认值。
  • mapperreducing操作将此映射器功能应用于所有流元素。在此示例中,映射器检索每个成员的年龄。
  • operation:操作功能用于减少映射值。在此示例中,操作功能将Integer值相加。

以下示例检索每种性别的成员的平均年龄:

  1. Map<Person.Sex, Double> averageAgeByGender = roster
  2. .stream()
  3. .collect(
  4. Collectors.groupingBy(
  5. Person::getGender,
  6. Collectors.averagingInt(Person::getAge)));