在 Kotlin 中使用集合

1. 简介

在许多应用中,您可能见过数据以列表形式显示:联系人、设置、搜索结果等。

46df844b170f4272.png

然而,到目前为止您编写的代码主要处理由单个值组成的数据,例如屏幕上显示的数字或文本片段。要构建涉及任意量数据的应用,您需要学习如何使用集合。

集合类型(有时称为数据结构)允许您以有组织的方式存储多个值,通常是相同数据类型的值。集合可以是有序列表、唯一值分组或一种数据类型的值到另一种数据类型的值的映射。有效使用集合的能力使您能够实现 Android 应用的常见功能,例如可滚动列表,以及解决涉及任意量数据的各种现实编程问题。

本 Codelab 讨论了如何在代码中使用多个值,并介绍了各种数据结构,包括数组、列表、集合和映射(maps)。

先决条件

  • 熟悉 Kotlin 中的面向对象编程,包括类、接口和泛型。

学习内容

  • 如何创建和修改数组。
  • 如何使用 ListMutableList
  • 如何使用 SetMutableSet
  • 如何使用 MapMutableMap

所需工具

  • 能够访问 Kotlin Playground 的网页浏览器。

2. Kotlin 中的数组

什么是数组?

数组是在程序中对任意数量的值进行分组的最简单方法。

就像一组太阳能电池板称为太阳能电池阵列一样,或者就像学习 Kotlin 为您的编程生涯开启了无限可能(an array of possibilities)一样,Array 代表了多个值。具体来说,数组是具有相同数据类型的一系列值。

33986e4256650b8b.png

  • 数组包含多个值,称为元素,有时也称为
  • 数组中的元素是有序的,通过索引访问。

什么是索引?索引是与数组中的元素相对应的整数。索引表示项与数组起始元素的距离。这称为零索引。数组的第一个元素位于索引 0,第二个元素位于索引 1,因为它距离第一个元素一个位置,依此类推。

bb77ec7506ac1a26.png

在设备的内存中,数组中的元素是相邻存储的。尽管底层细节超出了本 Codelab 的范围,但这有两个重要意义:

  • 通过索引访问数组元素速度很快。您可以通过索引访问数组中的任何随机元素,并且访问任何其他随机元素所需的时间大致相同。这就是为什么说数组具有随机访问特性。
  • 数组的大小是固定的。这意味着您无法向数组添加超出其大小的元素。尝试访问包含 100 个元素的数组中索引 100 处的元素将抛出异常,因为最高索引是 99(记住第一个索引是 0,而不是 1)。但是,您可以修改数组中索引处的值。

要在代码中声明数组,请使用 arrayOf() 函数。

69e283b32d35f799.png

arrayOf() 函数接受数组元素作为参数,并返回一个类型与传入参数相匹配的数组。这可能看起来与您见过的其他函数有些不同,因为 arrayOf() 具有可变数量的参数。如果您向 arrayOf() 传入两个参数,生成的数组将包含两个元素,索引为 0 和 1。如果您传入三个参数,生成的数组将有 3 个元素,索引从 0 到 2。

让我们通过一个小小的太阳系探索来看看数组的实际应用!

  1. 导航到 Kotlin Playground
  2. main() 中,创建一个 rockPlanets 变量。调用 arrayOf(),传入类型 String,以及四个字符串——太阳系中每个岩石行星一个。
val rockPlanets = arrayOf<String>("Mercury", "Venus", "Earth", "Mars")
  1. 因为 Kotlin 使用类型推断,所以在调用 arrayOf() 时可以省略类型名称。在 rockPlanets 变量下方,添加另一个变量 gasPlanets,无需在尖括号中传入类型。
val gasPlanets = arrayOf("Jupiter", "Saturn", "Uranus", "Neptune")
  1. 您可以用数组做一些很酷的事情。例如,就像数字类型 IntDouble 一样,您可以将两个数组相加。创建一个名为 solarSystem 的新变量,并使用加号 (+) 运算符将其设置为 rockPlanetsgasPlanets 的结果。结果是一个新数组,包含 rockPlanets 数组的所有元素和 gasPlanets 数组的元素。
val solarSystem = rockPlanets + gasPlanets
  1. 运行您的程序以验证其是否正常工作。您目前应该看不到任何输出。

访问数组中的元素

您可以通过索引访问数组中的元素。

1f8398eaee30c7b0.png

这称为下标语法。它由三部分组成:

  • 数组的名称。
  • 一个开括号 ([) 和一个闭括号 (])。
  • 方括号中的数组元素索引。

让我们通过索引访问 solarSystem 数组中的元素。

  1. main() 中,访问并打印 solarSystem 数组的每个元素。注意第一个索引是 0,最后一个索引是 7
println(solarSystem[0])
println(solarSystem[1])
println(solarSystem[2])
println(solarSystem[3])
println(solarSystem[4])
println(solarSystem[5])
println(solarSystem[6])
println(solarSystem[7])
  1. 运行您的程序。元素与您调用 arrayOf() 时列出的顺序相同。
Mercury
Venus
Earth
Mars
Jupiter
Saturn
Uranus
Neptune

您还可以通过索引设置数组元素的值。

9469e321ed79c074.png

访问索引与之前相同——数组名称,后跟包含索引的开方括号和闭方括号。然后是赋值运算符 (=) 和一个新值。

让我们练习修改 solarSystem 数组中的值。

  1. 让我们给火星一个新名字,以便未来的地球移民使用。访问索引 3 处的元素,并将其设置为 "Little Earth"
solarSystem[3] = "Little Earth"
  1. 打印索引 3 处的元素。
println(solarSystem[3])
  1. 运行您的程序。数组的第四个元素(索引 3 处)已更新。
...
Little Earth
  1. 现在假设科学家发现海王星之外存在第九颗行星,名为冥王星。前面我们提到过,数组的大小是固定的。如果您尝试这样做会发生什么?我们尝试将冥王星添加到 solarSystem 数组中。在索引 8 处添加冥王星,因为这是数组中的第九个元素。
solarSystem[8] = "Pluto"
  1. 运行您的代码。它会抛出 ArrayIndexOutOfBounds 异常。正如预期的那样,由于数组已经有 8 个元素,您无法简单地添加第九个元素。
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 8 out of bounds for length 8
  1. 移除向数组添加冥王星的代码。

要移除的代码

solarSystem[8] = "Pluto"
  1. 如果您想让数组比现有数组更大,则需要创建一个新数组。如所示,定义一个名为 newSolarSystem 的新变量。这个数组可以存储九个元素,而不是八个。
val newSolarSystem = arrayOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto")
  1. 现在尝试打印索引 8 处的元素。
println(newSolarSystem[8])
  1. 运行您的代码,并观察它是否顺利运行,没有抛出任何异常。
...
Pluto

干得好!掌握了数组知识,您几乎可以使用集合做任何事情。

等等,别急!虽然数组是编程的基础方面之一,但对于需要添加和移除元素、集合中的唯一性或将对象映射到其他对象的任务,使用数组并不简单或直观,而且您应用的代码会很快变得混乱。

这就是为什么包括 Kotlin 在内的大多数编程语言都实现了特殊的集合类型来处理现实应用中常见的场景。在以下部分中,您将学习三种常见集合:ListSetMap。您还将学习它们的常见属性和方法,以及在何种情况下使用这些集合类型。

3. 列表(Lists)

列表是一个有序、可变大小的集合,通常实现为可变大小的数组。当数组达到容量上限并尝试插入新元素时,数组会被复制到一个新的更大数组中。

a4970d42cd1d2b66.png

使用列表,您还可以在特定索引处在其他元素之间插入新元素。

a678d6a41e6afd46.png

这就是列表能够添加和移除元素的原因。在大多数情况下,向列表中添加任何元素所需的时间是相同的,无论列表中有多少元素。但偶尔,如果添加新元素会导致数组超出其定义的大小,数组元素可能需要移动以腾出空间容纳新元素。列表会为您处理所有这些,但幕后,它只是一个在需要时被替换为新数组的数组。

ListMutableList

您在 Kotlin 中遇到的集合类型实现一个或多个接口。正如您在本单元前面学习的 泛型、对象和扩展 Codelab 中了解到的,接口为类提供了要实现的标准属性和方法集。实现 List 接口的类为 List 接口的所有属性和方法提供实现。对于 MutableList 也是如此。

那么 ListMutableList 有什么作用呢?

  • List 是一个接口,定义了与只读有序项集合相关的属性和方法。
  • MutableList 通过定义修改列表的方法(例如添加和移除元素)来扩展 List 接口。

这些接口只指定了 List 和/或 MutableList 的属性和方法。由扩展它们的类来决定如何实现每个属性和方法。上面描述的基于数组的实现是您大多数(如果不是全部)时候都会使用的,但 Kotlin 允许其他类扩展 ListMutableList

listOf() 函数

arrayOf() 类似,listOf() 函数将项作为参数,但返回的是 List 而不是数组。

  1. 移除 main() 中的现有代码。
  2. main() 中,通过调用 listOf() 创建一个行星的 List,名为 solarSystem
fun main() {
    val solarSystem = listOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune")
}
  1. List 有一个 size 属性,用于获取列表中元素的数量。打印 solarSystem 列表的 size
println(solarSystem.size) 
  1. 运行您的代码。列表的大小应该为 8。
8

访问列表中的元素

与数组类似,您可以使用下标语法从 List 中访问特定索引处的元素。您也可以使用 get() 方法执行相同的操作。下标语法和 get() 方法接受一个 Int 作为参数,并返回该索引处的元素。与 Array 类似,ArrayList 是零索引的,因此例如,第四个元素将在索引 3 处。

  1. 使用下标语法打印索引 2 处的行星。
println(solarSystem[2])
  1. 通过在 solarSystem 列表上调用 get() 打印索引 3 处的元素。
println(solarSystem.get(3))
  1. 运行您的代码。索引 2 处的元素是 "Earth",索引 3 处的元素是 "Mars"
...
Earth
Mars

除了按索引获取元素外,您还可以使用 indexOf() 方法搜索特定元素的索引。indexOf() 方法在列表中搜索给定元素(作为参数传入),并返回该元素第一次出现的索引。如果列表中不存在该元素,则返回 -1

  1. 打印在 solarSystem 列表上调用 indexOf() 并传入 "Earth" 的结果。
println(solarSystem.indexOf("Earth"))
  1. 调用 indexOf(),传入 "Pluto",然后打印结果。
println(solarSystem.indexOf("Pluto"))
  1. 运行您的代码。有一个元素匹配 "Earth",因此打印索引 2。没有元素匹配 "Pluto",因此打印 -1
...
2
-1

使用 for 循环迭代列表元素

当您学习函数类型和 lambda 表达式时,您了解了如何使用 repeat() 函数来多次执行代码。

编程中的常见任务是为列表中的每个元素执行一次任务。Kotlin 包含一个称为 for 循环的特性,可以用简洁易读的语法实现这一目的。您经常会看到这被称为遍历列表(looping through a list)或迭代列表(iterating over a list)。

f11277e6af4459bb.png

要遍历列表,请使用 for 关键字,后跟一对开括号和闭括号。在括号内,包含一个变量名,后跟 in 关键字,后跟集合的名称。闭括号后是一对开大括号和闭大括号,您将希望为集合中每个元素执行的代码包含在此处。这被称为循环的主体body)。此代码每次执行称为一次迭代iteration)。

in 关键字之前的变量不是用 valvar 声明的——它被假定为只读。您可以随意命名它。如果列表使用复数名称,例如 planets,通常将变量命名为单数形式,例如 planet。将变量命名为 itemelement 也很常见。

这将被用作对应于集合中当前元素的临时变量——第一次迭代时为索引 0 处的元素,第二次迭代时为索引 1 处的元素,依此类推,可以在大括号内访问它。

为了看到它的实际应用,您将使用 for 循环将每个行星名称打印在新的一行上。

  1. main() 中,在最近一次调用 println() 下方,添加一个 for 循环。在括号内,将变量命名为 planet,并遍历 solarSystem 列表。
for (planet in solarSystem) {
}
  1. 在大括号内,使用 println() 打印 planet 的值。
for (planet in solarSystem) {
    println(planet)
}
  1. 运行您的代码。循环体内的代码会为集合中的每个项执行一次。
...
Mercury
Venus
Earth
Mars
Jupiter
Saturn
Uranus
Neptune

向列表添加元素

在集合中添加、移除和更新元素的能力是实现 MutableList 接口的类所独有的。如果您正在跟踪新发现的行星,您很可能希望能够频繁地向列表中添加元素。在创建您希望添加和移除元素的列表时,您需要专门调用 mutableListOf() 函数,而不是 listOf()

add() 函数有两个版本

  • 第一个 add() 函数有一个参数,类型与列表中的元素类型相同,它将该元素添加到列表末尾。
  • 另一个版本的 add() 有两个参数。第一个参数对应于应该插入新元素的索引。第二个参数是要添加到列表中的元素。

让我们看看它们的实际应用。

  1. solarSystem 的初始化更改为调用 mutableListOf() 而不是 listOf()。您现在可以调用 MutableList 中定义的方法了。
val solarSystem = mutableListOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune")
  1. 同样,我们可能希望将冥王星分类为行星。在 solarSystem 上调用 add() 方法,传入 "Pluto" 作为唯一参数。
solarSystem.add("Pluto")
  1. 一些科学家推测,在与地球相撞形成月球之前,曾存在一颗名为 Theia 的行星。在索引 3 处,即在 "Earth""Mars" 之间插入 "Theia"
solarSystem.add(3, "Theia")

更新特定索引处的元素

您可以使用下标语法更新现有元素

  1. 将索引 3 处的值更新为 "Future Moon"
solarSystem[3] = "Future Moon"
  1. 使用下标语法打印索引 39 处的值。
println(solarSystem[3])
println(solarSystem[9])
  1. 运行您的代码以验证输出。
Future Moon
Pluto

从列表中移除元素

使用 remove()removeAt() 方法移除元素。您可以通过将元素传入 remove() 方法来移除元素,或者使用 removeAt() 通过其索引移除元素。

让我们看看这两个移除元素的实际方法。

  1. solarSystem 上调用 removeAt(),传入 9 作为索引。这应该会从列表中移除 "Pluto"
solarSystem.removeAt(9)
  1. solarSystem 上调用 remove(),传入 "Future Moon" 作为要移除的元素。这应该会搜索列表,如果找到匹配的元素,则将其移除。
solarSystem.remove("Future Moon")
  1. List 提供了 contains() 方法,如果列表中存在某个元素,该方法会返回一个 Boolean 值。打印调用 contains() 检查 "Pluto" 的结果。
println(solarSystem.contains("Pluto"))
  1. 更简洁的语法是使用 in 运算符。您可以使用元素、in 运算符和集合来检查元素是否在列表中。使用 in 运算符检查 solarSystem 是否包含 "Future Moon"
println("Future Moon" in solarSystem)
  1. 运行您的代码。这两个语句都应该打印 false
...
false
false

4. 集合(Sets)

集合是一种没有特定顺序且不允许重复值的集合。

9de9d777e6b1d265.png

这样的集合是如何实现的呢?秘诀在于哈希码(hash code)。哈希码是任何 Kotlin 类的 hashCode() 方法生成的一个 Int 值。它可以被视为 Kotlin 对象的半唯一标识符。对象的一个微小变化,例如在 String 中添加一个字符,都会导致截然不同的哈希值。虽然两个对象可能具有相同的哈希码(称为哈希冲突),但 hashCode() 函数确保了一定程度的唯一性,大多数情况下,两个不同的值会有一个唯一的哈希码。

84842b78e78f2f58.png

集合有两个重要属性

  1. 在集合中搜索特定元素速度很快——与列表相比,特别是对于大型集合。虽然 ListindexOf() 方法需要从头开始检查每个元素直到找到匹配项,但平均而言,检查元素是否在集合中所需的时间是相同的,无论它是第一个元素还是第十万个元素。
  2. 集合通常比列表使用更多的内存来存储相同量的数据,因为通常需要比集合中数据更多的数组索引。

集合的好处在于确保唯一性。如果您正在编写一个程序来跟踪新发现的行星,集合提供了一种简单的方式来检查行星是否已被发现。对于大量数据,这通常优于检查元素是否在列表中,因为那需要遍历所有元素。

ListMutableList 类似,集合既有 Set 也有 MutableSetMutableSet 实现了 Set,因此任何实现 MutableSet 的类都需要同时实现两者。

691f995fde47f1ff.png

在 Kotlin 中使用 MutableSet

我们将在示例中使用 MutableSet 来演示如何添加和移除元素。

  1. 移除 main() 中的现有代码。
  2. 使用 mutableSetOf() 创建一个名为 solarSystem 的行星 Set。这会返回一个 MutableSet,其默认实现是 LinkedHashSet()
val solarSystem = mutableSetOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune")
  1. 使用 size 属性打印集合的大小。
println(solarSystem.size)
  1. MutableList 类似,MutableSet 有一个 add() 方法。使用 add() 方法将 "Pluto" 添加到 solarSystem 集合中。它只需要一个参数来指定要添加的元素。集合中的元素不一定有顺序,因此没有索引!
solarSystem.add("Pluto")
  1. 添加元素后打印集合的 size
println(solarSystem.size)
  1. contains() 函数接受一个参数,并检查集合中是否包含指定的元素。如果包含,则返回 true。否则,返回 false。调用 contains() 检查 solarSystem 中是否包含 "Pluto"
println(solarSystem.contains("Pluto"))
  1. 运行您的代码。大小已增加,contains() 现在返回 true
8
9
true
  1. 如前所述,集合不能包含重复元素。尝试再次添加 "Pluto"
solarSystem.add("Pluto")
  1. 再次打印集合的大小。
println(solarSystem.size)
  1. 再次运行您的代码。由于 "Pluto" 已在集合中,所以不会再次添加。这次大小应该不会增加。
...
9

remove() 函数接受一个参数,并从集合中移除指定的元素。

  1. 使用 remove() 函数移除 "Pluto"
solarSystem.remove("Pluto")
  1. 打印集合的大小,并再次调用 contains() 检查 "Pluto" 是否仍在集合中。
println(solarSystem.size)
println(solarSystem.contains("Pluto"))
  1. 运行您的代码。"Pluto" 不再集合中,大小现在是 8。
...
8
false

5. 映射集合(Map collection)

一个 Map 是一个由键和值组成的集合。之所以称为 map,是因为唯一的键被映射(mapped)到其他值。一个键及其伴随的值通常称为一个 key-value pair(键值对)。

8571494fb4a106b6.png

映射的键是唯一的。然而,映射的值则不是。两个不同的键可以映射到相同的值。例如,"Mercury"0 个卫星,"Venus" 也有 0 个卫星。

通过键从映射中访问值通常比搜索大型列表(例如使用 indexOf())要快。

可以使用 mapOf()mutableMapOf() 函数声明映射。映射需要两个用逗号分隔的泛型类型——一个用于键,另一个用于值。

affc23a0e1f2b223.png

如果映射有初始值,也可以使用类型推断。要用初始值填充映射,每个键值对由键组成,后跟 to 运算符,再后跟值。每个键值对之间用逗号分隔。

2ed99c3391c74ec4.png

让我们更仔细地看看如何使用映射以及一些有用的属性和方法。

  1. 移除 main() 中的现有代码。
  2. 使用 mutableMapOf() 创建一个名为 solarSystem 的映射,初始值如所示。
val solarSystem = mutableMapOf(
    "Mercury" to 0,
    "Venus" to 0,
    "Earth" to 1,
    "Mars" to 2,
    "Jupiter" to 79,
    "Saturn" to 82,
    "Uranus" to 27,
    "Neptune" to 14
)
  1. 与列表和集合一样,Map 提供了 size 属性,包含键值对的数量。打印 solarSystem 映射的大小。
println(solarSystem.size)
  1. 您可以使用下标语法设置额外的键值对。将键 "Pluto" 的值设置为 5
solarSystem["Pluto"] = 5
  1. 插入元素后,再次打印大小。
println(solarSystem.size)
  1. 您可以使用下标语法获取值。打印键 "Pluto" 对应的卫星数量。
println(solarSystem["Pluto"])
  1. 您也可以使用 get() 方法访问值。无论您使用下标语法还是调用 get(),都有可能您传入的键不在映射中。如果没有对应的键值对,它将返回 null。打印 "Theia" 的卫星数量。
println(solarSystem.get("Theia"))
  1. 运行您的代码。应该会打印出冥王星的卫星数量。然而,由于 Theia 不在映射中,调用 get() 会返回 null。
8
9
5
null

remove() 方法移除指定键的键值对。如果指定键不在映射中,它也会返回被移除的值,或者 null

  1. 打印调用 remove() 并传入 "Pluto" 的结果。
solarSystem.remove("Pluto")
  1. 为验证该项已被移除,再次打印大小。
println(solarSystem.size)
  1. 运行您的代码。移除条目后,映射的大小为 8。
...
8
  1. 下标语法或 put() 方法也可以修改已存在的键的值。使用下标语法将木星的卫星数量更新为 78 并打印新值。
solarSystem["Jupiter"] = 78
println(solarSystem["Jupiter"])
  1. 运行您的代码。现有键 "Jupiter" 对应的值已更新。
...
78

6. 总结

恭喜!您学习了编程中最基础的数据类型之一:数组,以及基于数组构建的几种方便的集合类型,包括 ListSetMap。这些集合类型允许您在代码中对值进行分组和组织。数组和列表通过索引提供对元素的快速访问,而集合和映射使用哈希码使其更容易在集合中查找元素。您将在未来的应用中频繁看到这些集合类型,了解如何使用它们将对您未来的编程生涯大有裨益。

摘要

  • 数组存储同类型有序数据,且大小固定。
  • 数组用于实现许多其他集合类型。
  • 列表是可变大小、有序的集合。
  • 集合是无序集合,不能包含重复元素。
  • 映射与集合类似,存储指定类型的键值对。

7. 了解更多