第一部分:集合(Collections)—— 传统数据容器
1. 什么是集合?
集合就是内存中真实存在的、存储了一组数据的容器。Kotlin 的集合完全基于 Java 集合框架(ArrayList、HashMap 等),但做了两件事:
- 把接口分为只读(
List、Set、Map)和可变(MutableList、MutableSet、MutableMap) - 给所有集合增加了丰富的函数式 API(
map、filter、reduce等)
// 只读集合(不可变引用,但元素对象本身可能可变)
val readOnlyList: List<String> = listOf("A", "B", "C")
// 可变集合
val mutableList: MutableList<String> = mutableListOf("A", "B", "C")
mutableList.add("D") // OK
// readOnlyList.add("D") // 编译错误
注意:Kotlin 的"只读"不等于"不可变"(immutable)。
List只是没有修改方法,但如果底层是MutableList,别的地方改它,List也能看到变化。
2. 集合的典型操作(链式调用)
集合最强大的地方在于可以连续调用转换函数:
val numbers = listOf(1, 2, 3, 4, 5, 6)
val result = numbers
.filter { it % 2 == 0 } // [2, 4, 6]
.map { it * it } // [4, 16, 36]
.take(2) // [4, 16]
println(result) // 输出 [4, 16]
关键特征(敲黑板):
- 每一步立即执行,并生成一个新的中间集合
-
filter执行完产生一个新List,map执行完再产生一个,take再产生一个 - 中间结果会占用内存,直到整个表达式结束才释放
3. 集合的优缺点
| 优点 | 缺点 |
|---|---|
| 语法直观,容易调试 | 链式操作会产生多个中间集合,内存开销大 |
| 数据量小时性能极佳(无额外迭代器开销) | 数据量大时容易 OOM(内存溢出) |
| 可以重复使用中间结果(把中间变量存下来) | 不支持无限数据流 |
第二部分:序列(Sequences)—— 惰性数据流
1. 什么是序列?
序列是一种惰性的数据流(lazy data stream)。它不存储数据,而是记录下你对数据要执行哪些操作(就像记在任务清单上),直到你真正"要结果"的时候,才开始逐个元素地处理。
你可以把序列想象成一条流水线:元素一个接一个进来,经过一道道工序,最后产出成品,而不是把所有原料堆在一起再做加工。
val numbers = listOf(1, 2, 3, 4, 5, 6)
val sequence = numbers.asSequence() // 转为序列
.filter { it % 2 == 0 } // 中间操作:记录,不执行
.map { it * it } // 中间操作:记录,不执行
.take(2) // 中间操作:记录,不执行
// 此时 sequence 只是一个"计划",还没有真正处理任何数据
val result = sequence.toList() // 终端操作:触发执行!
println(result) // 输出 [4, 16]
2. 序列的执行流程(重点理解)
为了让你直观看到区别,我们来"脑内模拟"序列的执行过程:
当调用 toList() 时:
- 取出原始数据第 1 个元素:
1-
filter检查:1 是偶数吗?否 → 丢弃,不再往后走
-
- 取出第 2 个元素:
2-
filter检查:2 是偶数吗?是 → 传给map -
map计算:2 × 2 = 4 → 传给take -
take记录:已取 1 个(还需要 1 个)
-
- 取出第 3 个元素:
3-
filter检查:3 是偶数吗?否 → 丢弃
-
- 取出第 4 个元素:
4-
filter检查:是 → 传给map -
map计算:4 × 4 = 16 → 传给take -
take记录:已取 2 个(达标)→ 立即停止遍历,后面的 5、6 连看都不看
-
这就是惰性和元素级流动的核心。
3. 如何创建序列?
除了 asSequence(),还有几种方式:
// 1. 从集合转换
val seq1 = listOf(1, 2, 3).asSequence()
// 2. 使用 generateSequence(可以生成无限序列)
val infiniteSeq = generateSequence(1) { it + 1 } // 1, 2, 3, 4, ...
val firstFive = infiniteSeq.take(5).toList() // 必须截断,否则无限
// 3. 使用 sequence 构建器(yield)
val seqBuilder = sequence {
yield(1)
yield(2)
yieldAll(listOf(3, 4, 5))
}
4. 序列的优缺点
| 优点 | 缺点 |
|---|---|
| 不产生中间集合,内存占用极小 | 每个元素流转需要迭代器对象,有额外 CPU 开销 |
支持提前截断(take、first),可避免处理无关数据 |
不能复用中间结果(因为根本没存中间结果) |
| 天然支持无限数据流 | 调试困难(断点查看中间状态不如集合直观) |
| 适合处理大文件、大数据流 | 数据量小时反而比集合慢 |
第三部分:并排对比(先有基础,再对比)
现在我们把两者放在一起,逐项对照,你就不会觉得抽象了:
| 对比维度 | 集合(Collections) | 序列(Sequences) |
|---|---|---|
| 数据存储 | 真实存储在内存中 | 不存储数据,只记录操作计划 |
| 求值时机 | 每一步立即求值(饥饿) | 终端操作触发才求值(惰性) |
| 中间产物 | 每个操作产生一个新集合 | 无中间集合,只有操作链 |
| 元素处理顺序 | 垂直:操作1处理全部 → 操作2处理全部 | 水平:元素1走完全流程 → 元素2走完全流程 |
| 截断能力 | 即使最终只取 1 个,前面的操作也处理了全部数据 | 一旦达到截断条件,立即停止,后面的数据不处理 |
| 适用数据量 | 小数据(通常 < 1 万) | 大数据(> 1 万)或流式数据 |
| 能否无限 | 不能(无限集合会导致内存耗尽) | 能(配合 take 截断) |
| 调试难度 | 容易(每一步结果可见) | 较难(中间状态不可见) |
| 典型场景 | UI 列表、配置数据、小规模计算 | 大文件逐行处理、网络流、复杂链式截断查询 |
补充:一个最经典的实战例子
// 场景:从一个超大文本文件中找出第一个以 "ERROR" 开头的非空行
// ❌ 用集合(会一次性把整个文件读进内存,可能 OOM)
File("huge.log").readLines() // 全部读入内存
.filter { it.isNotBlank() }
.map { it.trim() }
.first { it.startsWith("ERROR") }
// ✅ 用序列(逐行读取,找到就停止)
File("huge.log").useLines { lines -> // useLines 返回的就是 Sequence<String>
lines
.filter { it.isNotBlank() }
.map { it.trim() }
.first { it.startsWith("ERROR") }
}
// 内存中永远只保留当前处理的一行
最后的叮嘱
- 不要迷信序列:数据量小(几千条以内)用集合,代码更清晰,速度更快。
-
不要在序列中乱用
sorted:排序需要拿到所有元素,会打破惰性,强制全量加载。 - 记住一个口诀:集合是急性子,做一步算一步;序列是慢性子,最后一步才真正动手。