1. 简介
在很多应用中,您可能会看到以列表形式显示的数据:通讯录、设置、搜索结果等。
但是,在您到目前为止编写过的代码中,您使用的数据大多包含单个值(例如屏幕上显示的数字或文本段)。如需构建涉及任意数量的数据的应用,您需要了解如何使用集合。
集合类型(有时称为数据结构)可让您以井然有序的方式存储多个值,这些值通常属于同一数据类型。集合可以是有序列表、唯一值分组,或一种数据类型的值到另一种数据类型的值的映射。通过有效地使用集合,您可以实现 Android 应用的常见功能(例如滚动列表),以及解决各种涉及任意数量的数据的实际编程问题。
此 Codelab 讨论了如何在代码中处理多个值,并介绍了各种数据结构(包括数组、列表、集和映射)。
前提条件
- 熟悉如何使用 Kotlin 进行面向对象的编程,包括编写类、接口和泛型。
学习内容
- 如何创建和修改数组。
- 如何使用
List
和MutableList
。 - 如何使用
Set
和MutableSet
。 - 如何使用
Map
和MutableMap
。
所需条件
- 一个能够访问 Kotlin Playground 的网络浏览器。
2. Kotlin 中的数组
什么是数组?
数组是对程序中的任意数量的值进行分组的最简单方法。
比如说,太阳能板的分组称为太阳能数组,或者学习 Kotlin 可以为您的编程生涯开启无限可能(机会数组),Array
代表多个值。具体而言,数组是具有同一数据类型的一系列值。
- 数组包含多个值,这些值称为元素,有时也称为项。
- 数组中的元素是有序的,可以通过索引进行访问。
什么是索引?索引是与数组中的某个元素对应的整数。索引可以指示某个项与数组起始元素之间的距离。这称为“零索引”。数组的第一个元素位于索引 0 处,第二个元素位于索引 1 处,因为它与第一个元素相距一个位置,以此类推。
在设备的内存中,数组中的元素彼此相邻地存储在一起。虽然底层细节不在本 Codelab 的讨论范围之内,但这有两个重要影响:
- 通过索引可以快速地访问数组元素。您可以通过索引访问数组的任何随机元素,并且访问任何其他随机元素预计需要大约相同的时间。因此,有人说数组具有随机访问特性。
- 数组具有固定的大小。这意味着您向数组添加元素时不能超过该数组的大小。如果尝试访问某个数组(包含 100 个元素)中位于索引 100 处的元素,则会引发异常,因为最高索引为 99(请注意,第一个索引为 0,而不是 1)。但是,您可以修改数组中位于相关索引处的值。
如需在代码中声明数组,请使用 arrayOf()
函数。
arrayOf()
函数将数组元素作为形参,并返回类型与传入的形参相符的数组。这可能与您看到的其他函数略有不同,因为 arrayOf()
的参数数量会变化。如果您向 arrayOf()
传入两个参数,生成的数组将包含两个元素(索引为 0 和 1)。如果您传入三个实参,生成的数组将包含 3 个元素,索引为 0 到 2。
下面,我们对太阳系进行一些探索,以了解数组的实际运用!
- 前往 Kotlin 园地。
- 在
main()
中,创建一个rockPlanets
变量。调用arrayOf()
,传入String
类型以及四个字符串 - 太阳系中的每个岩石行星分别对应一个。
val rockPlanets = arrayOf<String>("Mercury", "Venus", "Earth", "Mars")
- 由于 Kotlin 使用类型推断,因此在调用
arrayOf()
时可以省略类型名称。在rockPlanets
变量下方,添加另一个变量gasPlanets
,而不将类型传递到尖括号中。
val gasPlanets = arrayOf("Jupiter", "Saturn", "Uranus", "Neptune")
- 您可以使用数组进行一些很酷的操作。例如,就像数字类型
Int
或Double
一样,您可以同时添加两个数组。创建一个名为solarSystem
的新变量,并使用加号 (+
) 运算符将其设为rockPlanets
和gasPlanets
的结果。结果是一个新数组,其中包含rockPlanets
数组的所有元素以及gasPlanets
数组的元素。
val solarSystem = rockPlanets + gasPlanets
- 运行程序,验证它是否正常运行。您目前应该不会看到任何输出。
访问数组中的元素
您可以通过索引访问数组的元素。
这称为下标语法。它包含三个部分:
- 数组的名称。
- 左方括号 (
[
) 和右方括号 (]
)。 - 方括号内数组元素的索引。
我们按索引访问 solarSystem
数组的各元素。
- 在
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])
- 运行程序。这些元素的顺序与您在调用
arrayOf()
时列出它们的顺序相同。
Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune
您还可以按索引设置数组元素的值。
访问索引的方式和之前一样:数组的名称,后跟包含索引的左方括号和右方括号,而后是赋值运算符 (=
) 和新值。
我们来练习修改 solarSystem
数组中的值。
- 为 Mars 取个适合其未来定居者的新名字。访问位于索引
3
处的元素,并将其设置为"Little Earth"
。
solarSystem[3] = "Little Earth"
- 输出位于索引
3
处的元素。
println(solarSystem[3])
- 运行程序。数组的第四个元素(位于索引
3
处)已更新。
... Little Earth
- 现在,假设科学家发现了海王星轨道之外的第九颗行星“冥王星”(Pluto)。我们之前提到过,您无法调整数组的大小。如果尝试一下,会出现什么情况?我们尝试将 Pluto 添加到
solarSystem
数组。在索引8
处添加 Pluto,因为它是数组中的第 9 个元素。
solarSystem[8] = "Pluto"
- 运行代码。它会抛出
ArrayIndexOutOfBounds
异常。由于该数组已包含 8 个元素(符合预期),因此您不能直接添加第 9 个元素。
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 8 out of bounds for length 8
- 移除添加到该数组中的 Pluto。
要移除的代码
solarSystem[8] = "Pluto"
- 如果您想增加数组的现有大小,则需要创建一个新数组。定义一个名为
newSolarSystem
的新变量,如下所示。此数组可以存储 9 个元素,而不是 8 个。
val newSolarSystem = arrayOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto")
- 现在,尝试输出位于索引
8
处的元素。
println(newSolarSystem[8])
- 运行代码,观察发现它运行正常,而不会出现任何异常。
... Pluto
太棒了!掌握了数组之后,您就可以使用集合执行几乎任何操作了。
等等,别着急!虽然数组是编程的一个基本方面,但是,如果处理的任务需要添加和移除元素、保持在集合中的唯一性或者将一些对象映射到其他对象,那么使用数组并不简单也不直接,而且应用代码很快就会变得混乱。
正因如此,大多数编程语言(包括 Kotlin)都实现了特殊的集合类型,以处理实际应用中常见的情况。在后面几部分中,您将了解三个常见的集合:List
、Set
和 Map
。您还将了解常见的属性和方法,以及使用这些集合类型的情况。
3. 列表
列表是有序且可调整大小的集合,通常作为可调整大小的数组实现。当数组达到容量上限时,如果您尝试插入新元素,需要将该数组复制到一个新的较大数组。
使用列表,您还可以在特定索引处(介于其他元素之间)插入新元素。
上图显示了在列表中添加和移除元素的方式。在大多数情况下,无论列表中包含多少个元素,向列表中添加任何元素所需的时间都是相同的。每隔一段时间,如果添加新元素会使数组超出其定义的大小,那么数组元素可能必须移动,以便为新元素腾出空间。列表会为您执行所有这些操作,但在后台,它只是一个在需要时换成新数组的数组。
List
和 MutableList
您在 Kotlin 中遇到的集合类型会实现一个或多个接口。正如您在本单元前面的“泛型、对象和扩展”Codelab 中所学到的,接口提供了一组供类实现的标准属性和方法。实现 List
接口的类可为 List
接口的所有属性和方法提供实现。MutableList
也是如此。
List
和 MutableList
有什么用?
List
是一个接口,用于定义与只读有序项集合相关的属性和方法。MutableList
通过定义修改列表的方法(例如添加和移除元素)来扩展List
接口。
这些接口仅指定 List
和/或 MutableList
的属性和方法。每个属性和方法的实现方式均由扩展这些接口的类决定。上述基于数组的实现将是您最常使用(如果不是始终使用)的实现,但 Kotlin 允许其他类扩展 List
和 MutableList
。
listOf()
函数
与 arrayOf()
类似,listOf()
函数将相关项作为形参,但返回 List
,而不是数组。
- 从
main()
中移除现有代码。 - 在
main()
中,通过调用listOf()
创建名为solarSystem
的行星List
。
fun main() {
val solarSystem = listOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune")
}
List
具有size
属性,用于获取列表中的元素数量。输出solarSystem
列表的size
。
println(solarSystem.size)
- 运行代码。列表的大小应为 8。
8
访问列表中的元素
与数组一样,您可以使用下标语法从 List
访问特定索引处的元素。您可以使用 get()
方法执行相同的操作。下标语法和 get()
方法接受 Int
作为参数,并在该索引处返回相应元素。与 Array
一样,ArrayList
也为零索引,因此第四个元素位于索引 3
处。
- 使用下标语法输出索引
2
处的行星。
println(solarSystem[2])
- 通过对
solarSystem
列表调用get()
,输出索引3
处的元素。
println(solarSystem.get(3))
- 运行代码。索引
2
处的元素为"Earth"
,索引3
处的元素为"Mars"
。
... Earth Mars
除了按索引获取元素之外,您还可以使用 indexOf()
方法搜索特定元素的索引。indexOf()
方法在列表中搜索指定元素(作为实参传入),并返回该元素在第一次出现时的索引。如果该元素未出现在列表中,则返回 -1
。
- 输出对
solarSystem
列表调用indexOf()
并传入"Earth"
的结果。
println(solarSystem.indexOf("Earth"))
- 调用
indexOf()
,传入"Pluto"
,并输出结果。
println(solarSystem.indexOf("Pluto"))
- 运行代码。某个元素与
"Earth"
匹配,因此输出索引2
。没有与"Pluto"
匹配的元素,因此输出-1
。
... 2 -1
使用 for
循环遍历列表元素
在学习函数类型和 lambda 表达式时,您已经了解如何使用 repeat()
函数多次执行代码。
编程中的一项常见任务是对列表中的每个元素执行一次某个任务。Kotlin 包含一个名叫 for
循环的功能,可利用简洁易懂的语法来实现此目的。这通常称为“循环遍历”列表或“遍历”列表。
如需循环遍历列表,请使用 for
关键字,后跟一组圆括号。在圆括号内添加一个变量名称,后跟 in
关键字,接着是集合的名称。右圆括号后面是一组大括号,其中包含您要针对集合中的每个元素执行的代码。这称为循环主体。每次执行此代码都称为一次迭代。
in
关键字之前的变量未使用 val
或 var
进行声明,它假定为 get-only。您可以随意为其命名。如果列表使用的是复数名称(如 planets
),则通常将该变量命名为单数形式,例如 planet
。将该变量命名为 item
或 element
的情况也很常见。
此变量将用作与集合中的当前元素(第一次迭代为索引 0
处的元素,第二次迭代为索引 1
处的元素,依此类推)对应的临时变量,可在大括号内访问。
若要查看其实际效果,您需要使用 for
循环在单独的一行上输出每个行星名称。
- 在
main()
中,在最近的println()
的调用之下,添加一个for
循环。在圆括号内将变量命名为planet
,然后循环遍历solarSystem
列表。
for (planet in solarSystem) {
}
- 在大括号内,使用
println()
输出planet
的值。
for (planet in solarSystem) {
println(planet)
}
- 运行代码。对集合中的每个项执行循环主体内的代码。
... Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune
向列表中添加元素
只有实现 MutableList
接口的类具有在集合中添加、移除和更新元素的功能。如果您一直在跟踪新发现的行星,则可能需要能够经常向列表中添加元素。在创建要向其中添加元素和从中移除元素的列表时,您需要专门调用 mutableListOf()
函数,而不是 listOf()
。
add()
函数有两个版本:
- 第一个
add()
函数具有一个属于列表中元素类型的参数,并将其添加到列表末尾。 add()
的另一个版本有两个参数。第一个参数对应于应该插入新元素的索引。第二个参数是要添加到列表中的元素。
我们来看看实际用例。
- 将
solarSystem
的初始化更改为调用mutableListOf()
,而不是listOf()
。您现在可以调用MutableList
中定义的方法。
val solarSystem = mutableListOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune")
- 同样,我们也可能需要将 Pluto 归类为行星。对
solarSystem
调用add()
方法,传入"Pluto"
作为单个参数。
solarSystem.add("Pluto")
- 有些科学家提出一种理论:过去曾有一颗名为 Theia 的行星,该行星后来与地球发生碰撞并形成月球。在索引
3
(介于"Earth"
和"Mars"
之间)处插入"Theia"
。
solarSystem.add(3, "Theia")
更新特定索引处的元素
您可以使用下标语法更新现有元素:
- 将索引
3
处的值更新为"Future Moon"
。
solarSystem[3] = "Future Moon"
- 使用下标语法输出位于索引
3
和9
处的值。
println(solarSystem[3])
println(solarSystem[9])
- 运行代码,验证输出。
Future Moon Pluto
从列表中移除元素
使用 remove()
或 removeAt()
方法可移除元素。您可以通过两种方法移除元素:将该元素传递到 remove()
方法中,或者使用 removeAt()
按索引移除该元素。
我们来看下这两种元素移除方法的实际操作效果。
- 对
solarSystem
调用removeAt()
,并传入索引9
。这应该会从列表中移除"Pluto"
。
solarSystem.removeAt(9)
- 对
solarSystem
调用remove()
,并传入"Future Moon"
作为要移除的元素。此操作应该会搜索此列表,如果找到匹配元素,则会将其移除。
solarSystem.remove("Future Moon")
List
可提供contains()
方法,该方法可在列表中存在某个元素时返回Boolean
。输出为"Pluto"
调用contains()
的结果。
println(solarSystem.contains("Pluto"))
- 更简洁的语法是使用
in
运算符。您可以使用元素、in
运算符和集合来检查该元素是否在列表中。使用in
运算符检查solarSystem
是否包含"Future Moon"
。
println("Future Moon" in solarSystem)
- 运行代码。两个语句都应输出
false
。
... false false
4. 集
集是指没有特定顺序且不允许出现重复值的集合。
如何实现这样的集合?秘诀是哈希代码。哈希代码是由任何 Kotlin 类的 hashCode()
方法生成的 Int
。可以将其视为 Kotlin 对象的半唯一标识符。如果对该对象稍作更改,例如向 String
中添加一个字符,则会产生截然不同的哈希值。虽然两个对象可以使用相同的哈希代码(称为哈希冲突),但 hashCode()
函数可在某种程度上确保唯一性,大多数情况下,两个不同的值各自具有唯一的哈希代码。
集具有两个重要属性:
- 与列表相比,可以更快地搜索集中的特定元素,尤其是对于大型集合。虽然
List
的indexOf()
要求从头开始检查每个元素,直到找到匹配项,但平均而言,检查某个元素是否在集中所用的时间相同,无论它是第一个元素还是第十万个元素。 - 对于相同数量的数据,集占用的内存往往比列表多,因为所需的数组索引通常比集中的数据多。
集的优势在于确保唯一性。如果您要编写一个程序来跟踪新发现的行星,则可以借助集轻松检查是否已发现某颗行星。如果数据量很大,通常最好检查列表中是否存在某个元素,这需要遍历所有元素。
与 List
和 MutableList
一样,既有 Set
也有 MutableSet
。MutableSet
会实现 Set
,因此任何实现 MutableSet
的类都需要同时实现这两者。
在 Kotlin 中使用 MutableSet
在本示例中,我们将使用 MutableSet
来演示如何添加和移除元素。
- 从
main()
中移除现有代码。 - 使用
mutableSetOf()
创建名为solarSystem
的行星Set
。这将返回MutableSet
,其默认实现为LinkedHashSet()
。
val solarSystem = mutableSetOf("Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune")
- 使用
size
属性输出该集的大小。
println(solarSystem.size)
- 与
List
类似,Set
具有add()
方法。使用add()
方法将"Pluto"
添加到solarSystem
集。它只需要为所添加的元素使用一个参数。集中的元素未必是有序的,因此没有索引!
solarSystem.add("Pluto")
- 添加相应元素后,输出该集的
size
。
println(solarSystem.size)
contains()
函数接受一个参数,并检查该集中是否包含指定的元素。如果是,则返回 true。否则,它将返回 false。调用contains()
以检查"Pluto"
是否在solarSystem
中。
println(solarSystem.contains("Pluto"))
- 运行代码。大小已增大,
contains()
现在会返回true
。
8 9 true
- 如前所述,集不能包含重复项。请尝试重新添加
"Pluto"
。
solarSystem.add("Pluto")
- 再次输出集的大小。
println(solarSystem.size)
- 再次运行您的代码。未添加
"Pluto"
,因为它已包含在集中。这次,大小应该不会增加。
... 9
remove()
函数接受一个参数,并从集中移除指定的元素。
- 使用
remove()
函数移除"Pluto"
。
solarSystem.remove("Pluto")
- 输出集合的大小,并再次调用
contains()
以检查"Pluto"
是否仍在集中。
println(solarSystem.size)
println(solarSystem.contains("Pluto"))
- 运行代码。
"Pluto"
不在集中,大小现在为 8。
... 8 false
5. 映射集合
Map
是由键和值组成的集合。之所以称之为映射,是因为唯一键会映射到其他值。键及其附带的值通常称为 key-value pair
。
映射的键具有唯一性,但映射的值不具有唯一性。两个不同的键可以映射到同一个值。例如,"Mercury"
有 0
颗卫星,"Venus"
也有 0
颗卫星。
通过相应的键访问映射的值通常比在大型列表中(例如使用 indexOf()
)搜索值更快。
您可以使用 mapOf()
或 mutableMapOf()
函数声明映射。映射需要两个泛型类型(以英文逗号隔开),一个用于键,另一个用于值。
如果映射具有初始值,则还可以使用类型推断。要使用初始值填充映射,每个键值对都由以下部分组成:首先是键,后跟 to
运算符,而后是值。每个键值对均以英文逗号隔开。
接下来,我们详细了解一下如何使用映射,以及一些有用的属性和方法。
- 从
main()
中移除现有代码。 - 使用
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
)
- 与列表和集一样,
Map
提供包含键值对数量的size
属性。输出solarSystem
映射的大小。
println(solarSystem.size)
- 您可以使用下标语法设置其他键值对。将
"Pluto"
键的值设置为5
。
solarSystem["Pluto"] = 5
- 插入元素后,再次输出大小。
println(solarSystem.size)
- 您可以使用下标语法来获取值。输出键
"Pluto"
的卫星数量。
println(solarSystem["Pluto"])
- 您还可以使用
get()
方法访问值。无论您使用下标语法还是调用get()
,您传入的键都可能不存在于映射中。如果没有键值对,它将返回 null。输出"Theia"
的卫星数量。
println(solarSystem["Theia"])
- 运行代码。系统应该会输出 Pluto 的卫星数量。不过,由于 Theia 不在映射中,因此调用
get()
会返回 null。
8 9 5 null
remove()
方法可移除具有指定键的键值对。它也会返回已移除的值,或者如果指定的键不在映射中,则返回 null
。
- 输出调用
remove()
并传入"Pluto"
的结果。
solarSystem.remove("Pluto")
- 若要验证相应项是否已移除,请再次输出大小。
println(solarSystem.size)
- 运行代码。移除该条目后,映射的大小将为 8。
... 8
- 下标语法或
put()
方法也可以修改已存在的键的值。使用下标语法将 Jupiter 的卫星数量更新为 78,并输出新值。
solarSystem["Jupiter"] = 78
println(solarSystem["Jupiter"])
- 运行代码。现有键
"Jupiter"
的值已更新。
... 78
6. 总结
恭喜!您已经了解了编程中的一种最基本的数据类型(即数组),以及一些基于数组构建的便捷集合类型(包括 List
、Set
和 Map
)。您可以在代码中使用这些集合类型对值进行分组和整理。数组和列表可让您通过索引快速访问元素,而集和映射使用哈希代码来让您更轻松地查找集合中的元素。您将看到未来的应用中会经常用到这些集合类型,了解如何使用这些集合类型对您日后的编程生涯大有裨益。
总结
- 数组存储的是同一类型的有序数据,且具有固定大小。
- 数组用于实现很多其他集合类型。
- 列表是可调整大小的有序集合。
- 集是无序集合,不能包含重复项。
- 映射的工作方式与集类似,用于存储指定类型的键/值对。