注册

Kotlin Sequence 是时候派上用场了

前言


在进入Flow世界之前,先来分析Sequence,进而自然延伸到Flow。

通过本篇文章,你将了解到:




  1. Java与Kotlin 对集合的处理
  2. Java Stream 的简单使用
  3. Sequence 的简单使用
  4. Sequence 的原理
  5. Sequence 的优劣势


1. Java与Kotlin 对集合的处理


场景分析


客户有个场景想考验一下Java和Kotlin:

从一堆数据里(0--10000000)找到大于1000的偶数的个数。


Java和Kotlin 均表示so easy,跃跃欲试。

秉着尊老爱幼的优良传统,老大哥Java先出场。


Java 出场


    public List<Integer> dealCollection() {
List<Integer> evenList = new ArrayList<>();
for (Integer integer : list) {
//筛选出偶数
if (integer % 2 == 0) {
evenList.add(integer);
}
}

List<Integer> bigList = new ArrayList<>();
for (Integer integer : evenList) {
//从偶数中筛选出大于1000的数
if (integer > 1000) {
bigList.add(integer);
}
}
//返回筛选结果列表
return bigList;
}

Java解释说:“先将偶数的结果保存到列表里,再从偶数列表里筛选出大于1000的数。”


Kotlin 出场


Kotlin 看到Java的解决方案,表示写法有点冗余,不够灵活,于是拿出自己的方案:


    fun testCollection() {
var time = measureTimeMillis {
var list = (0..10000000).filter {
it % 2 == 0
}.filter {
it > 1000
}
println("kotlin collection list size:${list.size}")
}
println("kotlin collection use time:$time")
}

Kotlin 说:“老大哥,看看我这个写法,只需要几行代码,简洁如斯。”

Java 淡定到:“确实够简洁,但是表面的简洁掩盖了背后的许多冗余,能一层一层剥开你的心吗?”

Kotlin道:“你我赤诚相对,士为知己者死,刀来!”

Java赶紧递上自己随身携带的水果刀...


Kotlin 反编译


遇事不决反编译:


    public final void testCollection() {
//构造迭代器
Iterable $this$filter$iv = (Iterable)(new IntRange(var8, 10000000));
//构造链表用来存储偶数
Collection destination$iv$iv = (Collection)(new ArrayList());
//取出迭代器
Iterator var13 = $this$filter$iv.iterator();

//遍历取出偶数
while(var13.hasNext()) {
element$iv$iv = var13.next();
it = ((Number)element$iv$iv).intValue();
var16 = false;
if (it % 2 == 0) {
destination$iv$iv.add(element$iv$iv);
}
}

$this$filter$iv = (Iterable)((List)destination$iv$iv);
$i$f$filter = false;
//构造链表用来存储>1000的偶数
destination$iv$iv = (Collection)(new ArrayList());
$i$f$filterTo = false;
//取出迭代器
var13 = $this$filter$iv.iterator();
//遍历链表
while(var13.hasNext()) {
element$iv$iv = var13.next();
it = ((Number)element$iv$iv).intValue();
var16 = false;
if (it > 1000) {
destination$iv$iv.add(element$iv$iv);
}
}

//最终的结果
List list = (List)destination$iv$iv;
}

看到这,Java恍然大悟到:“原来如此,你也是分步存储结果,我俩想到一起了,真机智啊。”

Kotlin:“彼此彼此。”


客户说:“你俩就不要商业互吹了,我就想要一个结果而已,你们就给我弄了两个循环,若是此时我再加一两个条件,你们是不是得要再加几个循环遍历?那不是白白增加耗时吗?”

Java 神秘的笑道:“非也非也,我好歹也是沉浸代码界几十年的存在,早有预案。”

客户说:“那就开始你的表演吧...”


2. Java Stream 的简单使用


什么是流


Java说:“我从Java8开始就支持Stream(流) API了,可以满足你的需求。”

客户不解道:“什么是流?”

Java:“流就是一个过程,比如说你之前的需求就可以当做一个流,可以在中途对流做一系列的处理,而后在流的末尾取出处理后的结果,这个结果就是最终的结果。”

Kotlin补充道:“老大哥,你说的比较抽象,我举个例子吧。”



image.png


在一个管道的入口处放入了各种鱼,如草鱼、鲤鱼、鲢鱼、金鱼等,管道允许接入不同的小管道用以筛选不同组合的鱼类。

比如有个客户只想要金鱼,于是它分别接了4个小管道,第一个管道用来将草鱼分流,第二个管道用来分流鲤鱼,第三个管道用来分流鲢鱼,最后剩下的就是金鱼。

当然,他也可以只分流草鱼,剩下的鲤鱼、鲢鱼、金鱼他都需要,这就增加了操作的灵活性。

客户说:“talk is cheap, show me the code。”


Java Stream


Java 撸起袖子,几个呼吸之间就写好了如下代码:


    public long dealCollectionWithStream() {
Stream<Integer> stream = list.stream();
return stream.filter(value -> value % 2 == 0)
.filter(value -> value > 1000)
.count();
}

客户不解地问:“这确实很简洁了,但是和Kotlin写法一样的嘛?”

Java道:“No No No,别被简洁的外表迷惑了,我们直接来看看处理的耗时即可。”


    public static void main(String args[]) {
Java8Stream java8Stream = new Java8Stream();
//普通集合耗时
long startTime = System.currentTimeMillis();
List<Integer> list = java8Stream.dealCollection();
System.out.println("java7 list size:" + list.size() + " use time:" + (System.currentTimeMillis() - startTime) + "ms");

//Stream API 的耗时
long startTime2 = System.currentTimeMillis();
long count = java8Stream.dealCollectionWithStream();
System.out.println("java8 stream list size:" + count + " use time:" + (System.currentTimeMillis() - startTime2) + "ms");
}

打印结果如下:



image.png


Java 继续解释:“既然只关心最后的结果,那么对于流来说,可以在各个位置指定条件对流的内容进行筛选,对于同一个内容来说只有上一个条件满足了,才会继续处理下一个条件,否则将会处理流里的其它内容。如此一来,再也不用反复存取中间结果了,对于大批量的数据来说,大大减少了耗时。”

客户赞赏:“不错,能解决我的痛点。”

Java 说:“不仅如此,我还可以并行操作流,最后将结果汇总,又可以减少一些耗时了。”

客户:“优秀,那我就选...”

Kotlin 急道:“住口...不,等等,我有话说。”

客户:“你快说,说不出子丑寅卯,我就...”


3. Sequence 的简单使用


Sequence 引入


Kotlin:“和Java老大哥一样,我也可以对流进行操作,主要是用sequence实现”


    fun testSequence() {
var time = measureTimeMillis {
val count = (0..10000000)
.asSequence()//转换为sequence
.filter {
it % 2 == 0//过滤偶数
}.filter {
it > 1000//过滤>1000
}.count() //统计个数
println("kotlin sequence list size:${count}")
}
println("kotlin sequence use time:$time")
}

和未使用sequence 对比耗时:


    public static void main(String args[]) {
SequenceDemo sequenceDemo = new SequenceDemo();
//使用集合操作
sequenceDemo.testCollection();
//使用sequence操作
sequenceDemo.testSequence();
}


image.png


可以看出,使用了sequence后,可以大大减少耗时。


Kotlin 反编译Sequence



image.png


由此可见,并没有对中间结果进行存储遍历,而是通过嵌套调用进而操作流的。


4. Sequence 的原理


集合转Sequence


(0..10000000)

这表示的是0到10000000的集合,它的实现类是:



image.png


IntRange 里定义了集合的开始值和结束值,重点在其父类:IntProgression。

IntProgression 实现了Iterable接口,并实现了该接口里的唯一方法:iterator()

具体实现类为:


internal class IntProgressionIterator(first: Int, last: Int, val step: Int) : IntIterator() {
private val finalElement: Int = last
private var hasNext: Boolean = if (step > 0) first <= last else first >= last
private var next: Int = if (hasNext) first else finalElement

override fun hasNext(): Boolean = hasNext

override fun nextInt(): Int {
val value = next
if (value == finalElement) {
if (!hasNext) throw kotlin.NoSuchElementException()
hasNext = false
}
else {
next += step
}
return value
}
}

通常来说,迭代器有三个重要元素:




  1. 起始值
  2. 步长
  3. 结束值


对应的两个核心方法:




  1. 检测是否还有下个元素
  2. 取出下个元素


对于当前的Int迭代器来说:它的起始值为0,步长是1,结束值是10000000,当我们调用迭代器时就可以取出里面的每个数。


迭代器有了,接下来看看如何构造为一个Sequence。


public fun <T> Iterable<T>.asSequence(): Sequence<T> {
//取当前的迭代器,也就是IntProgressionIterator
return Sequence { this.iterator() }
}
//构造一个Sequence
public inline fun <T> Sequence(crossinline iterator: () -> Iterator<T>): Sequence<T> = object : Sequence<T> {
override fun iterator(): Iterator<T> = iterator()
}

Sequence 是个接口,它的唯一接口是:


public interface Sequence<out T> {
public operator fun iterator(): Iterator<T>
}

结合两者分析可知:



asSequence() 构造了Sequence匿名内部类对象,而其实现的方法就是iterator(),该方法最终返回IntProgressionIterator 对象
也就是说Sequence初始迭代器即为Collection的迭代器



Sequence中间操作符


以filter为例:


public fun <T> Sequence<T>.filter(predicate: (T) -> Boolean): Sequence<T> {
//构造Sequence 子类,该子类用来过滤流
return FilteringSequence(this, true, predicate)
}

override fun iterator(): Iterator<T> = object : Iterator<T> {
//上一个Sequence的迭代器
val iterator = sequence.iterator()
var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue
var nextItem: T? = null

private fun calcNext() {
//先判断上一个Sequence的迭代器
while (iterator.hasNext()) {
val item = iterator.next()
//拿到值后判断本Sequence的逻辑
//是否符合过滤条件,符合就取出值,交个下一个条件,不符合则找下一个元素
if (predicate(item) == sendWhen) {
nextItem = item
nextState = 1
return
}
}
nextState = 0
}

//重写next()与hasNext(),里边调用了calcNext
override fun next(): T {
if (nextState == -1)
calcNext()
if (nextState == 0)
throw NoSuchElementException()
val result = nextItem
nextItem = null
nextState = -1
@Suppress("UNCHECKED_CAST")
return result as T
}

override fun hasNext(): Boolean {
if (nextState == -1)
calcNext()
return nextState == 1
}
}

我们调用了两次filter操作符,最终形成的结构如下:



image.png


此处用到了设计模式里的装饰模式:




  1. Sequence 只有普通的迭代功能,现在需要为它增强过滤偶数的功能,因此新建了FilteringSequence 对象A,并持有Sequence对象,当需要调用过滤偶数的功能时,先借助Sequence获取基本数据,再使用FilteringSequenceA过滤偶数
  2. 同样的,还需要在1的基础上继续增强FilteringSequence的过滤功能,再新建FilteringSequence B持有FilteringSequence对象A
    A,当需要调用过滤>1000的数时,先借助FilteringSequence 对象A获取偶数,再使用FilteringSequenceB过滤>1000的数


如此一来,通过嵌套调用就实现了众多操作过程。


Sequence终端操作符


你可能已经发现了:中间操作符仅仅只是建立了装饰(引用)关系,并没有触发迭代啊,那什么时候触发迭代呢?

这个时候就需要用到终端操作符(也叫末端操作符)。

比如count方法:


    public fun <T> Sequence<T>.count(): Int {
var count = 0
//触发遍历,统计个数
for (element in this) checkCountOverflow(++count)
return count
}

当调用了count()方法后,将会触发遍历,最终调用栈如下:



image.png


只有调用了终端操作符,流才会动起来,这也就是为啥说Sequence、Java Stream 中间操作符是惰性操作符的原因。


Sequence与普通集合链式调用区别


还是之前的Demo

普通集合链式调用



image.png


每次操作(如filter)都需要遍历集合找到符合条件的条目加入到新的集合,然后再在新的集合基础上再次进行操作。

如上图,先执行紫色区块,再执行蓝色区块。


Sequence 调用



image.png


每次先对某个条目进行所有的操作(比如filter),先判断每一步该条目是否符合,不符合则再找下一个条目进行所有的操作。

如上图:从左到右按顺序执行紫色区块。


5. Sequence 的优劣势


与普通集合链式调用对比,Sequence也有链式调用。

前者链式调用每次都需要完整遍历集合并将中间结果缓存,下一次调用依赖上一次调用缓存的结果。

而后者链式调用先是将每个操作关联起来,然后当触发终端操作符时针对每一个条目(元素)先执行所有的操作(这些操作在上一步已经关联)。

由此可见,如果集合里元素很多,Sequence可以大大节约时间(没有多次遍历,没有暂存结果)


除此之外,Sequence 只做最少的操作,尽可能地节约时间。

怎么理解呢?还是上面的例子,我们只想取前10个偶数,代码如下:


    fun testSequence1() {
var time = measureTimeMillis {
val count = (0..10000000)
.asSequence()//转换为sequence
.filter {
it % 2 == 0//过滤偶数
}.take(10).count()
println("kotlin sequence1 list size:${count}")
}
println("kotlin sequence1 use time:$time")
}

该序列只会执行到集合里的条目=18就终止了,因为到了0~18已经有10个偶数了,而剩下的一堆条目都无需执行,大大节省了时间。


因此,Sequence 优势:




  1. 不暂存数据,不进行多次遍历
  2. 只做最少的操作
  3. 可以产生无限的序列


以上以filter/take/count 操作符阐述了Sequence的原理及其优势,当然还有更多的具体的使用场景/方式待挖掘。


此时,Kotlin 迫不及待跳出来说:“怎么样,我这个Sequence 6吧?”

客户说:“看你字多的份上,我选择信你,那我就选...”

Java急忙道:“我有问题,我的Stream支持并行,你支持吗?”

Kotlin:“...”

想了一会儿,Kotlin继续道:“Sequence 虽然不支持切换线程,但是它的兄弟支持,它就是Flow。”

Java补充说:“你有Flow,我有LiveData,那我俩继续PK?”

没等Kotlin回话,客户急忙道:“哎哎,行了,时间不够了,下次再继续吧,散会...”

Java:“...”

Kotlin:“...”


第100篇博客,不忘初心,砥砺前行,继续输出高质量、成体系的博客。

下次将会进入Flow的世界。


本文基于Kotlin 1.5.3,文中完整Demo请点击


作者:小鱼人爱编程
链接:https://juejin.cn/post/7160910310100992014
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册