Koltin 集合与序列


第一部分:集合(Collections)—— 传统数据容器

1. 什么是集合?

集合就是内存中真实存在的、存储了一组数据的容器。Kotlin 的集合完全基于 Java 集合框架(ArrayListHashMap 等),但做了两件事:

  • 把接口分为只读ListSetMap)和可变MutableListMutableSetMutableMap
  • 给所有集合增加了丰富的函数式 APImapfilterreduce 等)
// 只读集合(不可变引用,但元素对象本身可能可变)
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 执行完产生一个新 Listmap 执行完再产生一个,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 个元素:1
    • filter 检查:1 是偶数吗?否 → 丢弃,不再往后走
  2. 取出第 2 个元素:2
    • filter 检查:2 是偶数吗?是 → 传给 map
    • map 计算:2 × 2 = 4 → 传给 take
    • take 记录:已取 1 个(还需要 1 个)
  3. 取出第 3 个元素:3
    • filter 检查:3 是偶数吗?否 → 丢弃
  4. 取出第 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 开销
支持提前截断(takefirst),可避免处理无关数据 不能复用中间结果(因为根本没存中间结果)
天然支持无限数据流 调试困难(断点查看中间状态不如集合直观)
适合处理大文件、大数据流 数据量小时反而比集合慢

第三部分:并排对比(先有基础,再对比)

现在我们把两者放在一起,逐项对照,你就不会觉得抽象了:

对比维度 集合(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:排序需要拿到所有元素,会打破惰性,强制全量加载。
  • 记住一个口诀集合是急性子,做一步算一步;序列是慢性子,最后一步才真正动手。
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容