Scala 3 — 书籍

集合类型

语言

此页面演示了常见的 Scala 3 集合及其随附的方法。Scala 附带了丰富的集合类型,但你只需从几个集合类型开始,然后根据需要再使用其他集合类型,即可走很长的路。类似地,每种集合类型都有几十种方法可以让你更轻松,但你只需从少数几个方法开始即可实现很多功能。

因此,本节将介绍和演示你入门所需的最常见类型和方法。当你需要更大的灵活性时,请参阅本节末尾的这些页面以了解更多详细信息。

三类主要集合

从高层次来看 Scala 集合,有三大类可供选择

  • 序列是元素的顺序集合,可以是索引(如数组)或线性(如链表)
  • 映射包含键/值对的集合,例如 Java Map、Python 字典或 Ruby Hash
  • 集合是唯一元素的无序集合

所有这些都是基本类型,并且具有用于特定目的的子类型,例如并发、缓存和流式处理。除了这三个主要类别之外,还有其他有用的集合类型,包括范围、堆栈和队列。

集合层次结构

作为简要概述,接下来的三幅图显示了 Scala 集合中的类和特征的层次结构。

第一幅图显示了包 scala.collection 中的集合类型。这些都是高级抽象类或特征,通常具有不可变可变实现。

General collection hierarchy

此图显示了包 scala.collection.immutable 中的所有集合

Immutable collection hierarchy

此图显示了包 scala.collection.mutable 中的所有集合

Mutable collection hierarchy

在详细查看了所有集合类型之后,以下部分介绍了一些你将定期使用的常见类型。

常见集合

你将定期使用的主要集合是

集合类型 不可变 可变 说明
列表   线性(链表)、不可变序列
向量   索引的、不可变序列
惰性列表   惰性不可变链表,其元素仅在需要时计算;适用于大型或无限序列。
数组缓冲区   可变、索引序列的常用类型
列表缓冲区   当你想要可变 列表 时使用;通常转换为 列表
映射 由键值对组成的可迭代集合。
集合 没有重复元素的可迭代集合

如所示,映射集合 都具有不可变和可变版本。

每种类型的基础知识将在以下部分中演示。

在 Scala 中,缓冲区(例如 数组缓冲区列表缓冲区)是可以增长和收缩的序列。

关于不可变集合的说明

在以下部分中,每当使用单词不可变时,可以安全地假设该类型旨在用于函数式编程 (FP) 样式。使用这些类型时,您不会修改集合;而是将函数方法应用于集合以创建新的结果。

选择序列

在选择序列(元素的顺序集合)时,您有两个主要决定

  • 序列应该是索引的(如数组),允许快速访问任何元素,还是应该实现为线性链表?
  • 您想要可变集合还是不可变集合?

针对可变/不可变和索引/线性组合的推荐通用“转到”顺序集合在此处显示

类型/类别 不可变 可变
索引 向量 数组缓冲区
线性(链表) 列表 列表缓冲区

例如,如果您需要不可变的索引集合,一般情况下您应该使用 Vector。相反,如果您需要可变的索引集合,请使用 ArrayBuffer

ListVector 通常在以函数式编写代码时使用。 ArrayBuffer 通常在以命令式编写代码时使用。 ListBuffer 在您混合样式(例如构建列表)时使用。

接下来的几个部分将简要演示 ListVectorArrayBuffer 类型。

列表

List 类型 是一个线性的不可变序列。这仅仅意味着它是一个您无法修改的链表。任何时候您想要添加或删除 List 元素时,您都会从现有的 List 创建一个新的 List

创建列表

以下是创建初始 List 的方法

val ints = List(1, 2, 3)
val names = List("Joel", "Chris", "Ed")

// another way to construct a List
val namesAgain = "Joel" :: "Chris" :: "Ed" :: Nil

如果您愿意,您还可以声明 List 的类型,尽管通常没有必要

val ints: List[Int] = List(1, 2, 3)
val names: List[String] = List("Joel", "Chris", "Ed")

一个例外是当您在集合中混合类型时;在这种情况下,您可能希望明确指定其类型

val things: List[Any] = List(1, "two", 3.0)
val things: List[String | Int | Double] = List(1, "two", 3.0) // with union types
val thingsAny: List[Any] = List(1, "two", 3.0)                // with any

向列表中添加元素

由于 List 是不可变的,因此您无法向其中添加新元素。相反,您可以通过将元素前置或追加到现有的 List 来创建新列表。例如,给定此 List

val a = List(1, 2, 3)

在使用 List 时,使用 :: 前置一个元素,并使用 ::: 前置另一个 List,如下所示

val b = 0 :: a              // List(0, 1, 2, 3)
val c = List(-1, 0) ::: a   // List(-1, 0, 1, 2, 3)

您还可以向 List 追加元素,但由于 List 是单链表,因此通常只应前置元素;向其追加元素是一个相对较慢的操作,尤其是在您处理大型序列时。

提示:如果你想在不可变序列前添加和追加元素,请改用 Vector

由于 List 是一个链表,你不应该尝试通过它们的索引值来访问大型列表的元素。例如,如果你有一个包含一百万个元素的 List,那么访问像 myList(999_999) 这样的元素将需要相对较长的时间,因为该请求必须遍历所有这些元素。如果你有一个大型集合并希望通过它们的索引来访问元素,请改用 VectorArrayBuffer

如何记住方法名称

如今,IDE 为我们提供了极大的帮助,但记住这些方法名称的一种方法是认为 : 字符表示序列所在的侧,因此当你使用 +: 时,你知道列表需要在右侧,如下所示

0 +: a

类似地,当你使用 :+ 时,你知道列表需要在左侧

a :+ 4

有更多技术性的方法来思考这个问题,但这可能是一种记住方法名称的有用方法。

此外,这些符号方法名称的一大优点是它们是一致的。相同的名称用于其他不可变序列,例如 SeqVector。如果你愿意,还可以使用非符号方法名称来追加和前置元素。

如何遍历列表

给定一个名称 List

val names = List("Joel", "Chris", "Ed")

你可以像这样打印每个字符串

for (name <- names) println(name)
for name <- names do println(name)

在 REPL 中的显示效果如下

scala> for (name <- names) println(name)
Joel
Chris
Ed
scala> for name <- names do println(name)
Joel
Chris
Ed

for 循环与集合一起使用的一大好处是 Scala 是一致的,并且相同的方法适用于所有序列,包括 ArrayArrayBufferListSeqVectorMapSet 等。

一点历史

对于那些对历史有点兴趣的人来说,Scala List 类似于 Lisp 编程语言 中的 List,该语言最初于 1958 年指定。事实上,除了像这样创建一个 List

val ints = List(1, 2, 3)

您还可以通过这种方式创建完全相同的列表

val list = 1 :: 2 :: 3 :: Nil

REPL 展示了它的工作原理

scala> val list = 1 :: 2 :: 3 :: Nil
list: List[Int] = List(1, 2, 3)

之所以能这样做,是因为 List 是一个以 Nil 元素结尾的单链表,而 :: 是一个 List 方法,其工作方式类似于 Lisp 的“cons”运算符。

旁注:LazyList

Scala 集合还包括一个 LazyList,它是一个惰性不可变链表。它被称为“惰性”——或非严格——因为它仅在需要时才计算其元素。

您可以在 REPL 中看到 LazyList 有多惰性

val x = LazyList.range(1, Int.MaxValue)
x.take(1)      // LazyList(<not computed>)
x.take(5)      // LazyList(<not computed>)
x.map(_ + 1)   // LazyList(<not computed>)

在所有这些示例中,什么都不会发生。事实上,在您强制它发生之前,什么都不会发生,例如通过调用其 foreach 方法

scala> x.take(1).foreach(println)
1

有关严格和非严格(惰性)集合的用途、优点和缺点的更多信息,请参阅 Scala 2.13 集合的架构 页面上的“严格”和“非严格”讨论。

向量

Vector 是一个索引的不可变序列。描述中的“索引”部分意味着它提供随机访问并在有效恒定时间内更新,因此您可以通过其索引值快速访问 Vector 元素,例如访问 listOfPeople(123_456_789)

一般来说,除了 (a) Vector 是索引的而 List 不是,以及 (b) List 具有 :: 方法之外,这两种类型的工作方式相同,因此我们将快速浏览以下示例。

以下是一些创建 Vector 的方法

val nums = Vector(1, 2, 3, 4, 5)

val strings = Vector("one", "two")

case class Person(name: String)
val people = Vector(
  Person("Bert"),
  Person("Ernie"),
  Person("Grover")
)

由于 Vector 是不可变的,因此您无法向其中添加新元素。相反,您可以通过将元素附加或前置到现有 Vector 来创建新序列。以下示例展示了如何将元素附加Vector

val a = Vector(1,2,3)         // Vector(1, 2, 3)
val b = a :+ 4                // Vector(1, 2, 3, 4)
val c = a ++ Vector(4, 5)     // Vector(1, 2, 3, 4, 5)

这是您前置元素的方式

val a = Vector(1,2,3)         // Vector(1, 2, 3)
val b = 0 +: a                // Vector(0, 1, 2, 3)
val c = Vector(-1, 0) ++: a   // Vector(-1, 0, 1, 2, 3)

除了快速随机访问和更新之外,Vector 还提供了快速的附加和前置时间,因此您可以根据需要使用这些功能。

请参阅 集合性能特征 以了解有关 Vector 和其他集合的性能详细信息。

最后,您在 for 循环中使用 Vector,就像使用 ListArrayBuffer 或任何其他序列一样

scala> val names = Vector("Joel", "Chris", "Ed")
val names: Vector[String] = Vector(Joel, Chris, Ed)

scala> for (name <- names) println(name)
Joel
Chris
Ed
scala> val names = Vector("Joel", "Chris", "Ed")
val names: Vector[String] = Vector(Joel, Chris, Ed)

scala> for name <- names do println(name)
Joel
Chris
Ed

数组缓冲区

当你在 Scala 应用程序中需要一个通用、可变的索引序列时,请使用 ArrayBuffer。它是可变的,因此你可以更改它的元素,还可以调整它的大小。由于它是索引的,因此快速随机访问元素。

创建 ArrayBuffer

要使用 ArrayBuffer,首先导入它

import scala.collection.mutable.ArrayBuffer

如果你需要以一个空的 ArrayBuffer 开始,只需指定它的类型

var strings = ArrayBuffer[String]()
var ints = ArrayBuffer[Int]()
var people = ArrayBuffer[Person]()

如果你知道 ArrayBuffer 最终需要的大致大小,则可以使用初始大小创建它

// ready to hold 100,000 ints
val buf = new ArrayBuffer[Int](100_000)

要使用初始元素创建一个新的 ArrayBuffer,只需指定它的初始元素,就像 ListVector

val nums = ArrayBuffer(1, 2, 3)
val people = ArrayBuffer(
  Person("Bert"),
  Person("Ernie"),
  Person("Grover")
)

向 ArrayBuffer 添加元素

使用 +=++= 方法将新元素追加到 ArrayBuffer。或者,如果你喜欢带有文本名称的方法,你还可以使用 appendappendAllinsertinsertAllprependprependAll

以下是 +=++= 的一些示例

val nums = ArrayBuffer(1, 2, 3)   // ArrayBuffer(1, 2, 3)
nums += 4                         // ArrayBuffer(1, 2, 3, 4)
nums ++= List(5, 6)               // ArrayBuffer(1, 2, 3, 4, 5, 6)

从 ArrayBuffer 中删除元素

ArrayBuffer 是可变的,因此它具有 -=--=clearremove 等方法。这些示例演示了 -=--= 方法

val a = ArrayBuffer.range('a', 'h')   // ArrayBuffer(a, b, c, d, e, f, g)
a -= 'a'                              // ArrayBuffer(b, c, d, e, f, g)
a --= Seq('b', 'c')                   // ArrayBuffer(d, e, f, g)
a --= Set('d', 'e')                   // ArrayBuffer(f, g)

更新 ArrayBuffer 元素

通过重新分配所需的元素或使用 update 方法来更新 ArrayBuffer 中的元素

val a = ArrayBuffer.range(1,5)        // ArrayBuffer(1, 2, 3, 4)
a(2) = 50                             // ArrayBuffer(1, 2, 50, 4)
a.update(0, 10)                       // ArrayBuffer(10, 2, 50, 4)

映射

Map 是一个可迭代集合,由键值对组成。Scala 同时具有可变和不可变的 Map 类型,本节演示如何使用不可变 Map

创建不可变映射

像这样创建一个不可变的 Map

val states = Map(
  "AK" -> "Alaska",
  "AL" -> "Alabama",
  "AZ" -> "Arizona"
)

一旦你有了 Map,你就可以像这样在 for 循环中遍历它的元素

for ((k, v) <- states)  println(s"key: $k, value: $v")
for (k, v) <- states do println(s"key: $k, value: $v")

REPL 展示了它的工作原理

scala> for ((k, v) <- states)  println(s"key: $k, value: $v")
key: AK, value: Alaska
key: AL, value: Alabama
key: AZ, value: Arizona
scala> for (k, v) <- states do println(s"key: $k, value: $v")
key: AK, value: Alaska
key: AL, value: Alabama
key: AZ, value: Arizona

访问 Map 元素

通过在括号中指定所需的键值来访问 map 元素

val ak = states("AK")   // ak: String = Alaska
val al = states("AL")   // al: String = Alabama

在实践中,你还可以使用 keyskeySetkeysIteratorfor 循环和 map 等高阶函数来处理 Map 键和值。

向 Map 中添加元素

使用 +++ 向不可变 map 中添加元素,记住将结果分配给新变量

val a = Map(1 -> "one")    // a: Map(1 -> one)
val b = a + (2 -> "two")   // b: Map(1 -> one, 2 -> two)
val c = b ++ Seq(
  3 -> "three",
  4 -> "four"
)
// c: Map(1 -> one, 2 -> two, 3 -> three, 4 -> four)

从 Map 中删除元素

使用 --- 和要删除的键值从不可变 map 中删除元素,记住将结果分配给新变量

val a = Map(
  1 -> "one",
  2 -> "two",
  3 -> "three",
  4 -> "four"
)

val b = a - 4       // b: Map(1 -> one, 2 -> two, 3 -> three)
val c = a - 4 - 3   // c: Map(1 -> one, 2 -> two)

更新 Map 元素

要更新不可变 map 中的元素,请使用 updated 方法(或 + 运算符),同时将结果分配给新变量

val a = Map(
  1 -> "one",
  2 -> "two",
  3 -> "three"
)

val b = a.updated(3, "THREE!")   // b: Map(1 -> one, 2 -> two, 3 -> THREE!)
val c = a + (2 -> "TWO...")      // c: Map(1 -> one, 2 -> TWO..., 3 -> three)

遍历 Map

如前所示,这是使用 for 循环手动遍历 map 中元素的常用方法

val states = Map(
  "AK" -> "Alaska",
  "AL" -> "Alabama",
  "AZ" -> "Arizona"
)

for ((k, v) <- states) println(s"key: $k, value: $v")
val states = Map(
  "AK" -> "Alaska",
  "AL" -> "Alabama",
  "AZ" -> "Arizona"
)

for (k, v) <- states do println(s"key: $k, value: $v")

话虽如此,有许多方法可以处理 map 中的键和值。常见的 Map 方法包括 foreachmapkeysvalues

Scala 有更多专门的 Map 类型,包括 CollisionProofHashMapHashMapLinkedHashMapListMapSortedMapTreeMapWeakHashMap 等。

使用集合

Scala Set 是一个可迭代集合,没有重复元素。

Scala 同时具有可变和不可变 Set 类型。本节演示不可变 Set

创建集合

像这样创建新的空集合

val nums = Set[Int]()
val letters = Set[Char]()

像这样创建包含初始数据的集合

val nums = Set(1, 2, 3, 3, 3)           // Set(1, 2, 3)
val letters = Set('a', 'b', 'c', 'c')   // Set('a', 'b', 'c')

向集合中添加元素

使用 +++ 向不可变 Set 中添加元素,记住将结果分配给新变量

val a = Set(1, 2)                // Set(1, 2)
val b = a + 3                    // Set(1, 2, 3)
val c = b ++ Seq(4, 1, 5, 5)     // HashSet(5, 1, 2, 3, 4)

请注意,当您尝试添加重复元素时,它们会被悄悄地丢弃。

还要注意,元素的迭代顺序是任意的。

从 Set 中删除元素

使用 --- 从不可变集合中删除元素,同样将结果分配给新变量

val a = Set(1, 2, 3, 4, 5)   // HashSet(5, 1, 2, 3, 4)
val b = a - 5                // HashSet(1, 2, 3, 4)
val c = b -- Seq(3, 4)       // HashSet(1, 2)

范围

Scala Range 通常用于填充数据结构和迭代 for 循环。这些 REPL 示例演示了如何创建范围

1 to 5         // Range(1, 2, 3, 4, 5)
1 until 5      // Range(1, 2, 3, 4)
1 to 10 by 2   // Range(1, 3, 5, 7, 9)
'a' to 'c'     // NumericRange(a, b, c)

您可以使用范围来填充集合

val x = (1 to 5).toList     // List(1, 2, 3, 4, 5)
val x = (1 to 5).toBuffer   // ArrayBuffer(1, 2, 3, 4, 5)

它们还用于 for 循环

scala> for (i <- 1 to 3) println(i)
1
2
3
scala> for i <- 1 to 3 do println(i)
1
2
3

上也有 range 方法

Vector.range(1, 5)       // Vector(1, 2, 3, 4)
List.range(1, 10, 2)     // List(1, 3, 5, 7, 9)
Set.range(1, 10)         // HashSet(5, 1, 6, 9, 2, 7, 3, 8, 4)

在运行测试时,范围对于生成测试集合也很有用

val evens = (0 to 10 by 2).toList     // List(0, 2, 4, 6, 8, 10)
val odds = (1 to 10 by 2).toList      // List(1, 3, 5, 7, 9)
val doubles = (1 to 5).map(_ * 2.0)   // Vector(2.0, 4.0, 6.0, 8.0, 10.0)

// create a Map
val map = (1 to 3).map(e => (e,s"$e")).toMap
    // map: Map[Int, String] = Map(1 -> "1", 2 -> "2", 3 -> "3")

更多详细信息

当您需要有关专门集合的更多信息时,请参阅以下资源

此页面的贡献者