简介
在上一章中,我们看到了集合(如列表和集合)如何用于完成单独 individual 变量无法轻松完成的事情。在本章中,我们将介绍另一种集合——映射。
合适的工具做合适的工作
”你必须使用正确的工具来完成工作。”这是 Andrews 先生教他年幼的儿子 Jim 的,Jim 刚开始学习如何像他父亲一样成为一个杂务工。”当你要钉钉子时,你需要使用锤子,而不是螺丝刀。”
为了帮助 Jim 挑选正确的工具,他画了一张桌子,上面列出了他工具箱里不同的五金工具和工具。
”现在,有了这张表,你就可以轻松查找所需的工具。只需扫描左列中你需要使用的五金工具,然后扫过右列查看要使用的正确工具即可。”
这样的表格表明事物之间存在关联——钉子与锤子关联,六角螺母与扳手关联,以此类推。在本章中,我们将构建 Kotlin 中类似的表格,但在此之前,让我们先创建一个单一的关联。
关联数据
关联两个值的一种简单方法是使用名为 Pair 的类。这个类的构造方法有两个参数。你可以调用它的构造方法,传入任意两个对象,无论它们的类型如何。在我们的例子中,让我们关联两个 String 对象——一个代表钉子,一个代表锤子。
val association = Pair("Nail", "Hammer")Pair 是一个非常简单的类,有两个属性 first 和 second,你可以用它们来获取里面的值。下面是一个显示 Pair 类的 UML 图。first 和 second 的类型取决于调用构造方法时传入参数的类型,所以在该图中,我们只使用 A 和 B 作为实际类型的占位符。
first 属性就是你调用构造方法时的第一个参数,而 second 就是第二个参数。
println(association.first) // Nail
println(association.second) // Hammer简单,对吧?
现在,有时使用名为 to() 的函数会更自然,它会为你调用 Pair 构造方法,而不是直接调用 Pair 类的构造方法。这个函数可以针对任何对象调用。让我们更新代码,使用 to() 函数。
val association = "Nail".to("Hammer")阅读这段代码时,你可能会说:”当我有一根钉子时,我应该to一把锤子。”清单 9.1 和 清单 9.3 做的是相同的事情——它们都创建一个 Pair,其中左边的值赋给名为 first 的属性,右边的值赋给名为 second 的属性。
to() 函数也有一个特殊的特性,让你可以在不使用标点符号的情况下使用它!所以,你也可以像这样创建同样的 Pair:
val association = "Nail" to "Hammer"注意,这和上面的清单 9.3是一样的,只是 .、( 和 ) 都省略了。当一个函数允许你以这种方式调用时,它被称为中缀函数。我们不会经常看到中缀函数,但了解它们的存在很重要,这样当看到这样的代码时就不会感到困惑。
到目前为止,我们使用了类型推断,这样就不必写出 association 变量的类型了。和上一章中的 List 和 Set 一样,Pair 变量的类型取决于它所包含的事物的类型。由于 “Nail” 和 “Hammer” 都是 String 类型,因此 association 变量的类型是 Pair<String, String>。
当然,我们也可以像这样显式指定类型:
val association: Pair<String, String> = "Nail" to "Hammer"既然我们已经成功创建了一个单一的关联,我们就可以对其余的工具做同样的事情……然后将它们全部放入一个映射中!
映射基础
让我们再看看 Andrews 先生的表格。
在 Kotlin 中,这样的表格称为映射(map)。你可能熟悉街道地图和藏宝图等地图,但我们这里说的不是这些。这个术语来自数学领域,映射定义了集合元素之间的对应关系。类似地,Kotlin 映射定义了左列中的每个条目与右列中相应条目之间的关联。
在我们创建第一个映射之前,先来了解几个重要的术语:
- 表格左列中的项目称为键(keys)。
- 右列中的项目称为值(values)。
- 映射中键和值的关联称为映射条目(entry)。
有一条重要规则需要记住——映射中的每个键都是唯一的。然而,值可以重复。换句话说,左列中不能有重复的条目,但右列中可以。你可以在下表中看到这一点,左列的条目是唯一的,但扳手在右列中出现了两次。
使用 mapOf() 创建映射
既然我们已经理解了主要概念,是时候创建我们的第一个映射了!为此,我们将使用 mapOf() 函数,传入一个我们想要在映射中的每个关联的 Pair。
val toolbox = mapOf(
"Nail" to "Hammer",
"Hex Nut" to "Wrench",
"Hex Bolt" to "Wrench",
"Slotted Screw" to "Slotted Screwdriver",
"Phillips Screw" to "Phillips Screwdriver",
)如果你读过上一章,你会发现这看起来类似于 listOf() 和 setOf(),只是这里的所有元素都有两部分——键和值,它们在 Pair 中连接在一起。
当你把 Kotlin 映射和 Andrews 先生的表格并排放在一起时,你可以看到它们之间的相似之处:
就像列表、集合和其他变量一样,你可以使用 println() 来打印映射的内容。
println(toolbox)当你打印时,映射的条目会出现在花括号之间。键在等号的左边,值在右边。
{Nail=Hammer, Hex Nut=Wrench, Hex Bolt=Wrench, Slotted Screw=Slotted Screwdriver, Phillips Screw=Phillips Screwdriver}
就像 Pair 一样,Map 变量的类型取决于键的类型和值的类型。
因此,我们可以像这样显式地写出类型:
val toolbox: Map<String, String> = mapOf(
"Nail" to "Hammer",
"Hex Nut" to "Wrench",
"Hex Bolt" to "Wrench",
"Slotted Screw" to "Slotted Screwdriver",
"Phillips Screw" to "Phillips Screwdriver",
)查找值
使用映射时最常见的事情是查找值。例如,当 Jim 有一颗钉子时,他需要查找该使用哪种工具。就像 Jim 会在左列中找到钉子,然后在旁边找到相应的工具一样,Kotlin 可以在你提供键时给你对应的值。你可以使用 get() 函数来完成这项工作。
val tool = toolbox.get("Nail")
println(tool) // Hammer与列表类似,你也可以使用映射的下标访问操作符>来获取值。
val tool = toolbox["Nail"]
println(tool) // Hammer如果你用不存在的键调用 get()(或使用下标访问操作符),它会返回 null。这意味着 get() 函数返回一个可空类型而不是非空类型!在清单 9.9 和 9.10 中,它返回 String? 而不是 String。
你可以使用在第 6 章>中学到的空安全工具(如空合并操作符>)将其恢复为非空类型。或者,你可以调用 getValue() 而不是 get()。getValue() 将返回非空类型,但要注意——如果你给它一个不存在的键,你会看到错误信息,你的代码将停止运行。
val tool = toolbox.getValue("Nail")
println(tool) // Hammer
val anotherTool = toolbox.getValue("Wing Nut") // Error at runtime你也可以使用 getOrDefault() 在键不存在时提供默认值。如果 Andrews 先生没有某种五金件对应的工具,他就只能用手拧紧它了!
val tool = toolbox.getOrDefault("Hanger Bolt", "Hand")修改映射
与其他集合类型一样,映射有两种可变性格式——MutableMap 和不可变的 Map。可变类型允许你更改其内容,而不可变映射则要求你创建一个新的映射实例,可以将其分配给新的或现有的变量。
让我们先看看如何更改 MutableMap。首先,我们需要使用 mutableMapOf() 来创建映射,而不是像在清单 9.6中那样只是用 mapOf()。
val toolbox = mutableMapOf(
"Nail" to "Hammer",
"Hex Nut" to "Wrench",
"Hex Bolt" to "Wrench",
"Slotted Screw" to "Slotted Screwdriver",
"Phillips Screw" to "Phillips Screwdriver",
)要添加新条目,可以使用 put() 函数,其中第一个参数是键,第二个参数是值。
toolbox.put("Lumber", "Saw")不过,就像 get() 函数一样,Kotlin 开发者通常使用下标访问操作符而不是直接调用 put() 函数。下面的代码实现与清单 9.14相同的功能。
toolbox["Lumber"] = "Saw"你也可以用完全相同的方式更改现有值。只需提供一个已存在的键。
toolbox["Hex Bolt"] = "Nut Driver"最后,你可以使用 remove() 函数删除条目。
toolbox.remove("Lumber")请注意,虽然你可以更改值,但不能更改键。相反,你可以删除一个键并插入一个新条目。
toolbox.remove("Phillips Screw")
toolbox["Cross Recess Screw"] = "Phillips Screwdriver"不可变映射
与不可变列表和集合一样,你可以在不可变映射上使用加法和减法操作符。请记住,这样做会创建新的映射实例,你通常会将其分配给一个变量。下面的代码演示了与上面相同的操作,但是在不可变映射上。请注意,我们在这里使用 var 关键字,这样我们就可以将每个结果重新分配给同一个 toolbox 变量!
var toolbox = mapOf(
"Nail" to "Hammer",
"Hex Nut" to "Wrench",
"Hex Bolt" to "Wrench",
"Slotted Screw" to "Slotted Screwdriver",
"Phillips Screw" to "Phillips Screwdriver",
)
// Add an entry
toolbox = toolbox + Pair("Lumber", "Saw")
// Update an entry
toolbox = toolbox + Pair("Hex Bolt", "Nut Driver")
// Remove an entry
toolbox = toolbox - "Lumber"
// Simulate changing a key
toolbox = toolbox - "Phillips Screw"
toolbox = toolbox + Pair("Cross Recess Screw", "Phillips Screwdriver")映射操作
与 List 和 Set 一样,Map 对象也有可以对其执行的操作,其中一些看起来非常熟悉。让我们从 forEach() 函数开始。
forEach()
forEach() 函数与 List 和 Set 对象上的函数几乎相同。它接受一个 lambda,你可以用它对映射中的每个条目做些什么。由于映射存储的是条目,lambda 的参数将是 Map.Entry 类型。
Map.Entry 与本章前面介绍的 Pair 类非常相似——它有两个属性,但不是叫 first 和 second,而是叫 key 和 value。
以下是你如何在 Map 上使用 forEach() 函数的方法。
toolbox.forEach { entry ->
println("Use a ${entry.value} on a ${entry.key}")
}当你运行这段代码时,你会看到以下输出。
Use a Hammer on a Nail
Use a Wrench on a Hex Nut
Use a Wrench on a Hex Bolt
Use a Slotted Screwdriver on a Slotted Screw
Use a Phillips Screwdriver on a Phillips Screw
因为它与你上一章看到的 forEach() 非常相似,你应该能够识别主要部分。它们是:
过滤
与列表和集合类似,你可以对映射进行 filter 过滤。请记住,就像我们之前在列表中看到的那样,这个函数不会修改现有映射——它会创建一个新的映射实例,所以你需要将结果分配给一个变量。
让我们把工具箱过滤到只剩下螺丝刀。
val screwdrivers = toolbox.filter { entry ->
entry.value.contains("Screwdriver")
}结果是一个只包含螺丝刀的新 Map。
在这个例子中,我们根据 value 进行筛选,但你也可以根据键来筛选。
val screwdrivers = toolbox.filter { entry ->
entry.key.contains("Screw")
}映射转换
是的,你可以对 Map 进行映射转换!只需使用 mapKeys() 和 mapValues() 函数来转换其键或值。就像我们在上一章看到的集合操作一样,你可以创建一个操作链。让我们在一个链中同时对键和值进行映射转换。
val newToolbox = toolbox
.mapKeys { entry -> entry.key.replace("Hex", "Flange") }
.mapValues { entry -> entry.value.replace("Wrench", "Ratchet") }Map 对象还有许多其他操作,你可以在 Kotlin 的API 文档中探索。不过,在继续之前,我们还要再研究一个操作——withDefault()。
设置默认值
正如我们之前看到的,你可以使用 getOrDefault() 来优雅地处理键不存在的情况。然而,如果你每次都使用相同的默认值,这很快就会失控……
val tool = toolbox.getOrDefault("Hanger Bolt", "Hand")
val anotherTool = toolbox.getOrDefault("Dowel Screw", "Hand")
val oneMoreTool = toolbox.getOrDefault("Eye Bolt", "Hand")相反,你可以使用一个名为 withDefault() 的操作,它将基于原始映射返回一个新映射。在这个新映射中,每当你使用一个不存在的键调用 getValue() 时,它会调用一个 lambda 并返回结果。就是这样:
toolbox = toolbox.withDefault { key -> "Hand" }现在,你不需要在每次尝试获取值时都提供默认值(如上面示例 9.24所示),你可以正常调用 getValue()。这很好,因为如果你想更改默认值,只需要在一个地方修改,而不是很多地方!
val tool = toolbox.getValue("Hanger Bolt")
val anotherTool = toolbox.getValue("Dowel Screw")
val oneMoreTool = toolbox.getValue("Eye Bolt")请记住,这适用于 getValue() 但不适用于 get() 或索引访问运算符,如果找不到键,它们将继续返回 null!
现在你知道如何创建和修改映射、如何从中获取值,以及如何使用集合操作。但当你开始将映射与其他集合结合使用时,事情会变得非常有趣!接下来让我们看看这一点。
从列表创建映射
我们使用 mapOf() 函数手动创建映射。也可以基于现有的列表或集合创建映射。通过一些重要的函数,你可以用许多不同的方式来处理你的数据!为了做到这一点,我们当然需要一个列表作为起点。
让我们创建一个类来表示 Andrews 先生工具箱中的工具,而不是使用简单的 String,这样它可以保存工具的名称、以盎司为单位的重量以及它所对应的五金件。
class Tool(
val name: String,
val weightInOunces: Int,
val correspondingHardware: String,
)现在,让我们创建一个 Tool 对象列表,包含 Andrews 先生工具箱中的工具。
val tools = listOf(
Tool("Hammer", 14, "Nail"),
Tool("Wrench", 8, "Hex Nut"),
Tool("Wrench", 8, "Hex Bolt"),
Tool("Slotted Screwdriver", 5, "Slotted Screw"),
Tool("Phillips Screwdriver", 5, "Phillips Screw"),
)现在我们有了列表,准备好从中创建一些映射了!
从对象列表中关联属性
你可以使用 associate() 函数从对象列表创建映射。首先,让我们使用 associate() 创建一个类似于示例 9.6中的映射:
val toolbox = tools.associate { tool ->
tool.correspondingHardware to tool.name
}希望你现在对集合操作感到越来越自在了。对于列表中的每个元素,associate() 函数会调用传递给它的 lambda。该 lambda 返回一个键值 Pair,其中包含你希望在结果映射中存在的键和值。
下面是 associate() 函数的详细解析。
通常,结果映射中的元素数量与原始列表中的元素数量相同。在某些情况下,它可能会更少。因为映射中的键都是唯一的,如果你尝试添加一个已存在的键,它将覆盖现有值。例如,让我们在 示例 9.29 的 lambda 中交换键和值的位置,使工具名称成为键,五金件成为值。
val toolbox = tools.associate { tool ->
tool.name to tool.correspondingHardware
}原始列表有两个 name 为 Wrench 的 Tool 对象,所以当 associate() 遇到第一个时,它被添加到映射中,但当它遇到第二个时,它会替换第一个值。因此,结果映射只包含 Hex Bolt 而不是 Hex Nut,因为两者中 Hex Bolt 是最后出现的。
所以在这种情况下,映射中的条目比原始列表中的元素少。也就是说,映射中只有 4 个条目,而列表中有 5 个元素。
其他关联函数
还有几个其他版本的 associate() 函数值得了解。如果你希望原始列表元素成为结果映射中的键或值,这些会特别有帮助。
例如,如果你想创建一个键为工具名称、值为 Tool 对象的映射,可以使用 associateBy()。这个函数的 lambda 只返回键。原始列表元素本身将是值。
val toolsByName = tools.associateBy { tool -> tool.name }使用这个映射,你可以通过名称轻松获取工具!
val hammer = toolsByName["Hammer"]反过来,如果你想创建一个键为 Tool 对象、值在 lambda 中指定的映射,可以使用 associateWith() 函数。这个函数的 lambda 返回值,原始列表元素将成为键。
val toolWeightInPounds = tools.associateWith { tool ->
tool.weightInOunces * 0.0625
}要获取锤子的重量,你需要已经有一个锤子对象。
val hammerWeightInPounds = toolWeightInPounds[hammer]将列表元素分组到映射列表中
有时候,当你有一个列表时,你想根据某些特征将其拆分为多个较小的列表。例如,我们可以获取 tools 列表并按重量对其进行分组。
为此,我们可以使用 groupBy() 函数。这个函数会为列表中的每个元素运行提供的 lambda。lambda 返回相同结果的元素将被组装成一个列表,并插入到映射中。
val toolsByWeight = tools.groupBy { tool ->
tool.weightInOunces
}结果是一个 Map,包含一个重量为 14 盎司的工具列表、另一个重量为 8 盎司的工具列表和第三个重量为 5 盎司的工具列表。映射的键是重量(盎司),映射的值是具有该重量的工具列表。
下面是 groupBy() 函数的详细解析:
如果你希望在结果列表中获得原始列表元素以外的东西,你也可以使用第二个参数调用此函数。为此,你需要传递一个 lambda,返回你希望在结果列表中需要的任何内容。例如,如果你只想要这些列表中工具的名称,你可以这样做:
val toolNamesByWeight = tools.groupBy(
{ tool -> tool.weightInOunces },
{ tool -> tool.name }
)总结
Jim 在成长为像他父亲一样出色的杂务工的道路上走得很好。通过你在过去九章中获得的知识,你在成长为一名出色的 Kotlin 开发者的道路上也走得很好!以下是您在本章中学到的内容:
- 如何使用
Pair关联两个值。 - 如何使用
mapOf()和mutableMapOf()创建简单映射。 - 如何通过键在映射中查找值。
- 如何修改映射。
- 如何对映射执行操作。
- 如何从现有对象列表中关联键和值。
- 如何从现有对象列表中对元素进行分组。
现在你已经学习了集合(如列表、集合和映射),你在代码中开启了许多可能性。在下一章>中,我们将学习接收者和扩展。