可变集合(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 可给初值(且 sum、mkString 内部就是 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"的替换。


背景:一个 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 常见事故源)。三条全否,局部可变就是安全的。
不可变用 +,可变用 +=;这组差异记一张小表:
| 操作 | 不可变 | 可变 |
|---|---|---|
| 追加元素 | 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,八成就是它。