简介
说起 Java 8,我们知道 Java 8 大改动之一就是增加函数式编程,而 Stream API 便是函数编程的主角,Stream API 是一种流式的处理数据风格,也就是将要处理的数据当作流,在管道中进行传输,并在管道中的每个节点对数据进行处理,如过滤、排序、转换等。
但很多人只知道Stream的用法,不知其原理,随便使用的话就有可能出现问题。
在踩过一次坑后,决心对这个经常使用的工具类的底层来一次梳理,加深一下理解,以防再次踩坑T T。
Stream原理解析
阅读源码前,先了解一下stream的一些基本知识。
Stream类结构
下面是几个实现Stream关键步骤的几个类,可以先记住下面类和类之间的关系,方便理解
BaseStream定义了流的迭代、并行、串行等基本特性;
Stream中定义了map、filter、flatmap等用户关注的常用操作;
PipelineHelper用于执行管道流中的操作以及捕获输出类型、并行度等信息
Head、StatelessOp、StatefulOp为ReferencePipeline中的内部子类,用于描述流的操作阶段。
操作分类
Stream中的操作可以分为两大类:中间操作(Intermediate operations)与结束操作(Terminal operations),中间操作只是对操作进行了记录,只有结束操作才会触发实际的计算(即惰性求值),这也是Stream在迭代大集合时高效的原因之一。中间操作又可以分为无状态(Stateless)操作与有状态(Stateful)操作,前者是指元素的处理不受之前元素的影响;后者是指该操作只有拿到所有元素之后才能继续下去。
结束操作又可以分为短路(short-circuiting)与非短路操作,这个应该很好理解,前者是指遇到某些符合条件的元素就可以得到最终结果;而后者是指必须处理所有元素才能得到最终结果。
之所以要进行如此精细的划分,是因为底层对每一种情况的处理方式不同。
例子
下面通过一个例子来看一下,平时我们使用的Stream到底是怎么运转的吧
public class TestStream {
public static void main(String[] args) {
testCollect();
}
public static void testCollect(){
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
List<Object> collect = Optional.ofNullable(list)
.orElse(Collections.emptyList())
.stream()
// .map(i -> null)
.map(i -> i+1)
.filter(i -> i<3)
.collect(Collectors.toList());
System.out.println(collect);
}
}
这个例子中我们分别使用两个无状态操作map和filter以及一个collect用作结束操作来将集合中的数据进行处理成我们想要的集合。今天我们仅对串行的流式处理作学习。
创建流Stream()
在开始流式操作之前,我们先会创建一个流,这个方法很简单,只是创建了一个Head头节点,并用parallel判断是串行还是并行进行流式操作。
往下继续看,我们发现这个Head节点最后会调用AbstractPipeLine的构造方法,我们查看AbstractPipeLine这个类发现,AbstractPipeLine成员变量中持有前一个节点,后一个节点以及头节点的引用。这不是一个典型的双向链表吗?
@SuppressWarnings("rawtypes")
private final AbstractPipeline sourceStage;//头节点
/**
* The "upstream" pipeline, or null if this is the source stage.
*/
@SuppressWarnings("rawtypes")
private final AbstractPipeline previousStage;//前一个节点
/**
* The operation flags for the intermediate operation represented by this
* pipeline object.
*/
protected final int sourceOrOpFlags;
/**
* The next stage in the pipeline, or null if this is the last stage.
* Effectively final at the point of linking to the next pipeline.
*/
@SuppressWarnings("rawtypes")
private AbstractPipeline nextStage;//下一个节点
/**
* The number of intermediate operations between this pipeline object
* and the stream source if sequential, or the previous stateful if parallel.
* Valid at the point of pipeline preparation for evaluation.
*/
private int depth;//深度
最后我们会创建一个头节点指向自己,深度为0的头节点出来,Stream流的创建也就完成了。
处理操作
map()
下面我们看看数据在流中是怎么被处理的?例子中,我们开始会对集合中的数据用map操作进行处理
由于map()是个无状态操作,所以这一步会创建一个StateLessOp对象,并实现他的opWrapSink方法。
opWrapSink方法就是真正处理数据的方法,这个之后会讲。我们可以先关注map这个动作仅仅只是执行了Function中的lamda表达式对流中的数据u进行了简单处理。
StatelessOp这个对象的构造方法最后也是聚焦到AbstractPipeLine的构造方法上,将刚刚创建的Stream流指向当前创建的PipeLine对象,最后返回这个无状态操作StatelessOp对象。
这样链表中就有两环了
filter()
filter()和map()一样,也是在链表中进行套环,只是对数据的处理逻辑不同。
最后创建一个无状态对象进行返回,并在链表中再增加一环,深度加一
后续不管多少操作,也只是在链表中增加节点个数,直到进行结束操作。最后形成的链表结构如下:
collect()
最后来看看最关键的结束操作,这块比较绕,需要加强记忆。以常用的collect()为例
举例中我们使用Collectors.toList()将数据打包成一个新集合,这个方法会生成一个collector对象
public static <T>
Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}
先记住这个CollectorImpl对象的两个lamda成员变量 ArrayList::new, 和List::add 最后会用这两个动作去构建新集合
然后这个collector对象会被封装成一个ReduceOps对象,作为最底层的节点为后续计算使用。
evaluate方法里会判断一下采用并行还是串行处理流中的数据
选择串行处理会调用刚创建的ReduceOps对象中的makeSink方法
makeSink方法会创建一个ReducingSink对象,这个对象有着刚刚创建collector对象中的所有动作,用于新集合的创建和添加对象
再来看看最后处理前的关键一步,wrapSink方法会把刚刚的Sink对象包装成一个含有map、filter等全部操作的sink对象。
具体是怎么做的呢?这里采取了一种遍历思想,从链表的最后开始一直到深度为0,层层包装递归成一个有着全部操作的sink对象
这里opWrapSink操作就是之前filter创建的StatelessOp中实现的方法,也就是链表中最后一个节点。
StateLessOp构造方法会将链表中后一个sink对象装到现在这个节点的成员变量downstream(也就是下一个流操作此处对应filter)里面去
这样迭代完之后得到一个从头节点开始执行操作的对象,该对象持有下一个节点的sink的引用用于执行下一次操作
begin执行的是创建新集合的操作,对应上文的ArrayList::new操作
接下来,不断迭代流中的操作,根据链表中的顺序进行操作,最后得到最终的数据。
这一步实现将数字+1,传给filter操作进行过滤
这一步将符合过滤条件的数据传给有collector行为的sink对象(最初传入的那个)
最后调用List::Add,将这个数据加入到集合中,数据全部加入集合后,调用end方法结束流程,最后返回这个集合。十分清晰干净的代码。
所有流程到这里就结束了。
总结
1.Stream的底层原理是用双向链表实现的,可以根据需要串行或并行执行操作。
2.Stream为数据的处理专门设置了一个Sink接口以及对应的实现类,会将所有操作递归封装到一个sink类里面去。从上往下依次调用无状态和有状态操作
3.Stream最后需要一个结束操作来结束流程