Java 教程是为 JDK 8 编写的。本页中描述的示例和实践未利用在后续版本中引入的改进。
Aggregate Operations 部分描述了以下操作管道,它计算集合 roster
中所有男性成员的平均年龄:
double average = roster .stream() .filter(p -> p.getGender() == Person.Sex.MALE) .mapToInt(Person::getAge) .average() .getAsDouble();
JDK 包含许多终端操作(例如 average
,sum
,min
,max
和 count
)通过组合流的内容返回一个值。这些操作称为 reduction operations (缩减操作)。JDK 还包含返回集合而不是单个值的缩减操作。许多缩减操作执行特定任务,例如查找值的平均值或将元素分组到类别中。但是,JDK 为你提供了通用的缩减操作 reduce
和 collect
,本节将详细介绍。
本节包括以下主题:
你可以在示例 ReductionExamples
中找到本节中描述的代码片段。
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 元素既是缩减的初始值,又是流中没有元素时的默认结果。在此示例中,identity 元素为 0
;这是年龄总和的初始值,如果集合 roster
中没有成员则是默认值。
accumulator
:累加器函数有两个参数:缩减操作的部分结果(在本例中,是到目前为止所有处理过的整数的总和)和流的下一个元素(在本例中为整数)。它返回一个新的部分结果。在此示例中,累加器函数是一个 lambda 表达式,它添加两个 Integer
值并返回 Integer
值:
(a, b) -> a + b
reduce
操作始终返回新值。但是,累加器函数每次处理流的元素时也会返回一个新值。假设你要将流的元素减少为更复杂的对象,例如集合。这可能会妨碍你的应用程序的性能。如果 reduce
操作涉及向集合添加元素,那么每次累加器函数处理元素时,它都会创建一个包含元素的新集合,这是低效的。相反,更新现有集合会更有效。你可以使用 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
:supplier 是工厂函数;它构造了新的实例。对于 collect
操作,它会创建结果容器的实例。在此示例中,它是 Averager
类的新实例。accumulator
:累加器函数将流元素合并到结果容器中。在此示例中,它修改 Averager
结果容器,通过将 count
变量递增 1,并将流元素的值添加到 total
成员变量,其是表示男性成员年龄的整数。combiner
:组合器函数接受两个结果容器并合并其内容。在此示例中,它修改 Averager
结果容器,通过将另一个 Averager
实例的 count
成员变量增加到 count
变量,并将另一个 Averager
实例的 total
成员变量增加到 total
变量。请注意以下事项:
reduce
操作中的 identity 元素那样的值。collect
操作;有关详细信息,请参阅 Parallelism 部分。(如果使用并行流运行 collect
方法,则每当组合器函数创建新对象时,JDK 都会创建一个新线程,例如本例中的 Averager
对象。因此,你不必担心同步。)虽然 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.MALE
和 Person.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
操作需要三个参数:
identity
:与 Stream.reduce
操作类似,identity 元素既是缩减的初始值,又是流中没有元素时的默认结果。在此示例中,identity 元素为 0
;这是年龄总和的初始值,如果不存在成员,这是默认值。mapper
:reducing
操作将此映射器函数应用于所有流元素。在此示例中,映射器获取每个成员的年龄。operation
:操作函数用于缩减映射值。在此示例中,操作函数添加 Integer
值。以下示例获取每个性别的成员的平均年龄:
Map<Person.Sex, Double> averageAgeByGender = roster .stream() .collect( Collectors.groupingBy( Person::getGender, Collectors.averagingInt(Person::getAge)));