现在位置: 首页 > Kotlin 教程 > 正文

Kotlin 序列

序列(Sequence)是一种惰性求值的集合视图,它把多个操作合并成一次遍历,避免生成大量中间集合。

集合的 filter、map 每调用一次就会生成一个新列表,数据量大时内存和时间都会被浪费;序列则让元素逐个流过整条操作链。

本章节讲清楚序列和集合的区别、如何创建序列,以及在什么场景下值得改用序列。


什么是序列

序列由接口 Sequence<T> 表示,它只承诺一件事:可以逐个产出元素。

与 List、Set 不同,序列本身不存储元素,它保存的是「如何计算下一个元素」的规则。

实例

fun main() {
    // 一个普通的列表
    val list = listOf("runoob", "RUNOOB", "kotlin")

    // asSequence() 把列表包装成序列,此时不会做任何计算
    val sequence = list.asSequence()

    // 只有调用末端操作时,元素才会真正被处理
    println(sequence.first())
    println(sequence.count())
}
runoob
3

提示:序列不是一种新的数据结构,它更像是给集合套上的一层「处理管道」,管道里的加工步骤要到真正取结果时才执行。


序列与集合的区别

集合操作是立即求值的,每个函数都会完整执行并返回一个新集合。

集合的立即求值与序列的惰性求值对比:集合链每一步生成新集合,序列链元素逐个流过并在 first 处短路

序列操作是惰性求值的,中间操作只记录步骤,末端操作才触发计算。

对比项集合(Iterable)序列(Sequence)
求值时机立即求值,每个操作都执行惰性求值,末端操作才执行
中间结果每一步都生成一个新集合不生成中间集合,元素逐个流过
遍历次数每个操作各遍历一次所有操作合并成一次遍历
能否无限长不能,必须先把元素装进内存可以,配合 take 截取有限部分
小数据量性能更快,没有额外包装开销略慢,多了迭代器和 lambda 开销
适用场景元素不多,或需要多次遍历元素很多,或有提前终止的操作

下面的例子把同一个链式操作分别作用在集合和序列上,用计数器记录 lambda 实际执行了多少次。

实例

fun main() {
    val data = (1..10).toList()

    var listSteps = 0
    // 集合:filter 先把 10 个元素全部走一遍,map 再把得到的 5 个偶数走一遍
    val listResult = data
        .filter { listSteps++; it % 2 == 0 }
        .map { listSteps++; it * it }
        .first { it > 20 }

    var seqSteps = 0
    // 序列:每个元素依次穿过 filter 和 map,找到第一个满足条件的元素就立刻停止
    val seqResult = data.asSequence()
        .filter { seqSteps++; it % 2 == 0 }
        .map { seqSteps++; it * it }
        .first { it > 20 }

    println("集合结果:$listResult,lambda 执行次数:$listSteps")
    println("序列结果:$seqResult,lambda 执行次数:$seqSteps")
}
集合结果:36,lambda 执行次数:15
序列结果:36,lambda 执行次数:9

结果相同,但集合执行了 15 次 lambda,序列只执行了 9 次,因为序列在拿到 36 之后就不再处理后面的元素了。


创建序列

创建序列有四种常用方式,分别适合不同的数据来源。

方式用途示例
sequenceOf用已知的若干元素创建序列sequenceOf("runoob", "RUNOOB")
asSequence把已有的集合、数组转成序列listOf(1, 2).asSequence()
generateSequence由种子和「下一个值」的规则生成序列,可以无限长generateSequence(1) { it * 2 }
sequence { }用 yield 手动产出元素,适合复杂逻辑sequence { yield(1) }

实例

fun main() {
    // 1. sequenceOf:元素已知时最直接
    println(sequenceOf("runoob", "RUNOOB", "kotlin").toList())

    // 2. generateSequence:种子 1,每次乘 2,这是一个无限序列
    val powers = generateSequence(1) { it * 2 }
    println(powers.take(5).toList())

    // 加上 takeWhile 就能在满足条件时停下来
    println(generateSequence(1) { it * 2 }.takeWhile { it <= 100 }.toList())

    // 3. sequence { }:用 yield 逐个产出,适合有状态的逻辑
    val fib = sequence {
        var a = 0
        var b = 1
        while (true) {          // 无限循环,靠外部 take 控制取多少
            yield(a)            // 产出一个值并暂停,等下次迭代再继续
            val next = a + b
            a = b
            b = next
        }
    }
    println(fib.take(8).toList())
}
[runoob, RUNOOB, kotlin]
[1, 2, 4, 8, 16]
[1, 2, 4, 8, 16, 32, 64]
[0, 1, 1, 2, 3, 5, 8, 13]

注意:sequence { } 的代码块是挂起函数,里面只能调用 yieldyieldAll 这类 SequenceScope 提供的挂起函数,不能直接调用别的 suspend 函数。这个构建器从 Kotlin 1.3 起提供。

Iterator.asSequence() 包装得到的序列只能遍历一次,第二次遍历会抛异常;需要防止误用时可以调用 constrainOnce()(Kotlin 1.3 起)把它显式标记为一次性。


中间操作与末端操作

序列上的函数分两类:中间操作返回新的序列,末端操作返回一个具体结果并触发计算。

中间操作只是把步骤记录下来,不会立即执行,这就是「惰性」的来源。

类别常见函数返回值
中间操作filter、map、mapNotNull、flatMap、take、drop、distinct、sorted、onEach新的 Sequence
末端操作toList、toSet、first、find、count、sum、any、all、forEach、joinToString具体值或集合

实例

fun main() {
    // 这条链只是被"描述"出来,还没有任何元素被处理
    val pipeline = sequenceOf(1, 2, 3, 4, 5, 6)
        .filter { println("filter $it"); it % 2 == 0 }
        .map { println("map $it"); it * 10 }

    println("--- 到这里还没有输出 ---")

    // toList() 是末端操作,此时元素才逐个流过 filter 和 map
    val result = pipeline.toList()
    println(result)
}
--- 到这里还没有输出 ---
filter 1
filter 2
map 2
filter 3
filter 4
map 4
filter 5
filter 6
map 6
[20, 40, 60]

从输出顺序可以看出,序列是「一个元素走完整条链,再处理下一个元素」,而不是「先全部 filter,再全部 map」。

注意:sortedsortedBygroupBy 这类需要看到全部元素才能给出结果的中间操作,会先把整个序列读进内存,因此在无限序列上不能直接使用。


性能对比示例

数据量大且有提前终止条件时,序列的优势最明显。

下面的例子在 500 万个元素上做同样的筛选,只取第一个满足条件的结果。

实例

import kotlin.system.measureTimeMillis

fun main() {
    val data = (1..5_000_000).toList()

    // 集合:filter 和 map 各自生成一个约 167 万元素的中间列表
    val listTime = measureTimeMillis {
        data.filter { it % 3 == 0 }
            .map { it * 2 }
            .first { it > 1_000 }
    }

    // 序列:找到第一个满足条件的元素后立即停止,几乎不分配额外内存
    val seqTime = measureTimeMillis {
        data.asSequence()
            .filter { it % 3 == 0 }
            .map { it * 2 }
            .first { it > 1_000 }
    }

    println("集合耗时:${listTime}ms")
    println("序列耗时:${seqTime}ms")
}
集合耗时:96ms
序列耗时:2ms

提示:上面的耗时是某次运行的示例值,实际数字会随机器性能波动,但两者的数量级差距是稳定的。


什么时候用序列

序列不是性能银弹,小数据量下它反而更慢,因为它为每个元素都引入了迭代器和 lambda 调用的开销。

场景推荐原因
元素数量很小(几十个以内)集合没有中间集合的负担,直接遍历更快
数据量大且链式操作多序列避免生成多个庞大的中间集合
链中带 first、find、take、any 等提前终止操作序列拿到结果就停,不必处理剩余元素
需要无限或未知长度的数据流序列集合无法装下无限数据
需要多次遍历同一份数据集合序列每次末端操作都会重新计算
需要随机访问、按索引取值集合序列只能顺序迭代,不支持下标

一个实用的判断方法是:如果这条链的末端操作是 toList() 而且元素数量不多,用集合就好;如果链的末端是 first()take()find() 或者数据量很大,改用序列通常更划算。


常见问题

下面汇总序列使用中的典型疑问。

问题原因解决办法
写完序列操作却没有输出只写了中间操作,没有调用末端操作补上 toList、forEach、count 等末端操作
序列第二次遍历报错序列由 Iterator 包装而来,只能遍历一次重新调用 asSequence,或用 constrainOnce 显式声明
sorted 之后卡住或内存暴涨排序是有状态操作,需要读完整个序列不要用在无限序列上,先 take 限定范围
小列表改用序列反而更慢序列有额外的包装与调度开销数据量小就继续用集合
在 sequence 块里调用普通挂起函数报错SequenceScope 只允许 yield 系列的挂起调用把逻辑写成普通函数,再在块里 yield 结果