在 Kotlin 中使用集合

1. 简介

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

46df844b170f4272.png

但是,在您迄今编写的代码中,您主要处理的是由单个值组成的数据,例如屏幕上显示的数字或文本。要构建涉及任意数量数据的应用,您需要学习如何使用集合。

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

本代码实验室讨论了如何在代码中处理多个值,并介绍了各种数据结构,包括数组、列表、集合和映射。

先决条件

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

您将学到什么

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

您需要什么

  • 一个可以访问 Kotlin Playground 的网络浏览器。

2. Kotlin 中的数组

什么是数组?

数组是在程序中组合任意数量值的简单方法。

就像太阳能电池板的组合称为太阳能电池阵列,或者学习 Kotlin 为您的编程职业打开了一系列可能性一样,Array 表示**多个值**。具体来说,数组是所有具有相同数据类型的值的序列。

33986e4256650b8b.png

  • 数组包含多个称为元素或有时称为的值。
  • 数组中的元素是有序的,并使用索引访问。

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

bb77ec7506ac1a26.png

在设备的内存中,数组中的元素彼此相邻存储。虽然底层细节超出了本代码实验室的范围,但这有两个重要的含义

  • 通过其索引访问数组元素的速度很快。您可以通过索引访问数组的任何随机元素,并期望访问任何其他随机元素花费大约相同的时间。这就是为什么说数组具有随机访问
  • 数组的大小是固定的。这意味着您无法向数组添加超出此大小的元素。尝试访问 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 处,因为这是数组中的第 9 个元素。
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. 列表

列表是有序的可调整大小的集合,通常实现为可调整大小的数组。当数组已满并且您尝试插入新元素时,数组将复制到一个新的更大数组。

a4970d42cd1d2b66.png

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

a678d6a41e6afd46.png

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

ListMutableList

你在 Kotlin 中遇到的集合类型都实现了 1 个或多个接口。正如你在本单元前面学习的泛型、对象和扩展 代码实验室中所学,接口为类提供了一套标准的属性和方法来实现。实现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 是从 0 开始索引的,例如,第四个元素位于索引 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 循环的功能,可以使用简洁易读的语法来完成此任务。你通常会看到这被称为遍历列表或迭代列表。

f11277e6af4459bb.png

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

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. 一些科学家推测一颗名为忒伊亚的行星在与地球碰撞并形成月球之前曾经存在过。在索引 3 处插入 "Theia",位于 "Earth""Mars" 之间。
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. 集合

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

9de9d777e6b1d265.png

这样的集合是如何实现的?秘密在于一个哈希码。哈希码是由任何 Kotlin 类中的 hashCode() 方法生成的 Int 类型的值。可以将其视为 Kotlin 对象的半唯一标识符。对对象进行少量更改,例如在 String 中添加一个字符,会导致哈希值发生巨大变化。虽然两个对象可能具有相同的哈希码(称为哈希冲突),但 hashCode() 函数确保了一定程度的唯一性,在大多数情况下,两个不同的值都具有唯一的哈希码。

84842b78e78f2f58.png

集合具有两个重要的属性

  1. 在集合中搜索特定元素的速度很快——与列表相比,尤其是在大型集合中。虽然 ListindexOf() 需要从开头检查每个元素直到找到匹配项,但平均而言,检查元素是否在集合中所需的时间相同,无论是第一个元素还是第 10 万个元素。
  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() 以检查 "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 是一个由键和值组成的集合。它被称为映射,因为唯一的键被映射到其他值。键及其伴随的值通常称为 键值对

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. 了解更多