在 Kotlin 中使用集合

1. 简介

在很多应用中,您可能会看到以列表形式显示的数据:通讯录、设置、搜索结果等。

9fbd3bf7cb6adc72.png

但是,在您到目前为止编写过的代码中,您使用的数据大多包含单个值(例如屏幕上显示的数字或文本段)。如需构建涉及任意数量的数据的应用,您需要了解如何使用集合。

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

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

前提条件

  • 熟悉如何使用 Kotlin 进行面向对象的编程,包括编写类、接口和泛型。

学习内容

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

所需条件

  • 一个能够访问 Kotlin Playground 的网络浏览器。

2. Kotlin 中的数组

什么是数组?

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

比如说,太阳能板的分组称为太阳能数组,或者学习 Kotlin 可以为您的编程生涯开启无限可能(机会数组),Array 代表多个值。具体而言,数组是具有同一数据类型的一系列值。

960e34f4c96e2fd9.png

  • 数组包含多个值,这些值称为元素,有时也称为项。
  • 数组中的元素是有序的,可以通过索引进行访问。

什么是索引?索引是与数组中的某个元素对应的整数。索引可以指示某个项与数组起始元素之间的距离。这称为“零索引”。数组的第一个元素位于索引 0 处,第二个元素位于索引 1 处,因为它与第一个元素相距一个位置,以此类推。

5baf880a3670720d.png

在设备的内存中,数组中的元素彼此相邻地存储在一起。虽然底层细节不在本 Codelab 的讨论范围之内,但这有两个重要影响:

  • 通过索引可以快速地访问数组元素。您可以通过索引访问数组的任何随机元素,并且访问任何其他随机元素预计需要大约相同的时间。因此,有人说数组具有随机访问特性。
  • 数组具有固定的大小。这意味着您向数组添加元素时不能超过该数组的大小。如果尝试访问某个数组(包含 100 个元素)中位于索引 100 处的元素,则会引发异常,因为最高索引为 99(请注意,第一个索引为 0,而不是 1)。但是,您可以修改数组中位于相关索引处的值。

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

9d5c8c00b30850cb.png

arrayOf() 函数将数组元素作为形参,并返回类型与传入的形参相符的数组。这可能与您看到的其他函数略有不同,因为 arrayOf() 的参数数量会变化。如果您向 arrayOf() 传入两个参数,生成的数组将包含两个元素(索引为 0 和 1)。如果您传入三个实参,生成的数组将包含 3 个元素,索引为 0 到 2。

下面,我们对太阳系进行一些探索,以了解数组的实际运用!

  1. 前往 Kotlin 园地
  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. 为 Mars 取个适合其未来定居者的新名字。访问位于索引 3 处的元素,并将其设置为 "Little Earth"
solarSystem[3] = "Little Earth"
  1. 输出位于索引 3 处的元素。
println(solarSystem[3])
  1. 运行程序。数组的第四个元素(位于索引 3 处)已更新。
...
Little Earth
  1. 现在,假设科学家发现了海王星轨道之外的第九颗行星“冥王星”(Pluto)。我们之前提到过,您无法调整数组的大小。如果尝试一下,会出现什么情况?我们尝试将 Pluto 添加到 solarSystem 数组。在索引 8 处添加 Pluto,因为它是数组中的第 9 个元素。
solarSystem[8] = "Pluto"
  1. 运行代码。它会抛出 ArrayIndexOutOfBounds 异常。由于该数组已包含 8 个元素(符合预期),因此您不能直接添加第 9 个元素。
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 8 out of bounds for length 8
  1. 移除添加到该数组中的 Pluto。

要移除的代码

solarSystem[8] = "Pluto"
  1. 如果您想增加数组的现有大小,则需要创建一个新数组。定义一个名为 newSolarSystem 的新变量,如下所示。此数组可以存储 9 个元素,而不是 8 个。
val newSolarSystem = arrayOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto")
  1. 现在,尝试输出位于索引 8 处的元素。
println(newSolarSystem[8])
  1. 运行代码,观察发现它运行正常,而不会出现任何异常。
...
Pluto

太棒了!掌握了数组之后,您就可以使用集合执行几乎任何操作了。

等等,别着急!虽然数组是编程的一个基本方面,但是,如果处理的任务需要添加和移除元素、保持在集合中的唯一性或者将一些对象映射到其他对象,那么使用数组并不简单也不直接,而且应用代码很快就会变得混乱。

正因如此,大多数编程语言(包括 Kotlin)都实现了特殊的集合类型,以处理实际应用中常见的情况。在后面几部分中,您将了解三个常见的集合:ListSetMap。您还将了解常见的属性和方法,以及使用这些集合类型的情况。

3. 列表

列表是有序且可调整大小的集合,通常作为可调整大小的数组实现。当数组达到容量上限时,如果您尝试插入新元素,需要将该数组复制到一个新的较大数组。

a4970d42cd1d2b66.png

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

27afd8dd880e1ae5.png

上图显示了在列表中添加和移除元素的方式。在大多数情况下,无论列表中包含多少个元素,向列表中添加任何元素所需的时间都是相同的。每隔一段时间,如果添加新元素会使数组超出其定义的大小,那么数组元素可能必须移动,以便为新元素腾出空间。列表会为您执行所有这些操作,但在后台,它只是一个在需要时换成新数组的数组。

ListMutableList

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

ListMutableList 有什么用?

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

这些接口仅指定 List 和/或 MutableList 的属性和方法。每个属性和方法的实现方式均由扩展这些接口的类决定。上述基于数组的实现将是您最常使用(如果不是始终使用)的实现,但 Kotlin 允许其他类扩展 ListMutableList

listOf() 函数

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

  1. main() 中移除现有代码。
  2. main() 中,通过调用 listOf() 创建名为 solarSystem 的行星 List
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 循环的功能,可利用简洁易懂的语法来实现此目的。这通常称为“循环遍历”列表或“遍历”列表。

1245a226a9ceeba1.png

如需循环遍历列表,请使用 for 关键字,后跟一组圆括号。在圆括号内添加一个变量名称,后跟 in 关键字,接着是集合的名称。右圆括号后面是一组大括号,其中包含您要针对集合中的每个元素执行的代码。这称为循环主体。每次执行此代码都称为一次迭代。

in 关键字之前的变量未使用 valvar 进行声明,它假定为 get-only。您可以随意为其命名。如果列表使用的是复数名称(如 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. 同样,我们也可能需要将 Pluto 归类为行星。对 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。输出为 "Pluto" 调用 contains() 的结果。
println(solarSystem.contains("Pluto"))
  1. 更简洁的语法是使用 in 运算符。您可以使用元素、in 运算符和集合来检查该元素是否在列表中。使用 in 运算符检查 solarSystem 是否包含 "Future Moon"
println("Future Moon" in solarSystem)
  1. 运行代码。两个语句都应输出 false
...
false
false

4. 集

集是指没有特定顺序且不允许出现重复值的集合。

ce127adf37662aa4.png

如何实现这样的集合?秘诀是哈希代码。哈希代码是由任何 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. List 类似,Set 具有 add() 方法。使用 add() 方法将 "Pluto" 添加到 solarSystem 集。它只需要为所添加的元素使用一个参数。集中的元素未必是有序的,因此没有索引!
solarSystem.add("Pluto")
  1. 添加相应元素后,输出该集的 size
println(solarSystem.size)
  1. contains() 函数接受一个参数,并检查该集中是否包含指定的元素。如果是,则返回 true。否则,它将返回 false。调用 contains() 以检查 "Pluto" 是否在 solarSystem 中。
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 是由键和值组成的集合。之所以称之为映射,是因为唯一键会映射到其他值。键及其附带的值通常称为 key-value pair

8571494fb4a106b6.png

映射的键具有唯一性,但映射的值不具有唯一性。两个不同的键可以映射到同一个值。例如,"Mercury"0 颗卫星,"Venus" 也有 0 颗卫星。

通过相应的键访问映射的值通常比在大型列表中(例如使用 indexOf())搜索值更快。

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

affc23a0e1f2b223.png

如果映射具有初始值,则还可以使用类型推断。要使用初始值填充映射,每个键值对都由以下部分组成:首先是键,后跟 to 运算符,而后是值。每个键值对均以英文逗号隔开。

8719ffc353f652f.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["Theia"])
  1. 运行代码。系统应该会输出 Pluto 的卫星数量。不过,由于 Theia 不在映射中,因此调用 get() 会返回 null。
8
9
5
null

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

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

6. 总结

恭喜!您已经了解了编程中的一种最基本的数据类型(即数组),以及一些基于数组构建的便捷集合类型(包括 ListSetMap)。您可以在代码中使用这些集合类型对值进行分组和整理。数组和列表可让您通过索引快速访问元素,而集和映射使用哈希代码来让您更轻松地查找集合中的元素。您将看到未来的应用中会经常用到这些集合类型,了解如何使用这些集合类型对您日后的编程生涯大有裨益。

总结

  • 数组存储的是同一类型的有序数据,且具有固定大小。
  • 数组用于实现很多其他集合类型。
  • 列表是可调整大小的有序集合。
  • 集是无序集合,不能包含重复项。
  • 映射的工作方式与集类似,用于存储指定类型的键/值对。

7. 了解更多内容