序列 (Sequences)
在 Kotlin 标准库中,Sequence 是一种与 Iterable(如 List)平行的集合操作机制。尽管 API 相似,但它们采用了截然不同的执行策略:Iterable 是及早求值 (Eager Evaluation),而 Sequence 是惰性求值 (Lazy Evaluation)。
执行模型对比
为了理解序列的优势,我们需要透视两者的执行流程。
Iterable:水平分层处理
每一步操作都会立即执行,并创建并返回一个完整的中间集合。
text
Input: [1, 2, 3]
↓ map { x * 2 }
Temp1: [2, 4, 6] (创建了第一个中间 ArrayList)
↓ filter { x > 3 }
Result: [4, 6] (创建了第二个结果 ArrayList)性能陷阱
对于大数据集(如 100 万条),每一步操作都会在内存中复制一份百万级的新列表。这不仅消耗内存,还增加了 GC 压力。
Sequence:垂直管道处理
序列不产生中间集合。它构建了一个操作流水线,数据元素逐个流过整个管道。
text
Input: 1 Input: 2 Input: 3
↓ ↓ ↓
map(1*2) -> 2 map(2*2) -> 4 map(3*2) -> 6
↓ ↓ ↓
filter(2>3)? ❌ filter(4>3)? ✅ filter(6>3)? ✅
↓ ↓
Output: 4 Output: 6核心优势:短路 (Short-circuiting)
由于序列是逐个处理元素的,当遇到 first()、find() 或 any() 等操作时,一旦找到满足条件的元素,后续元素的处理会被立即终止。
kotlin
val result = (1..1_000_000).asSequence()
.map { it * 2 }
.filter { it > 10 }
.first()
// 实际执行过程:
// 1 -> map(2) -> filter(2>10?) ❌
// 2 -> map(4) -> filter(4>10?) ❌
// ...
// 6 -> map(12) -> filter(12>10?) ✅ -> 找到结果 12
// 🛑 停止!剩下的 999,994 个元素完全没碰过。kotlin
// ❌ 灾难:先对 100 万个数做乘法,再对 100 万个数做过滤
// 最后只为了拿第 1 个数。
val result = (1..1_000_000)
.map { it * 2 }
.filter { it > 10 }
.first()创建序列的姿势
1. 转换:asSequence
最常用的方式,将现有集合转为序列。
kotlin
val list = listOf("a", "b", "c")
val seq = list.asSequence()2. 生成器:generateSequence
用于创建(可能无限的)序列。
kotlin
// 斐波那契数列生成器
val fibonacci = generateSequence(Pair(0, 1)) { Pair(it.second, it.first + it.second) }
.map { it.first }
.take(10) // 必须截断,否则是无限流
.toList()3. 协程构建器:sequence
这是 Kotlin 最具魔法特性的构建器。它允许你用命令式风格编写生成逻辑,并在内部使用 yield 挂起执行权。
底层原理
sequence 构建器使用了 限制性挂起 (Restricted Suspension)。它本质上是一个微型状态机。每当调用 yield(value) 时,状态机挂起并将值返回给迭代器;当下一次调用 next() 时,状态机从 yield 处恢复执行。
kotlin
val seq = sequence {
println("Start")
yield(1) // 返回 1 并挂起
println("Resume 1")
yield(2) // 返回 2 并挂起
println("Resume 2")
yieldAll(3..5) // 批量返回
}
println(seq.first())
// 输出: Start -> 1。 "Resume 1" 不会打印,因为迭代器没被继续驱动。操作符分类与状态
序列的操作符分为两类,这决定了它们是否支持无限流。
无状态 (Stateless)
如 map, filter, take。它们不需要知晓流的上下文,来一个处理一个。支持无限流。
有状态 (Stateful)
如 sorted, distinct。它们必须读取完所有元素才能决定下一个输出(例如排序必须先拿到所有数才能排)。
- 限制:不支持无限流(会导致死循环或 OOM)。
- 性能:会触发全量计算,丧失惰性优势。
kotlin
// ❌ 危险:对无限流排序
generateSequence(1) { it + 1 }
.sorted() // 死循环!永远在等待流结束
.take(5)选型建议
| 场景 | 推荐 | 原因 |
|---|---|---|
| 数据量小 (<100) | Iterable | 创建 Sequence 对象的开销反而大于遍历开销。 |
| 多步链式操作 | Sequence | 避免创建多个中间临时集合。 |
| 包含短路操作 | Sequence | 避免处理不必要的数据。 |
| 需要排序 (sorted) | Iterable | 排序破坏了惰性,Sequence 没有优势且有额外装箱开销。 |
| 只读一次流 | Sequence | Sequence 是只能迭代一次的(部分实现如 const sequence 除外),适合一次性消费。 |
核心准则总结
- 大数据链式处理首选 asSequence():这是优化集合操作最简单的手段。
- 警惕有状态操作符:
sorted和distinct会强制序列化为列表进行处理,在使用时需评估内存影响。 - Yield 是强大的状态机:利用
sequence { yield() }可以将复杂的递归逻辑扁平化为线性流。