第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的核心知识点串在了一起:

Sequence 核心知识体系 Sequence 构建方式 asSequence() sequenceOf() generateSequence() 核心特性:惰性求值 中间操作(map/filter) 终端操作(toList()) 按元素流水线处理 性能对比 大数数据: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