Java基础 - Reduction
原文链接:https://docs.oracle.com/javase/tutorial/collections/streams/reduction.html
在"聚合操作"这一小节描述了下面的管道操作,就是计算集合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,本节将对此进行详细描述。
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元素既是归约的初始值,也是如果流中无数据时的默认结果identity元素既是归约的初始值,也是流中没有元素的默认结果。在本例中,identity元素为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:supplier是一个工厂函数;它构建新的实例。对于collect操作,它创建结果容器的实例。在本例中,它是Averager类的一个新实例。
- accumulator:累加器函数将流元素合并到结果容器中。在本例中,它修改平均结果容器,将计数变量递增1,并将表示男性成员年龄的整数流元素的值添加到总成员变量中。
- combiner:组合器函数接受两个结果容器并合并它们的内容。在本例中,it modifies an Averager result container by incrementing the count variable by the count member variable of the other Averager instance and adding to the total member variable the value of the other Averager instance's total member variable.
注意:
- supplier是lambda表达式(或方法引用),而不是reduce操作中的identity元素这样的值。
- accumulate和combiner函数不返回值。
- 您可以将collect操作与并行流一起使用;有关更多信息,请参见并行部分。(如果您使用并行流运行collect方法,那么每当combiner函数创建一个新对象时,例如本本例中的Averager对象,JDK就会创建一个新线程。因此,您不必担心同步。)
虽然dk为您提供了计算流中元素的平均值的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函数)。
Collection类包含许多有用的归约操作,例如将元素累加到集合中,并根据各种标准总结元素。这些归约操作返回Collector类的实例,因此可以将它们用作collect操作的参数。
此示例使用Collectiontors.toList操作,该操作将流元素累加到List的新实例中。与Collectors类中的大多数操作一样,toList运算符返回的是Collector的实例,而不是集合。
以下示例按性别将集合roster成员分组:
Map<Person.Sex, List<Person>> byGender =
roster
.stream()
.collect(
Collectors.groupingBy(Person::getGender));
The groupingBy
operation returns a map whose keys are the values that result from applying the lambda expression specified as its parameter (which is called a classification function). In this example, the returned map contains two keys, Person.Sex.MALE
and Person.Sex.FEMALE
. The keys' corresponding values are instances of List
that contain the stream elements that, when processed by the classification function, correspond to the key value. For example, the value that corresponds to key Person.Sex.MALE
is an instance of List
that contains all male members.
以下示例检索集合roster中每个成员的姓名,并按性别分组:
Map<Person.Sex, List<String>> namesByGender =
roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.mapping(
Person::getName,
Collectors.toList())));
The groupingBy
operation in this example takes two parameters, a classification function and an instance of Collector
. The Collector
parameter is called a downstream collector. This is a collector that the Java runtime applies to the results of another collector. Consequently, this groupingBy
operation enables you to apply a collect
method to the List
values created by the groupingBy
operator. This example applies the collector mapping
, which applies the mapping function Person::getName
to each element of the stream. Consequently, the resulting stream consists of only the names of members. A pipeline that contains one or more downstream collectors, like this example, is called a 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:reduce操作将此映射函数应用于所有流元素。在本例中,映射程序检索每个成员的年龄。
- operation:操作函数用于reduce映射值。在本例中,操作函数将添加Integer值。
以下示例检索每个性别成员的平均年龄:
Map<Person.Sex, Double> averageAgeByGender = roster
.stream()
.collect(
Collectors.groupingBy(
Person::getGender,
Collectors.averagingInt(Person::getAge)))