本页演示了常见的 Scala 3 集合及其附带的方法。 Scala 提供了丰富的集合类型,但您可以从其中的几个开始,然后根据需要使用其他的。 同样,每种集合类型都有数十种方法可以让您的生活更轻松,但您可以从其中的少数几个开始使用,就可以有很多收获。
因此,本节介绍并演示了在开始时,需要使用的最常见的类型和方法。 当您需要更大的灵活性时,请参阅本节末尾的这些页面以获取更多细节。
三大类集合
从高层次看 Scala 集合,有三个主要类别可供选择:
- 序列是元素的顺序集合,可以是_有索引的_(如数组)或_线性的_(如链表)
- 映射 包含键/值对的集合,例如 Java
Map
、Python 字典或 RubyHash
- 集合 是无重复元素的无序集合
所有这些都是基本类型,并且具有用于特定目的的子类型,例如并发、缓存和流式传输。 除了这三个主要类别之外,还有其他有用的集合类型,包括范围、堆栈和队列。
集合层次结构
作为简要概述,接下来的三个图显示了 Scala 集合中类和 trait 的层次结构。
第一张图显示了_scala.collection_包中的集合类型。 这些都是高级抽象类或 traits,它们通常有_不可变_和_可变_的实现。
此图显示包 scala.collection.immutable 中的所有集合:
此图显示包 scala.collection.mutable 中的所有集合:
在查看了所有集合类型的详细视图后,以下部分将介绍一些经常使用的常见类型。
常用集合
您经常使用的主要集合是:
集合类型 | 不可变 | 可变 | 说明 |
---|---|---|---|
List |
✓ | 线性(链表)、不可变序列 | |
Vector |
✓ | 一个索引的、不可变的序列 | |
LazyList |
✓ | 一个惰性不可变链表,它的元素仅在需要时才计算;适用于大型或无限序列。 | |
ArrayBuffer |
✓ | 可变索引序列的首选类型 | |
ListBuffer |
✓ | 当你想要一个可变的 List 时使用;通常转换为“列表” |
|
Map |
✓ | ✓ | 由键和值对组成的可迭代集合。 |
Set |
✓ | ✓ | 没有重复元素的可迭代集合 |
如图所示,Map
和 Set
有不可变和可变版本。
以下部分演示了每种类型的基础知识。
在 Scala 中,缓冲——例如
ArrayBuffer
和ListBuffer
——是一个可以增长和缩小的序列。
关于不可变集合的说明
在接下来的部分中,无论何时使用_不可变_这个词,都可以安全地假设该类型旨在用于_函数式编程_(FP) 风格。 使用这些类型,您无需修改集合;您将函数式方法应用于该集合以创建新的结果。
选择序列
选择_序列_ – 一个顺序集合元素时 – 您有两个主要决定:
- 是否应该对序列进行索引(如数组),允许快速访问任何元素,还是应该将其实现为线性链表?
- 你想要一个可变的还是不可变的集合?
此处显示了推荐的通用顺序集合,用于可变/不可变和索引/线性组合:
类型/类别 | 不可变 | 可变 |
---|---|---|
索引 | Vector |
ArrayBuffer |
线性(链表) | List |
ListBuffer |
例如,如果您需要一个不可变的索引集合,通常您应该使用 Vector
。
相反,如果您需要一个可变的索引集合,请使用 ArrayBuffer
。
List
和Vector
在以函数式风格编写代码时经常使用。ArrayBuffer
通常在以命令式风格编写代码时使用。ListBuffer
用于混合样式时,例如构建列表。
接下来的几节简要介绍了 List
、Vector
和 ArrayBuffer
类型。
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
插在这个 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
元素,例如访问 listOfPeople(123_456_789)
。
一般来说,除了 (a) Vector
有索引而 List
没有索引,以及 (b) List
有 ::
方法这两个不同外,这两种类型的工作方式相同,所以我们将快速过一下示例。
以下是创建“向量”的几种方法:
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
,首先导入它:
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
。
或者,如果您更喜欢具有文本名称的方法,您也可以使用 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
是可变的,所以它有 -=
、--=
、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)
更新数组缓冲区元素
通过重新分配所需元素或使用 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
访问映射元素
通过在括号中指定所需的键值来访问映射元素:
val ak = states("AK") // ak: String = Alaska
val al = states("AL") // al: String = Alabama
在实践中,您还将使用诸如 keys
、keySet
、keysIterator
、for
循环之类的方法以及 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)
从映射中删除元素
使用 -
或 --
和要删除的键值从不可变映射中删除元素,记住将结果分配给新变量:
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)
更新映射元素
要更新不可变映射中的元素,请在将结果分配给新变量时使用 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)
遍历映射
如前所述,这是使用 for
循环手动遍历映射中元素的常用方法:
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
方法包括 foreach
、map
、keys
和 values
。
Scala 有许多更专业的Map
类型,包括CollisionProofHashMap
、HashMap
、LinkedHashMap
、ListMap
、SortedMap
、TreeMap
、WeakHashMap
等等。
使用集合
Scala 集合 是一个没有重复元素的可迭代集合。
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)
请注意,当您尝试添加重复元素时,它们会被悄悄删除。
另请注意,元素的迭代顺序是任意的。
从集合中删除元素
使用 -
和 --
从不可变集合中删除元素,再次将结果分配给新变量:
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")
更多细节
当您需要特定集合更多的信息,请参阅以下资源:
Contributors to this page:
Contents
- 导言
- Scala 3 特性
- 为什么是 Scala 3 ?
- Scala 的味道
- Hello, World!
- The REPL
- 变量和数据类型
- 控制结构
- 领域建模
- 方法
- 头等函数
- 单例对象
- 集合
- 上下文抽象
- 顶层定义
- 总结
- 类型初探
- 字符串插值
- 控制结构
- 领域建模
- 工具
- OOP 领域建模
- 函数式领域建模
- 方法
- 方法特性
- main 方法
- 总结
- 函数
- 匿名函数
- 函数变量
- Eta 扩展
- 高阶函数
- 自定义 map 函数
- 创建可以返回函数的方法
- 总结
- 打包和导入
- Scala 集合
- 集合类型
- 集合方法
- 总结
- 函数式编程
- 什么是函数式编程?
- 不可变值
- 纯函数
- 函数是值
- 函数式错误处理
- 总结
- 类型和类型系统
- 类型推断
- 泛型
- 相交类型
- 联合类型
- 代数数据类型
- 型变
- 不透明类型
- 结构化类型
- 依赖函数类型
- 其他类型
- 上下文抽象
- 扩展方法
- Given 实例和 Using 语句
- 上下文绑定
- Given 导入
- 实现类型类
- 多元相等性
- 隐式转换
- 总结
- 并发
- Scala 工具
- 使用 sbt 构建和测试 Scala 项目
- worksheet
- 与 Java 交互
- 向 Java 开发者介绍Scala
- Scala for JavaScript Developers
- Scala for Python Developers
- 下一步去哪