Kotlin Sequence 是时候派上用场了

作者: 小鱼人爱编程 | 来源:发表于2022-11-01 12:37 被阅读0次

    前言

    在进入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 协程系列全面解读

    相关文章

      网友评论

        本文标题:Kotlin Sequence 是时候派上用场了

        本文链接:https://www.haomeiwen.com/subject/hhsltdtx.html