文档

Java™ 教程-Java Tutorials 中文版
缩减
Trail: Collections
Lesson: Aggregate Operations

缩减

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

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

JDK 包含许多终端操作(例如 averagesumminmaxcount)通过组合流的内容返回一个值。这些操作称为 reduction operations (缩减操作)。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("Average age of male members: " +
    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 操作返回一个 map,其键是应用指定为其参数的 lambda 表达式产生的值(称为 classification function (分类函数) )。在此示例中,返回的 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 参数称为 downstream collector (下游收集器)。这是 Java 运行时应用于另一个收集器的结果的收集器。因此,通过此 groupingBy 操作,你可以将 collect 方法应用于 groupingBy 运算符创建的 List 值。此示例应用收集器 mapping,它将映射函数 Person::getName 应用于流的每个元素。因此,生成的流只包含成员的名称。包含一个或多个下游收集器的管道(如此示例)称为 multilevel reduction (多级缩减)

以下示例获取每个性别的成员的总年龄:

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

Previous page: Aggregate Operations
Next page: Parallelism