Scala 集合统一在 Iterable 之下,分 Seq(有序)、Set(去重)、Map(键值) 三大分支,每支各有不可变与可变两套实现。理解这张层级树,选型就从"背 API"变成"做推论"。

不加限定直接写 List(1,2,3) 得到的是不可变版本——默认导入把不可变别名置前,这是语言在用命名权鼓励 3.1 节的不可变默认。
统一的 Iterable 父类型带来一个被严重低估的能力:集合变换保持类型。Set(...).map 返回 Set,Map(...).map 仍围绕键值结构。算法写在 Iterable 层,所有具体集合免费复用:
def total(ns: Iterable[Int]): Int = ns.sum total(List(1,2,3)) // 6 total(Vector(1,2,3)) // 6 total(Set(1,2,3)) // 6
| 结构 | 头部/追加 | 随机访问 | 查找 | 典型场景 |
|---|---|---|---|---|
| List | 头 O(1),追加 O(n) | O(n) | O(n) | 递归处理、栈式 |
| Vector | O(1) 均摊 | O(1) 近似 | O(log n) | 通用默认、大量随机读 |
| ArrayBuffer(可变) | 尾部 O(1) 均摊 | O(1) | O(n) | 循环构建大集合 |
| HashSet / HashMap | — | — | O(1) 均摊 | 去重、按键取值 |
| ListMap | — | — | O(n) | 极小映射或需保插入序 |
两条经验:不确定就用 Vector(各操作都不差);递归算法的首尾拆分用 List(head/tail 都是 O(1),且结构共享免费)。
String 与 Array 不是 Iterable 子类,但通过隐式转换可与集合互操作("abc".toSeq、Array(1,2).toVector)。跨边界时主动 toVector/toList 转成不可变集合,把可变数组挡在边界之外,是常见的防腐做法。

集合层级的说服力在于"换实现不换代码"。下面这段在 REPL 里逐行执行,观察同一个 map/filter 在四种集合上的表现:
val xs = List(1, 2, 3, 4) xs.map(_ * 2) // List(2, 4, 6, 8) xs.view.map(_ * 2).filter(_ > 4).toList // view:惰性视图,最后才落地 Vector.from(xs).map(_ * 2) // Vector(2, 4, 6, 8) (xs.iterator.map(_ * 2)).take(2).toList // Iterator:一次性,只能走一遍
四行代码对应四种代价模型:List 立即求值、view 惰性且可重复使用、Vector 立即但随机访问快、Iterator 惰性且一次性。接口一致,语义细节不同——集合选型选的正是这层代价模型。
Scala 2 时代最顶层的 Traversable 在 Scala 3 中已移除,Iterable 成为集合金字塔的塔尖:能给出 iterator 的一切都是集合。这条变化的实际影响是老代码迁移时 Traversable 一律换成 Iterable 即可。再往下,Seq、Set、 Map 三分天下,各有 Sorted、Indexed、Linear 变体——把这张族谱放进类型层级图里对照看,选型时按"需要有序、需要去重、需要键值"三问走,基本不会迷路。
List.range(1, 6) // List(1, 2, 3, 4, 5) Vector.tabulate(3)(i => i * i) // Vector(0, 1, 4) Map.from(List("a" -> 1)) // Map(a -> 1) List.fill(3)("x") // List(x, x, x)
这些工厂函数是搭建测试数据最快的路——写测试时(第八章)一行就能造出确定性输入,不必手写样例数据。
集合类型在 IDE 里展开后常常是这个样子:Seq[+A]、Map[K, +V]。加号预告了第五章的型变课题,这里先建立手感:
val ns: Seq[Int] = Vector(1, 2, 3) val anys: Seq[Any] = ns // Seq[+A]:Int 是 Any 的子类型,容器关系随之放宽 val m: Map[String, Int] = Map("a" -> 1) val m2: Iterable[(String, Int)] = m // Map 也是 Iterable 的子类型
两条赋值都能通过,靠的正是层级设计:Seq 协变让"整数序列也是任意值序列";Map 继承 Iterable 让所有对 Iterable 有效的操作对 Map 直接可用。集合层级不是教科书装饰,而是每天在赋值号两侧生效的契约。
val it = List(1, 2, 3).iterator it.next() // 1 it.next() // 2 it.sum // 3 —— 只剩一个元素可求 it.sum // 0 —— 已耗尽,静默返回空结果
Iterator 的一次性是新手事故高发区:第二次使用的返回值不是报错而是"看起来正常的空结果"。凡是签名里出现 Iterator 的中间值,要么立即 toList 落地,要么确认只消费一次。这条纪律同样适用于第六章的流式处理:数据源一旦被 Future 持有,重复触发就会拿到空流,排查起来相当费时。顺带记住 companion 里的 empty、singleton 两个常用工厂,构造空集合与单元素集合时比手写更清晰。