4.1 集合框架与类型层级


4.1 集合框架与类型层级

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),且结构共享免费)。

字符串与数组也在体系边缘

StringArray 不是 Iterable 子类,但通过隐式转换可与集合互操作("abc".toSeqArray(1,2).toVector)。跨边界时主动 toVector/toList 转成不可变集合,把可变数组挡在边界之外,是常见的防腐做法。

图 4-1 Scala 集合类型层级鸟瞰

图 4-1 Scala 集合类型层级鸟瞰

用 REPL 亲手摸一遍共同接口

集合层级的说服力在于"换实现不换代码"。下面这段在 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 惰性且一次性。接口一致,语义细节不同——集合选型选的正是这层代价模型。

Iterable 与 Traversable 的分水

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)

这些工厂函数是搭建测试数据最快的路——写测试时(第八章)一行就能造出确定性输入,不必手写样例数据。

泛型签名怎么读: Seq 的型变预告

集合类型在 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 直接可用。集合层级不是教科书装饰,而是每天在赋值号两侧生效的契约。

IterableOnce 与迭代的一次性

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 两个常用工厂,构造空集合与单元素集合时比手写更清晰。

本节要点回顾

  • 三大分支:Seq、Set、Map,各有不可变(默认)与可变两套。
  • 统一 Iterable 让变换保持类型、算法全族复用。
  • 复杂度表决定选型:不确定选 Vector,递归拆分选 List,按键查选 HashMap。
  • Array/String 是体系边缘,进系统先转不可变集合。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U