1. 概述
本文将深入探讨 Kotlin 中的 Map
集合类型。我们将从 Map 的基本定义和特性讲起,接着介绍如何创建 Map。
后续内容会覆盖常见的操作场景:读取条目、修改数据以及集合转换等实战技巧。对于有经验的开发者来说,这些是日常编码中高频使用的技能,掌握它们能显著提升代码可读性和执行效率。
2. Kotlin 中的 Map 类型
Map 是计算机科学中最常用的数据结构之一,在其他语言中也被称为字典(dictionary)或关联数组(associative array)。它用于存储零个或多个键值对(key-value pairs)。
✅ 核心特性:
- 每个 key 在 Map 中必须唯一,且只能对应一个 value
- 同一个 value 可以被多个 key 引用
Kotlin 提供了标准的 Map
接口,支持泛型定义:
interface Map<K, out V>
其中,每个键值对被称为 Entry,由 Entry
接口表示:
interface Entry<out K, out V>
⚠️ 重要提示:默认的 Map
实例是不可变的(immutable),创建后无法添加、删除或修改条目。
如果需要可变操作,应使用 MutableMap
:
interface MutableMap<K, V> : Map<K, V>
底层通常基于哈希实现,查找和插入时间复杂度接近 O(1),因此即使处理大量数据也能保持高效性能。这也是为什么 Map 成为高性能编程中的首选结构之一。
3. 创建 Map
Kotlin 标准库提供了多种 Map 实现,最常用的是 LinkedHashMap
和 HashMap
。
区别在于:
HashMap
:不保证遍历顺序LinkedHashMap
:维护插入顺序 ✅(适合需要稳定输出顺序的场景)
虽然可以直接通过构造函数实例化:
val iceCreamInventory = LinkedHashMap<String, Int>()
iceCreamInventory["Vanilla"] = 24
但更推荐使用 Collections API 提供的工厂方法,简洁且语义清晰。
3.1. 工厂函数构建
使用 mapOf
和 mutableMapOf
可以在一行内完成声明与初始化:
val iceCreamInventory = mapOf("Vanilla" to 24, "Chocolate" to 14, "Rocky Road" to 7)
📌 注意:
mapOf
返回不可变 Map ❌ 无法后续修改mutableMapOf
返回可变 Map ✅ 支持增删改
建议优先返回不可变对象,除非明确需要修改 —— 这符合函数式编程的最佳实践,减少副作用。
3.2. 条件化初始化 Map
有时我们希望根据条件决定是否将某个键值对加入 Map。例如有以下四个候选 Pair:
val chocolatePair = "Chocolate" to 3
val strawberryPair = "Strawberry" to 7
val vanillaPair = "Vanilla" to 5
val rockyRoadPair = "Rocky Road" to 10
要求:
"Chocolate"
和"Strawberry"
必须加入"Vanilla"
和"Rocky Road"
仅当值 > 5 时才加入
由于 vanillaPair.value == 5
,不符合条件,所以最终结果应为:
val expectedMap = mapOf(chocolatePair, strawberryPair, rockyRoadPair)
方案一:结合 takeIf
与 listOfNotNull
利用 takeIf
在条件不满足时返回 null,并用 listOfNotNull
自动过滤 null 值:
val map1 = listOfNotNull(
chocolatePair,
strawberryPair,
vanillaPair.takeIf { it.second > 5 },
rockyRoadPair.takeIf { it.second > 5 }
).toMap()
assertEquals(expectedMap, map1)
💡 技巧点:
it.second
表示 Pair 的第二个元素(即 value)takeIf{}
条件失败 → 返回 null → 被listOfNotNull
忽略
方案二:使用 buildMap
构建器(推荐)
更直观的方式是使用内置的 DSL 风格构建器:
val map2 = buildMap {
put(chocolatePair.first, chocolatePair.second)
put(strawberryPair.first, strawberryPair.second)
if (vanillaPair.second > 5) {
put(vanillaPair.first, vanillaPair.second)
}
if (rockyRoadPair.second > 5) {
put(rockyRoadPair.first, rockyRoadPair.second)
}
}
assertEquals(expectedMap, map2)
✅ 优势:
- 逻辑清晰,易于调试
- 支持任意复杂判断条件
- 是官方推荐的灵活初始化方式
3.3. 使用 Kotlin 函数式 API 生成 Map
Kotlin 的集合 API 极大简化了从其他结构转换为 Map 的过程。
假设我们有一组冰淇淋发货记录:
data class IceCreamShipment(val flavor: String, val quantity: Int)
val shipments = listOf(
IceCreamShipment("Chocolate", 3),
IceCreamShipment("Strawberry", 7),
IceCreamShipment("Vanilla", 5),
IceCreamShipment("Chocolate", 5),
IceCreamShipment("Vanilla", 1),
IceCreamShipment("Rocky Road", 10),
)
目标:统计每种口味的总库存量。
传统写法(不推荐)
val iceCreamInventory = mutableMapOf<String, Int>()
for (shipment in shipments) {
val currentQuantity = iceCreamInventory[shipment.flavor] ?: 0
iceCreamInventory[shipment.flavor] = currentQuantity + shipment.quantity
}
虽然可行,但代码冗长,且暴露了可变状态。
函数式写法(推荐)
val iceCreamInventory = shipments
.groupBy({ it.flavor }, { it.quantity })
.mapValues { it.value.sum() }
拆解说明:
groupBy(keySelector, valueTransform)
:按 flavor 分组,提取 quantity 列表mapValues{}
:将每个分组的 quantity 列表求和
🎯 如果你知道原始列表中每个 key 不重复,也可以用 associateBy
或直接 map
转换。
⚠️ 踩坑提醒:不要为了“先填充再返回”而滥用
mutableMapOf
。大多数情况下,都可以用函数式组合替代,写出更安全、更易测试的代码。
4. 访问 Map 条目
获取值的标准方式是 get()
方法,Kotlin 还支持中括号语法糖:
val map = mapOf("Vanilla" to 24)
assertEquals(24, map.get("Vanilla"))
assertEquals(24, map["Vanilla"]) // 等价
但要注意异常处理策略的选择:
方法 | 行为 | 推荐场景 |
---|---|---|
getValue(key) |
key 不存在时抛出 NoSuchElementException |
明确知道 key 存在 |
getOrElse(key) { default } |
不存在时执行 lambda 返回默认值 | 需要日志/计算默认值 |
getOrDefault(key, default) |
直接返回传入的默认值 | 默认值简单固定 |
示例:
// ❌ 抛异常
assertThrows(NoSuchElementException::class.java) { map.getValue("Banana") }
// ✅ 自定义逻辑 + 返回默认值
assertEquals(0, map.getOrElse("Banana") {
println("Warning: Flavor not found in map")
0
})
// ✅ 最简形式
assertEquals(0, map.getOrDefault("Banana", 0))
📌 建议:生产环境慎用 getValue
,避免意外崩溃;优先使用带默认值的方法。
5. 添加与更新条目
仅限 MutableMap
,可通过 put
或 []
添加新条目:
val iceCreamSales = mutableMapOf<String, Int>()
iceCreamSales.put("Chocolate", 1)
iceCreamSales["Vanilla"] = 2
批量添加支持两种方式:
// 方式一:putAll
iceCreamSales.putAll(setOf("Strawberry" to 3, "Rocky Road" to 2))
// 方式二:+= 操作符(更简洁)
iceCreamSales += mapOf("Maple Walnut" to 1, "Mint Chocolate" to 4)
⚠️ 所有上述方法都会覆盖已有 key 的值。若需合并旧值(如累加销量),应使用 merge
:
val iceCreamSales = mutableMapOf("Chocolate" to 2)
iceCreamSales.merge("Chocolate", 1, Int::plus) // old + new
assertEquals(3, iceCreamSales["Chocolate"])
参数解释:
- 第一个参数:key
- 第二个参数:要合并的新值
- 第三个参数:合并函数(remapping function)
适用于计数、累加、字符串拼接等场景。
6. 删除条目
MutableMap
提供删除能力:
val map = mutableMapOf("Chocolate" to 14, "Strawberry" to 9)
map.remove("Strawberry")
map -= "Chocolate" // 等价于 remove
assertNull(map["Strawberry"])
assertNull(map["Chocolate"])
✅ 注意:
- 删除不存在的 key 不会抛异常 ✅ 安全调用
- 可使用
-=
操作符实现相同效果 clear()
方法可清空整个 Map
7. Map 的转换操作
Kotlin 提供丰富的高阶函数来变换 Map 数据。以下示例基于初始库存数据:
val inventory = mutableMapOf(
"Vanilla" to 24,
"Chocolate" to 14,
"Strawberry" to 9,
)
7.1. 过滤(Filtering)
常用过滤方法:
filterKeys {}
:按 key 过滤filterValues {}
:按 value 过滤filter {}
:同时判断 key 和 value
示例:找出库存大于 10 的口味
val lotsLeft = inventory.filterValues { qty -> qty > 10 }
assertEquals(setOf("Vanilla", "Chocolate"), lotsLeft.keys)
反之可用 filterNot
排除符合条件的项。
7.2. 映射(Mapping)
map
将每个 Entry 转换为新类型,返回 List<T>
:
val asStrings = inventory.map { (flavor, qty) -> "$qty tubs of $flavor" }
assertTrue(asStrings.containsAll(
setOf("24 tubs of Vanilla", "14 tubs of Chocolate", "9 tubs of Strawberry")
))
assertEquals(3, asStrings.size)
📌 解构语法 (flavor, qty)
让代码更清晰。
7.3. 遍历操作:forEach
forEach
对每个条目执行动作,常用于副作用操作(如更新状态)。
场景:一天营业结束后,根据销售和进货数据更新库存:
val sales = mapOf("Vanilla" to 7, "Chocolate" to 4, "Strawberry" to 5)
val shipments = mapOf("Chocolate" to 3, "Strawberry" to 7, "Rocky Road" to 5)
with(inventory) {
sales.forEach { merge(it.key, it.value, Int::minus) } // 销售:减去
shipments.forEach { merge(it.key, it.value, Int::plus) } // 进货:加上
}
// 验证结果
assertEquals(17, inventory["Vanilla"]) // 24 - 7 + 0
assertEquals(13, inventory["Chocolate"]) // 14 - 4 + 3
assertEquals(11, inventory["Strawberry"]) // 9 - 5 + 7
assertEquals(5, inventory["Rocky Road"]) // 0 - 0 + 5
📌 使用 with(inventory)
减少重复前缀,提升可读性。
💡 这个例子展示了 Kotlin 如何用几行代码完成原本需要循环+条件判断的复杂逻辑。
8. 总结
本文系统介绍了 Kotlin 中 Map 的使用方式,涵盖创建、访问、修改和转换等核心操作。
关键要点回顾:
- 优先使用不可变 Map(
mapOf
),必要时才用mutableMapOf
- 初始化条件化数据推荐
buildMap {}
- 集合转换优先考虑函数式 API(
groupBy
,mapValues
等) - 更新已有条目用
merge
而非直接put
- 遍历时善用
forEach
+with
提升表达力
Kotlin 的集合 API 设计精良,远不止文中所列功能。建议经常查阅官方文档:Map API 文档
所有示例代码及扩展案例已上传至 GitHub:
👉 https://github.com/Baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-collections-2