前言
stream是怎么做到一次迭代中将所有流操作进行叠加?stream怎么做到只有在终止操作时进行元素遍历?那中间操作是做了些什么?
Stream 与集合的区别
- 集合是内存中的数据结构抽象,描述了数据在内存中是如何存储的。
- 流描述了对数据处理的过程,是一系列运算操作的叠加。对 stream 的任何修改都不会修改背后的数据源,比如对 stream 执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新 stream。
流可以是无限流,而集合不能是无限集合。
Stream 生成
工厂方法
Stream integerStream = Stream.of(1, 2, 3, 5);
Stream.generate(Math::random);
Stream.iterate(1, item -> item > 10, item -> item + 1)
集合转化
Collection 接口有一个 stream(),所以其所有子类都可以通过该方法获取相应的 Stream 对象。
Map<String, Double> nameToScore = new HashMap<>();
double totalScore = nameToScore.entrySet().mapToDouble(Entry::getValue).sum();
数组转化
int temperature = {21, 23, 27};
int sum = Arrays.stream(temperature).sum();
// int sum = Arrays.stream(temperature).reduce(Integer::sum).orElse(0);
使用
对 stream 的操作分为为两类,中间操作和结束操作:
- 中间操作总是会惰式执行,调用中间操作只会生成一个标记了该操作的新 stream。中间操作又可以分为无状态的和有状态的。
- 无状态中间操作是指元素的处理不受前面元素的影响。
- 而有状态的中间操作必须等到所有元素处理之后才知道最终结果。
- 结束操作会触发实际计算,计算发生时会把所有中间操作积攒的操作以 pipeline 的方式执行,这样可以减少迭代次数。计算完成之后 stream 就会失效。结束操作又可以分为短路操作和非短路操作。
- 短路操作是指不用处理全部元素就可以返回结果,比如找到第一个满足条件的元素。
优点
流的优点是可以通过组合语义明确的 operator,使最终的代码更加简洁且可读性更强。
通过一个例子来看流与非流的实现的区别:从一个字符串列表中找到以 a 开头、最长的字符串长度。
非 Stream 实现
List<String> strings = Arrays.asList("a", "bb", "ccc", "abcd");
List<String> startWithAList = new ArrayList<>();
for (String string: strings) {
if(string.startsWith("a")){
startWithAList.add(string); // 1. filter(), 保留以A开头的字符串
}
}
List<Integer> lengths = new ArrayList<>();
for (String string : startWithAList) {
lengths.add(string.length()); // 2. mapToInt(), 转换成长度
}
int maxLength = 0;
for(Integer length : lengths){
maxLength = Math.max(length, maxLength); // 3. max(), 保留最长的长度
}
System.out.println(String.format("the maxLength of %s is %d", strings, maxLength));
这中实现方式有两个明显的弊端:
- 对原始数据迭代次数过多
- 需要对迭代过程中产生的中间结果进行额外存储
更好的实现应该为:
List<String> strings = Arrays.asList("a", "bb", "ccc", "abcd");
int maxLength = 0;
for(String str : strings){
if(str.startsWith("a")){ // 1. filter(), 保留以A开头的字符串
int len = str.length(); // 2. mapToInt(), 转换成长度
maxLength = Math.max(len, maxLength); // 3. max(), 保留最长的长度
}
}
System.out.println(String.format("the maxLength of %s is %d", strings, maxLength));
这其实就是 stream 内部实际的执行过程,这是 stream 的作者抽象出了丰富的 operator,并构建好了这些 operator 的通用流程,只将 operator 中与业务逻辑有关的操作通过接口的形式(lambda 也是一种接口)暴露给程序员,从而增强了代码的复用性的可读性。
Stream 实现
List<String> strings = Arrays.asList("a", "bb", "ccc", "abcd");
int maxLength = strings.stream()
.filter(s -> s.startsWith('a'))
.mapToInt(String::length())
.max().orElse(0);
System.out.println(String.format("the maxLength of %s is %d", strings, maxLength));
Stream 原理
AbstractPipeline
上图通过 Stream.of 方法得到 Head Stage,紧接着调用一系列的中间操作,不断产生新的 stream,这些 Stream 对象以双向链表(AbstractPipeline#previousStage、AbstractPipeline#nextStage)的形式组织在一起,构成整个流水线。
Sink
- An extension of {@link Consumer} used to conduct values through the stages of a stream pipeline, with additional methods to manage size information, control flow, etc. Before calling the {@code accept()} method on a {@code Sink} for the first time, you must first call the {@code begin()} method to inform it that data is coming (optionally informing the sink how much data is coming), and after all data has been sent, you must call the {@code end()} method. After calling {@code end()}, you should not call {@code accept()} without again calling {@code begin()}. {@code Sink} also offers a mechanism by which the sink can cooperatively signal that it does not wish to receive any more data (the {@code cancellationRequested()} method), which a source can poll before sending more data to the {@code Sink}.
A stream pipeline consists of a source, zero or more intermediate stages (such as filtering or mapping), and a terminal stage, such as reduction or for-each.
A Sink instance is used to represent each stage of this pipeline , The Sink implementations associated with a given stage is expected to know the data type for the next stage, and call the correct {@code accept} method on its downstream {@code Sink}. Similarly, each stage must implement the correct {@code accept} method corresponding to the data type it accepts.
如果说 Pipeline 是为了将对源数据以 stage 的形式将操作串联起来,那么 Sink 就是为了封装对源数据的操作。前一个stage 只需调用后一个 stage 的 accept() 方法即可,并不需要知道其内部是如何处理的。
- 当然对于有状态的操作,Sink#begin() 和 Sink#end() 方法也是必须实现的。比如 Stream#sorted() 是一个有状态的中间操作,其对应的 Sink#begin() 方法会创建一个临时容器,而 Sink#accept() 方法负责将元素添加到该容器中, Sink#end() 则负责对容器进行排序。
- 对于短路操作,Sink#cancellationRequested() 也是必须实现的。比如 Stream.findFirst() ,只要找到一个元素,cancellationRequested() 就应该返回 true,以便调用者尽快结束查找。
Sink 的四个接口方法常常相互协作,共同完成计算任务。实际上 Stream API 内部实现的的本质,就是如何重载 Sink 的这四个接口方法。
下面我们结合具体例子看看 Stream 的中间操作是如何将自身的操作包装成 Sink 以及 Sink 是如何将处理结果转发给下一个 Sink 的。先看 Stream.map() 方法:
// java.util.stream.SortedOps.RefSortingSink:371行
// Stream.sorted()方法用到的Sink实现,由java.util.stream.SortedOps.OfRef#opWrapSink:133行 触发
class RefSortingSink<T> extends AbstractRefSortingSink<T> {
private ArrayList<T> list;// 存放用于排序的元素
RefSortingSink(Sink<? super T> downstream, Comparator<? super T> comparator) {
super(downstream, comparator);
}
@Override
public void begin(long size) {
...
// 1. 创建一个存放排序元素的列表
list = (size >= 0) ? new ArrayList<T>((int) size) : new ArrayList<T>();
}
@Override
public void end() {
list.sort(comparator);// 3. 只有元素全部接收之后才能开始排序
downstream.begin(list.size());
if (!cancellationWasRequested) {// 下游Sink不包含短路操作
list.forEach(downstream::accept);// 将处理结果传递给流水线下游的Sink
}
else {// 4. 下游Sink包含短路操作
for (T t : list) {// 每次都调用cancellationRequested()询问是否可以结束处理。
if (downstream.cancellationRequested()) break;
downstream.accept(t);// 将处理结果传递给流水线下游的Sink
}
}
downstream.end();
list = null;
}
@Override
public void accept(T t) {
list.add(t);// 2. 使用当前Sink包装动作处理t,只是简单的将元素添加到中间列表当中
}
}
当 stream 遇到终态操作时,会触发流水线上的所有 sink 从前到后执行。Stream 类库的设计者通过 Sink AbstractPipeline.opWrapSink(int flags, Sink downstream)
返回一个新的包含了当前 stage 代表的操作以及能够将结果传递给downstream 的 Sink 对象。这样就可以从流水线的最后一个 stage 开始,不断调用上一个 stage 的 opWrapSink()
方法直到最开始(不包括 stage0,因为 stage0 代表数据源,不包含操作),就可以得到一个代表了流水线上所有操作的 Sink,用代码表示就是这样:
// java.util.stream.AbstractPipeline#wrapSink
// 如果最初传入的 Sink 代表结束操作,函数返回时就可以得到一个代表了流水线上所有操作的 Sink
@Override
@SuppressWarnings("unchecked")
final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
Objects.requireNonNull(sink);
// 从下游向上游不断包装 Sink
for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
}
return (Sink<P_IN>) sink;
}
opWrapSink 实现示例:
// java.util.stream.IntPipeline#map
@Override
public final IntStream map(IntUnaryOperator mapper) {
Objects.requireNonNull(mapper);
return new StatelessOp<Integer>(this, StreamShape.INT_VALUE,
StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) {
@Override
Sink<Integer> opWrapSink(int flags, Sink<Integer> sink) {
return new Sink.ChainedInt<Integer>(sink) {
@Override
public void accept(int t) {
// 只是简单封装了下:调用流水线的下一个 stage 执行操作
downstream.accept(mapper.applyAsInt(t));
}
};
}
};
}
现在流水线上从开始到结束的所有的操作都被包装到了一个Sink里,执行这个Sink就相当于执行整个流水线,执行Sink的代码如下:
// java.util.stream.AbstractPipeline#copyInto
// 对 spliterator 代表的数据执行 wrappedSink 代表的操作
@Override
final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
Objects.requireNonNull(wrappedSink);
if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
wrappedSink.begin(spliterator.getExactSizeIfKnown());// 通知开始遍历
spliterator.forEachRemaining(wrappedSink);// 迭代
wrappedSink.end();// 通知遍历结束
}
else {
copyIntoWithCancel(wrappedSink, spliterator);
}
}