Scala集合
简介
Scala的集合库是其最强大的功能之一,提供了一个丰富、统一的API来处理序列、集合和映射。该库默认强调不可变性,同时在需要性能关键代码时提供可变替代方案。
集合层次结构区分不可变和可变变体,不可变集合是默认的。关键集合类型包括List、Vector、Set、Map、Array及其专业变体。该库在所有集合类型上提供一致的变换操作。
本技能涵盖不可变与可变集合、序列(List、Vector、Array)、集合和映射、集合操作(map、filter、fold)、for-comprehensions、惰性求值、并行集合和性能特征。
不可变与可变集合
不可变集合提供线程安全性和可预测性,而可变集合为密集更新提供性能优势。
// 不可变List(默认)
val immutableList = List(1, 2, 3, 4, 5)
val newList = immutableList :+ 6 // 创建新列表
val prepended = 0 :: immutableList // 前置元素
// 原始不变
println(immutableList) // List(1, 2, 3, 4, 5)
println(newList) // List(1, 2, 3, 4, 5, 6)
// 可变ListBuffer
import scala.collection.mutable
val mutableList = mutable.ListBuffer(1, 2, 3)
mutableList += 4 // 原地突变
mutableList ++= List(5, 6)
mutableList -= 2
println(mutableList) // ListBuffer(1, 3, 4, 5, 6)
// 不可变Set
val immutableSet = Set(1, 2, 3)
val addedSet = immutableSet + 4
val removedSet = immutableSet - 2
// 可变Set
val mutableSet = mutable.Set(1, 2, 3)
mutableSet += 4
mutableSet -= 2
// 不可变Map
val immutableMap = Map("a" -> 1, "b" -> 2, "c" -> 3)
val updatedMap = immutableMap + ("d" -> 4)
val removedMap = immutableMap - "b"
// 可变Map
val mutableMap = mutable.Map("a" -> 1, "b" -> 2)
mutableMap("c") = 3
mutableMap += ("d" -> 4)
mutableMap -= "b"
// 在不可变和可变之间转换
val immutable = List(1, 2, 3)
val asMutable = immutable.toBuffer // 可变副本
asMutable += 4
val backToImmutable = asMutable.toList
// 不可变Vector(高效随机访问)
val vector = Vector(1, 2, 3, 4, 5)
val updatedVector = vector.updated(2, 10) // 高效创建新向量
// 选择不可变与可变
// 使用不可变:
// - 默认选择
// - 并发访问
// - 函数式变换
// - 公共API
// 使用可变:
// - 性能关键循环
// - 大规模更新
// - 仅局部范围
// - 构建器模式
// 构建器模式,返回不可变结果
def buildList(): List[Int] = {
val builder = List.newBuilder[Int]
for (i <- 1 to 100) {
builder += i
}
builder.result()
}
// 不可变集合与更新
case class User(name: String, age: Int, email: String)
val users = List(
User("Alice", 30, "alice@example.com"),
User("Bob", 25, "bob@example.com")
)
val updatedUsers = users.map { user =>
if (user.name == "Alice") user.copy(age = 31)
else user
}
默认优先选择不可变集合,以确保安全性和简单性,仅在性能分析显示瓶颈时使用可变集合。
序列:List、Vector和Array
不同序列类型为不同访问模式提供不同的性能特征。
// List:链表,O(1)前置,O(n)随机访问
val list = List(1, 2, 3, 4, 5)
val prepended = 0 :: list // O(1)
val concatenated = list ::: List(6, 7) // O(n)
val appended = list :+ 6 // O(n)
// 列表的模式匹配
def sumList(list: List[Int]): Int = list match {
case Nil => 0
case head :: tail => head + sumList(tail)
}
// 列表构造
val range = List.range(1, 11) // List(1, 2, ..., 10)
val filled = List.fill(5)(0) // List(0, 0, 0, 0, 0)
val tabulated = List.tabulate(5)(i => i * i) // List(0, 1, 4, 9, 16)
// Vector:索引序列,所有操作O(log32 n)
val vector = Vector(1, 2, 3, 4, 5)
val vectorUpdated = vector.updated(2, 10) // O(log n)
val vectorAppended = vector :+ 6 // O(log n)
val vectorPrepended = 0 +: vector // O(log n)
// 随机访问
println(vector(3)) // O(log n) - 高效
// Vector更适合:
// - 随机访问
// - 两端操作
// - 大集合
// Array:可变、固定大小、O(1)随机访问
val array = Array(1, 2, 3, 4, 5)
array(2) = 10 // 可变更新
println(array.mkString(", "))
// Array操作返回Arrays
val doubled = array.map(_ * 2)
// ArrayBuffer:可变、可调整大小
import scala.collection.mutable.ArrayBuffer
val buffer = ArrayBuffer(1, 2, 3)
buffer += 4
buffer ++= Array(5, 6)
buffer.insert(0, 0)
buffer.remove(2)
// Seq:通用序列特征
def processSeq(seq: Seq[Int]): Int = seq.sum
println(processSeq(List(1, 2, 3)))
println(processSeq(Vector(1, 2, 3)))
println(processSeq(Array(1, 2, 3)))
// IndexedSeq用于高效随机访问
def processIndexed(seq: IndexedSeq[Int]): Int = {
var sum = 0
for (i <- seq.indices) {
sum += seq(i)
}
sum
}
// LinearSeq用于高效头/尾操作
def processLinear(seq: collection.LinearSeq[Int]): Int = seq match {
case head :: tail => head + processLinear(tail)
case _ => 0
}
// Range:惰性、内存高效序列
val range1 = 1 to 10 // 1到10包含
val range2 = 1 until 10 // 1到9
val range3 = 1 to 100 by 10 // 1, 11, 21, ..., 91
// Stream(已弃用,使用LazyList)
val lazyList = LazyList.from(1).take(5)
println(lazyList.toList)
// 选择正确序列:
// List - 默认,函数式风格,前置密集
// Vector - 大,随机访问,两端操作
// Array - 与Java互操作,可变,性能关键
// ArrayBuffer - 可变,频繁更新
为函数式编程选择List,为随机访问选择Vector,为Java互操作或性能关键代码选择Array。
集合和映射
集合提供唯一元素存储,而映射存储键值对,两者都有高效的查找操作。
// 不可变Set
val set1 = Set(1, 2, 3, 4, 5)
val set2 = Set(4, 5, 6, 7, 8)
// Set操作
val union = set1 union set2 // Set(1, 2, 3, 4, 5, 6, 7, 8)
val intersection = set1 intersect set2 // Set(4, 5)
val difference = set1 diff set2 // Set(1, 2, 3)
// Set方法
println(set1.contains(3)) // true
println(set1(3)) // true(同contains)
val added = set1 + 6
val removed = set1 - 3
val multiAdd = set1 ++ Set(6, 7, 8)
// 可变Set
import scala.collection.mutable
val mutableSet = mutable.Set(1, 2, 3)
mutableSet += 4
mutableSet ++= Set(5, 6)
mutableSet -= 2
// 不同Set实现
val hashSet = mutable.HashSet(1, 2, 3) // 无序,快速
val linkedHashSet = mutable.LinkedHashSet(1, 2, 3) // 维护插入顺序
val treeSet = collection.immutable.TreeSet(1, 2, 3) // 排序
// SortedSet
val sortedSet = collection.immutable.SortedSet(5, 2, 8, 1)
println(sortedSet) // TreeSet(1, 2, 5, 8)
// 不可变Map
val map = Map(
"Alice" -> 30,
"Bob" -> 25,
"Charlie" -> 35
)
// Map访问
println(map("Alice")) // 30
println(map.get("Alice")) // Some(30)
println(map.get("David")) // None
println(map.getOrElse("David", 0)) // 0
// Map操作
val updated = map + ("David" -> 28)
val removed = map - "Bob"
val merged = map ++ Map("Eve" -> 32)
// Map变换
val ages = map.values.toList
val names = map.keys.toList
val pairs = map.toList
val incremented = map.map { case (name, age) => (name, age + 1) }
val filtered = map.filter { case (_, age) => age > 30 }
// 可变Map
val mutableMap = mutable.Map("a" -> 1, "b" -> 2)
mutableMap("c") = 3
mutableMap += ("d" -> 4)
mutableMap.update("e", 5)
// Map变体
val hashMap = mutable.HashMap("a" -> 1, "b" -> 2) // 无序,快速
val linkedHashMap = mutable.LinkedHashMap("a" -> 1, "b" -> 2) // 插入顺序
val treeMap = collection.immutable.TreeMap("c" -> 3, "a" -> 1, "b" -> 2) // 排序
// SortedMap
val sortedMap = collection.immutable.SortedMap(
"charlie" -> 35,
"alice" -> 30,
"bob" -> 25
)
println(sortedMap) // TreeMap(alice -> 30, bob -> 25, charlie -> 35)
// MultiMap模式
val multiMap = mutable.Map[String, mutable.Set[Int]]()
def addToMultiMap(key: String, value: Int): Unit = {
multiMap.getOrElseUpdate(key, mutable.Set()) += value
}
addToMultiMap("even", 2)
addToMultiMap("even", 4)
addToMultiMap("odd", 1)
addToMultiMap("odd", 3)
// 分组为Map
val numbers = List(1, 2, 3, 4, 5, 6)
val grouped = numbers.groupBy(_ % 2 == 0)
// Map(false -> List(1, 3, 5), true -> List(2, 4, 6))
// 词频计数
val text = "the quick brown fox jumps over the lazy dog"
val wordFreq = text.split(" ")
.groupBy(identity)
.view.mapValues(_.length)
.toMap
// 带默认值的Map
val withDefault = map.withDefaultValue(0)
println(withDefault("Unknown")) // 0
val withDefaultFunc = map.withDefault(key => key.length)
println(withDefaultFunc("Unknown")) // 7
使用Set进行唯一性约束和快速成员测试,使用Map进行键值查找和分组操作。
集合变换
Scala提供丰富的变换方法,在所有集合类型上一致工作。
// Map:变换每个元素
val numbers = List(1, 2, 3, 4, 5)
val squared = numbers.map(x => x * x)
val doubled = numbers.map(_ * 2)
// FlatMap:映射并扁平化
val nested = List(List(1, 2), List(3, 4), List(5))
val flattened = nested.flatMap(identity) // List(1, 2, 3, 4, 5)
val pairs = numbers.flatMap(x => numbers.map(y => (x, y)))
// Filter:选择元素
val evens = numbers.filter(_ % 2 == 0)
val odds = numbers.filterNot(_ % 2 == 0)
// Partition:拆分为两个集合
val (evenPart, oddPart) = numbers.partition(_ % 2 == 0)
// Take和Drop
val first3 = numbers.take(3) // List(1, 2, 3)
val last3 = numbers.takeRight(3) // List(3, 4, 5)
val skip2 = numbers.drop(2) // List(3, 4, 5)
// TakeWhile和DropWhile
val taken = numbers.takeWhile(_ < 4) // List(1, 2, 3)
val dropped = numbers.dropWhile(_ < 4) // List(4, 5)
// Slice:提取范围
val slice = numbers.slice(1, 4) // List(2, 3, 4)
// Fold和Reduce
val sum = numbers.foldLeft(0)(_ + _)
val product = numbers.foldLeft(1)(_ * _)
// FoldRight:右结合
val rightFold = numbers.foldRight(0)(_ + _)
// Reduce:类似fold但无初始值
val reduced = numbers.reduce(_ + _)
val max = numbers.reduce((a, b) => if (a > b) a else b)
// Scan:Fold带中间结果
val cumulative = numbers.scan(0)(_ + _) // List(0, 1, 3, 6, 10, 15)
// Zip:组合集合
val letters = List("a", "b", "c")
val zipped = numbers.zip(letters) // List((1,a), (2,b), (3,c))
val withIndex = numbers.zipWithIndex // List((1,0), (2,1), (3,2), ...)
// Unzip:拆分对
val (nums, chars) = zipped.unzip
// GroupBy:创建组Map
val grouped = numbers.groupBy(_ % 3)
// Map(0 -> List(3), 1 -> List(1, 4), 2 -> List(2, 5))
// Sorted, SortBy, SortWith
val sorted = List(3, 1, 4, 1, 5).sorted
val sortedDesc = List(3, 1, 4, 1, 5).sorted(Ordering[Int].reverse)
case class Person(name: String, age: Int)
val people = List(Person("Alice", 30), Person("Bob", 25))
val byAge = people.sortBy(_.age)
val byName = people.sortWith(_.name < _.name)
// Distinct:移除重复
val withDups = List(1, 2, 2, 3, 3, 3, 4)
val unique = withDups.distinct // List(1, 2, 3, 4)
// Find, Exists, ForAll
val found = numbers.find(_ > 3) // Some(4)
val exists = numbers.exists(_ > 10) // false
val all = numbers.forall(_ > 0) // true
// Count:匹配元素数量
val count = numbers.count(_ % 2 == 0) // 2
// Collect:偏函数变换
val result = numbers.collect {
case x if x % 2 == 0 => x * 2
}
// Sliding:滑动窗口
val windows = numbers.sliding(2).toList
// List(List(1, 2), List(2, 3), List(3, 4), List(4, 5))
val windows3 = numbers.sliding(3, 2).toList
// List(List(1, 2, 3), List(3, 4, 5))
// Grouped:固定大小块
val chunks = numbers.grouped(2).toList
// List(List(1, 2), List(3, 4), List(5))
// Transpose:矩阵转置
val matrix = List(List(1, 2, 3), List(4, 5, 6))
val transposed = matrix.transpose // List(List(1, 4), List(2, 5), List(3, 6))
// Combinations和Permutations
val combinations = List(1, 2, 3).combinations(2).toList
// List(List(1, 2), List(1, 3), List(2, 3))
val permutations = List(1, 2, 3).permutations.toList
// 列表的所有排列
// 字符串特定操作
val words = List("hello", "world", "scala")
val concatenated = words.mkString(", ") // "hello, world, scala"
val joined = words.mkString("[", ", ", "]") // "[hello, world, scala]"
掌握这些变换,以最小的代码编写表达性强的函数式数据处理流水线。
For-Comprehensions与集合
For-comprehensions为复杂的集合操作提供优雅语法,特别是多序列操作。
// 基本for-comprehension
val numbers = List(1, 2, 3)
val letters = List("a", "b")
val combined = for {
num <- numbers
letter <- letters
} yield (num, letter)
// List((1,a), (1,b), (2,a), (2,b), (3,a), (3,b))
// 带过滤
val filtered = for {
num <- numbers
if num % 2 != 0
letter <- letters
} yield (num, letter)
// 多生成器
val result = for {
i <- 1 to 3
j <- 1 to 3
if i < j
} yield (i, j)
// 脱糖为flatMap和map
val manual = numbers.flatMap { num =>
letters.map { letter =>
(num, letter)
}
}
// 嵌套for-comprehensions
val matrix = List(List(1, 2), List(3, 4), List(5, 6))
val flattened = for {
row <- matrix
elem <- row
} yield elem * 2
// 生成器中的模式匹配
case class Person(name: String, age: Int)
val people = List(Person("Alice", 30), Person("Bob", 25), Person("Charlie", 35))
val names = for {
Person(name, age) <- people
if age > 26
} yield name
// 组合Options
def getUserById(id: Int): Option[Person] =
if (id == 1) Some(Person("Alice", 30)) else None
def getEmail(person: Person): Option[String] =
Some(s"${person.name.toLowerCase}@example.com")
val email = for {
person <- getUserById(1)
email <- getEmail(person)
} yield email
// 笛卡尔积
val xs = List(1, 2, 3)
val ys = List(10, 20)
val products = for {
x <- xs
y <- ys
} yield x * y
// 带变量绑定
val computed = for {
x <- List(1, 2, 3)
y = x * 2
z <- List(y, y + 1)
} yield z
// 并行赋值
val pairs = for {
(x, y) <- List((1, 2), (3, 4), (5, 6))
} yield x + y
// For循环(副作用)
for {
i <- 1 to 5
j <- 1 to 5
} {
print(s"($i,$j) ")
}
// 使用for-comprehension读取文件
import scala.io.Source
def readLines(filename: String): List[String] = {
val source = Source.fromFile(filename)
try {
source.getLines().toList
} finally {
source.close()
}
}
// 复杂数据变换
case class Order(id: Int, userId: Int, total: Double)
case class User(id: Int, name: String)
val users = List(User(1, "Alice"), User(2, "Bob"))
val orders = List(Order(1, 1, 100), Order(2, 1, 150), Order(3, 2, 200))
val userTotals = for {
user <- users
userOrders = orders.filter(_.userId == user.id)
total = userOrders.map(_.total).sum
} yield (user.name, total)
For-comprehensions使复杂的集合操作可读且可维护,特别是多嵌套操作。
惰性求值和视图
惰性求值推迟计算直到需要结果,提高了大数据集和无限序列的性能。
// 视图:惰性集合变换
val numbers = (1 to 1000000).toList
// 急切求值(创建中间列表)
val eager = numbers
.map(_ + 1)
.filter(_ % 2 == 0)
.map(_ * 2)
.take(10)
// 使用视图的惰性求值(无中间集合)
val lazy = numbers.view
.map(_ + 1)
.filter(_ % 2 == 0)
.map(_ * 2)
.take(10)
.toList
// LazyList(原Stream)
val infiniteNums = LazyList.from(1)
val first10 = infiniteNums.take(10).toList
// 使用LazyList的斐波那契
def fibonacci: LazyList[BigInt] = {
def fib(a: BigInt, b: BigInt): LazyList[BigInt] =
a #:: fib(b, a + b)
fib(0, 1)
}
val fibs = fibonacci.take(20).toList
// 使用LazyList的质数
def sieve(nums: LazyList[Int]): LazyList[Int] =
nums.head #:: sieve(nums.tail.filter(_ % nums.head != 0))
val primes = sieve(LazyList.from(2))
val first20Primes = primes.take(20).toList
// Iterator:一次性惰性遍历
val iterator = Iterator(1, 2, 3, 4, 5)
val doubled = iterator.map(_ * 2)
// 只能遍历一次
println(doubled.toList)
// println(doubled.toList) // 空 - 已消耗
// 用于大变换的视图
val largeList = (1 to 1000000).toList
val result = largeList.view
.filter(_ % 2 == 0)
.map(x => x * x)
.filter(_ % 3 == 0)
.take(100)
.toList
// 结合急切和惰性
val mixed = numbers.view
.map(_ * 2)
.filter(_ > 100)
.force // 强制求值,返回严格集合
// 带Options的惰性求值
def expensiveComputation(x: Int): Int = {
println(s"Computing for $x")
x * 2
}
lazy val lazyValue = expensiveComputation(5)
// 尚未计算
println("Before access")
println(lazyValue) // 现在计算
println(lazyValue) // 缓存,不重新计算
// 性能比较
def timeIt[T](block: => T): (T, Long) = {
val start = System.nanoTime()
val result = block
val elapsed = (System.nanoTime() - start) / 1000000
(result, elapsed)
}
val data = (1 to 10000000).toList
val (eagerResult, eagerTime) = timeIt {
data
.map(_ + 1)
.filter(_ % 2 == 0)
.map(_ * 2)
.take(10)
}
val (lazyResult, lazyTime) = timeIt {
data.view
.map(_ + 1)
.filter(_ % 2 == 0)
.map(_ * 2)
.take(10)
.toList
}
println(s"Eager time: ${eagerTime}ms")
println(s"Lazy time: ${lazyTime}ms")
使用视图在大型集合上链接多个变换,以避免中间集合创建。
并行集合
并行集合自动将操作分布在多个线程上,在多核系统上提高性能。
import scala.collection.parallel.CollectionConverters._
// 转换为并行集合
val numbers = (1 to 1000000).toList
val parallelNumbers = numbers.par
// 并行操作
val sum = parallelNumbers.sum
val doubled = parallelNumbers.map(_ * 2)
val filtered = parallelNumbers.filter(_ % 2 == 0)
// 并行fold(仅结合性操作)
val total = parallelNumbers.fold(0)(_ + _)
// Aggregate:比fold更灵活
val result = parallelNumbers.aggregate(0)(
(acc, x) => acc + x, // 顺序操作
(acc1, acc2) => acc1 + acc2 // 并行组合
)
// 任务支持以控制并行性
import scala.collection.parallel.ForkJoinTaskSupport
import java.util.concurrent.ForkJoinPool
val customParallel = numbers.par
customParallel.tasksupport = new ForkJoinTaskSupport(new ForkJoinPool(4))
// 性能比较
def benchmark[T](name: String)(block: => T): T = {
val start = System.nanoTime()
val result = block
val elapsed = (System.nanoTime() - start) / 1000000
println(s"$name: ${elapsed}ms")
result
}
val data = (1 to 10000000).toList
benchmark("Sequential") {
data.map(x => x * x).filter(_ % 2 == 0).sum
}
benchmark("Parallel") {
data.par.map(x => x * x).filter(_ % 2 == 0).sum
}
// 何时使用并行集合:
// - 大数据集(> 10,000元素)
// - CPU密集型操作
// - 结合性和交换性操作
// - 多核可用
// 何时避免:
// - 小数据集(开销 > 收益)
// - I/O操作(非CPU绑定)
// - 非结合性操作
// - 顺序依赖操作
// 并行集合分组
val grouped = parallelNumbers.groupBy(_ % 10)
// 并行中的副作用(不安全)
var counter = 0
// parallelNumbers.foreach(x => counter += 1) // 竞态条件
// 安全累加
val counts = parallelNumbers.aggregate(0)(
(count, _) => count + 1,
_ + _
)
对多核系统上的大型数据集CPU密集型操作使用并行集合。
最佳实践
-
默认优先不可变集合以确保线程安全性和函数式编程优势
-
根据访问模式选择正确的集合类型:List用于顺序,Vector用于随机访问
-
使用for-comprehensions处理带多生成器和过滤器的复杂变换
-
对大型变换应用视图以避免创建中间集合
-
利用groupBy和partition分类数据,而不是手动过滤
-
仅对大型CPU密集型操作在多核系统上使用并行集合
-
避免在惰性集合上调用size因为它强制求值整个序列
-
优先foldLeft而不是可变累加以函数式聚合值
-
使用Option而不是null处理可能缺失的集合元素
-
在所有集合类型上应用一致的变换模式以保持代码可维护
常见陷阱
-
使用List进行随机访问导致O(n)性能而不是Vector的O(1)
-
忘记转换视图回严格集合留下每次访问计算的惰性集合
-
在并行中突变集合导致竞态条件和非确定性结果
-
在reduce操作中未处理空集合导致运行时异常
-
使用var与不可变集合破坏不可变性目的
-
在空集合上调用head抛出异常而不是使用headOption
-
在fold中低效字符串连接应使用StringBuilder或mkString
-
未考虑内存与保留引用的大惰性序列
-
过度使用并行集合在小数据集上增加开销而无收益
-
混合可变和不可变集合导致意外突变和错误
何时使用此技能
在Scala开发中应用集合操作进行数据变换和处理。
使用不可变集合于并发应用和公共API以确保线程安全。
利用for-comprehensions处理多序列或嵌套结构。
应用带视图的惰性求值于大数据集或多变换链。
对多核系统上的大型数据集CPU密集型操作使用并行集合。
基于特定访问模式和性能需求选择专业集合类型(Set、Map、Vector)。