Skip to content

序列 (Sequences)

源: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 没有优势且有额外装箱开销。
只读一次流SequenceSequence 是只能迭代一次的(部分实现如 const sequence 除外),适合一次性消费。

核心准则总结

  1. 大数据链式处理首选 asSequence():这是优化集合操作最简单的手段。
  2. 警惕有状态操作符sorteddistinct 会强制序列化为列表进行处理,在使用时需评估内存影响。
  3. Yield 是强大的状态机:利用 sequence { yield() } 可以将复杂的递归逻辑扁平化为线性流。