第5章 集合

List、Vector、Map、Set、Tuple:Scala 的集合库是“不可变优先 + 一组统一的高阶操作”。

集合第7-8周3 讲9 段示例代码

本章学习目标

  • 能按场景选 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 的创建、取头和两端追加
SCALA
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 产生新集合
SCALA
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 串起来用
SCALA
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 的读取、默认值和不可变更新
SCALA
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 的保序差别
SCALA
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 配对
SCALA
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
SCALA
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 的不同排序方式
SCALA
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 二分
SCALA
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 的迭代顺序当成已排序。