第65章:序列(Sequence):惰性求值、Sequence的构建、与集合的性能对比
说实话,刚接触Kotlin那会儿,我对Sequence是有点不屑的。
当时心里想:不就是个懒加载的集合吗?集合API那么全,我用它不香吗?
直到有一次,我在处理一个百万级的数据源时,集合链式操作直接OOM了。嗯,从那以后,我再也不敢小看Sequence了。
什么是惰性求值?
惰性求值,说白了就是「用到的时候才算」。
我们平时用的List、Set这些集合,调用map、filter时,每一步都会立刻生成一个新集合。这就是「饿汉式」求值。
而Sequence正好相反——它把所有的操作步骤记下来,等到真正需要结果时,才一口气算完。
核心区别一句话:
集合是「每步都算」,Sequence是「最后一起算」。
Sequence的构建方式
我个人习惯用三种方式来构建Sequence,每种都有它的适用场景。
1. 从集合转换
最简单的方式,直接调用.asSequence():
val list = listOf(1, 2, 3, 4, 5)
val seq = list.asSequence()
2. 使用sequenceOf
跟listOf类似,但生成的是Sequence:
val seq = sequenceOf(1, 2, 3, 4, 5)
3. 使用generateSequence
这个我特别喜欢,适合生成无限序列或递推序列:
// 生成自然数序列
val naturalNumbers = generateSequence(1) { it + 1 }
// 生成斐波那契数列
val fibonacci = generateSequence(0 to 1) { it.second to it.first + it.second }
.map { it.first }
小技巧:generateSequence生成的序列是无限的,一定要用take(n)限制数量,否则会无限循环。
Sequence与集合的性能对比
我曾经在项目中做过一个测试,数据量是100万条,做三次map和一次filter。结果让我印象深刻。
| 操作 | 集合(List) | Sequence |
|---|---|---|
| 3次map + 1次filter | 生成4个中间集合,内存暴涨 | 只生成1个最终结果,内存稳定 |
| 执行时间(10万条) | 约120ms | 约85ms |
| 执行时间(100万条) | 约1.2s,可能OOM | 约900ms,内存平稳 |
为什么会这样?
因为集合的每一步都会创建一个新集合。你想想看,100万条数据,每做一次map就生成一个新的100万条数据的List,连续三次map就是300万条数据的临时内存开销。再加上filter,内存压力可想而知。
而Sequence呢?它把三次map和一次filter合并成一个流水线,每次只处理一个元素,从头走到尾。内存开销几乎可以忽略不计。
避坑指南:什么时候不该用Sequence?
我曾经犯过一个错误——在数据量很小的时候也强行用Sequence。结果代码变得啰嗦,性能反而更差。
为什么?因为Sequence有额外的函数调用开销。对于几十条、几百条数据,这点开销虽然不大,但完全没有必要。
我的建议:
- 数据量 < 1000条:直接用集合,代码更简洁
- 数据量 1000~10万条:看情况,如果链式操作超过3步,用Sequence
- 数据量 > 10万条:强烈建议用Sequence,内存安全第一
核心知识体系
下面这张图是我自己总结的,把Sequence的核心知识点串在了一起:
实战代码对比
我们来看一个真实的例子。假设有一个用户列表,我们需要找出所有活跃用户的名字,并转为大写:
// 集合方式
val result = users
.filter { it.isActive }
.map { it.name.uppercase() }
// 这里生成了一个中间List<User>和一个中间List<String>
// Sequence方式
val result = users.asSequence()
.filter { it.isActive }
.map { it.name.uppercase() }
.toList()
// 没有中间集合,直接生成最终List<String>
看起来差不多对吧?但内存差异巨大。
当users有50万条时,集合方式会先创建一个50万条的filter结果,再创建一个50万条的map结果。而Sequence只会在toList()时,逐个处理每个元素。
记住这个原则:
如果链式操作超过两步,且数据量超过1万条,优先考虑Sequence。
一个常见的坑
我曾经在项目里看到有人这样写:
val result = list.asSequence()
.filter { ... }
.map { ... }
.toList()
.filter { ... }
.map { ... }
嗯,这里toList()之后又回到了集合操作。前面的Sequence白用了。
正确的做法是:把所有的中间操作都放在Sequence里,最后只调用一次终端操作。
总结
Sequence不是银弹,但它是处理大数据量的好帮手。我个人习惯在写数据处理逻辑时,先问自己三个问题:
- 数据量大不大?
- 链式操作多不多?
- 内存够不够?
如果前两个答案是「是」,第三个答案是「不够」,那就用Sequence。
如果数据量很小,直接用集合,代码更清爽。
公众号:蓝海资料掘金营,微信deep3321