前语

协程系列文章:

  • 一个小故事讲了解进程、线程、Kotlin 协程究竟啥联系?
  • 少年,你可知 Kotlin 协程最初的样子?
  • 讲真,Kotlin 协程的挂起/恢复没那么奥秘(故事篇)
  • 讲真,Kotlin 协程的挂起/恢复没那么奥秘(原理篇)
  • Kotlin 协程调度切换线程是时分解开真相了
  • Kotlin 协程之线程池探索之旅(与Java线程池PK)
  • Kotlin 协程之取消与反常处理探索之旅(上)
  • Kotlin 协程之取消与反常处理探索之旅(下)
  • 来,跟我一同撸Kotlin runBlocking/launch/join/async/delay 原理&运用
  • 持续来,同我一同撸Kotlin Channel 深水区
  • Kotlin 协程 Select:看我怎么多路复用
  • Kotlin Sequence 是时分派上用场了
  • Kotlin Flow啊,你将流向何方?
  • Kotlin Flow 背压和线程切换竟然如此类似
  • Kotlin SharedFlow&StateFlow 暖流究竟有多热?

在进入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补充道:“老大哥,你说的比较抽象,我举个比方吧。”

Kotlin Sequence 是时候派上用场了

在一个管道的入口处放入了各种鱼,如草鱼、鲤鱼、鲢鱼、金鱼等,管道答应接入不同的小管道用以筛选不同组合的鱼类。
比方有个客户只想要金鱼,所以它分别接了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");
    }

打印成果如下:

Kotlin Sequence 是时候派上用场了

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();
    }

Kotlin Sequence 是时候派上用场了

能够看出,运用了sequence后,能够大大削减耗时。

Kotlin 反编译Sequence

Kotlin Sequence 是时候派上用场了

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

4. Sequence 的原理

调集转Sequence

(0..10000000)

这表明的是0到10000000的调集,它的完成类是:

Kotlin Sequence 是时候派上用场了

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操作符,终究构成的结构如下:

Kotlin Sequence 是时候派上用场了

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

  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()办法后,将会触发遍历,终究调用栈如下:

Kotlin Sequence 是时候派上用场了

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

Sequence与一般调集链式调用区别

还是之前的Demo
一般调集链式调用

Kotlin Sequence 是时候派上用场了

每次操作(如filter)都需求遍历调集找到契合条件的条目加入到新的调集,然后再在新的调集基础上再次进行操作。
如上图,先履行紫色区块,再履行蓝色区块。

Sequence 调用

Kotlin Sequence 是时候派上用场了

每次先对某个条目进行一切的操作(比方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 协程系列全面解读