第5章 集合
List、Vector、Map、Set、Tuple:Scala 的集合库是“不可变优先 + 一组统一的高阶操作”。
本章学习目标
- 能按场景选 List / Vector / Array / Seq
- 会做 Map 与 Set 的增删查、遍历与统计
- 能用 groupBy / sortBy / zip / mkString 完成常见数据处理
- 知道可变集合(ListBuffer、mutable.Map)何时才用
本章导览
核心内容
5.1
List、Array、Vector:创建与常用操作
分清三种序列的取舍,并会用 map/filter 处理数据。
能创建 List、Array、Vector 并读取、追加元素能解释三者在不可变性与访问复杂度上的差别能用 map、filter、foldLeft、zip 完成常见数据变换
知识点
先分清“可不可变”和“能不能改元素”
- List 与 Vector 是不可变序列,任何“修改”都返回新集合,原集合保持不变。
- Array 长度固定,元素可以原地替换,也能直接传给 Java 方法。
- 需要频繁按下标读取时别用 List,它的随机访问是 O(n)。
List 的头部是它的主场
- Nil 是空表,List(1, 2, 3) 等价于 1 :: 2 :: 3 :: Nil。
- “::”是右结合,0 :: xs 把 0 放到头部,耗时 O(1)。
- “:+”在尾部追加,需要复制整个链表,复杂度 O(n)。
Vector 与 Array 的日常操作
- Vector 的 updated 返回新 Vector,索引读取接近 O(1),适合大量随机访问。
- Array 用 a(i) 读写;直接 println 一个数组只会得到 [I@...。
- map、filter、foldLeft 对三者都适用,返回类型通常跟随原集合。
代码示例
▶ List 的创建、取头和两端追加
object Main {
def main(args: Array[String]): Unit = {
val xs = List(3, 1, 4, 1, 5)
println(xs)
println(xs.head)
println(xs.tail)
println(0 :: xs)
println(xs :+ 9)
println(xs.length)
println(xs.sum)
}
}👀 运行结果
List(3, 1, 4, 1, 5) 3 List(1, 4, 1, 5) List(0, 3, 1, 4, 1, 5) List(3, 1, 4, 1, 5, 9) 5 14
▶ Array 原地改元素,Vector 用 updated 产生新集合
object Main {
def main(args: Array[String]): Unit = {
val a = Array(10, 20, 30)
a(1) = 99
println(a.mkString(","))
println(a.toList)
val v = Vector(1, 2, 3)
val v2 = v.updated(0, 100)
println(v)
println(v2)
println(v :+ 4)
println(0 +: v)
}
}👀 运行结果
10,99,30 List(10, 99, 30) Vector(1, 2, 3) Vector(100, 2, 3) Vector(1, 2, 3, 4) Vector(0, 1, 2, 3)
▶ map、filter、foldLeft、zip 串起来用
object Main {
def main(args: Array[String]): Unit = {
val xs = List(1, 2, 3, 4, 5, 6)
println(xs.map(_ * 2))
println(xs.filter(_ % 2 == 0))
println(xs.take(3))
println(xs.drop(3))
println(xs.reverse)
println(xs.foldLeft(0)(_ + _))
println(xs.zip(List("a", "b", "c")))
}
}👀 运行结果
List(2, 4, 6, 8, 10, 12) List(2, 4, 6) List(1, 2, 3) List(4, 5, 6) List(6, 5, 4, 3, 2, 1) 21 List((1,a), (2,b), (3,c))
常见错误
⚠️ 踩坑与解决
把 xs :: 0 当成尾部追加
→ “::”是右结合,这行实际调用 0.::(xs),Int 没有该方法,编译报 value :: is not a member of Int。;追加用 xs :+ 0,前置用 0 :: xs。用 xs(5) 访问越界下标
→ List 按索引访问是线性扫描,越界时抛 java.lang.IndexOutOfBoundsException。;先检查 length,或用 xs.lift(5) 得到 Option。直接打印 Array 得到 [I@1b6d3586
→ Array 没有友好的 toString,println 调用的是数组对象的默认实现。;用 a.mkString(",")、a.toList 或 a.foreach(println)。
💡 示例统一按 Scala 2.13 + sbt 编写;默认先选 List 表达只读数据,频繁下标访问或更新换 Vector,需要原地修改或对接 Java 时再用 Array。
5.2
Map、Set、Tuple:键值对、去重与配对
用 Map 存键值、Set 去重、Tuple 临时打包数据。
能用不可变 Map 完成增删改查并安全处理缺失键能用 Set 去重并做交集、并集、差集能读写 Tuple,并用元组解构处理配对数据
知识点
Map:查不到键不要硬取
- Map 是不可变键值集合,加号添加、减号删除都会返回新 Map。
- m(k) 在键不存在时抛 NoSuchElementException,安全写法是 get 或 getOrElse。
- 遍历顺序不要依赖,需要顺序时用 ListMap 或对 keys 排序。
Set:去重靠它,顺序别靠它
- Set 自动去除重复元素,contains、+、- 是最常用的操作。
- 默认 Set 不保证迭代顺序;要保留首次出现顺序用 List.distinct。
- 交集用 &,并集用 |,差集用 --,都返回新 Set。
Tuple:把多个值临时捆在一起
- 元素用 _1、_2、_3 访问,不是 t(1);编号从 1 开始。
- case (k, v) 可以直接解构元组,适合处理 Map 的 entry 或 zip 结果。
- 超过三四个字段就该改用 case class,可读性和类型安全都会更好。
代码示例
▶ Map 的读取、默认值和不可变更新
object Main {
def main(args: Array[String]): Unit = {
val scores = Map("Alice" -> 90, "Bob" -> 75)
println(scores("Alice"))
println(scores.get("Carol"))
println(scores.getOrElse("Carol", 0))
val updated = scores + ("Carol" -> 60)
println(updated("Carol"))
println(updated.size)
println(scores.size)
println(updated.keys.toList.sorted)
println(updated.values.sum)
}
}👀 运行结果
90 None 0 60 3 2 List(Alice, Bob, Carol) 225
▶ Set 去重、集合运算与 List.distinct 的保序差别
object Main {
def main(args: Array[String]): Unit = {
val s = Set(1, 2, 2, 3, 3, 3)
println(s.size)
println(s.contains(2))
println(s.toList.sorted)
println((s + 4).toList.sorted)
println((s - 1).toList.sorted)
println((s & Set(2, 3, 4)).toList.sorted)
println((s | Set(3, 4)).toList.sorted)
println(List(3, 1, 3, 2, 1).distinct)
}
}👀 运行结果
3 true List(1, 2, 3) List(1, 2, 3, 4) List(2, 3) List(2, 3) List(1, 2, 3, 4) List(3, 1, 2)
▶ Tuple 的按下标访问、解构与 zip 配对
object Main {
def main(args: Array[String]): Unit = {
val t = ("Alice", 90, true)
println(t._1)
println(t._2)
println(t._3)
val (name, score, passed) = t
println(s"$name: $score, passed=$passed")
val pairs = List("a" -> 1, "b" -> 2)
println(pairs.map { case (k, v) => s"$k=$v" })
val swapped = t match {
case (n, s, _) => (s, n)
}
println(swapped)
}
}👀 运行结果
Alice 90 true Alice: 90, passed=true List(a=1, b=2) (90,Alice)
常见错误
⚠️ 踩坑与解决
用 m("不存在的键") 直接取值
→ Map.apply 等价于 get(...).get,键不存在时抛 NoSuchElementException: key not found。;改用 m.get(k) 得到 Option,或用 m.getOrElse(k, 默认值)。以为 Set 会按插入顺序打印
→ Set 去重后不承诺顺序,元素一多底层就是哈希表,顺序更不可控。;要固定顺序用 List(...).distinct、SortedSet,或输出前 .toList.sorted。对元组写 t(0) 或 t._0
→ Tuple 的访问器是 _1 到 _n,没有 apply,也没有 _0。;用 t._1,或模式匹配 case (name, score, _) => ...。
💡 Map 取键默认走 getOrElse;Tuple 只做临时聚合,一旦字段变多就升级成 case class。
5.3
不可变与可变集合:ListBuffer、排序与分组
用 ListBuffer 高效构建序列,并掌握排序与分组。
能在不可变 List 与可变 ListBuffer 之间正确取舍能用 sorted、sortBy、sortWith 完成不同排序需求能用 groupBy、partition 把数据按条件切开
知识点
不可变优先,构建阶段才用可变集合
- List 的 :+ 每次复制,循环里频繁追加会退化成 O(n²)。
- ListBuffer 支持常数时间的 += 与 ++=,构建完成后 toList 一次定型。
- toList 得到的是不可变快照,之后再改 ListBuffer 不会影响这个 List。
排序的三种问法
- sorted 用元素自带的 Ordering;String、Int 等标准类型可直接排。
- sortBy 先映射出排序键,sortWith 直接比较两个元素,适合自定义先后。
- 排序是稳定的:键相同的元素保持原有相对顺序。
groupBy 与 partition 把数据切开
- groupBy(f) 返回 Map[K, List[A]],每组保持原集合中的顺序。
- partition(p) 一次得到满足与不满足条件的两部分。
- 结果 Map 的顺序不要依赖,展示前对 keys 排序。
代码示例
▶ ListBuffer 追加、删除并转成 List
import scala.collection.mutable.ListBuffer
object Main {
def main(args: Array[String]): Unit = {
val buf = ListBuffer[Int]()
buf += 1
buf += 2
buf ++= List(3, 4)
println(buf)
println(buf.toList)
buf -= 2
println(buf)
}
}👀 运行结果
ListBuffer(1, 2, 3, 4) List(1, 2, 3, 4) ListBuffer(1, 3, 4)
▶ sorted、sortBy、sortWith 的不同排序方式
object Main {
def main(args: Array[String]): Unit = {
val words = List("banana", "apple", "cherry", "date")
println(words.sorted)
println(words.sortBy(_.length))
val mixed = List("aaa", "b", "cccc", "dd")
println(mixed.sortWith(_.length > _.length))
val nums = List(3, -1, 4, -2)
println(nums.sortBy(n => math.abs(n)))
}
}👀 运行结果
List(apple, banana, cherry, date) List(date, apple, banana, cherry) List(cccc, aaa, dd, b) List(-1, -2, 3, 4)
▶ groupBy 分组、map 整理每组元素、partition 二分
object Main {
def main(args: Array[String]): Unit = {
val people = List(("Alice", "A"), ("Bob", "B"), ("Cara", "A"), ("Dan", "B"))
val byTeam = people.groupBy(_._2)
println(byTeam.keys.toList.sorted)
println(byTeam("A"))
val names = byTeam.map { case (k, v) => k -> v.map(_._1) }.toList.sortBy(_._1)
println(names)
val (evens, odds) = List(1, 2, 3, 4, 5).partition(_ % 2 == 0)
println(evens)
println(odds)
}
}👀 运行结果
List(A, B) List((Alice,A), (Cara,A)) List((A,List(Alice, Cara)), (B,List(Bob, Dan))) List(2, 4) List(1, 3, 5)
常见错误
⚠️ 踩坑与解决
写 val xs = List(1, 2); xs += 3
→ List 不可变,根本没有 += 方法,编译报 value += is not a member of List[Int]。;改用 var xs = List(1, 2); xs = xs :+ 3,或构建阶段用 ListBuffer。写 xs(0) = 9 想修改 List 元素
→ List 不提供 update,编译报 value update is not a member of List[Int]。;用 xs.updated(0, 9) 得到新 List;必须原地修改时用 Array 或可变 Buffer。在循环里对 List 反复 :+
→ 每次 :+ 都要复制整个链表,追加 n 次的总代价是 O(n²)。;先 += 到 ListBuffer,构建完成后统一 toList。
💡 构建期用 ListBuffer,交付期用 List;输出前先排序,不要把 Map 或 Set 的迭代顺序当成已排序。