Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对Java集合运算和表达的高阶抽象。
这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
+--------------------+ +------+ +------+ +---+ +-------+
| stream of elements +-----> |filter+-> |sorted+-> |map+-> |collect|
+--------------------+ +------+ +------+ +---+ +-------+
Stream(流)是一个来自数据源的元素队列并支持聚合操作
和以前的Collection操作不同,Stream操作还有两个基础的特征:
- Pipelining:中间操作都会返回流对象本身。这样多个操作可以串联成一个管道,如同流式风格(fluent style)。 这样做可以对操作进行优化,比如延迟执行(laziness)和短路( short-circuiting)。
- 内部迭代:以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代,这叫做外部迭代。 Stream提供了内部迭代的方式,通过访问者模式(Visitor)实现。
在 Java 8 中, 集合接口有两个方法来生成流:
- stream() − 为集合创建串行流。
- parallelStream() − 为集合创建并行流。
java.util.stream.Stream中定义了许多流操作的方法,为了更好的理解Stream API掌握它常用的操作非常重要。
流的操作其实可以分为两类:处理操作、聚合操作。
- 有状态 sorted(),必须等上一步操作完拿到全部元素后才可操作
- 无状态 filter(),该操作的元素不受上一步操作的影响
- 短路操作findFirst(),找到一个则返回,也就是break当前的循环
- 非短路操作forEach(),遍历全部元素
以上操作决定了Stream一定是先构建完毕再执行的特点,也就是延迟执行,当需要结果(终端操作时)开始执行流水线。Stream做到的是对于多次调用合并到一次迭代中处理完所有的调用方式。大概思路是记录下每一步的操作,然后终端操作时对其迭代依次执行每一步的操作,最后再一次循环中处理。
Stream上的所有操作分为两类:
- 中间操作:中间操作只是一种标记,只有结束操作才会触发实际计算。中间操作又可以分为无状态的(Stateless)和有状态的(Stateful),无状态中间操作是指元素的处理不受前面元素的影响,而有状态的中间操作必须等到所有元素处理之后才知道最终结果,比如排序是有状态操作,在读取所有元素之前并不能确定排序结果;
- 结束操作:结束操作又可以分为短路操作和非短路操作,短路操作是指不用处理全部元素就可以返回结果,比如找到第一个满足条件的元素。之所以要进行如此精细的划分,是因为底层对每一种情况的处理方式不同。
下面汇总了Stream的所有操作:
Stream操作分类 | ||||
中间操作(Intermediate operations) | ||||
无状态(Stateless) | unordered() filter() map() mapToInt() mapToLong() mapToDouble() flatMap() flatMapToInt() flatMapToLong() flatMapToDouble() peek() | |||
有状态(Stateful) | distinct() sorted() limit() skip() | |||
结束操作(Terminal operations) | ||||
非短路操作 | forEach() forEachOrdered() toArray() reduce() collect() max() min() count() | |||
短路操作(short-circuiting) | anyMatch() allMatch() noneMatch() findFirst() findAny() |
规约reduce()
reduce操作可以实现从一组元素中生成一个值,sum()、max()、min()、count()
等都是reduce操作,将他们单独设为函数只是因为常用。reduce()的方法定义有三种重写形式:
- Optional
- T reduce(T identity, BinaryOperator
- U reduce(U identity, BiFunction accumulator, BinaryOperator combiner)
虽然函数定义越来越长,但语义不曾改变,多的参数只是为了指明初始值(参数identity),或者是指定并行执行时多个部分结果的合并方式(参数combiner)。reduce()最常用的场景就是从一堆值中生成一个值。用这么复杂的函数去求一个最大或最小值,你是不是觉得设计者有病。其实不然,因为“大”和“小”或者“求和”有时会有不同的语义。
需求:从一组单词中找出最长的单词。这里“大”的含义就是“长”。
// 找出最长的单词
Stream stream = Stream.of("I", "love", "you", "too");
Optional longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);
//Optional longest = stream.max((s1, s2) -> s1.length()-s2.length());
System.out.println(longest.get());
上述代码会选出最长的单词love,其中Optional是(一个)值的容器,使用它可以避免null值的麻烦。当然可以使用Stream.max(Comparator super T> comparator)
方法来达到同等效果,但reduce()自有其存在的理由。
需求:求出一组单词的长度之和。这是个“求和”操作,操作对象输入类型是String,而结果类型是Integer。
// 求单词长度之和
Stream stream = Stream.of("I", "love", "you", "too");
Integer lengthSum = stream.reduce(0, // 初始值 // (1)
(sum, str) -> sum+str.length(), // 累加器 // (2)
(a, b) -> a+b); // 部分和拼接器,并行执行时才会用到 // (3)
// int lengthSum = stream.mapToInt(str -> str.length()).sum();
System.out.println(lengthSum);
上述代码标号(2)处将i. 字符串映射成长度,ii. 并和当前累加和相加。这显然是两步操作,使用reduce()函数将这两步合二为一,更有助于提升性能。如果想要使用map()和sum()组合来达到上述目的,也是可以的。
reduce()擅长的是生成一个值,如果想要从Stream生成一个集合或者Map等复杂的对象,那就需要使用收集器collect()!
接口的静态方法和默认方法
Function是一个接口,那么Function.identity()是什么意思呢?这要从两方面解释:
identity()
就是Function接口的一个静态方法。Function.identity()
返回一个输出跟输入一样的Lambda表达式对象,等价于形如t -> t
形式的Lambda表达式。上面的解释是不是让你疑问更多?不要问我为什么接口中可以有具体方法,也不要告诉我你觉得t -> t
比identity()
方法更直观。我会告诉你接口中的default方法是一个无奈之举,在Java 7及之前要想在定义好的接口中加入新的抽象方法是很困难甚至不可能的,因为所有实现了该接口的类都要重新实现。试想在Collection接口中加入一个stream()抽象方法会怎样?default方法就是用来解决这个尴尬问题的,直接在接口中实现新加入的方法。既然已经引入了default方法,为何不再加入static方法来避免专门的工具类呢!
方法引用
诸如String::length的语法形式叫做方法引用(method references),这种语法用来替代某些特定形式Lambda表达式。如果Lambda表达式的全部内容就是调用一个已有的方法,那么可以用方法引用来替代Lambda表达式。方法引用可以细分为四类:
方法引用类别 | 举例 |
---|---|
引用静态方法 | Integer::sum |
引用某个对象的方法 | list::add |
引用某个类的方法 | String::length |
引用构造方法 | HashMap::new |
收集器Collector
收集器(Collector)是为Stream.collect()方法量身打造的工具接口(类)。考虑一下将一个Stream转换成一个容器(或者Map)需要做哪些工作?我们至少需要两样东西:
结合以上分析,collect()方法定义为:
三个参数依次对应上述三条分析。不过每次调用collect()都要传入这三个参数太麻烦,收集器Collector就是对这三个参数的简单封装,所以collect()的另一定义为:
。 Collectors工具类可通过静态方法生成各种常用的Collector。举例来说,如果要将Stream规约成List可以通过如下两种方式实现:
// 将Stream规约成List
Stream stream = Stream.of("I", "love", "you", "too");
List list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1
//List list = stream.collect(Collectors.toList());// 方式2
System.out.println(list);
通常情况下我们不需要手动指定collect()的三个参数,而是调用collect(Collector super T,A,R> collector)
方法,并且参数中的Collector
对象大都是直接通过Collectors工具类获得。实际上传入的收集器的行为决定了collect()
的行为。
使用collect()生成Map
前面已经说过Stream背后依赖于某种数据源,数据源可以是数组、容器等,但不能是Map。反过来从Stream生成Map是可以的,但我们要想清楚Map的key和value分别代表什么,根本原因是我们要想清楚要干什么。通常在三种情况下collect()的结果会是Map:
1. 使用Collectors.toMap()生成的收集器,用户需要指定如何生成Map的key和value。
1. 使用Collectors.partitioningBy()生成的收集器,对元素进行二分区操作时用到。
1. 使用Collectors.groupingBy()生成的收集器,对元素做group操作时用到。
情况1:使用toMap()生成的收集器,这种情况是最直接的,前面例子中已提到,这是和Collectors.toCollection()并列的方法。如下代码展示将Person列表转换成由
package java8.lambda;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Person {
public String firstName;
public String lastName;
private float math;
private float english;
public Float computeGPA(Person person) {
return (person.math + person.english) / 2;
}
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
将Person列表转换成由
List people = new ArrayList<>();
people.add(Person.builder().firstName("lingxiao").english(8.5f).math(96.5f).build());
people.add(Person.builder().firstName("lingxiao").english(98.5f).math(96.5f).build());
people.add(Person.builder().firstName("lx").english(56.5f).math(85.5f).build());
people.add(Person.builder().firstName("lx").english(78.5f).math(73.5f).build());
Map personFloatMap = people.stream().
collect(Collectors.toMap(Function.identity(), person -> person.computeGPA(person)));
情况2:使用partitioningBy()
生成的收集器,这种情况适用于将Stream中的元素依据某个二值逻辑(满足条件,或不满足)分成互补相交的两部分,比如GPA是否大于60.5。下列代码展示GPA是否大于60.5的两部分。
Map> listMap = people.stream().
collect(Collectors.partitioningBy(person -> person.computeGPA(person)>= 60.5));
情况3:使用groupingBy()
生成的收集器,这是比较灵活的一种情况。跟SQL中的group by语句类似,这里的groupingBy()
也是按照某个属性对数据进行分组,属性相同的元素会被对应到Map的同一个key上。下列代码展示将人按照人名字进行分组:
Map> listMap = people.stream().collect(Collectors.groupingBy(Person::getFirstName));
以上只是分组的最基本用法,有些时候仅仅分组是不够的。在SQL中使用group by是为了协助其他查询,比如1. 先按照人名字分组,2. 然后统计名字相同的人数。Java类库设计者也考虑到了这种情况,增强版的groupingBy()能够满足这种需求。增强版的groupingBy()允许我们对元素分组之后再执行某种运算,比如求和、计数、平均值、类型转换等。这种先将元素分组的收集器叫做上游收集器,之后执行其他运算的收集器叫做下游收集器(downstream Collector)。
Map stringIntegerMap = people.stream().
collect(Collectors.groupingBy(Person::getFirstName, Collectors.counting()));
上面代码的逻辑是不是越看越像SQL?高度非结构化。还有更高级的操作,下游收集器还可以包含更下游的收集器,这绝不是为了炫技而增加的把戏,而是实际场景需要。 考虑将按照人名字分组的场景,如果我们想得到每个人的名字(字符串)可通过如下方式做到:
// 将按照人名字分组的场景,如果我们想得到每个人的名字(字符串)
Map> byDept = people.stream()
.collect(Collectors.groupingBy(Person::getFirstName,
Collectors.mapping(Person::getFirstName,// 下游收集器
Collectors.toList())));// 更下游的收集器
考虑考虑将按照人名字分组的场景,如果我们想得到每个Person对象列表,可通过如下方式做到:
Map> stringListMap = people.stream().
collect(Collectors.groupingBy(Person::getFirstName,
Collectors.mapping(Function.identity(),// 下游收集器
Collectors.toList())));// 更下游的收集器
以上操作完整的代码实例如下:
package java8.stream;
import java8.lambda.Person;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author: 凌霄
* @time: Created in 16:10 2018/2/5
* @desc
*/
public class ReduceTest {
public static void main(String[] args) {
Stream stream = Stream.of("A", "B", "C", "D");
// List list = stream.collect(Collectors.toList());
// List list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
// System.out.println(list.toString());
// Set set = stream.collect(Collectors.toSet());
// Set set = stream.collect(HashSet::new, HashSet::add, HashSet::addAll);
// System.out.println(set.toString());
// 使用toCollection()指定规约容器的类型
// ArrayList arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3)
// HashSet hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)
Map map = stream.collect(Collectors.toMap(Function.identity(), String::toLowerCase));
System.out.println(map.toString());
List people = new ArrayList<>();
people.add(Person.builder().firstName("lingxiao").english(8.5f).math(96.5f).build());
people.add(Person.builder().firstName("lingxiao").english(98.5f).math(96.5f).build());
people.add(Person.builder().firstName("lx").english(56.5f).math(85.5f).build());
people.add(Person.builder().firstName("lx").english(78.5f).math(73.5f).build());
// Map personFloatMap = people.stream().collect(Collectors.toMap(Function.identity(), person -> person.computeGPA(person)));
// Map> listMap = people.stream().collect(Collectors.partitioningBy(person -> person.computeGPA(person)>= 60.5));
Map> listMap = people.stream().collect(Collectors.groupingBy(Person::getFirstName));
Map stringIntegerMap = people.stream().collect(Collectors.groupingBy(Person::getFirstName, Collectors.counting()));
Map> stringListMap = people.stream().
collect(Collectors.groupingBy(Person::getFirstName,
Collectors.mapping(Function.identity(),// 下游收集器
Collectors.toList())));// 更下游的收集器
// 将按照人名字分组的场景,如果我们想得到每个人的名字(字符串)
Map> byDept = people.stream()
.collect(Collectors.groupingBy(Person::getFirstName,
Collectors.mapping(Person::getFirstName,// 下游收集器
Collectors.toList())));// 更下游的收集器
System.out.println(stringListMap.toString());
// 使用Collectors.joining()拼接字符串
Stream stream1 = Stream.of("I", "love", "you");
//String joined = stream.collect(Collectors.joining());// "Iloveyou"
//String joined = stream.collect(Collectors.joining(","));// "I,love,you"
String joined = stream1.collect(Collectors.joining(",", "{", "}"));// "{I,love,you}"
System.out.println(joined);
}
}
问题:
注意这里使用的是“操作(operation)”一词,指的是“Stream中间操作”的操作,很多Stream操作会需要一个回调函数(Lambda表达式),因此一个完整的操作是 <数据来源,操作,回调函数> 构成的三元组。Stream中使用Stage的概念来描述一个完整的操作,并用某种实例化后的PipelineHelper来代表Stage,将具有先后顺序的各个Stage连到一起,就构成了整个流水线。跟Stream相关类和接口的继承关系图1所示。
图1:Stream相关类和接口的继承关系
还有IntPipeline, LongPipeline,DoublePipeline没在图中画出,这三个类专门为三种基本类型(不是包装类型)而定制的,跟ReferencePipeline是并列关系。图中Head用于表示第一个Stage,即调用调用诸如Collection.stream()方法产生的Stage,很显然这个Stage里不包含任何操作;StatelessOp和StatefulOp分别表示无状态和有状态的Stage,对应于无状态和有状态的中间操作。
Stream流水线组织结构示意图2如下:
图2:Stream流水线组织结构
操作是如何记录下来的
1. Head记录Stream起始操作
1. StatelessOp记录中间操作
1. StatefulOp记录有状态的中间操作
这三个操作实例化会指向其父类AbstractPipeline,也就是在AbstractPipeline中建立了双向链表。AbstractPipeline源码如下:
对于Head
//Stream流水线中Head的构造函数
AbstractPipeline(Spliterator> source,
int sourceFlags, boolean parallel) {
this.previousStage = null;
this.sourceSpliterator = source;
this.sourceStage = this;
this.sourceOrOpFlags = sourceFlags & StreamOpFlag.STREAM_MASK;
// The following is an optimization of:
// StreamOpFlag.combineOpFlags(sourceOrOpFlags, StreamOpFlag.INITIAL_OPS_VALUE);
this.combinedFlags = (~(sourceOrOpFlags << 1)) & StreamOpFlag.INITIAL_OPS_VALUE;
this.depth = 0;
this.parallel = parallel;
}
对于其他Stage:
AbstractPipeline(AbstractPipeline, E_IN, ?> previousStage, int opFlags) {
if (previousStage.linkedOrConsumed)
throw new IllegalStateException(MSG_STREAM_LINKED);
previousStage.linkedOrConsumed = true;
previousStage.nextStage = this;
this.previousStage = previousStage;
this.sourceOrOpFlags = opFlags & StreamOpFlag.OP_MASK;
this.combinedFlags = StreamOpFlag.combineOpFlags(opFlags, previousStage.combinedFlags);
this.sourceStage = previousStage.sourceStage;
if (opIsStateful())
sourceStage.sourceAnyStateful = true;
this.depth = previousStage.depth + 1;
}
调用过程如此用双向链表串联起来,每一步都得知其上一步与下一步的操作.
以上只是解决了操作记录的问题,要想让流水线起到应有的作用我们需要一种将所有操作叠加到一起的方案。你可能会觉得这很简单,只需要从流水线的head开始依次执行每一步的操作(包括回调函数)就行了。这听起来似乎是可行的,但是你忽略了前面的Stage并不知道后面Stage到底执行了哪种操作,以及回调函数是哪种形式。换句话说,只有当前Stage本身才知道该如何执行自己包含的动作。这就需要有某种协议来协调相邻Stage之间的调用关系。
这种协议由Sink接口完成,Sink接口包含的方法如下表所示:
方法名 | 作用 |
---|---|
void begin(long size) |
开始遍历元素之前调用该方法,通知Sink做好准备。 |
void end() |
所有元素遍历完成之后调用,通知Sink没有更多的元素了。 |
boolean cancellationRequested() |
是否可以结束操作,可以让短路操作尽早结束。 |
void accept(T t) |
遍历元素时调用,接受一个待处理元素,并对元素进行处理。Stage把自己包含的操作和回调方法封装到该方法里,前一个Stage 只需要调用当前Stage.accept(T t) 方法就行了。 |
有了上面的协议,相邻Stage之间调用就很方便了,每个Stage都会将自己的操作封装到一个Sink里,前一个Stage只需调用后一个Stage
的accept()
方法即可,并不需要知道其内部是如何处理的。当然对于有状态的操作,Sink的begin()和end()方法也是必须实现的。比如Stream.sorted()
是一个有状态的中间操作,其对应的Sink.begin()
方法可能创建一个存放结果的容器,而accept()
方法负责将元素添加到该容器,最后end()
负责对容器进行排序。对于短路操作,Sink.cancellationRequested()
也是必须实现的,比如Stream.findFirst()
是短路操作,只要找到一个元素,cancellationRequested()
就应该返回true,以便调用者尽快结束查找。Sink的四个接口方法常常相互协作,共同完成计算任务。实际上Stream API内部实现的的本质,就是如何重载Sink的这四个接口方法。
有了Sink对操作的包装,Stage之间的调用问题就解决了,执行时只需要从流水线的head开始对数据源依次调用每个Stage对应的Sink.{begin(), accept(), cancellationRequested(), end()}
方法就可以了。一种可能的Sink.accept()方法流程是这样的:
void accept(U u){
1. 使用当前Sink包装的回调函数处理u
2. 将处理结果传递给流水线下游的Sink
}
Sink接口的其他几个方法也是按照这种 [处理->转发] 的模型实现。
下面结合具体例子看看Stream的中间操作是如何将自身的操作包装成Sink以及Sink是如何将处理结果转发给下一个Sink的。先看Stream.map()方法:
// Stream.map(),调用该方法将产生一个新的Stream
public final Stream map(Function super P_OUT, ? extends R> mapper) {
...
return new StatelessOp(this, StreamShape.REFERENCE,
StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
@Override /*opWripSink()方法返回由回调函数包装而成Sink*/
Sink opWrapSink(int flags, Sink downstream) {
return new Sink.ChainedReference(downstream) {
@Override
public void accept(P_OUT u) {
R r = mapper.apply(u);// 1. 使用当前Sink包装的回调函数mapper处理u
downstream.accept(r);// 2. 将处理结果传递给流水线下游的Sink
}
};
}
};
}
上述代码看似复杂,其实逻辑很简单,就是将回调函数mapper包装到一个Sink当中。由于Stream.map()是一个无状态的中间操作,所以map()方法返回了一个StatelessOp内部类对象(一个新的Stream),调用这个新Stream的opWripSink()方法将得到一个包装了当前回调函数的Sink。Stage之间的Sink调用关系如下图3:
图3:Stage之间的Sink调用关系
再来看一个复杂一点的例子。Stream.sorted()方法将对Stream中的元素进行排序,显然这是一个有状态的中间操作,因为读取所有元素之前是没法得到最终顺序的。抛开模板代码直接进入问题本质,sorted()方法是如何将操作封装成Sink的呢?sorted()一种可能封装的Sink代码如下:
// Stream.sort()方法用到的Sink实现
class RefSortingSink extends AbstractRefSortingSink {
private ArrayList list;// 存放用于排序的元素
RefSortingSink(Sink super T> downstream, Comparator super T> comparator) {
super(downstream, comparator);
}
@Override
public void begin(long size) {
...
// 创建一个存放排序元素的列表
list = (size >= 0) ? new ArrayList((int) size) : new ArrayList();
}
@Override
public void end() {
list.sort(comparator);// 只有元素全部接收之后才能开始排序
downstream.begin(list.size());
if (!cancellationWasRequested) {// 下游Sink不包含短路操作
list.forEach(downstream::accept);// 2. 将处理结果传递给流水线下游的Sink
}
else {// 下游Sink包含短路操作
for (T t : list) {// 每次都调用cancellationRequested()询问是否可以结束处理。
if (downstream.cancellationRequested()) break;
downstream.accept(t);// 2. 将处理结果传递给流水线下游的Sink
}
}
downstream.end();
list = null;
}
@Override
public void accept(T t) {
list.add(t);// 1. 使用当前Sink包装动作处理t,只是简单的将元素添加到中间列表当中
}
}
sorted的end方法中,其依赖上一次操作的结果集,按照调用链来说结果集必须在accept()调用完才会产生.那也就说明sorted操作需要在end中,然后再重新开启调用链.那么就相当于sorted给原有操作断路了一次,然后又重新接上,再次遍历.sorted操作如下图4:
图4:sorted操作
上述代码完美的展现了Sink的四个接口方法是如何协同工作的:
Sink完美封装了Stream每一步操作,并给出了 [处理->转发] 的模式来叠加操作。这一连串的齿轮已经咬合,就差最后一步拨动齿轮启动执行。是什么启动这一连串的操作呢?也许你已经想到了启动的原始动力就是结束操作(Terminal Operation),一旦调用某个结束操作,就会触发整个流水线的执行。
结束操作之后不能再有别的操作,所以结束操作不会创建新的流水线阶段(Stage),直观的说就是流水线的链表不会在往后延伸了。结束操作会创建一个包装了自己操作的Sink,这也是流水线中最后一个Sink,这个Sink只需要处理数据而不需要将结果传递给下游的Sink(因为没有下游)。对于Sink的 [处理->转发] 模型,结束操作的Sink就是调用链的出口。
我们再来考察一下上游的Sink是如何找到下游Sink的。一种可选的方案是在PipelineHelper中设置一个Sink字段,在流水线中找到下游Stage并访问Sink字段即可。但Stream类库的设计者没有这么做,而是设置了一个Sink AbstractPipeline.opWrapSink(int flags, Sink downstream)
方法来得到Sink,该方法的作用是返回一个新的包含了当前Stage代表的操作以及能够将结果传递给downstream的Sink对象。为什么要产生一个新对象而不是返回一个Sink字段?这是因为使用opWrapSink()可以将当前操作与下游Sink(上文中的downstream参数)结合成新Sink。试想只要从流水线的最后一个Stage开始,不断调用上一个Stage的opWrapSink()方法直到最开始(不包括stage0,因为stage0代表数据源,不包含操作),就可以得到一个代表了流水线上所有操作的Sink,用代码表示就是这样:
// AbstractPipeline.wrapSink()
// 从下游向上游不断包装Sink。如果最初传入的sink代表结束操作,
// 函数返回时就可以得到一个代表了流水线上所有操作的Sink。
final Sink wrapSink(Sink sink) {
Objects.requireNonNull(sink);
for ( AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
}
return (Sink) sink;
}
现在流水线上从开始到结束的所有的操作都被包装到了一个Sink里,执行这个Sink就相当于执行整个流水线,执行Sink的代码如下:
// AbstractPipeline.copyInto(), 对spliterator代表的数据执行wrappedSink代表的操作。
final void copyInto(Sink wrappedSink, Spliterator spliterator) {
Objects.requireNonNull(wrappedSink);
if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
wrappedSink.begin(spliterator.getExactSizeIfKnown());// 通知开始遍历
spliterator.forEachRemaining(wrappedSink);// 迭代
wrappedSink.end();// 通知遍历结束
}
else {
copyIntoWithCancel(wrappedSink, spliterator);
}
}
上述代码首先调用wrappedSink.begin()方法告诉Sink数据即将到来,然后调用spliterator.forEachRemaining()方法对数据进行迭代,最后调用wrappedSink.end()方法通知Sink数据处理结束。逻辑如此清晰。
例如foreach操作
@Override
public void forEach(Consumer super P_OUT> action) {
//evaluate就是开关,一旦调用就立即执行整个Stream
evaluate(ForEachOps.makeRef(action, false));
}
执行前会对操作从末尾到起始反向包裹起来,得到调用链
Sink opWrapSink(int flags, Sink sink) ;
//这个Sink是终端操作所对应的Sink
final Sink wrapSink(Sink sink) {
Objects.requireNonNull(sink);
for ( AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
}
return (Sink) sink;
}
最后一个问题是流水线上所有操作都执行后,用户所需要的结果(如果有)在哪里?首先要说明的是不是所有的Stream结束操作都需要返回结果,有些操作只是为了使用其副作用(Side-effects),比如使用Stream.forEach()方法将结果打印出来就是常见的使用副作用的场景(事实上,除了打印之外其他场景都应避免使用副作用),对于真正需要返回结果的结束操作结果存在哪里呢?
特别说明:副作用不应该被滥用,也许你会觉得在Stream.forEach()里进行元素收集是个不错的选择,就像下面代码中那样,但遗憾的是这样使用的正确性和效率都无法保证,因为Stream可能会并行执行。大多数使用副作用的地方都可以使用归约操作更安全和有效的完成。
// 错误的收集方式
ArrayList results = new ArrayList<>();
stream.filter(s -> pattern.matcher(s).matches())
.forEach(s -> results.add(s)); // Unnecessary use of side-effects!
// 正确的收集方式
Listresults =
stream.filter(s -> pattern.matcher(s).matches())
.collect(Collectors.toList()); // No side-effects!
回到流水线执行结果的问题上来,需要返回结果的流水线结果存在哪里呢?这要分不同的情况讨论,下表给出了各种有返回结果的Stream结束操作。
返回类型 | 对应的结束操作 |
---|---|
boolean | anyMatch() allMatch() noneMatch() |
Optional | findFirst() findAny() |
归约结果 | reduce() collect() |
数组 | toArray() |
本文详细介绍了Stream流水线的组织方式和执行过程,学习本文将有助于理解原理并写出正确的Stream代码,同时打消你对Stream API效率方面的顾虑。如你所见,Stream API实现如此巧妙,即使我们使用外部迭代手动编写等价代码,也未必更加高效。