Reduction

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

double average = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .mapToInt(Person::getAge)
    .average()
    .getAsDouble();

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

本节涵盖以下主题:

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

Stream.reduce 方法

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

Integer totalAge = roster
    .stream()
    .mapToInt(Person::getAge)
    .sum();

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

Integer totalAgeReduce = roster
   .stream()
   .map(Person::getAge)
   .reduce(
       0,
       (a, b) -> a + b);

此示例中的reduce操作带有两个参数:

  • identity:如果流中没有元素,则 identity 元素既是 reduce 的初始值,也是默认结果。在此示例中,标识元素为0;这是年龄总和的初始值,如果集合roster中不存在成员,则为默认值。

  • accumulator:累加器函数具有两个参数:约简的部分结果(在此示例中,是到目前为止所有已处理整数的总和)和流的下一个元素(在此示例中,是整数)。它返回一个新的部分结果。在此示例中,累加器函数是一个 lambda 表达式,该表达式将两个Integer值相加并返回Integer值:

(a, b) -> a + b

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

Stream.collect 方法

reduce方法在处理元素时始终会创建新值,而collect方法与修改或突变现有值不同。

考虑如何查找流中的平均值。您需要两个数据:值的总数和这些值的总和。但是,与reduce方法和所有其他归约方法一样,collect方法仅返回一个值。您可以创建一个包含成员变量的新数据类型,该成员变量跟踪值的总数和这些值的总和,例如以下类Averager

class Averager implements IntConsumer
{
    private int total = 0;
    private int count = 0;
        
    public double average() {
        return count > 0 ? ((double) total)/count : 0;
    }
        
    public void accept(int i) { total += i; count++; }
    public void combine(Averager other) {
        total += other.total;
        count += other.count;
    }
}

以下管道使用Averager类和collect方法来计算所有男性成员的平均年龄:

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());

此示例中的collect操作采用三个参数:

  • supplier:供应商具有工厂职能;它构造了新的实例。对于collect操作,它将创建结果容器的实例。在此示例中,它是Averager类的新实例。

  • accumulator:累加器功能将流元素合并到结果容器中。在此示例中,它通过将count变量增加 1 并将_stream 元素的值添加到total成员变量来修改Averager结果容器,该元素是表示男性成员年龄的整数。

  • combiner:合并器功能接收两个结果容器并合并其内容。在此示例中,它通过将count变量增加另一个Averager实例的count成员变量并将另一个Averager实例的total成员变量的值添加到total成员变量来修改Averager结果容器。

请注意以下几点:

  • 供应商是一个 lambda 表达式(或方法引用),与reduce操作中的 identity 元素之类的值相反。

  • 累加器和合并器函数不返回值。

  • 您可以对并行流使用collect操作;有关更多信息,请参见Parallelism部分。 (如果对并行流运行collect方法,那么只要组合器函数创建新对象(例如本例中的Averager对象),JDK 就会创建一个新线程。因此,您不必担心同步。)

尽管 JDK 为您提供了average操作来计算流中元素的平均值,但是如果需要从流中的元素中计算多个值,则可以使用collect操作和自定义类。

collect操作最适合集合。下面的示例通过collect操作将男性成员的姓名放入集合中:

List<String> namesOfMaleMembersCollect = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .map(p -> p.getName())
    .collect(Collectors.toList());

此版本的collect操作采用一个类型为Collector的参数。此类封装了在collect操作中用作参数的函数,该函数需要三个参数(供应商,累加器和组合器函数)。

Collectors类包含许多有用的归约运算,例如将元素累积到集合中并根据各种标准对元素进行汇总。这些归约运算返回Collector类的实例,因此您可以将它们用作collect运算的参数。

本示例使用Collectors.toList操作,该操作将流元素累积到List的新实例中。与Collectors类中的大多数操作一样,toList运算符返回Collector的实例,而不是集合。

以下示例按性别对集合roster的成员进行分组:

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

groupingBy操作返回一个 Map,该 Map 的键是应用指定为其参数的 lambda 表达式(称为分类函数)而得到的值。在此示例中,返回的 Map 包含两个键Person.Sex.MALEPerson.Sex.FEMALE。键的对应值是List的实例,该实例包含流元素,当由分类功能处理时,这些流元素对应于键值。例如,对应于键Person.Sex.MALE的值是List的实例,其中包含所有男性成员。

下面的示例检索集合roster中每个成员的名称,并按性别将其分组:

Map<Person.Sex, List<String>> namesByGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(
                Person::getGender,                      
                Collectors.mapping(
                    Person::getName,
                    Collectors.toList())));

在此示例中,groupingBy操作采用两个参数,即分类函数和Collector的实例。 Collector参数称为下游收集器。这是一个收集器,Java 运行时将其应用于另一个收集器的结果。因此,通过groupingBy操作,您可以将collect方法应用于groupingBy运算符创建的List值。此示例将收集器mapping应用于流的每个元素的 Map 函数Person::getName。因此,结果流仅包含成员名称。像此示例一样,包含一个或多个下游收集器的管道称为多级缩减

以下示例检索每个性别成员的总年龄:

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

reducing操作采用三个参数:

  • identity:与Stream.reduce操作一样,如果流中没有元素,则 identity 元素既是 reduce 的初始值,又是默认结果。在此示例中,标识元素为0;这是年龄总和的初始值,如果不存在成员,则为默认值。

  • mapperreducing操作将此 Map 器功能应用于所有流元素。在此示例中,Map 器检索每个成员的年龄。

  • operation:操作功能用于减少 Map 值。在此示例中,操作功能将Integer个值相加。

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

Map<Person.Sex, Double> averageAgeByGender = roster
    .stream()
    .collect(
        Collectors.groupingBy(
            Person::getGender,                      
            Collectors.averagingInt(Person::getAge)));