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 实现,最常用的是 LinkedHashMapHashMap

区别在于:

  • HashMap:不保证遍历顺序
  • LinkedHashMap:维护插入顺序 ✅(适合需要稳定输出顺序的场景)

虽然可以直接通过构造函数实例化:

val iceCreamInventory = LinkedHashMap<String, Int>()
iceCreamInventory["Vanilla"] = 24

但更推荐使用 Collections API 提供的工厂方法,简洁且语义清晰。

3.1. 工厂函数构建

使用 mapOfmutableMapOf 可以在一行内完成声明与初始化:

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)

方案一:结合 takeIflistOfNotNull

利用 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() }

拆解说明:

  1. groupBy(keySelector, valueTransform):按 flavor 分组,提取 quantity 列表
  2. 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


原始标题:Working With Maps in Kotlin