关于jdk8的重大改进,其中java对函数式编程的重视程度看看加入函数式编程后,扩充了多少功能,重写了多少基础类库可见一斑。引入Lambda表达式和Stream也是工作中应用最广泛的。关于Stream推荐参考《Java8函数式编程》。
一、Stream初始化
stream初始化的方式主要有以下几种:
Collection.stream()
或者Collection.parallelStream()
方法Arrays.stream(T[] array)
方法虽然Stream是通过 Collection.stream()
初始化的但是和Collections
有以下不同之处:
下面具体了解一下Stream流初始化的源码:
图一:Collection.stream()
最终都是调用到 StreamSupport.stream这个方法。我们看到这里第一个参数是获取一个Spliterator的实例,它表示从数据源中获取元素的方式。相当于升级版的Iterator。第二个参数是是否并行。大概介绍一下Spliterator接口方法。
boolean tryAdvance(Consumer action);
该方法会处理每个元素,如果没有元素处理,则应该返回false,否则返回true。default void forEachRemaining(Consumer action)
对每个剩余元素执行给定对动作,依次处理知道所有元素被处理或者异常终止,默认方法调用tryAdvance
方法。Spliterator trySplit();
将一个Spliterator分割成多个Spliterator。分割的Spliterator被用于每个子线程进行处理,从而达到并发处理的效果返回一个新对Spliterator
迭代器。long estimateSize();
用于估算还剩下多少个元素需要遍历。int characteristics();
给出stream流具有的特性,不同的特性,不仅是会对流的计算有优化作用,更可能对计算结果会产生影响。如:Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED。
default Comparator getComparator()
对sorted的流,给出比较器。如果Spliterator对list是通过Comparator排序对,则返回Comparator。如果list是自然排序返回null,其他情况下抛错。这里主要讲Stream关于Spliterator内部实现大家可以通过源码了解一下,这里不赘述。
>>>继续进入StreamSupport类:
图二:StreamSupport.stream()
我们可以看到最终调用对是ReferencePipeLine.Head,从图四可以看到ReferencePipeLine和InitPipeLine LongPipeLine DoublePipeLine 都继承AbstractPipeline和ReferencePipeLine时并行关系。之所以为三种基本类型定制,主要用于频繁拆装箱。AbstractPipeline时流水线核心抽象类,用于构建和管理流水线。
>>>AbstractPipeline:
AbstractPipeline中定义类三个变量:sourceStage(源阶段),previousStage(上游pipeline,前一阶段),nextStage(下一阶段)。
图三:AbstractPipeline
图四:Stream对主要接口类对关系图
讲完Stream对初始化,下面我们来了解一下Stream对中间操作,Stream对操作类型分外中间操作和结束操作
图五:Stream操作类型
二、中间操作(Intermediate operations)
1、返回结果是stream的非静态方法
2、中间操作是一种lazy操作可以多个,实际上并没有执行,而是等最终操作时执行。这也是Stream在迭代大集合时高效等原因之一。
3、中间操作分为无状态(Stateless)操作和有状态(Stateful)操作,无状态是指不依赖前面元素的影响。而有状态必须等前面执行完成之后才能知道结果。
通常一个完整的Stream操作是由:数据源 操作 回调函数组成的双向链表结构。如图六
图六:Stream流水线双向链表结构
通过Collection.Head初始化,后面调用一系列调中间操作不断产生新调Stream。这些Stream以双向链表调方式组织起来,形成一个流水线。由于每个stage记录了前一次操作调回调函数,这样就建立起对数据源对所有操作。
前面我们也提到了这种方式可以提高大集合迭代的高效那么stream是做的呢?
>>>Stream叠加操作
Stream通过Sink接口来协调相邻Stage之间的调用关系。sink接口包含下面几个方法:
void begin(Long size);
开始遍历元素之前调用该方法,通知Sink做好准备。void end();
所有元素遍历完成之后调用。boolean cancellationReauested();
是否可以结束操作,可以让短路操作尽早结束。void accept(T t);
遍历元素时调用,接受一个待处理元素,并对元素进行处理。Stage把自己包含的操作和回调方法封装到该方法里,前一个Stage只需要调用当前Stage.accept(T t)方法就行了。 通过Sink接口相邻Stage之间调用就很清晰了,每个Stage都将自己都操作封装到一个Sink里,前一个Stage调用后一个Stage到accept()
方法。我们来看一下Sink.map的源码。
Sink封装了Stream的每一步操作,但是我们前面提到Stream的中间操作是Lazy操作,真正启动这些操作的是结束操作来执行的。
三、结束操作(Terminal operations)
1、只能有一个最终操作,不一定有返回结果。
2、分为非短路操作和短路操作。非短路操作所有元素处理完,短路操作不用处理全部元素就可以返回结果
以ReferencePipeline.forEachOrder为例,PS:简单提一下Stream.forEach不能保证顺序,forEachOrder能保证顺序但效率不高。直接上源码:
ForEachOps是用户创建TerminalOp实例的工厂类。TerminalOp是终止操作最顶层的一个接口。TerminalOp接口的实现类有ForEachOp, ReduceOp,FindOp, MatchOp。
先看ForEachOps.makeRef()方法:
OfRef是引用流的默认实现类,这里新建了一个OfRef的实例,构造方法如下:
将我们实现的Consumer函数式接口赋值给成员变量。回到evaluate方法:
跟到串行流的实现,实现在ForEachOp中:
PipelineHelper类型其实是AbstractPipeline的父类,而AbstractPipeline又是ReferencePipeline的父类。再跟进helper.wrapAndCopyInto方法,是现在AbstractPipeline中:
可以看到通过ReferencePipeline的双向链表,从最后一个操作(也就是终止操作)往前遍历,将所有的操作都串联起来,最终返回一个指向第一个操作的Sink引用。
对于Stream操作性能:
所以,如果出于性能考虑,1. 对于简单操作推荐使用外部迭代手动实现,2. 对于复杂操作,推荐使用Stream API, 3. 在多核情况下,推荐使用并行Stream API来发挥多核优势,4.单核情况下不建议使用并行Stream API。
如果出于代码简洁性考虑,使用Stream API能够写出更短的代码。即使是从性能方面说,尽可能的使用Stream API也另外一个优势,那就是只要Java Stream类库做了升级优化,代码不用做任何修改就能享受到升级带来的好处。
参考资料:
https://www.cnblogs.com/CarpenterLee/p/6637118.html
https://www.cnblogs.com/Dorae/p/7779246.html
https://www.cnblogs.com/CarpenterLee/p/6675568.html