文档

Java™教程
隐藏目录
减少
路径: 集合
课程: 聚合操作

归约

在集合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操作接受两个参数:

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("男性成员的平均年龄为: " +
    averageCollect.average());

在这个例子中,collect操作有三个参数:

注意以下几点:

尽管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操作中使用的三个参数(supplier、accumulator和combiner函数)。

Collectors类包含许多有用的归约操作,比如将元素累积到集合中,以及根据不同的条件对元素进行汇总。这些归约操作返回Collector类的实例,因此可以将它们作为collect操作的参数使用。

这个例子使用了Collectors.toList操作,它将流元素累积到一个新的List实例中。和Collectors类的大多数操作一样,toList操作符返回的是一个Collector实例,而不是一个集合。

下面的例子将集合roster按性别分组:

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

groupingBy操作返回一个映射,其键是应用作为参数的lambda表达式(称为分类函数)的结果。在这个例子中,返回的映射包含两个键,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收集器,它将映射函数Person::getName应用于流的每个元素。因此,结果流只包含成员的姓名。像这个例子一样包含一个或多个下游收集器的管道被称为多级减少。

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

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

reducing操作接受三个参数:

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

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

上一页: 聚合操作
下一页: 并行处理