Skip to content

8 Set、Map 集合与模式匹配 ​

笔记衔接前序《Scala Seq 序列集合》,严格匹配课程体系,针对有 Java 基础的大数据专业大学生设计,全程Java 语法对比 + 课程配套源码拆解 + Spark 大数据场景应用 + 避坑指南 + 作业全解析。Set、Map 是 Scala 最核心的键值 / 去重集合,模式匹配是 Scala 最具特色的语法特性,三者是 Spark/Flink 大数据开发的核心基础,Spark PairRDD 的算子设计完全对标 Map 操作,模式匹配更是 Spark 源码中随处可见的核心语法。

  • 前置知识:Scala 集合体系、Seq 序列、面向对象基础、函数式编程

  • 开发环境:JDK1.8+、Scala2.12.x、IDEA + Scala 插件

  • 核心学习目标:

    1. 掌握 Scala Set 集合的核心特性、常用操作与特殊实现类,明确可变与不可变 Set 的适用场景
  1. 熟练掌握 Scala Map 映射的增删改查、过滤排序等全量操作,理解与 Java HashMap 的核心差异
  2. 掌握 Scala 各类集合的性能特性,能够根据大数据业务场景选择最优的集合类型
  3. 深入理解 Scala 模式匹配的核心思想,熟练掌握 8 种常用模式匹配类型,能够灵活应用于业务开发
  4. 独立完成课程配套作业,掌握 Map 索引构建、二叉树递归求和等实战技巧

第一章 Scala Set 集合 ​

Set 是无序、元素不可重复的集合,核心作用是元素去重、快速判重,与 Java 的 Set 接口核心思想一致,但 Scala 同样提供了不可变和可变两大分支,默认使用不可变 Set。

1.1 Set 核心概述 ​

1.1.1 核心特性 ​

  1. 元素唯一性:Set 会自动对元素去重,重复添加相同元素不会生效;
  2. 无序性:默认 HashSet 不保证元素的存储顺序,SortedSet 可实现有序;
  3. 两大分支:默认使用scala.collection.immutable.Set,可变 Set 需手动导入scala.collection.mutable.Set;
  4. 高效判重:基于哈希表实现的 Set,contains方法时间复杂度为 O (1),远优于 Seq 的 O (n),大数据场景下判重 / 去重优先使用 Set。
  5. Scala的Set继承关系

image-20250120014826911

1.1.2 与 Java Set 的核心差异 ​

特性Scala SetJava Set
默认类型不可变 Set,线程安全可变 HashSet,非线程安全
语法简洁性支持Set(1,2,3)直接创建,支持set(元素)直接判重需new HashSet<>()创建,判重需调用contains()方法
不可变支持原生提供不可变实现,修改操作返回新集合需通过Collections.unmodifiableSet()包装,修改会抛出异常
函数式支持内置 map/filter/reduce 等高阶函数Java8 前无 Lambda 支持,需循环实现

1.2 Set 核心常用操作 ​

对应课程配套源码RunSet.scala,分为不可变 Set 与可变 Set 两大类操作。

1.2.1 基础判重与子集判断 ​

scala
import scala.collection.mutable

object RunSet {
  def main(args: Array[String]): Unit = {
    // 1. 创建Set,自动去重
    val s1 = Set(1,2,3,3,2,5)
    println(s1) // 输出:Set(1,2,3,5),自动去重

    // 2. 元素判重,两种写法等价
    println(s1.contains(1), s1.contains(6)) // 输出:(true,false)
    println(s1(2), s1(8)) // 输出:(true,false),Scala特有简洁写法

    // 3. 子集判断subsetOf
    val s2 = Set(3,5)
    val s3 = Set(4,5)
    println(s"s2 是 s1 的子集:${s2.subsetOf(s1)}") // 输出:true
    println(s"s3 是 s1 的子集:${s3.subsetOf(s1)}") // 输出:false
  }
}

1.2.2 不可变 Set 的增删操作 ​

不可变 Set 的增删操作不会修改原集合,只会返回一个全新的 Set,原集合保持不变。

scala
object RunSet {
  def main(args: Array[String]): Unit = {
    // 不可变Set
    var imms = Set(4,5,6)
    // 加单个元素,返回新Set
    val imm1 = imms + 3
    // 加多个元素
    val imm2 = imm1 + (22,33)
    // 加整个集合
    val imm3 = imm2 ++ List(44,55)
    // 删除单个元素
    val imm4 = imm3 - 22
    // 删除多个元素
    val imm5 = imm4 -- List(33,44)

    println(imm5) // 输出:Set(4,5,6,3,55)
    println(imms) // 原集合不变,输出:Set(4,5,6)
  }
}

1.2.3 可变 Set 的增删操作 ​

可变 Set 的增删操作直接修改原集合,无返回值(或返回是否操作成功),性能更高,适合局部高频增删场景。

scala
import scala.collection.mutable

object RunSet {
  def main(args: Array[String]): Unit = {
    // 创建可变Set
    var ms = mutable.Set(7,8,9)
    // 1. 添加元素
    ms += 10 // 加单个元素
    ms += (11,12,12) // 加多个元素,重复元素自动忽略
    ms ++= Set(13,14) // 加整个集合
    println(ms) // 输出:Set(9,13,14,12,7,8,10,11)

    // 2. 条件删除:保留满足条件的元素,删除不满足的
    ms.retain(_ > 9)
    println(ms) // 输出:Set(13,14,12,10,11)

    // 3. 删除元素
    ms.remove(12) // 删除指定元素,返回是否删除成功
    ms -= 10 // 单个删除
    ms --= List(11,13) // 批量删除
    println(ms) // 输出:Set(14)

    // 4. 清空集合
    ms.clear()
    println(ms) // 输出:Set()
  }
}

1.2.4 集合二元操作(交、并、差) ​

操作方法说明
并集set1 ++ set2 / `set1set2`两个集合的所有元素,自动去重
交集set1 & set2 / set1.intersect(set2)两个集合都包含的元素
差集set1 -- set2 / set1 &~ set2set1 中包含、set2 中不包含的元素

1.3 Set 特殊实现类 ​

Scala 提供了多种 Set 的特殊实现类,适配不同的业务场景,课程中重点讲解了 SortedSet 和 LinkedHashSet。

1.3.1 SortedSet(排序 Set) ​

SortedSet 会自动对元素进行排序,分为不可变SortedSet和可变mutable.SortedSet,默认实现是TreeSet,基于红黑树实现。

  • 基础类型(Int、String 等)可直接排序,Scala 已提供默认排序规则;
  • 自定义类型必须实现Ordered特质,重写compare方法定义排序规则。
scala
import scala.collection.immutable.SortedSet
import scala.collection.mutable

object RunSet {
  def main(args: Array[String]): Unit = {
    // 1. 基础类型排序Set
    val ss1 = mutable.SortedSet(10,3,11,2)
    val ss3 = SortedSet(7,3,11)
    println(ss1) // 输出:TreeSet(2, 3, 10, 11),自动升序
    println(ss3) // 输出:TreeSet(3,7,11)

    // 字符串按字典序排序
    val ss2 = mutable.SortedSet("banana","apple","orange")
    println(ss2) // 输出:TreeSet(apple, banana, orange)

    // 2. 自定义类型排序,实现Ordered特质
    class Person0701(var name:String, var age:Int) extends Ordered[Person0701] {
      // 重写compare方法,按年龄降序排序
      override def compare(that: Person0701): Int = {
        if(this.age == that.age) 0
        else if (this.age < that.age) 1
        else -1
      }
      override def toString = s"Person0701($name, $age)"
    }

    // 自定义类型的SortedSet
    val p1 = new Person0701("scala",12)
    val p2 = new Person0701("java",2)
    val p3 = new Person0701("c",20)
    val pSet = mutable.SortedSet(p1,p2,p3)
    println(pSet) // 按年龄降序输出:TreeSet(Person0701(c, 20), Person0701(scala, 12), Person0701(java, 2))
  }
}

1.3.2 LinkedHashSet(插入有序 Set) ​

mutable.LinkedHashSet会保留元素的插入顺序,基于哈希表 + 双向链表实现,兼顾了 O (1) 的判重效率和插入有序的特性。

scala
import scala.collection.mutable

object RunSet {
  def main(args: Array[String]): Unit = {
    val lhs = mutable.LinkedHashSet(1,8,4,2,8)
    println(lhs) // 输出:LinkedHashSet(1, 8, 4, 2),保留插入顺序,自动去重
  }
}

第二章 Scala Map 映射集合 ​

Map 是键值对 (KV) 形式的集合,每个键对应唯一的值,键不可重复,值可重复,对应 Java 的 Map 接口,是 Scala 最常用的集合之一,Spark PairRDD 的所有算子都对标 Map 的操作逻辑。

2.1 Map 核心概述 ​

2.1.1 核心特性 ​

  1. KV 结构:每个元素都是(键, 值)的元组,或用键->值的形式表示;
  2. 键唯一性:键不可重复,重复添加相同的键会覆盖原有值;
  3. 两大分支:默认使用scala.collection.immutable.Map,可变 Map 需手动导入scala.collection.mutable.Map;
  4. 高效查询:基于哈希表实现的 Map,按键查询的时间复杂度为 O (1),是大数据场景下索引构建、数据关联的核心工具。
  5. Scala的map继承关系

image-20250120014947462

2.1.2 Map 的两种创建方式 ​

Scala 提供两种创建 Map 的方式,语义完全一致,可根据习惯选择:

scala
// 方式1:使用 -> 符号创建,最常用
val map1 = Map("scala1"->1, "scala2"->2)
// 方式2:使用元组创建
val map2 = Map(("scala1",1), ("scala2",2))

2.1.3 与 Java Map 的核心差异 ​

特性Scala MapJava Map
默认类型不可变 Map,线程安全可变 HashMap,非线程安全
语法简洁性支持Map(k->v)直接创建,支持map(key)直接取值需new HashMap<>()创建,取值需调用get(key)
不可变支持原生提供不可变实现,修改操作返回新 Map需通过Collections.unmodifiableMap()包装,修改会抛出异常
函数式支持内置 map/filter/flatMap 等高阶函数,支持按键 / 值过滤排序Java8 前无 Lambda 支持,操作繁琐
空值处理提供getOrElse、Option类型,从根源避免空指针异常取值为 null 时直接抛出空指针异常,需手动判空

2.2 不可变 Map 常用操作 ​

不可变 Map 一旦创建,键值对无法修改,所有增删改操作都会返回一个全新的 Map,原 Map 保持不变,对应课程源码RunMap.scala。

2.2.1 基础查询操作 ​

scala
object RunMap {
  def main(args: Array[String]): Unit = {
    // 创建不可变Map
    val m1 = Map("scala1"->1, "scala2"->2)

    // 1. 直接取值:键不存在会抛出NoSuchElementException
    val v1 = m1("scala1")
    println(s"v1=$v1") // 输出:v1=1
    // val v2 = m1("scala3") // 键不存在,抛出异常

    // 2. 安全取值:get方法返回Option类型,键不存在返回None,避免空指针
    val v3 = m1.get("scala1")
    val v4 = m1.get("scala4")
    println(s"v3=$v3, v4=$v4") // 输出:v3=Some(1), v4=None

    // 3. 带默认值的安全取值:键不存在返回指定默认值
    val v5 = m1.getOrElse("scala5", "default")
    println(s"v5=$v5") // 输出:v5=default

    // 4. 判断键是否存在
    if(m1.contains("scala2")) {
      println("find " + m1.get("scala2")) // 输出:find Some(2)
    } else {
      println("un find ")
    }
  }
}

2.2.2 增删改操作 ​

scala
object RunMap {
  def main(args: Array[String]): Unit = {
    val m1 = Map("scala1"->1, "scala2"->2)

    // 1. 添加键值对,返回新Map
    val m2 = m1 + ("scala3"->3)
    val m3 = m2 + ("scala4"->4, "scala5"->5) // 批量添加
    println(s"m3=$m3") // 输出:m3=Map(scala1 -> 1, scala2 -> 2, scala3 -> 3, scala4 -> 4, scala5 -> 5)

    // 2. 更新键值对:相同的键会覆盖原有值
    val m4 = m3 + ("scala3"->30)
    println(s"m4=$m4") // scala3的值被覆盖为30
    println(s"原m3=$m3") // 原集合不变

    // 3. 删除键值对
    val m5 = m4 - "scala4" // 删除单个键
    val m6 = m5 - "scala4" - "scala3" // 批量删除,不存在的键会忽略
    println(s"m6=$m6") // 输出:m6=Map(scala1 -> 1, scala2 -> 2, scala5 -> 5)
  }
}

2.2.3 遍历操作 ​

Scala 提供多种遍历 Map 的方式,适配不同的业务场景:

scala
object RunMap {
  def main(args: Array[String]): Unit = {
    val mm1 = Map("java1"->1, "java2"->2, "java3"->3)

    // 1. 遍历键值对元组
    for((k,v) <- mm1) println(s"for key=$k,value=$v")
    mm1.foreach(x => println(s"foreach key=${x._1},value=${x._2}"))

    // 2. 只遍历键
    mm1.keySet.foreach(println(_))
    // 3. 只遍历值
    mm1.values.foreach(println(_))
  }
}

2.3 可变 Map 常用操作 ​

可变 Map 支持在原集合上直接修改键值对,无需生成新 Map,性能更高,适合高频更新、索引构建的场景,对应课程源码RunMap.scala。

scala
import scala.collection.mutable

object RunMap {
  def main(args: Array[String]): Unit = {
    // 创建可变Map
    val mm1 = mutable.Map("java1"->1, "java2"->2)

    // 1. 添加/更新键值对
    mm1("java3") = 3 // 键不存在则添加,存在则更新
    val r1 = mm1.put("java3", 3) // put方法返回旧值,键不存在返回None
    val r2 = mm1.put("java1", 10) // 键存在返回Some(旧值)
    println(s"r1=$r1, r2=$r2") // 输出:r1=None, r2=Some(1)
    println(s"mm1=$mm1") // 输出:mm1=HashMap(java1 -> 10, java2 -> 2, java3 -> 3)

    // 批量添加
    mm1 += ("java4"->4, "java5"->5)
    mm1 ++= Map("java6"->6, "java7"->7)

    // 2. 删除键值对
    val re1 = mm1.remove("java1") // remove返回被删除的值,键不存在返回None
    val re2 = mm1.remove("java8")
    println(s"re1=$re1, re2=$re2") // 输出:re1=Some(10), re2=None
    mm1 -= "java2" // 单个删除
    mm1 --= List("java3", "java4") // 批量删除
    println(s"remove mm1=$mm1") // 输出:remove mm1=HashMap(java5 -> 5, java6 -> 6, java7 -> 7)

    // 3. 清空Map
    mm1.clear()
    println(s"clear mm1=$mm1") // 输出:clear mm1=HashMap()
  }
}

2.4 Map 高级操作 ​

对应课程源码RunMapSFM.scala,Map 提供了丰富的过滤、排序、最值等高级操作,是大数据数据处理的核心能力。

2.4.1 过滤操作 ​

scala
import scala.collection.mutable

object RunMapSFM {
  def main(args: Array[String]): Unit = {
    // 可变Map条件过滤:retain保留满足条件的键值对,直接修改原Map
    val mp = mutable.Map(1->"a", 2->"b", 3->"c")
    mp.retain((k,v) => k > 2) // 保留键>2的元素
    println(s"mp=$mp") // 输出:mp=HashMap(3 -> c)

    // 不可变Map过滤:filterKeys按键过滤,filter按KV过滤,返回新Map
    val m = Map(4->"d", 5->"e", 6->"f")
    val m1 = m.filterKeys(_ > 4) // 按键过滤,保留键>4的元素
    val m2 = m.filterKeys(Set(4,6)) // 保留键在Set中的元素
    val m3 = m.filter(x => x._1 > 5) // 按完整KV过滤
    println(s"m1=$m1") // 输出:m1=Map(5 -> e, 6 -> f)
    println(s"m2=$m2") // 输出:m2=Map(4 -> d, 6 -> f)
    println(s"m3=$m3") // 输出:m3=Map(6 -> f)
  }
}

2.4.2 最值操作 ​

scala
object RunMapSFM {
  def main(args: Array[String]): Unit = {
    val mm = Map("ab"->12, "e"->4, "byyy"->99, "muuuuu"->17)

    // 1. 按键的字典序取最值
    val mm1 = mm.max
    val min1 = mm.min
    println(s"按键排序max=$mm1, min=$min1") // 输出:按键排序max=(muuuuu,17), min=(ab,12)

    // 2. 按值取最值
    val minv = mm.valuesIterator.min
    val maxv = mm.valuesIterator.max
    println(s"值min=$minv, 值max=$maxv") // 输出:值min=4, 值max=99

    // 3. 按键的长度取最值
    val maxKeyLength = mm.keysIterator.reduceLeft((x,y) => if(x.length > y.length) x else y)
    println(s"最长的键=$maxKeyLength") // 输出:最长的键=muuuuu
  }
}

2.4.3 排序操作 ​

Map 本身是无序的,排序后会转为 Seq 序列,可再转回 Map(注意:普通 HashMap 会丢失排序,需用 SortedMap/ListMap 保留排序)。

scala
object RunMapSFM {
  def main(args: Array[String]): Unit = {
    val mm = Map("ab"->12, "e"->4, "byyy"->99, "muuuuu"->17)

    // 1. 按键升序排序
    val sq1 = mm.toSeq.sortBy(_._1)
    // 2. 按值升序排序
    val sq2 = mm.toSeq.sortBy(_._2)
    // 3. 按值降序排序
    val sq3 = mm.toSeq.sortWith(_._2 > _._2)

    println(s"按键升序=$sq1") // 输出:List((ab,12), (byyy,99), (e,4), (muuuuu,17))
    println(s"按值升序=$sq2") // 输出:List((e,4), (ab,12), (muuuuu,17), (byyy,99))
    println(s"按值降序=$sq3") // 输出:List((byyy,99), (muuuuu,17), (ab,12), (e,4))

    // 转回有序Map:ListMap保留插入/排序顺序
    val sortedMap = sq3.to(ListMap)
    println(s"有序Map=$sortedMap")
  }
}

第三章 Scala 集合性能选型指南 ​

Scala 提供了丰富的集合实现类,不同集合的操作性能差异极大,大数据场景下,集合选型直接决定了程序的执行效率和内存占用,课程中重点讲解了各类集合的性能特性。

3.1 核心性能指标说明 ​

表格中性能标识说明:

  • eC:常数时间 O (1),效率极高
  • C:接近常数时间,受分块等因素影响极小
  • Log:对数时间 O (log n),效率优秀
  • L:线性时间 O (n),效率随元素数量增长线性下降
  • aC:均摊常数时间,大部分情况 O (1),少数情况 O (n)

3.2 不可变序列性能对比 ​

集合类型head 头操作tail 尾部操作apply 索引访问update 更新prepend 头部添加append 尾部添加insert 中间插入
ListeCeCLLeCLL
VectoreCeCeCeCeCeCL
RangeeCeCeC----
StringeCLeCLLLL

选型结论:

  • 频繁头部增删、顺序遍历:优先选List
  • 频繁随机访问、头尾增删:优先选Vector(通用不可变序列首选)
  • 固定数值区间:优先选Range,几乎不占用内存

3.3 可变序列性能对比 ​

集合类型head 头操作tail 尾部操作apply 索引访问update 更新prepend 头部添加append 尾部添加insert 中间插入
ArrayBuffereCLeCeCLaCL
ListBuffereCeCLLeCaCL
StringBuildereCLeCeCLaCL
ArrayeC-eCeC---

选型结论:

  • 频繁随机访问、尾部增删:优先选ArrayBuffer(通用可变序列首选)
  • 频繁头尾增删、顺序遍历:优先选ListBuffer
  • 字符串拼接:优先选StringBuilder
  • 固定长度、数值计算:优先选Array

3.4 Map/Set 性能对比 ​

集合类型lookup 查询add 添加remove 删除min 最小值适用场景
不可变 HashMap/HashSeteCeCeCL通用场景,高频增删查
可变 HashMap/HashSeteCeCeCL局部高频更新、大数据量索引构建
TreeMap/TreeSetLogLogLogeC需要按键排序的场景
BitSeteCeCeCeC非负整型数据的去重、判重
ListMap/ListSetLLLL需要保留插入顺序的小数据量场景

3.5 大数据场景下的集合选型最佳实践 ​

  1. 优先使用不可变集合:不可变集合线程安全、无副作用,完美适配 Spark 分布式不可变 RDD 模型,避免并发修改导致的数据异常,仅在局部单线程高频增删的场景下使用可变集合。
  2. 判重 / 去重优先用 Set:Set 的contains方法是 O (1),远优于 Seq 的 O (n),大数据量去重、判重绝对禁止用 List 的contains。
  3. 索引构建优先用可变 HashMap:大数据场景下构建字符 / 数据索引,优先使用mutable.HashMap,避免不可变 Map 频繁创建新集合带来的性能开销。
  4. 排序场景用 SortedSet/SortedMap:需要有序的 KV / 去重数据,优先使用 TreeSet/TreeMap,避免手动排序带来的性能损耗。
  5. 超大集合遍历用迭代器:百万级以上的超大集合,优先使用迭代器遍历,避免一次性加载所有数据到内存导致 OOM。

第四章 Scala 模式匹配 ​

模式匹配是 Scala 最具特色、最强大的语法特性,类似于 Java 的switch case,但功能远更强大,支持常量、变量、类型、元组、集合、样例类等多种匹配模式,是 Scala 函数式编程的核心语法,Spark 源码中随处可见模式匹配的应用。

4.1 模式匹配核心概述 ​

4.1.1 与 Java switch 的核心差异 ​

特性Scala 模式匹配Java switch case
匹配类型支持常量、变量、类型、元组、集合、样例类等几乎所有类型仅支持 byte、short、int、char、枚举、String(Java7+)
穿透性无 case 穿透问题,匹配成功后自动终止,无需 break默认 case 穿透,必须手动加 break 终止,否则会执行后续所有 case
返回值每个 case 都可返回值,整个 match 表达式有返回值无返回值,仅能执行语句
守卫条件支持 if 守卫条件,实现复杂的条件匹配仅能在 case 中写 if 判断,无原生守卫支持
变量绑定支持变量绑定,可在匹配的同时给匹配的内容赋值变量无此特性

4.1.2 基础语法 ​

scala
待匹配的值 match {
  case 匹配模式1 => 执行语句/返回值
  case 匹配模式2 => 执行语句/返回值
  case _ => 兜底默认执行语句/返回值 // 通配符_匹配所有情况,必须放在最后
}
  • 匹配规则:从上到下依次匹配,匹配到第一个符合的 case 后立即执行,不会穿透到后续 case;
  • 兜底规则:必须用case _处理所有未匹配的情况,否则会抛出MatchError异常;
  • 返回值:整个 match 表达式会返回匹配到的 case 的执行结果,可直接赋值给变量。

4.1.3 基础示例 ​

对应课程源码RunModel.scala,最基础的常量模式匹配:

scala
object RunModel {
  def main(args: Array[String]): Unit = {
    // 基础匹配:1-5的奇数匹配,偶数兜底
    for (i <- 1 to 5) {
      i match {
        case 1 => println(1)
        case 3 => println(3)
        case 5 => println(5)
        case _ => println("even") // 兜底,匹配所有其他情况
      }
    }

    // 多类型常量匹配
    def patternShow(x: Any) = x match {
      case Nil => println("empty List")
      case null => println("null")
      case true => println("true")
      case "scala" => println("scala")
      case _ => println("default")
    }
    patternShow(List()) // 输出:empty List
    patternShow(null) // 输出:null
    patternShow(100) // 输出:default
  }
}

4.2 8 种常用模式匹配类型 ​

课程中重点讲解了 8 种常用的模式匹配类型,覆盖了绝大多数业务场景,对应源码RunModel.scala。

4.2.1 常量模式 ​

常量模式用于匹配指定的常量值,是最基础的模式匹配,支持所有字面量常量(数字、字符串、布尔值、单例对象等)。

scala
def constantMatch(x: Any): String = x match {
  case 1 => "数字1"
  case "hello" => "字符串hello"
  case true => "布尔值true"
  case Nil => "空列表"
  case _ => "未知常量"
}

4.2.2 变量模式 ​

变量模式会将匹配到的值赋值给定义的变量,可在 case 语句中使用该变量,变量模式会匹配所有值,因此必须放在兜底case _之前,或配合守卫使用。

scala
object RunModel {
  def main(args: Array[String]): Unit = {
    def patterVarible(x: Any) = x match {
      case x => println(s"匹配到变量x=$x") // 变量模式,匹配所有值
      // 注意:后续case永远不会执行,因为变量模式已经匹配了所有值,编译器会警告
    }
    patterVarible(1) // 输出:匹配到变量x=1
  }
}

4.2.3 守卫模式(if 条件匹配) ​

守卫模式通过if条件表达式,给 case 添加额外的匹配条件,实现复杂的逻辑匹配,是模式匹配中最常用的扩展功能。

scala
object RunModel {
  def main(args: Array[String]): Unit = {
    // 奇偶判断,配合守卫模式
    for (i <- 1 to 10) {
      i match {
        case x if x % 2 == 0 => println(s"$x 是偶数")
        case _ => println(s"$i 是奇数")
      }
    }

    // 范围匹配
    def rangeMatch(x: Int): String = x match {
      case x if x > 0 && x < 10 => "0-10之间"
      case x if x >= 10 && x < 100 => "10-100之间"
      case _ => "超出范围"
    }
  }
}

4.2.4 元组模式 ​

元组模式用于匹配元组,可精准匹配元组的长度、指定位置的元素,支持通配符_忽略不需要的元素。

scala
object RunModel {
  def main(args: Array[String]): Unit = {
    val t1 = (2, 3)
    val t2 = (4, 5, 6)
    val t3 = (7, 8, 9, 0)
    val t4 = (1, 2, 3, 4, 5)

    def patterTuple(x: Any) = x match {
      case (first, second) => println(s"二元组:first=$first,second=$second")
      case (x1, x2, x3, x4) => println(s"四元组:first=$x1,second=$x2,x3=$x3,x4=$x4")
      case (x1, _, _, _, x5) => println(s"五元组:第一个=$x1,第五个=$x5") // 用_忽略中间元素
      case _ => println("其他元组")
    }

    patterTuple(t1) // 输出:二元组:first=2,second=3
    patterTuple(t2) // 输出:其他元组
    patterTuple(t3) // 输出:四元组:first=7,second=8,x3=9,x4=0
    patterTuple(t4) // 输出:五元组:第一个=1,第五个=5
  }
}

4.2.5 序列 / 集合模式 ​

序列模式用于匹配数组、List 等集合,可精准匹配集合的长度、指定位置的元素,支持_*通配符匹配剩余的所有元素。

scala
object RunModel {
  def main(args: Array[String]): Unit = {
    val arr = Array(1, 3, 6, 7)
    val list = List(2, 9)
    val list1 = List(2, 3, 45, 56, 6, 0, 199)
    val list2 = List(2)

    def patterSeq(x: Any) = x match {
      case Array(first, second) => println(s"二元数组:first=$first,second=$second")
      case Array(x1, x2, x3, x4) => println(s"四元数组:first=$x1,second=$x2,x3=$x3,x4=$x4")
      case List(first, second, _*) => println(s"列表:第一个=$first,第二个=$second,后续元素忽略") // _*匹配剩余所有元素
      case _ => println("其他集合")
    }

    patterSeq(arr) // 输出:四元数组:first=1,second=3,x3=6,x4=7
    patterSeq(list) // 输出:列表:第一个=2,第二个=9,后续元素忽略
    patterSeq(list1) // 输出:列表:第一个=2,第二个=3,后续元素忽略
    patterSeq(list2) // 输出:其他集合(长度不足2)
  }
}

4.2.6 类型模式 ​

类型模式用于匹配值的类型,替代 Java 中的instanceof类型判断,语法更简洁,同时会自动进行类型转换,无需强制类型转换。

scala
object RunModel {
  def main(args: Array[String]): Unit = {
    class A08
    class B08 extends A08

    def patterType(x: Any) = x match {
      case s: String => println("匹配到String类型")
      case s: Int => println("匹配到Int类型")
      case s: Double => println("匹配到Double类型")
      case a: A08 => println("匹配到A08类型")
      case 88 => println("匹配到常量88") // 常量模式可与类型模式混用
      case _ => println("其他类型")
    }

    patterType(3) // 输出:匹配到Int类型
    patterType("scala") // 输出:匹配到String类型
    patterType(2.0) // 输出:匹配到Double类型
    patterType(true) // 输出:其他类型
    patterType(88) // 输出:匹配到常量88
    patterType(new B08()) // 输出:匹配到A08类型(多态匹配)
  }
}

注意:类型匹配会遵循多态规则,子类实例会匹配到父类类型,因此父类类型的 case 要放在子类之后。

4.2.7 构造器模式 ​

构造器模式是模式匹配最强大的功能之一,用于匹配样例类(case class)的构造参数,可精准匹配样例类的属性值,支持嵌套匹配,是 Spark 中样例类数据解析的核心方式。

核心前提 ​

样例类会自动生成unapply解构方法,这是构造器模式的基础,普通类需手动实现unapply方法才能支持构造器模式。

scala
object RunModel {
  def main(args: Array[String]): Unit = {
    // 样例类,自动支持构造器模式
    case class Dog08(var name:String, val age:Int)
    // 普通类,手动实现unapply方法支持构造器模式
    class Dog07(var name:String, val age:Int){
      override def toString = s"Dog07($name, $age)"
    }
    object Dog07{
      // 手动实现unapply解构方法
      def unapply(arg: Dog07): Option[(String, Int)] = if (arg!=null) Some(arg.name,arg.age) else None
    }

    def patterConstrcutor(x: Any) = x match {
      case Dog08(name, age) => println(s"Dog08:name=$name,age=$age") // 匹配所有属性
      case Dog08(_, age) => println(s"Dog08:仅匹配年龄age=$age") // 用_忽略不需要的属性
      case d@Dog07(name, age) => println(s"Dog07完整对象=$d, name=$name,age=$age") // 变量绑定
      case _ => println("其他对象")
    }

    patterConstrcutor(Dog08("wangwang",30)) // 输出:Dog08:name=wangwang,age=30
    patterConstrcutor(Dog07("wangwang07",70)) // 输出:Dog07完整对象=Dog07(wangwang07, 70), name=wangwang07,age=70
  }
}

4.2.8 变量绑定模式 ​

变量绑定模式通过变量名@的语法,在匹配模式的同时,将整个匹配到的对象赋值给变量,可在 case 语句中直接使用该对象,无需重新构建。

scala
object RunModel {
  def main(args: Array[String]): Unit = {
    val list_1 = List(List(1,2,3,4), List(4,5,6,7,8,9,10))

    def patterBind(x: Any) = x match {
      // e1绑定整个外层List,e2绑定内层的第二个List
      case e1@List(_, e2@List(5, _*)) => println(s"外层List=$e1, 内层List=$e2")
      case _ => println("其他列表")
    }

    patterBind(list_1) // 输出:外层List=List(List(1, 2, 3, 4), List(4, 5, 6, 7, 8, 9, 10)), 内层List=List(5, 6, 7, 8, 9, 10)
  }
}

4.3 模式匹配的注意事项 ​

  1. case 顺序问题:匹配规则是从上到下,范围大的 case 必须放在范围小的 case 之后,否则后续 case 永远不会执行,编译器会给出警告。
  2. 兜底 case 必须有:必须用case _处理所有未匹配的情况,否则运行时会抛出MatchError异常。
  3. 无 case 穿透:Scala 模式匹配匹配成功后自动终止,无需像 Java 一样加 break,不会出现 case 穿透问题。
  4. 样例类优先:构造器模式优先使用样例类,无需手动实现 unapply 方法,语法更简洁,避免手动实现的错误。
  5. 泛型擦除问题:类型模式无法匹配泛型类型(如List[String]),因为 JVM 有泛型擦除,运行时无法获取泛型参数,仅能匹配List[_]。

第五章 课后作业全解析 ​

对应课程配套作业文档,包含简答题与编程题全解析,所有代码可直接复制运行。

一、简答题 ​

(1)Scala 有哪些模式匹配? ​

答:Scala 模式匹配功能极其强大,提供了 8 种常用的匹配模式,覆盖了绝大多数业务场景,具体分类如下:

  1. 常量模式:匹配指定的字面量常量,如数字、字符串、布尔值、单例对象、空集合 Nil 等,是最基础的模式匹配。
  2. 变量模式:将匹配到的值赋值给定义的变量,可在 case 语句中使用该变量,会匹配所有输入值。
  3. 守卫模式:在 case 中通过if条件表达式添加额外的匹配规则,实现复杂的条件匹配,如范围判断、奇偶判断等。
  4. 元组模式:匹配元组类型,可精准匹配元组的长度、指定位置的元素,支持通配符_忽略不需要的元素。
  5. 序列 / 集合模式:匹配数组、List 等集合类型,可精准匹配集合长度、指定位置元素,支持_*通配符匹配剩余所有元素。
  6. 类型模式:匹配值的类型,替代 Java 的instanceof判断,同时自动完成类型转换,无需强制类型转换,支持多态匹配。
  7. 构造器模式:针对样例类(case class)的核心匹配模式,可精准匹配样例类的构造参数,支持嵌套匹配,是 Scala 模式匹配最强大的功能。
  8. 变量绑定模式:通过@语法,在匹配的同时将整个匹配对象赋值给变量,可在 case 语句中直接使用完整的匹配对象。

除此之外,Scala 还支持可选值 Option 模式、正则表达式模式、提取器模式等高级匹配模式,可适配更复杂的业务场景。

二、编程题 ​

(1)利用模式匹配编写一个 swap 函数,用于交换数组中的前两个元素的位置(数组长度 > 2)。 ​

scala
object Homework1 {
  /**
   * 交换数组前两个元素的位置,使用模式匹配实现
   * @param arr 输入的数组,要求长度>2
   * @return 交换后的新数组,原数组保持不变
   */
  def swap(arr: Array[Any]): Array[Any] = {
    // 模式匹配数组的前两个元素,剩余元素用_*匹配
    arr match {
      // 匹配至少两个元素的数组,交换前两个,剩余元素保持不变
      case Array(first, second, rest @ _*) => Array(second, first) ++ rest
      // 数组长度不足2,直接返回原数组
      case _ => arr
    }
  }

  def main(args: Array[String]): Unit = {
    // 测试用例
    val testArr1 = Array(1,2,3,4,5)
    val testArr2 = Array("scala","java","python","c")
    val testArr3 = Array(1) // 长度不足2
    val testArr4 = Array(true, false, 10.5, "hello")

    println(s"原数组:${testArr1.mkString(",")},交换后:${swap(testArr1).mkString(",")}")
    println(s"原数组:${testArr2.mkString(",")},交换后:${swap(testArr2).mkString(",")}")
    println(s"原数组:${testArr3.mkString(",")},交换后:${swap(testArr3).mkString(",")}")
    println(s"原数组:${testArr4.mkString(",")},交换后:${swap(testArr4).mkString(",")}")
  }
}

(2)编写一个函数,计算 List [Option [Int]] 中所有非 None 值的和,不得使用 match 语句。 ​

scala
object Homework2 {
  /**
   * 计算List[Option[Int]]中所有非None值的和,不使用match语句
   * @param list 输入的Option[Int]列表
   * @return 所有非None值的总和
   */
  def sumOptionList(list: List[Option[Int]]): Int = {
    // 方案1:flatMap展开Option,自动过滤None,再求和
    list.flatMap(option => option).sum

    // 方案2:foldLeft折叠求和
    // list.foldLeft(0)((acc, opt) => acc + opt.getOrElse(0))

    // 方案3:filter过滤Some,map取值,再求和
    // list.filter(_.isDefined).map(_.get).sum
  }

  def main(args: Array[String]): Unit = {
    val testList = List(Some(1), None, Some(3), Some(5), None, Some(10))
    val emptyList = List(None, None, None)
    val fullList = List(Some(2), Some(4), Some(6))

    println(s"测试列表总和:${sumOptionList(testList)}") // 输出:1+3+5+10=19
    println(s"空列表总和:${sumOptionList(emptyList)}") // 输出:0
    println(s"全值列表总和:${sumOptionList(fullList)}") // 输出:12
  }
}

(3)制作这样的树更好的做法是使用样例类。我们不妨从二叉树开始,编写一个函数计算所有叶子节点中的元素之和。 ​

样例类定义:

scala
sealed abstract class BinaryTree
case class Leaf(value : Int) extends BinaryTree
case class Node(left : BinaryTree,right : BinaryTree) extends BinaryTree
scala
// 二叉树样例类定义
sealed abstract class BinaryTree
case class Leaf(value : Int) extends BinaryTree
case class Node(left : BinaryTree, right : BinaryTree) extends BinaryTree

object Homework3 {
  /**
   * 递归计算二叉树所有叶子节点的元素之和
   * @param tree 二叉树根节点
   * @return 所有叶子节点的值总和
   */
  def sumLeaf(tree: BinaryTree): Int = tree match {
    // 叶子节点,直接返回值
    case Leaf(value) => value
    // 分支节点,递归计算左右子树的叶子和,相加
    case Node(left, right) => sumLeaf(left) + sumLeaf(right)
  }

  def main(args: Array[String]): Unit = {
    // 构建测试二叉树:
    //        root
    //       /    \
    //    node1   node2
    //   /   \      \
    // Leaf(3) Leaf(8) Leaf(5)
    val testTree = Node(
      Node(Leaf(3), Leaf(8)),
      Node(Leaf(2), Leaf(5))
    )

    // 单叶子节点测试
    val singleLeaf = Leaf(10)
    // 空分支测试
    val emptyTree = Node(Leaf(1), Node(Leaf(2), Leaf(3)))

    println(s"测试二叉树叶子总和:${sumLeaf(testTree)}") // 输出:3+8+2+5=18
    println(s"单叶子节点总和:${sumLeaf(singleLeaf)}") // 输出:10
    println(s"三层二叉树总和:${sumLeaf(emptyTree)}") // 输出:1+2+3=6
  }
}

(4)编写一个函数,给定字符串,产生出一个包含所有字符的下标的映射。举例来说,index ("Mississippi") 应返回一个映射,让 'M' 对应集 {0},'i' 对应集 {1,4,7,10},依此类推。使用字符到可变集的映射。另外,你如何保证集是经过排序的? ​

scala
import scala.collection.mutable

object Homework4 {
  /**
   * 构建字符串中每个字符的下标映射
   * @param str 输入的字符串
   * @param sorted 是否返回排序的下标集,默认true
   * @return 字符到下标集的映射,key为字符,value为该字符出现的所有下标
   */
  def charIndexMap(str: String, sorted: Boolean = true): Map[Char, Set[Int]] = {
    // 1. 创建可变Map,key为字符,value为可变的下标集
    val indexMap = mutable.Map[Char, mutable.Set[Int]]()

    // 2. 遍历字符串,记录每个字符的下标
    for ((char, index) <- str.zipWithIndex) {
      // 如果字符不存在,初始化空集;存在则直接添加下标
      indexMap.getOrElseUpdate(char, 
        if (sorted) mutable.SortedSet else mutable.HashSet
      ) += index
    }

    // 3. 转为不可变Map返回
    indexMap.mapValues(_.toSet).toMap
  }

  def main(args: Array[String]): Unit = {
    val testStr = "Mississippi"
    val result = charIndexMap(testStr)
    val unSortedResult = charIndexMap(testStr, sorted = false)

    println(s"字符串:$testStr")
    println("排序后的下标映射:")
    result.foreach{ case (char, indexes) =>
      println(s"$char -> $indexes")
    }

    // 输出结果:
    // M -> TreeSet(0)
    // i -> TreeSet(1, 4, 7, 10)
    // s -> TreeSet(2, 3, 5, 6)
    // p -> TreeSet(8, 9)
  }
}

排序保证方案:

  1. 使用SortedSet(默认实现 TreeSet)替代普通的 HashSet,SortedSet会自动对下标进行升序排序;
  2. 遍历字符串时,下标是从小到大递增的,也可使用LinkedHashSet保留插入顺序,实现下标有序;
  3. 最终返回时,可通过toList.sorted.toSet对下标集进行排序,保证结果有序。

学习总结 ​

  1. Set 集合:Set 是无序、元素不可重复的集合,核心作用是去重和快速判重,contains方法 O (1) 的时间复杂度远优于 Seq,大数据场景下判重优先使用 Set;分为不可变 Set(默认)和可变 Set,特殊实现类 SortedSet 可实现元素排序,LinkedHashSet 可保留插入顺序。
  2. Map 集合:Map 是 KV 键值对集合,是大数据索引构建、数据关联的核心工具,分为不可变 Map(默认)和可变 Map;不可变 Map 线程安全,适合常量映射;可变 Map 性能更高,适合高频更新的索引构建;Map 提供了丰富的过滤、排序、最值操作,Spark PairRDD 的算子设计完全对标 Map 的操作逻辑。
  3. 集合性能选型:大数据场景下,集合选型直接决定程序性能,通用场景不可变集合优先选 Vector,可变集合优先选 ArrayBuffer;高频增删查选 HashMap/HashSet,排序场景选 TreeMap/TreeSet,绝对禁止用 List 做大数据量的判重操作。
  4. 模式匹配:模式匹配是 Scala 最具特色的语法,功能远强于 Java 的 switch case,支持 8 种常用匹配模式,无 case 穿透问题,每个 case 都有返回值;构造器模式配合样例类,可实现复杂的对象解构,是 Spark 数据解析、异常处理、分支判断的核心语法,在 Spark 源码中随处可见。
  5. 大数据开发适配:Scala 的 Set、Map 操作是 Spark RDD 算子的基础,模式匹配是 Spark 业务开发的必备语法,掌握本章内容,就掌握了 Spark 大数据开发的核心语法基础,能够大幅提升 Spark 业务代码的开发效率和可读性。

下一章预习内容:Scala 隐式转换、Scala 与 Java 交互。

基于 Vite 强力驱动 | 纯静态轻量托管