Kotlin Sequence 是时候派上用场了

6,135 阅读12分钟

前言

协程系列文章:

在进入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请点击

您若喜欢,请点赞、关注、收藏,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Kotlin

1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列
17、Android Jetpack 前置基础系列
18、Android Jetpack 易学易懂系列
19、Kotlin 轻松入门系列
20、Kotlin 协程系列全面解读