4.3 可变集合与集合操作选型


4.3 可变集合与集合操作选型

可变集合(ArrayBuffer、ListBuffer、mutable.Map 等)是性能热点上的合法工具;组合子的选型与性能则决定集合代码的生产质量。本节合并讲"何时破例用可变"与"组合子怎么选"。

破例的三种正当情形

import scala.collection.mutable // 1. 热点循环构建大集合:ListBuffer 尾部 O(1) val buf = mutable.ListBuffer[Int]() for i <- 1 to 1000000 do buf += i val big = buf.toList // 构建完转不可变,可变性不外泄 // 2. 高频原地更新计数 val counter = mutable.Map.empty[String, Int] words.foreach(w => counter(w) = counter.getOrElse(w, 0) + 1) // 3. 与 Java API 交互的缓冲区 val out = new java.util.ArrayList[String]()

共同模式是:可变只活在函数体内,出口处转回不可变。可变性像脚手架,房子交付前拆掉。

组合子的"不可变替代"往往已经够快,先测再换:

words.groupMapReduce(identity)(_ => 1)(_ + _) // 上面计数的一次到位写法 (1 to 1000000).toVector // 直接生成,无需循环

常用组合子分组记忆

组合子 一句话
变换 map、flatMap、collect 一进一出,collect 只留匹配的
过滤 filter、filterNot、takeWhile、dropWhile 缩小规模
聚合 foldLeft、reduce、sum、max 多并一
重排 sorted、sortBy、groupBy、distinct 改变组织
探测 find、exists、forall、count 出布尔或首个
拆合 zip、unzip、partition、sliding 结构重织

几个容易忽视的性能点:

  • xs.filter(p).head 换成 xs.find(p),找到即停。
  • 多趟扫描合成一趟:xs.map(f).filter(g) 在大集合上不如 view 链。
  • foldLeft vs reduce:reduce 空集合抛异常,foldLeft 可给初值(且 summkString 内部就是 fold)。

案例:日志统计一条龙

val lines: List[String] = ??? val topPaths = lines .view .map(_.trim) .filter(_.nonEmpty) .collect { case s"$method $path $_" => path } .groupMapReduce(identity)(_ => 1)(_ + _) .toList .sortBy(-_._2) .take(10)

view 惰性贯穿 collect,groupMapReduce 一次成表,sortBy 加负号倒序。整段没有一层嵌套循环,每一步都能单独替换——这就是组合子管线对"命令式三重 for"的替换。

不可变管线 vs 手写可变循环的成本直觉

不可变管线 vs 手写可变循环的成本直觉

图 4-3 集合选型决策路径

图 4-3 集合选型决策路径

完整案例:从不可变切到可变的一次性能急救

背景:一个 ETL 任务要把千万行日志按分钟桶聚合,初版全用不可变 Map 的 + 更新,耗时与 GC 压力都超标。操作过程分两步,先定位再替换:

// 初版:每行日志都复制整张 Map,千万次更新等于千万次拷贝 val r1 = lines.foldLeft(Map.empty[Int, Int]) { (m, line) => val bucket = minuteOf(line) m + (bucket -> (m.getOrElse(bucket, 0) + 1)) } // 急救版:聚合阶段局部使用可变 Map,结束后冻结成不可变值返回 import scala.collection.mutable val tmp = mutable.Map.empty[Int, Int] for line <- lines do val b = minuteOf(line) tmp(b) = tmp.getOrElse(b, 0) + 1 val r2 = tmp.toList.toMap // 边界处交出不可变快照

结果:耗时下降一个数量级,GC 停顿显著减少。解读:可变性本身不是罪,失控的可变性才是——把它圈在局部作用域、出口处冻结,外面的人拿到的仍是不可变值,安全边界清晰。变式:Scala 2.13 以上的合并利器是 Map 的 merged 与 groupMapReduce,一行等价于上面的循环,且内部已做优化,能上库函数就不手写。

逃逸检查:怎么判断可变性有没有漏出去

三条自检:可变集合是否出现在公开方法的返回类型或参数里;是否被赋给比方法作用域更长的 val;是否被闭包捕获后异步执行(6.1 节 Future 常见事故源)。三条全否,局部可变就是安全的。

可变集合的 API 差异速记

不可变用 +,可变用 +=;这组差异记一张小表:

操作 不可变 可变
追加元素 xs :+ x / m + kv buf += x / m += kv
删除 xs.filterNot / m - k buf -= x / m.remove(k)
就地排序 xs.sorted(新值) buf.sortInPlace()
清空 造新的空集合 buf.clear()

最阴险的坑是 ArrayBuffer 上写 xs.sorted 然后丢弃返回值——sorted 返回新序列,原缓冲纹丝不动,程序"看起来跑了"但结果没变。见到编译器警告 a pure expression does nothing in statement position,八成就是它。

本节要点回顾

  • 可变的三种正当场合:热点构建、高频原地更新、与 Java 交互,出口转回不可变。
  • 组合子分组记忆:变换、过滤、聚合、重排、探测、拆合。
  • find 换 filter 加 head、view 合并多趟、reduce 小心空集合。
  • 先测后优化:groupMapReduce 这类一步到位 API 常已消除换可变的动机。

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