** 写在前面的话:本人作为一枚纯技术爱好者,一直喜欢利用闲暇写一点自己研究的收获和体会。虽然力求谨慎,但个人见解难免会有有失偏颇的时候,还望各位读者批评指正! **
要想吃透Stream的设计,最好还是从它的UML设计图开始。通过上图我们可以看到,貌似BaseStream是作为核心流操作的规范存在,那么我们就先以它为切入点开始剖析整个Stream的体系。
AutoCloseable接口
但在BaseStream之上仍然有一个AutoCloseable接口,虽然这个接口很简单,但我们还是先看一下吧。
This construction ensures prompt release, avoiding resource exhaustion exceptions and errors that may otherwise occur.
这段截取自该接口的注释,大致意思就是这个结构保证了资源的及时释放,避免发生资源枯竭带来的异常。
Stream顾名思义“流”,这个流可能来自文件、网络I/O……所以JDK在顶层定义了这样一个接口,规定了流的实现类在资源使用完成或者遇到异常的时候及时释放资源,避免发生资源枯竭。
所以这个接口也只声明了一个方法: void close() throws Exception 也就是关闭流操作。
BaseStream 接口
现在正式开始介绍BaseStream,我们先来大致看一下它都定义了哪些规范。
Iterator<T> iterator()
定义了每个Stream都需要提供返回一个迭代子对象的方法。
Spliterator<T> spliterator()
和iterator() 方法类似,但返回的对象是Spliterator而不是Iterator。Spliterator可以将元素分割成多份,分别交于不于的线程去遍历,以提高效率。
使用Iterator的时候,我们可以顺序地遍历容器中的元素,使用Spliterator的时候,我们可以将元素分割成多份,分别交于不于的线程去遍历,以提高效率。
boolean isParallel()
判断该流是不是并行执行。
S sequential()
返回一个串行流,如果该流本身就是串行的,则返回他本身。
S parallel()
返回一个并行流,如果该流本身就是并行的,则返回他本身。
S unordered()
返回一个无序流,如果该流本身就是无序的,则返回他本身。
S onClose(Runnable closeHandler)
返回一个带着关闭处理的流。closeHandler线程将在调用close()方法时执行。
乍一看定义的方法很多,但实际上只有三类。第一类是iterator() 和 spliterator()操作提供了遍历元素的方法。
第二类是 sequential()、parallel()、unordered()对流进行转化,返回一个指定的流。
第三类就是对关闭流方法的处理。
总结来讲,BaseStream并没有对具体流的操作做任何说明,只是规定了三种流的形式和提供了两种遍历流的方式——单线程、多线程两种遍历方式。
最后再提一下关于BaseStream这个接口的定义: interface BaseStream<T, S extends BaseStream<T, S>>
这个定义方式也是很奇特——泛型的第二个参数必须是自己的子类。
看了JDK关于T和S的解释,T是指定stream元素的类型,S是stream的实现类。
看完也就理解了,它使用S规定了创建流的类型,而这个接口有4个方法会返回指定流,所以才需要规定流的类型必须是BaseStream的实现类。
Stream 接口
实际上BaseStream并没有定义任何与具体流的操作,所以我们只能把目光转向它的子接口。 BaseStream 接口下面有4个继承接口和一个抽象类,之所以选Stream接口是因为从命名上来看IntStream、LongStream、DoubleStream与是作为特殊数据特殊处理的Stream而存在的,所以我们姑且可以先看Stream接口。
这篇文章的目的是拆解分析整个Java8 Stream体系,过多介绍某个接口或实现类的方法并没有太大的意义,所以挑了几个比较具有代表性的方法。
Stream<T> filter(Predicate<? super T> predicate)
该方法接收一个lambda表达式(Predicate类型)过滤出符合条件的元素,并返回一个新流。
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
该方法接收一个lambda表达式(Function类型)过滤出符合条件的元素,并返回一个新流。
T reduce(T identity, BinaryOperator<T> accumulator)
该方法接收一个lambda表达式(BiFunction类型)过滤出符合条件的元素,并返回结果。
void forEach(Consumer<? super T> action)
和以上方法类似,传入一个lambda表达式(Consumer类型),遍历流,无返回值。
这些接口看着差别很大,但仔细研究一下就会发现,他们都是传入一个Lambda表达式,这个表达式会对流的元素进行一定操作,只是返回值有差异而已。filter、map是返回一个新流,reduce是返回结果,forEach是对元素遍历。
对比于List接口定义的一些抽象——get、set、add、remove……,我们可以发现传统的Collection的关注点在元素,而Stream的关注点在于函数,更确切的讲是对数据的计算。所以我们认为Stream的出现是对Collection的一种补充。
一些新的概念
JDK的作者有一个很好的习惯,就是在重要的接口定义上会有非常详细的注释。Stream也不例外,一个简单的Stream有着100多行的注释。
随着对注释的解读,我发现作为对Collection的补充,Stream不仅有着很便捷的语法,对大数据量的处理也有更高的效率,以下是本人对一些比较重要的特点的一些总结。
-
关于stream的操作,大体可以划分为两类——
intermediate operations
和terminal operation
。这两者有什么区别呢?
intermediate operations
按照原文的解释就是:which transform a stream into an other stream, such as Stream#filter(Predicate)
意思就是一个stream处理以后返回的是一个新的stream那么就称之为intermediate operations
相对的 terminal operation
对应的操作就是那些产生了一定结果或者其他影响的操作,例如: Stream#count()} 或者 Stream#forEach(Consumer)}
为什么JDK会把stream的操作区分为以上两种形式呢?原因他们也给了解释: Streams are lazy; computation on the source data is only performed when the terminal operation is initiated, and source elements are consumed only as needed.
Streams 是懒加载的,仅在启动terminal操作时才执行源数据的计算,并且仅根据需要消耗源元素。
-
Stream与Collection的本质区别
根据JDK作者的解释,虽然他们之间有着很多相似的地方,但他们设计的目标是不一样的。
集合主要关注的是管理和访问他们的元素,相比之下,流不直接访问或操纵元素提供了一种手段,而是关注对流进行聚合等计算操作。
当然了,流也提供了
iterator()和spliterator()
两个方法对流进行遍历。
总结:
关于Java8 Stream的引入是对原先Collection的一个扩展。
Java8之前的Collection,例如List、Map等更关注对单个元素的操作,更适用于的访问、修改。还有一个很重要的特性就是一旦对象被创建,那么Collection将直接占用相应的堆内存。
而Stream则不同,它使用"流"的概念,注重对数据的过滤、聚合等操作,但并不修改数据源本身。而且还有一个很重要的特性就是Stream只有在使用到的时候才会加入内存,并且仅根据需要消耗源元素。所以,流式计算更适合于大数据量的过滤、统计等操作。
最后夹带一点私货,看了JDK关于接口的设计,真的是非常有收获,当我看到BaseStream这个接口的时候,下意识以为这个接口至少会定义一些流的基本操作,但是实际上它仅仅规定了流的方向——迭代子遍历,串行、并行以及他们之间的转化,而真正对于流的操作则是放到了Stream接口里。** 阅读源码最大的好处不是知道怎么去用这些方法,而是学习他们怎么去设计这些接口、管理他们之间的复杂关系(所以我才从UML入手)。 **这也是我孜孜不倦的原因,共勉!
*** To be continue……***
** 关于文章开头UML图中的XXXPipeline也是非常重要且优雅的设计,我会在下一章揭开它的神秘面纱。**