Skip to content

Latest commit

 

History

History
689 lines (464 loc) · 27.6 KB

ch04.md

File metadata and controls

689 lines (464 loc) · 27.6 KB
@ def stdDev(a: Array[Double]): Double = {
    val mean = a.sum / a.length
    val squareErrors = a.map(x => x - mean).map(x => x * x)
    math.sqrt(squareErrors.sum / a.length)
  }

代码片段 4.1:使用Scala集合操作计算数组的标准差

Scala标准库的核心是它的集合:所有Scala程序都共享的一组通用容器和数据结构。Scala的集合使你可以方便地操作数组、链表、集合、映射和其他数据结构,内置了许多实现应用程序所需的数据结构。

本章将逐步讨论适用于所有集合类型的常见操作,然后再讨论这些数据结构,以及在实践中可能会在什么场景中使用它们。

4.1 操作(Operations)

Scala集合提供了许多常见的操作,用于构造、查询或转换集合。这些操作存在于我们在上一章Scala基础中看到的数组中,但它们也适用于本章将涉及的所有集合:向量(Vectors)(4.2.2)、集合(Sets)(4.2.4)、映射(Maps)(4.2.5)等。

4.1.1 构造器(Builders)

@ val b = Array.newBuilder[Int]
b: mutable.ArrayBuilder[Int] = ArrayBuilder.ofInt

@ b += 1

@ b += 2

@ b.result()
res158: Array[Int] = Array(1, 2)

构造器可以让你高效地构造一个未知长度的集合,在最后将其 "冻结 "成你想要的集合。这对于构造Arrays或不可变的集合最有用,因为一旦构造好了集合,你就无法添加或删除元素。

4.1.2 工厂方法(Factory Methods)

@ Array.fill(5)("hello") // 数组中"hello"重复5次
res159: Array[String] = Array("hello", "hello", "hello", "hello", "hello")

@ Array.tabulate(5)(n => s"hello $n") // 数组由5个项目组成,每个项目由其索引计算得出
res160: Array[String] = Array("hello 0", "hello 1", "hello 2", "hello 3", "hello 4")

@ Array(1, 2, 3) ++ Array(4, 5, 6) // 将两个数组串联成一个大的数组
res168: Array[Int] = Array(1, 2, 3, 4, 5, 6)

4.1.3 转换(Transforms)

@ Array(1, 2, 3, 4, 5).map(i => i * 2) // 每个元素乘以2
res162: Array[Int] = Array(2, 4, 6, 8, 10)

@ Array(1, 2, 3, 4, 5).filter(i => i % 2 == 1) // 只保留不能被2整除的元素
res163: Array[Int] = Array(1, 3, 5)

@ Array(1, 2, 3, 4, 5).take(2) // 保留前两个个元素
res164: Array[Int] = Array(1, 2)

@ Array(1, 2, 3, 4, 5).drop(2) // 舍弃前两个元素
res165: Array[Int] = Array(3, 4, 5)

@ Array(1, 2, 3, 4, 5).slice(1, 4) // 保留索引在1到4的元素
res166: Array[Int] = Array(2, 3, 4)

@ Array(1, 2, 3, 4, 5, 4, 3, 2, 1, 2, 3, 4, 5, 6, 7, 8).distinct // 移出所有重复的
res22: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8)

变换一个现有的集合,并以某种方式创建一个新的集合时。请注意,这些变换创建的是集合的副本,而不改变原来的内容。这意味着如果你仍在使用原始数组,其内容不会被变换所修改。

@ val a = Array(1, 2, 3, 4, 5)
a: Array[Int] = Array(1, 2, 3, 4, 5)

@ val a2 = a.map(x => x + 10)
a2: Array[Int] = Array(11, 12, 13, 14, 15)

@ a(0)
res2: Int = 1

@ a2(0)
res3: Int = 11

在这些集合转换中涉及的复制确实有一些开销,但在大多数情况下不会引起问题。如果某段代码确实是导致程序运行缓慢的瓶颈,你可以随时替换.map,.filter等,将代码转换为对原始数组的转换运算,以优化性能,或使用可变集合和就地操作(4.3.4)

4.1.4 查询(Queries)

返回一个包含与给定谓词函数匹配的值的选项(如果没有找到值,则为None):

@ Array(1, 2, 3, 4, 5, 6, 7).find(i => i % 2 == 0 && i > 4)
res1: Option[Int] = Some(6)

@ Array(1, 2, 3, 4, 5, 6, 7).find(i => i % 2 == 0 && i > 10)
res2: Option[Int] = None

@ Array(1, 2, 3, 4, 5, 6, 7).exists(x => x > 1) // 有大于1的元素吗?
res10: Boolean = true

@ Array(1, 2, 3, 4, 5, 6, 7).exists(_ < 0) // 同样的是否存在(x => x < 0)
res11: Boolean = false

查询可以让你在没有集合的情况下搜索元素,返回一个布尔值,表示是否存在匹配的元素,或者返回一个包含找到元素的Option。这可以让你方便地在你的集合中找到东西,而不需要写for循环来逐一检查元素。

4.1.5 聚合(Aggregations)

4.1.5.1 左折叠(foldLeft)

获取一个初始值和一个函数,该函数用于将集合中的每个元素与初始值组合起来,以生成最终结果

@ Array(1, 2, 3, 4, 5, 6, 7).foldLeft(0)((x, y) => x + y) // 所有元素的和
res19: Int = 28

@ Array(1, 2, 3, 4, 5, 6, 7).foldLeft(1)((x, y) => x * y) // 所有元素的乘积
res20: Int = 5040

@ Array(1, 2, 3, 4, 5, 6, 7).foldLeft(1)(_ * _) // 同上,是简写语法
res21: Int = 5040

一般来说,foldLeft类似于for循环和累加器的变种,上面的对所有元素求和的foldLeft调用可以等价地写成:

@ {
  var total = 0
  for (i <- Array(1, 2, 3, 4, 5, 6, 7)) total += i
  total
  }
total: Int = 28

4.1.5.2 分组(groupBy)

根据一个key将你的集合分组,然后映射到一个更小的集合中:

@ val grouped = Array(1, 2, 3, 4, 5, 6, 7).groupBy(_ % 2)
grouped: Map[Int, Array[Int]] = Map(0 -> Array(2, 4, 6), 1 -> Array(1, 3, 5, 7))

@ grouped(0)
res24: Array[Int] = Array(2, 4, 6)

@ grouped(1)
res25: Array[Int] = Array(1, 3, 5, 7)

4.1.6 合并操作(Combining Operations)

通常会将多个操作链在一起来实现你想要的。例如,以下函数可计算数字数组的标准差:

@ def stdDev(a: Array[Double]): Double = {
    val mean = a.foldLeft(0.0)(_ + _) / a.length
    val squareErrors = a.map(_ - mean).map(x => x * x)
    math.sqrt(squareErrors.foldLeft(0.0)(_ + _) / a.length)
  }

@ stdDev(Array(1, 2, 3, 4, 5))
res11: Double = 1.4142135623730951

@ stdDev(Array(3, 3, 3))
res12: Double = 0.0

Scala集合提供了一个非常方便的辅助方法.sum,相当于.foldLeft(0.0)(_ + _),因此上面的代码可以简化为:

@ def stdDev(a: Array[Double]): Double = {
    val mean = a.sum / a.length
    val squareErrors = a.map(_ - mean).map(x => x * x)
    math.sqrt(squareErrors.sum / a.length)
  }

再举一个例子,使用函数.exists.map.distinct检查输入的数字网格是否是有效数独网格:

@ def isValidSudoku(grid: Array[Array[Int]]): Boolean = {
    !Range(0, 9).exists{i =>
      val row = Range(0, 9).map(grid(i)(_))
      val col = Range(0, 9).map(grid(_)(i))
      val square = Range(0, 9).map(j => grid((i % 3) * 3 + j % 3)((i / 3) * 3 + j / 3))
      row.distinct.length != row.length ||
      col.distinct.length != col.length ||
      square.distinct.length != square.length
    }
  }

本实现接收一个数独网格,用二维Array[Array[Int]]表示。对于每一个从0到9的i,我们挑选出一个单独的行、列和3x3块。通过调用.deparate方法来检查每个这样的行/列/块是否有9个唯一的数字,并且去除重复的数字。然后通过.length方法检测数独网格是否因删除而改变。

我们可以在一些示例网格上对此进行测试以验证其是否有效:

@ isValidSudoku(Array(
    Array(5, 3, 4, 6, 7, 8, 9, 1, 2),
    Array(6, 7, 2, 1, 9, 5, 3, 4, 8),
    Array(1, 9, 8, 3, 4, 2, 5, 6, 7),
    Array(8, 5, 9, 7, 6, 1, 4, 2, 3),
    Array(4, 2, 6, 8, 5, 3, 7, 9, 1),
    Array(7, 1, 3, 9, 2, 4, 8, 5, 6),
    Array(9, 6, 1, 5, 3, 7, 2, 8, 4),
    Array(2, 8, 7, 4, 1, 9, 6, 3, 5),
    Array(3, 4, 5, 2, 8, 6, 1, 7, 9)
  ))
res24: Boolean = true
@ isValidSudoku(Array(
    Array(5, 3, 4, 6, 7, 8, 9, 1, 2),
    Array(6, 7, 2, 1, 9, 5, 3, 4, 8),
    Array(1, 9, 8, 3, 4, 2, 5, 6, 7),
    Array(8, 5, 9, 7, 6, 1, 4, 2, 3),
    Array(4, 2, 6, 8, 5, 3, 7, 9, 1),
    Array(7, 1, 3, 9, 2, 4, 8, 5, 6),
    Array(9, 6, 1, 5, 3, 7, 2, 8, 4),
    Array(2, 8, 7, 4, 1, 9, 6, 3, 5),
    Array(3, 4, 5, 2, 8, 6, 1, 7, 8)
  )) // 右下角的单元格应该是9
res25: Boolean = false

以这样的方式链式集合变换总是会有一些开销,但是对于大多数的使用案例来说,这些开销是值得的,因为这些变换给你带来方便和简单。如果集合变换确实成为了瓶颈,你可以使用Views(4.1.8)、In-Place Operations(4.3.4),或者最后通过对原始数组进行循环。

4.1.7 转换(Converters)

你可以使用.to方法在数组、向量和其他集合之间进行转换:

@ Array(1, 2, 3).to(Vector)
res212: Vector[Int] = Vector(1, 2, 3)

@ Vector(1, 2, 3).to(Array)
res213: Array[Int] = Array(1, 2, 3)

@ Array(1, 1, 2, 2, 3, 4).to(Set)
res214: Set[Int] = Set(1, 2, 3, 4)

元组的集合也可以转换为Maps,反之亦然:

@ Array((1, "one"), (2, "two"), (3, "three")).to(Map)
res3: Map[Int, String] = Map(1 -> "one", 2 -> "two", 3 -> "three")

@ Map(1 -> "one", 2 -> "two", 3 -> "three").to(Vector)
res4: Vector[(Int, String)] = Vector((1, "one"), (2, "two"), (3, "three"))

4.1.8 视图(Views)

当你将多个转换链接到一个集合上时,我们将创建许多中间集合,这些中间集合将立即被丢弃。例如,在以下片段中:

@ val myArray = Array(1, 2, 3, 4, 5, 6, 7, 8, 9)

@ val myNewArray = myArray.map(x => x + 1).filter(x => x % 2 == 0).slice(1, 3)
myNewArray: Array[Int] = Array(4, 6)

.map .filter .slice操作链最终遍历集合三次,创建三个新集合,但只有最后一个集合最后被存储在myNewArray中,其他的则被丢弃

1

中间集合的创建和遍历是浪费的。在你有长链集合转换的情况下,这些转换将成为性能瓶颈,你可以使用.view.to方法来 "融合 "这些操作:

@ val myNewArray = myArray.view.map(_ + 1).filter(_ % 2 == 0).slice(1, 3).to(Array)
myNewArray: Array[Int] = Array(4, 6)

在map/filter/slice转换操作之前使用.view将实际的遍历和新集合的创建推迟延后,当我们调用.to,将其转换为具体的集合类型。

2

这样,我们就可以只用一次遍历就可以完成这一连串的map/filter/slice变换,并且只创建一个输出集合,减少了不必要的处理和内存分配。

4.2不可变集合

虽然数组是最原始的,但是大多数Scala应用程序都是建立在其可变和不可变的集合之上的: Vectors, Lists, Sets, 和Maps这两种集合中,不可变集合是最常见的。

不可变集合排除了由于意外修改而导致的全部bug,在多线程场景中尤其有用,在这种场景中,您可以安全地在线程之间传递不可变集合,而不必担心线程安全问题。大多数不可变集合使用结构共享(4.2.3)来降低复制更新的成本,可以允许你在所有的代码中使用它们,但最关键的性能代码除外。

4.2.1 不可变向量(Immutable Vectors)

Vectors是固定大小、不可变的线性序列。它们是一种很好的通用序列数据结构,可以为大多数操作提供高效$O\log(n)$性能。

@ val v = Vector(1, 2, 3, 4, 5)
v: Vector[Int] = Vector(1, 2, 3, 4, 5)

@ v(0)
res174: Int = 1

@ val v2 = v.updated(2, 10)
v2: Vector[Int] = Vector(1, 2, 10, 4, 5)

@ v2
res176: Vector[Int] = Vector(1, 2, 10, 4, 5)

@ v // note that `v` did not change when we created `v2` with an updated element!
res177: Vector[Int] = Vector(1, 2, 3, 4, 5)
@ val v = Vector[Int]()
v: Vector[Int] = Vector()

@ val v1 = v :+ 1
v1: Vector[Int] = Vector(1)

@ val v2 = 4 +: v1
v2: Vector[Int] = Vector(4, 1)

@ val v3 = v2.tail
v3: Vector[Int] = Vector(1)

Arrays不同的是,当Arraysa(...)=...时会在原突变,而Vector.update方法会返回一个新的Vector,同时保留旧的Vector不变。由于结构共享(4.2.3),这是一个合理有效的$O\log(n)$操作。同样,使用:++:来创建一个新的Vector,并在两边增加元素,或者使用.tail来创建一个新的Vector,并去掉一个元素,也都是$O\log(n)$:

向量支持与数组和其他集合一样的操作(Opertions)(4.1):构建器(builders)(4.1.1)、工厂方法(factory methods)(4.1.2)、变换(transoforms)(4.1.3)等。

一般来说,当你知道一个序列不会改变,但需要灵活处理的时候,使用Vectors是很方便的,它们的树结构使大多数操作相当高效,尽管它们永远也不会像Arrays(就地更新)或Immutable Lists (4.2.5)那样在前面添加和删除元素的速度快。

4.2.2 结构共享(Structural Sharing)

Vectors通过重用树结构的一部分来实现其Olog(n)复制和更新操作。这让Vector避免了复制整个树,从而产生了一个 "新的 "Vector,只需稍作修改就可以共享旧树结构的大部分内容。

考虑一个大的Vectors,v1:

@ val v1 = Vector(1, 2, 0, 9,  7, 2, 9, 6,  ...,  3, 2, 5, 5,  4, 8, 4, 6)

这在内存中表现为一个树状结构,其广度和深度取决于Vectors的大小。

3

这个例子稍微简化了一些 -Scala中的一个向量在每个树节点上有32个元素,而不是这里显示的4个元素- 但是它可以很好地说明向量数据结构是如何工作的。

如果我们要执行更新-在这里用值8替换上面向量中的第五个值7:

@ val v2 = v1.updated(4, 8)

@ v2
res1: Vector[Int] = Vector(1, 2, 0, 9, 8, 2, 9, 6, ..., 3, 2, 5, 5, 4, 8, 4, 6)

这样做是通过对树上的节点进行更新,将树上直接路径中的节点复制到我们希望更新的值,但重新使用其他所有的节点不变。

4

在这个有9个节点的Vector示例中,只有3个节点最终需要被复制。在一个大的Vector中,需要复制的节点数量与树的高度成正比,而其他的节点可以重复使用:这种结构共享使得更新的Vector副本只需要Olog(n)时间就可以创建。这比对一个可变数组或其他数据结构进行完整复制所需的O(n)时间要少得多。

然而,更新Vecto总是会涉及到一定量的复制,而且永远不会像就地更新可变数据结构那样快。在某些情况下,如果性能很重要,而且你要非常频繁地更新一个集合,你可以考虑使用可变的ArrayDeque(4.3.1),它有更快的O(1)​ update,append,prepend操作,如果你事先知道你的集合的大小,你可以考虑使用原始Arrays

d类似的树形数据结构也被用于实现Immutable Sets(4.2.3)和Immutable Maps(4.2.4)。

4.2.3 不可变集(Immutable Sets)

Scala 的 immutable Sets 是没有重复的元素的无序集合,提供了一个高效的 O(log n) .contains 方法。Sets可以通过+来构造,元素可以通过-来移除,也可以通过++来组合。注意,重复的元素会被丢弃。

@ val s = Set(1, 2, 3)
s: Set[Int] = Set(1, 2, 3)

@ s.contains(2)
res208: Boolean = true

@ s.contains(4)
res209: Boolean = false
@ Set(1, 2, 3) + 4 + 5
res216: Set[Int] = HashSet(5, 1, 2, 3, 4)

@ Set(1, 2, 3) - 2
res217: Set[Int] = Set(1, 3)

@ Set(1, 2, 3) ++ Set(2, 3, 4) // 请注意,重复的内容被丢弃
res218: Set[Int] = Set(1, 2, 3, 4)

当你想确保一个集合中的项不包含任何重复的项时,Set中的项的唯一性有时也很有用。

你可以使用for循环遍历Sets,但项目的顺序是未定义的,因此不应该依赖:

@ for (i <- Set(1, 2, 3, 4, 5)) println(i)
5
1
2
3
4

大多数不可变的集合操作来改变集合的大小的时间复杂度为O(logn)。对于大多数情况来说,这已经足够快了。但是在不快的情况下,你总是可以使用可变 Sets (4.3.2)来获得更好的性能。Sets也支持所有集合通用的标准操作。

4.2.4 不可变映射(Immutable Maps)

不可变映射(Immutable maps)的键和值的无序集合,允许通过键进行有效的查找。

@ val m = Map("one" -> 1, "two" -> 2, "three" -> 3)
m: Map[String, Int] = Map("one" -> 1, "two" -> 2, "three" -> 3)

@ m.contains("two")
res218: Boolean = true

@ m("two")
res219: Int = 2

@ m.contains("four")
res220: Boolean = false

假如你不确定一个map是否包含一个key ,可以使用.get方法。如果存在key,则返回Some(v),如果不存在,则返回None

@ val m = Map("one" -> 1, "two" -> 2, "three" -> 3)
m: Map[String, Int] = Map("one" -> 1, "two" -> 2, "three" -> 3)

@ m.get("one")
res27: Option[Int] = Some(1)

@ m.get("four")
res28: Option[Int] = None

虽然映射支持与其他集合相同的操作集,但它们被视为表示每个键-值对的元组集合。因此,操作.to需要一个元组集合来进行转换,+操作将元组作为键-值对添加到映射中,并且for循环遍历元组:

@ Vector(("one", 1), ("two", 2), ("three", 3)).to(Map)
res224: Map[String, Int] = Map("one" -> 1, "two" -> 2, "three" -> 3)

@ Map[String, Int]() + ("one" -> 1) + ("three" -> 3)
res225: Map[String, Int] = Map("one" -> 1, "three" -> 3)

@ for ((k, v) <- m) println(k + " " + v)
one 1
two 2
three 3

Sets一样,在Map上迭代时,元素的顺序是没有定义的,并且不能被依赖。与不可变的Sets一样,大多数不可变Map操作的时间复杂度为O(logn)​,n代表Map的大小

4.2.5 不可变列表(Immutable Lists)

@ val myList = List(1, 2, 3, 4, 5)
myList: List[Int] = List(1, 2, 3, 4, 5)

@ myList.head
res2: Int = 1

@ val myTail = myList.tail
myTail: List[Int] = List(2, 3, 4, 5)

@ val myOtherList = 0 :: myList
myOtherList: List[Int] = List(0, 1, 2, 3, 4, 5)

@ val myThirdList = -1 :: myList
myThirdList: List[Int] = List(-1, 1, 2, 3, 4, 5)

Scala 的 不可变列表(immutable Lists) 是一种单链式的列表数据结构,由节点组成,每个节点都有一个值和指向列表中的下一个节点的指针,以 Nil 节点为终点。Lists有一个快速的O(1) .head方法来查找列表中的第一个项目,一个快速的$O(1)$.tail方法来创建一个没有第一个元素的列表,一个快速的$O(1)$::操作符来创建一个新的List而在Lsit前面多加一个元素。

.tail::是高效的,因为它们可以共享现有List的大部分内容。.tail返回一个引用到单链结构中的下一个节点,而 :: 则在前面增加一个新的节点。事实上,多个列表可以共享节点,这意味着在上面的例子中,myListmyTailmyOtherListmyThirdList实际上大多是同一个列表。

5

如果你有大量的集合,而这些集合的一侧有相同的元素,比如说文件系统中的路径都有相同的前缀,那么这可以节省大量的内存。相比之下,创建一个带附加元素的Array需要$O(n)$时间和内存来复制整个集合,而创建一个带附加元素的Vector则需要O(log n)复制树节点。

Lists的缺点是,通过myList(i)进行索引查找是一个缓慢的O(n)操作,因为你需要从左侧开始遍历整个列表来找到你想要的元素。在列表右侧添加或则删除元素也是一个缓慢的$O(n)$操作,因为它需要对整个列表进行复制。对于需要快速索引查找或在右侧快速追加或删除元素的使用情况,你应该考虑使用 Vectors(4.2.2) 或可变的 ArrayDeques(4.3.1) 来代替。

4.3可变集合(Mutable Collections)

当用于就地操作时,可变集合通常比不可变集合快。然而,可变性是有代价的:您需要更小心地在程序的不同部分之间共享它们。在共享可变集合意外更新的地方很容易产生bug,迫使你去寻找大量代码中哪一行正在执行不需要的更新。

一种常见的方法是在存在性能瓶颈的函数或类的私有函数中局部使用可变集合,但是在其他地方使用不可变集合时,速度就不那么重要了。这让你在最重要的地方实现了可变集合的高性能,同时又不会牺牲不可变集合在整个应用逻辑中的安全性。

4.3.1可变数组双端队列( Mutable ArrayDeques)

ArrayDeques是一种通用的、可变的、线性的集合,它在左右两端提供高效的 O(1) 索引查找、O(1)索引更新和O(1)插入和删除:

@ val myArrayDeque = collection.mutable.ArrayDeque(1, 2, 3, 4, 5)
myArrayDeque: collection.mutable.ArrayDeque[Int] = ArrayDeque(1, 2, 3, 4, 5)

@ myArrayDeque.removeHead()
res1: Int = 1

@ myArrayDeque.append(6)
res2: collection.mutable.ArrayDeque[Int] = ArrayDeque(2, 3, 4, 5, 6)

@ myArrayDeque.removeHead()
res3: Int = 2

@ myArrayDeque
res4: collection.mutable.ArrayDeque[Int] = ArrayDeque(3, 4, 5, 6)

ArrayDeques是以循环缓冲区的形式实现的,在缓冲区内有指向集合的逻辑起点和终点的指针。上面的操作从左至右可以直观地表示为:

6

一个ArrayDeque试图尽可能地重用相同的底层Array,只在元素被添加或删除时,移动起始和结束指针。只有当元素的总数超过了当前的容量时,底层Array才会被重新分配,并且大小会以固定的倍数增加,以使这种重新分配的平摊代价较小。

因此,ArrayDeque上的操作比不可变Vector上的等效操作要快得多,后者必须为执行的每个操作分配 O(log n) 个新树节点。

ArrayDeques有标准的操作套件(4.1)。它们可以发挥多种作用。

  • 一个可以增长的数组:普通的Array.newBuilder不允许在建立数组时进行索引查找或修改,并且最终的Array不允许添加更多元素
  • 如果你发现使用:+/+:.tail/.init从两端添加/删除项是你的代码中的瓶颈,那么ArrayDeques一个更快的、可变的Vectors替代品。在ArrayDeques中添加和预添加的速度要比等效的Vector操作快得多。
  • 一个先入先出的队列,通过.append向右插入项,然后通过.deleteHead删除项。
  • 一个先入后出堆栈,通过.append向右插入项,然后通过.removeLast删除项。

如果你想把一个可变的 ArryDeque "冻结"成一个不可变的Vector,你可以使用.to(Vector)

@ myArrayDeque.to(Vector)
res250: Vector[Int] = Vector(1, -10, 3, 4, 5)

注意,这将复制整个集合。

ArrayDeques实现了抽象的collection.mutable.Buffer接口,也可以通过collection.mutable.Buffer(...)语法构造。

4.3.2可变集(Mutable Sets)

Scala标准库提供了 Mutable Sets,作为我们前面看到的不可变的Sets的对应。可变Sets也提供了高效的.contains检查(O(1)),但是mutable Sets不是通过+和-构造Set的新副本,而是通过.add.remove从Set中添加和删除元素。

@ val s = collection.mutable.Set(1, 2, 3)
s: mutable.Set[Int] = HashSet(1, 2, 3)

@ s.contains(2)
res248: Boolean = true

@ s.contains(4)
res249: Boolean = false
@ s.add(4)

@ s.remove(1)

@ s
res256: mutable.Set[Int] = HashSet(2, 3, 4)

你可以通过使用.to(Set)将一个可变的Set "冻结 "成一个不可变的Set,这使得一个副本不能使用.add.remove进行更改,并以相同的方式将其转换回一个可变Set。注意,每一次这样的转换都是对整个Set进行复制。

4.3.3 可变映射(Mutable Maps)

可变映射与不可变映射一样,但也允许你修改映射以添加或删除键-值对:

@ val m = collection.mutable.Map("one" -> 1, "two" -> 2, "three" -> 3)
m: mutable.Map[String, Int] = HashMap("two" -> 2, "three" -> 3, "one" -> 1)

@ m.remove("two")
res263: Option[Int] = Some(2)

@ m("five") = 5

@ m
res265: mutable.Map[String, Int] = HashMap("five" -> 5, "three" -> 3, "one" -> 1)

可变 Maps 有一个便利的getOrElseUpdate函数,如果没有这个值的话,它允许你通过键查找一个值,并计算和存储该值

@ val m = collection.mutable.Map("one" -> 1, "two" -> 2, "three" -> 3)

@ m.getOrElseUpdate("three", -1) // 已存在,返回现有值
res31: Int = 3

@ m // `m` is unchanged
res32: mutable.Map[String, Int] = HashMap("two" -> 2, "three" -> 3, "one" -> 1)

@ m.getOrElseUpdate("four", -1) // 不存在,在map中存储新值并返回
res33: Int = -1

@ m // `m` now contains "four" -> -1
res34: mutable.Map[String, Int] = HashMap(
  "two" -> 2,
  "three" -> 3,
  "four" -> -1,
  "one" -> 1
)

.getOrElseUpdate使使用一个可变映射作为缓存变得很方便:.getOrElseUpdate的第二个参数是一个惰性的by-name参数,因此只有在映射中没有找到键时才进行计算。这提供了常见的“检查键是否存在,如果存在则返回值,否则插入新值并返回”开箱即用的工作流。

可变映射是哈希表实现的,有m(…)查找和m(…)=…更新是高效的 O(1) 操作。

4.3.4 就地操作(In-Place Operations)

所有可变集合,包括Arrays都具有许多常见集合操作的就地版本。这些允许你在可变集合上执行操作,而不需要进行转换复制:

@ val a = collection.mutable.ArrayDeque(1, 2, 3, 4)
a: mutable.ArrayDeque[Int] = ArrayDeque(1, 2, 3, 4)

@ a.mapInPlace(_ + 1)
res10: mutable.ArrayDeque[Int] = ArrayDeque(2, 3, 4, 5)

@ a.filterInPlace(_ % 2 == 0)
res11: mutable.ArrayDeque[Int] = ArrayDeque(2, 4)

@ a // `a` 被修改了
res12: mutable.ArrayDeque[Int] = ArrayDeque(2, 4)

除了以上所示,还有dropInPlacesliceInPlacesortInPlace等。使用就地操作而不是常规转换可以避免分配新转换的集合的成本,并且可以在性能关键的场景中提供帮助。

4.4通用接口(Common Interfaces)

在很多情况下,一段代码并不关心它到底在处理什么集合。

  • 代码只需要可以顺序迭代的内容,而不管它是Vector还是List。 在这种情况下,您可以采用Seq [T]
@ def iterateOverSomething[T](items: Seq[T]) = {
    for (i <- items) println(i)
  }

@ iterateOverSomething(Vector(1, 2, 3))
1
2
3

@ iterateOverSomething(List(("one", 1), ("two", 2), ("three", 3)))
(one,1)
(two,2)
(three,3)
  • 代码需要提供高效索引查找时并不关心它是Array还是Vector,但是不能使用List。在这种情况下,你的代码可以使用IndexedSeq[T]
@ def getIndexTwoAndFour[T](items: IndexedSeq[T]) = (items(2), items(4))

@ getIndexTwoAndFour(Vector(1, 2, 3, 4, 5))
res288: (Int, Int) = (3, 5)

@ getIndexTwoAndFour(Array(2, 4, 6, 8, 10))
res289: (Int, Int) = (6, 10)

我们目前看到的数据类型的层次结构如下:

7

因此,根据您希望代码能够接受的内容,您可以在层次结构中选择相关类型:IterableIndexedSeqSeqcollection.Seq等等。

通常,大多数代码默认使用不可变集合:SeqsSetsMaps。在collection.mutable包下的可变集合只在必要的时候使用,最好将它们保持在函数内的局部位置或类的私有位置。

所有的Scala集合,无论是可变的还是不可变的,都继承自相同的通用接口:collection.{Seq,Set,Map}。像mapfilter这样的转换,像Foo(...)full、或tabulate这样的构造函数,以及通过.to(...)进行的转换对标准库中的所有集合都是通用的。

4.5结论

在这一章中,我们回顾了Scala标准库的基本集合:Array、不可变的Vector/Set/Map/List和可变的ArrayDeque/Set/Map。我们已经看到了如何构造集合、查询集合、将一个集合转换为另一个集合,以及编写多种集合类型的函数。

由于Scala的集合库被广泛应用于Scala程序中,那么本章为你能够熟练地使用它们打下了一个基础。现在我们将介绍Scala语言的一些比较独特的功能,让你对Scala的入门画上一个圆满的句号。