此页面演示了常见的 Scala 3 集合及其随附的方法。Scala 附带了丰富的集合类型,但你只需从几个集合类型开始,然后根据需要再使用其他集合类型,即可走很长的路。类似地,每种集合类型都有几十种方法可以让你更轻松,但你只需从少数几个方法开始即可实现很多功能。
因此,本节将介绍和演示你入门所需的最常见类型和方法。当你需要更大的灵活性时,请参阅本节末尾的这些页面以了解更多详细信息。
三类主要集合
从高层次来看 Scala 集合,有三大类可供选择
- 序列是元素的顺序集合,可以是索引(如数组)或线性(如链表)
- 映射包含键/值对的集合,例如 Java
Map
、Python 字典或 RubyHash
- 集合是唯一元素的无序集合
所有这些都是基本类型,并且具有用于特定目的的子类型,例如并发、缓存和流式处理。除了这三个主要类别之外,还有其他有用的集合类型,包括范围、堆栈和队列。
集合层次结构
作为简要概述,接下来的三幅图显示了 Scala 集合中的类和特征的层次结构。
第一幅图显示了包 scala.collection 中的集合类型。这些都是高级抽象类或特征,通常具有不可变和可变实现。
此图显示了包 scala.collection.immutable 中的所有集合
此图显示了包 scala.collection.mutable 中的所有集合
在详细查看了所有集合类型之后,以下部分介绍了一些你将定期使用的常见类型。
常见集合
你将定期使用的主要集合是
集合类型 | 不可变 | 可变 | 说明 |
---|---|---|---|
列表 |
✓ | 线性(链表)、不可变序列 | |
向量 |
✓ | 索引的、不可变序列 | |
惰性列表 |
✓ | 惰性不可变链表,其元素仅在需要时计算;适用于大型或无限序列。 | |
数组缓冲区 |
✓ | 可变、索引序列的常用类型 | |
列表缓冲区 |
✓ | 当你想要可变 列表 时使用;通常转换为 列表 |
|
映射 |
✓ | ✓ | 由键值对组成的可迭代集合。 |
集合 |
✓ | ✓ | 没有重复元素的可迭代集合 |
如所示,映射
和 集合
都具有不可变和可变版本。
每种类型的基础知识将在以下部分中演示。
在 Scala 中,缓冲区(例如
数组缓冲区
和列表缓冲区
)是可以增长和收缩的序列。
关于不可变集合的说明
在以下部分中,每当使用单词不可变时,可以安全地假设该类型旨在用于函数式编程 (FP) 样式。使用这些类型时,您不会修改集合;而是将函数方法应用于集合以创建新的结果。
选择序列
在选择序列(元素的顺序集合)时,您有两个主要决定
- 序列应该是索引的(如数组),允许快速访问任何元素,还是应该实现为线性链表?
- 您想要可变集合还是不可变集合?
针对可变/不可变和索引/线性组合的推荐通用“转到”顺序集合在此处显示
类型/类别 | 不可变 | 可变 |
---|---|---|
索引 | 向量 |
数组缓冲区 |
线性(链表) | 列表 |
列表缓冲区 |
例如,如果您需要不可变的索引集合,一般情况下您应该使用 Vector
。相反,如果您需要可变的索引集合,请使用 ArrayBuffer
。
List
和Vector
通常在以函数式编写代码时使用。ArrayBuffer
通常在以命令式编写代码时使用。ListBuffer
在您混合样式(例如构建列表)时使用。
接下来的几个部分将简要演示 List
、Vector
和 ArrayBuffer
类型。
列表
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)
这样的元素将需要相对较长的时间,因为该请求必须遍历所有这些元素。如果你有一个大型集合并希望通过它们的索引来访问元素,请改用 Vector
或 ArrayBuffer
。
如何记住方法名称
如今,IDE 为我们提供了极大的帮助,但记住这些方法名称的一种方法是认为 :
字符表示序列所在的侧,因此当你使用 +:
时,你知道列表需要在右侧,如下所示
0 +: a
类似地,当你使用 :+
时,你知道列表需要在左侧
a :+ 4
有更多技术性的方法来思考这个问题,但这可能是一种记住方法名称的有用方法。
此外,这些符号方法名称的一大优点是它们是一致的。相同的名称用于其他不可变序列,例如 Seq
和 Vector
。如果你愿意,还可以使用非符号方法名称来追加和前置元素。
如何遍历列表
给定一个名称 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 是一致的,并且相同的方法适用于所有序列,包括 Array
、ArrayBuffer
、List
、Seq
、Vector
、Map
、Set
等。
一点历史
对于那些对历史有点兴趣的人来说,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
,就像使用 List
、ArrayBuffer
或任何其他序列一样
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
,只需指定它的初始元素,就像 List
或 Vector
val nums = ArrayBuffer(1, 2, 3)
val people = ArrayBuffer(
Person("Bert"),
Person("Ernie"),
Person("Grover")
)
向 ArrayBuffer 添加元素
使用 +=
和 ++=
方法将新元素追加到 ArrayBuffer
。或者,如果你喜欢带有文本名称的方法,你还可以使用 append
、appendAll
、insert
、insertAll
、prepend
和 prependAll
。
以下是 +=
和 ++=
的一些示例
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
是可变的,因此它具有 -=
、--=
、clear
、remove
等方法。这些示例演示了 -=
和 --=
方法
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
在实践中,你还可以使用 keys
、keySet
、keysIterator
、for
循环和 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
方法包括 foreach
、map
、keys
和 values
。
Scala 有更多专门的 Map
类型,包括 CollisionProofHashMap
、HashMap
、LinkedHashMap
、ListMap
、SortedMap
、TreeMap
、WeakHashMap
等。
使用集合
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")
更多详细信息
当您需要有关专门集合的更多信息时,请参阅以下资源