Java教程是针对JDK 8编写的。本页面中描述的示例和实践不利用后续版本中引入的改进,并可能使用不再可用的技术。
请参阅Java语言更改,了解Java SE 9及后续版本中更新的语言功能的摘要。
请参阅JDK发行说明,了解所有JDK版本的新功能、增强功能以及已删除或弃用选项的信息。
在集合roster
中,聚合操作部分描述了以下操作流程,用于计算所有男性成员的平均年龄:
double average = roster .stream() .filter(p -> p.getGender() == Person.Sex.MALE) .mapToInt(Person::getAge) .average() .getAsDouble();
JDK包含许多终端操作(例如average
、sum
、min
、max
和count
),它们通过组合流的内容返回一个值。这些操作被称为归约操作。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
:身份元素既是缩减的初始值,也是在流中没有元素时的默认结果。在此示例中,身份元素为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("男性成员的平均年龄为: " + averageCollect.average());
在这个例子中,collect
操作有三个参数:
supplier
: 供应商是一个工厂函数,它创建新的实例。对于collect
操作,它创建结果容器的实例。在这个例子中,它是Averager
类的一个新实例。accumulator
: 累加器函数将一个流元素合并到结果容器中。在这个例子中,它通过将count
变量加一并将流元素的值(表示男性成员的年龄)添加到Averager
结果容器的total
成员变量中来修改Averager
结果容器。combiner
: 合并函数将两个结果容器合并成一个。在这个例子中,它通过将另一个Averager
实例的count
成员变量加到当前Averager
结果容器的count
变量中,并将另一个Averager
实例的total
成员变量的值加到当前Averager
结果容器的total
成员变量中来修改Averager
结果容器。注意以下几点:
reduce
操作中的标识元素那样的值。collect
操作;有关更多信息,请参阅并行性部分。(如果你在并行流上运行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
操作返回一个映射,其键是应用作为参数的lambda表达式(称为分类函数)的结果。在这个例子中,返回的映射包含两个键,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
参数被称为下游收集器。这是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
操作接受三个参数:
identity
:像Stream.reduce
操作一样,标识元素既是减少的初始值,也是如果流中没有元素的默认结果。在这个例子中,标识元素是0
;这是年龄总和的初始值,也是如果没有成员存在的默认值。mapper
:reducing
操作将这个映射函数应用于所有流元素。在这个例子中,映射函数检索每个成员的年龄。operation
:操作函数用于减少映射值。在这个例子中,操作函数将添加Integer
值。以下示例检索每个性别成员的平均年龄:
Map<Person.Sex, Double> averageAgeByGender = roster .stream() .collect( Collectors.groupingBy( Person::getGender, Collectors.averagingInt(Person::getAge)));