Stream如何提高遍历集合效率?
上一讲中,我在讲 List 集合类,那我想你一定也知道集合的顶端接口 Collection。在 Java8 中,Collection 新增了两个流方法,分别是 Stream() 和 parallelStream()。 通过英文名不难猜测,这两个方法肯定和 Stream 有关,那进一步猜测,是不是和我们熟悉 的 InputStream 和 OutputStream 也有关系呢?集合类中新增的两个 Stream 方法到底有 什么作用?今天,我们就来深入了解下 Stream。
什么是 Stream?
现在很多大数据量系统中都存在分表分库的情况。例如,电商系统中的订单表,常常使用用户 ID 的 Hash 值来实现分表分库,这样是为了减 少单个表的数据量,优化用户查询订单的速度。 但在后台管理员审核订单时,他们需要将各个数据源的数据查询到应用层之后进行合并操 作。
例如,当我们需要查询出过滤条件下的所有订单,并按照订单的某个条件进行排序,单个数据源查询出来的数据是可以按照某个条件进行排序的,但多个数据源查询出来已经排序好的数据,并不代表合并后是正确的排序,所以我们需要在应用层对合并数据集合重新进行排 序。
在 Java8 之前,我们通常是通过 for 循环或者 Iterator 迭代来重新排序合并数据,又或者通过重新定义 Collections.sorts 的 Comparator 方法来实现,这两种方式对于大数据量系统来说,效率并不是很理想。
Java8 中添加了一个新的接口类 Stream,他和我们之前接触的字节流概念不太一样, Java8 集合中的 Stream 相当于高级版的 Iterator,他可以通过 Lambda 表达式对集合进 行各种非常便利、高效的聚合操作(Aggregate Operation),或者大批量数据操作 (Bulk Data Operation)。 Stream 的聚合操作与数据库 SQL 的聚合操作 sorted、filter、map 等类似。
我们在应用 层就可以高效地实现类似数据库 SQL 的聚合操作了,而在数据操作方面,Stream 不仅可 以通过串行的方式实现数据操作,还可以通过并行的方式处理大批量数据,提高数据的处理 效率。
接下来我们就用一个简单的例子来体验下 Stream 的简洁与强大。 这个 Demo 的需求是过滤分组一所中学里身高在 160cm 以上的男女同学,我们先用传统 的迭代方式来实现,代码如下:
Stream 如何优化遍历?
上面我们初步了解了 Java8 中的 Stream API,那 Stream 是如何做到优化迭代的呢?并行 又是如何实现的?下面我们就透过 Stream 源码剖析 Stream 的实现原理。
1.Stream 操作分类
在了解 Stream 的实现原理之前,我们先来了解下 Stream 的操作分类,因为他的操作分类 其实是实现高效迭代大数据集合的重要原因之一。为什么这样说,分析完你就清楚了。
官方将 Stream 中的操作分为两大类:中间操作(Intermediate operations)和终结操作 (Terminal operations)。中间操作只对操作进行了记录,即只会返回一个流,不会进行 计算操作,而终结操作是实现了计算操作。
中间操作又可以分为无状态(Stateless)与有状态(Stateful)操作,前者是指元素的处理 不受之前元素的影响,后者是指该操作只有拿到所有元素之后才能继续下去。
终结操作又可以分为短路(Short-circuiting)与非短路(Unshort-circuiting)操作,前 者是指遇到某些符合条件的元素就可以得到最终结果,后者是指必须处理完所有元素才能得 到最终结果。操作分类详情如下图所示:
2.Stream 源码实现
在了解 Stream 如何工作之前,我们先来了解下 Stream 包是由哪些主要结构类组合而成的,各个类的职责是什么。参照下图:
BaseStream 和 Stream 为最顶端的接口类。BaseStream 主要定义了流的基本接口方法, 例如,spliterator、isParallel 等;
Stream 则定义了一些流的常用操作方法,例如, map、filter 等。 ReferencePipeline 是一个结构类,他通过定义内部类组装了各种操作流。他定义了 Head、StatelessOp、StatefulOp 三个内部类,实现了 BaseStream 与 Stream 的接口方 法。
Sink 接口是定义每个 Stream 操作之间关系的协议,他包含 begin()、end()、 cancellationRequested()、accpt() 四个方法。ReferencePipeline 最终会将整个 Stream 流操作组装成一个调用链,而这条调用链上的各个 Stream 操作的上下关系就是通过 Sink 接口协议来定义实现的。
3.Stream 操作叠加
我们知道,一个 Stream 的各个操作是由处理管道组装,并统一完成数据处理的。在 JDK 中每次的中断操作会以使用阶段(Stage)命名。
管道结构通常是由 ReferencePipeline 类实现的,前面讲解 Stream 包结构时,我提到过 ReferencePipeline 包含了 Head、StatelessOp、StatefulOp 三种内部类。
Head 类主要用来定义数据源操作,在我们初次调用 names.stream() 方法时,会初次加载 Head 对象,此时为加载数据源操作;接着加载的是中间操作,分别为无状态中间操作 StatelessOp 对象和有状态操作 StatefulOp 对象,此时的 Stage 并没有执行,而是通过 AbstractPipeline 生成了一个中间操作 Stage 链表;当我们调用终结操作时,会生成一个 最终的 Stage,通过这个 Stage 触发之前的中间操作,从最后一个 Stage 开始,递归产生 一个 Sink 链。如下图所示:
这个例子的需求是查找出一个长度最长,并且以张为姓氏的名字。
从代码角度来看,你可能 会认为是这样的操作流程:
首先遍历一次集合,得到以“张”开头的所有名字;然后遍历一 次 filter 得到的集合,将名字转换成数字长度;最后再从长度集合中找到最长的那个名字并 且返回。
这里我要很明确地告诉你,实际情况并非如此。我们来逐步分析下这个方法里所有的操作是 如何执行的。 首先 ,因为 names 是 ArrayList 集合,所以 names.stream() 方法将会调用集合类基础接 口 Collection 的 Stream 方法:
再调用 filter 和 map 方法,这两个方法都是无状态的中间操作,所以执行 filter 和 map 操作时,并没有进行任何的操作,而是分别创建了一个 Stage 来标识用户的每一次操作。
而通常情况下 Stream 的操作又需要一个回调函数,所以一个完整的 Stage 是由数据来源、操作、回调函数组成的三元组来表示。如下图所示,分别是 ReferencePipeline 的 filter 方法和 map 方法:
new StatelessOp 将会调用父类 AbstractPipeline 的构造函数,这个构造函数将前后的 Stage 联系起来,生成一个 Stage 链表: