引言
Scala 集合库(Scala Collections)是函数式编程的「弹药库」。List(1,2,3).map(...) 这样一行代码背后,是设计精良的不可变数据结构与数百个操作方法的组合。但集合库的强大也带来困惑:List 和 Vector 到底选谁?map、flatMap、fold 什么时候用哪个?数据量大时怎么避免内存爆炸?
本文系统梳理 Scala 集合库:先分清不可变与可变两大阵营,再逐个剖析 List/Vector/Set/Map 的性能特征与选型,随后给出最常用的转换操作速查表与 groupBy/sliding 等高阶技巧,最后讲惰性求值(LazyList/View)与并行集合在大型数据上的应用。
前置:函数式编程基础(https://plumephp.com/scala-functional-programming/)。集合库是函数式代码的载体,两者密不可分。
目录
- 1. 集合库全景:不可变 vs 可变
- 2. 核心集合选择:List、Vector、Set、Map
- 3. 不可变集合的性能特征
- 4. 常用转换操作速查
- 5. 分组与窗口:groupBy、sliding 与 grouped
- 6. 惰性求值:LazyList 与 View
- 7. 并行集合:数据量大时提效
- 8. 与 Java 集合互操作
- 9. 总结:集合选型决策表
- 延伸阅读
1. 集合库全景:不可变 vs 可变
1.1 两大阵营
| 阵营 | 特点 | 常用类型 |
|---|---|---|
| 不可变 | 默认、线程安全、可安全共享 | List、Vector、Set、Map |
| 可变 | 需要时用(性能/局部) | ArrayBuffer、mutable.Set、mutable.Map |
1.2 默认用不可变
val list = List(1, 2, 3) // 不可变
val map = Map("a" -> 1) // 不可变
// 需要可变时显式导入
import scala.collection.mutable
val buf = mutable.ArrayBuffer(1, 2)
buf += 3 // 可变操作
1.3 为什么默认不可变
- 并发安全(无需锁)。
- 函数式语义(操作返回新集合,原集合不变)。
- 容易推理与测试。
2. 核心集合选择:List、Vector、Set、Map
2.1 一图看懂层级
Iterable
├── Seq (有序、可索引)
│ ├── List (链表,头部操作 O(1))
│ ├── Vector (树形,任意索引 O(log32 n))
│ └── Range (整数区间)
├── Set (无序、唯一)
└── Map (键值)
2.2 选型决策
| 需求 | 选择 | 理由 |
|---|---|---|
| 前序/头部频繁操作 | List | prepend O(1) |
| 随机索引频繁 | Vector | 索引近 O(1) |
| 唯一性/去重 | Set | 哈希查找 |
| 键值查找 | Map | 哈希查找 |
| 数字区间 | Range | 惰性、省内存 |
| 动态增删尾部 | ArrayBuffer | 可变,尾部 O(1) |
2.3 常用创建方式
val list = List(1, 2, 3)
val vec = Vector(1, 2, 3)
val set = Set(1, 2, 3)
val map = Map("a" -> 1, "b" -> 2)
val range = 1 to 10 by 2 // Range(1,3,5,7,9)
3. 不可变集合的性能特征
3.1 List:链表
List 结构: Cons(1, Cons(2, Cons(3, Nil)))
- 头部取/加: O(1)
- 尾部操作/索引: O(n) ← 慢
适合栈式访问(前序、头部匹配、递归处理)。
3.2 Vector:树形结构
Vector: 树(分支因子32)
- 索引: O(log32 n) ≈ 近常数
- 头尾增删: 接近 O(1)(持久化结构共享)
适合随机访问与大规模数据,是 List 之外最常用的不可变序列。
3.3 对比
| 操作 | List | Vector |
|---|---|---|
| 头部取/加 | O(1) | 近O(1) |
| 索引访问 | O(n) | 近O(1) |
| 尾部操作 | O(n) | 近O(1) |
| 内存 | 每元素一个节点 | 树节点 |
4. 常用转换操作速查
4.1 变换类
List(1,2,3).map(_ * 2) // List(2,4,6)
List(1,2,3,4).filter(_ % 2 == 0) // List(2,4)
List(1,2,3).flatMap(x => List(x, x)) // List(1,1,2,2,3,3)
List(1,2,3).collect { case x if x > 1 => x * 10 } // List(20,30)
4.2 聚合类
List(1,2,3,4).foldLeft(0)(_ + _) // 10
List(1,2,3,4).reduceLeft(_ + _) // 10(非空)
List(1,2,3).sum // 6
List(1,2,3).max // 3
List(1,2,3).mkString(",") // "1,2,3"
4.3 子集类
List(1,2,3,4).take(2) // List(1,2)
List(1,2,3,4).drop(2) // List(3,4)
List(1,2,3,4).headOption // Some(1)
List().headOption // None
List(1,2,3,4).tail // List(2,3,4)
4.4 排序与去重
List(3,1,2).sorted // List(1,2,3)
List(3,1,2).sortBy(-_) // List(3,2,1)
List(1,1,2,3).distinct // List(1,2,3)
5. 分组与窗口:groupBy、sliding 与 grouped
5.1 groupBy:按条件分组
case class User(name: String, age: Int)
val users = List(User("a", 20), User("b", 30), User("c", 20))
val byAge = users.groupBy(_.age)
// Map(20 -> List(User(a,20), User(c,20)), 30 -> List(User(b,30)))
5.2 sliding:滑动窗口
List(1,2,3,4,5).sliding(3).toList
// List(List(1,2,3), List(2,3,4), List(3,4,5)) ← 重叠窗口
适合时间序列/相邻关系处理(如计算滑动平均)。
5.3 grouped:固定大小批次
List(1,2,3,4,5).grouped(2).toList
// List(List(1,2), List(3,4), List(5)) ← 不重叠分块
适合分批处理(如批量写库、并发任务分配)。
5.4 partition 与 span
val (even, odd) = List(1,2,3,4).partition(_ % 2 == 0)
// even=List(2,4), odd=List(1,3)
val (pre, post) = List(1,2,3,4).span(_ < 3)
// pre=List(1,2), post=List(3,4) ← 直到条件不成立
6. 惰性求值:LazyList 与 View
6.1 为什么需要惰性
// ❌ 急切求值:全部计算出来,再取前 3
(1 to 10000000).map(_ * 2).take(3).toList // 浪费:算了 1000 万次
// ✅ 惰性求值:只算需要的部分
LazyList.range(1, 10000000).map(_ * 2).take(3).toList
6.2 LazyList:无限/大规模序列
val fibs: LazyList[BigInt] =
BigInt(0) #:: BigInt(1) #::
fibs.zip(fibs.tail).map { case (a, b) => a + b }
fibs.take(10).toList // 只算前 10 个斐波那契
6.3 View:对既有集合惰性
val big = Vector.range(1, 10000000)
val result = big.view.filter(_ % 3 == 0).map(_ * 2).take(5).toList
// view 让 filter/map 惰性,避免中间集合
6.4 何时用
| 场景 | 用 |
|---|---|
| 无限序列 / 大数据管道 | LazyList |
| 大集合的多步转换 | View |
| 小数据集 | 直接急切即可 |
7. 并行集合:数据量大时提效
7.1 用法
import scala.collection.parallel.immutable.ParVector
val data = ParVector(1 to 10000)
val sum = data.map(_ * 2).sum // 自动并行
7.2 并行安全要求
并行集合对纯函数、无共享可变状态的操作才安全。有副作用(如修改外部变量)的代码绝不能并行。
7.3 并行化的收益与开销
| 因素 | 影响 |
|---|---|
| 数据量 | 足够大才有收益 |
| 操作成本 | 计算重的操作收益大 |
| 结果合并 | reduce 类操作适合 |
| 元素少 | 并行反而慢(调度开销) |
经验:数据量大 + 操作纯才用并行;先测单线程基线,再对比收益。
8. 与 Java 集合互操作
8.1 转换
import scala.jdk.CollectionConverters.*
val javaList: java.util.List[Int] = List(1,2,3).asJava
val scalaList: List[Int] = javaList.asScala.toList
val javaMap = Map("a" -> 1).asJava
val backScala = javaMap.asScala
8.2 与 Stream 对接
// Scala 集合 → Java Stream
val stream = List(1,2,3).asJava.stream()
// Java Stream → Scala
val fromStream = stream.toArray().toSeq
8.3 注意事项
- 转换是视图包装,底层可能共享;转
.toX得到独立集合。 - 大量 Java 代码里用
asScala能让函数式代码无缝衔接。
9. 总结:集合选型决策表
9.1 一句话选型
需要头部操作 → List
需要随机索引 → Vector
需要唯一性 → Set
需要键值对 → Map
数据量大 → View/LazyList 惰性
数据量大+纯 → 并行集合
需要可变 → ArrayBuffer/mutable.*
9.2 最佳实践
| 原则 | 说明 |
|---|---|
| 默认不可变 | 线程安全、可推理 |
| 用对结构 | List vs Vector 影响大 |
| 惰性只在需要时 | 别为小数据增加复杂度 |
| 并行要谨慎 | 纯函数才安全 |
延伸阅读
- https://plumephp.com/scala-functional-programming/ — 集合操作背后的函数式思维
- https://plumephp.com/scala-type-system/ — 泛型与集合类型的底层
- Scala Collections 官方文档
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。