Scala 集合库实战:不可变集合、性能选择与常用操作速查

系统讲解 Scala 集合库:不可变 vs 可变集合、List/Vector/Set/Map 的选择与性能特征、常用转换操作(map/filter/flatMap/groupBy/sliding)、视图与惰性求值、并行集合、以及与 Java 集合的互操作。

引言

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 可变

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 选型决策

需求选择理由
前序/头部频繁操作Listprepend 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 对比

操作ListVector
头部取/加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 官方文档

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「scala」更多文章

  1. 纯函数式效果系统实战:Cats Effect IO 与 ZIO
  2. Scala.js 与 Scala Native:跨平台编译、互操作与工程实践
  3. Scala 领域建模实战:ADT、类型驱动设计与模块化架构