1. 概述

在 Kotlin 中,我们可以通过交换键值对的方式来反转一个 Map。这种操作在某些场景下非常实用,比如我们需要根据值来查找键时,而不是传统的通过键找值。

本文将介绍多种在 Kotlin 中反转 Map 的方法,并讨论如何处理 Map 中存在重复值的情况。


2. 问题简介

举个例子,我们有如下表示商品价格的 Map:

val priceMap = mapOf(
    "Apple" to 1.99,
    "Orange" to 3.49,
    "Milk" to 1.79,
    "Pizza" to 4.99,
)

我们的目标是将其反转,将 Map<String, Double> 转换为 Map<Double, String>,期望结果如下:

val expectedMap = mapOf(
    1.99 to "Apple",
    3.49 to "Orange",
    1.79 to "Milk",
    4.99 to "Pizza",
)

接下来,我们将使用不同的方式实现这一目标,并用单元测试验证结果。


3. 使用 map()toMap()

这是最常见的一种方式:

val result = priceMap.map { (k, v) -> v to k }.toMap()
assertEquals(expectedMap, result)

实现原理:

  • map():将每个键值对 (K, V) 转换为 (V, K)Pair
  • toMap():将 List<Pair<V, K>> 转换为 Map<V, K>

✅ 简洁明了,适合键值唯一的情况。


4. 使用 associateBy()associate()

Kotlin 提供了 associateBy()associate() 函数,可以将 Iterable 转换为 Map

方法一:associateBy

val result = priceMap.entries.associateBy({ it.value }) { it.key }
assertEquals(expectedMap, result)

方法二:associate

val result = priceMap.entries.associate { (k, v) -> v to k }
assertEquals(expectedMap, result)

区别:

  • associateBy 需要分别提供 key 和 value 的映射函数。
  • associate 直接返回一个 Pair

✅ 两种方式都简洁,适合大多数场景。


5. 手动创建 MutableMap 并使用 put()

如果你希望更显式地控制反转过程,也可以手动创建一个 MutableMap

val result = mutableMapOf<Double, String>().apply {
    priceMap.forEach { (k, v) -> put(v, k) }
}.toMap()
assertEquals(expectedMap, result)

特点:

  • 更直观,适合需要在插入时添加额外逻辑的场景。
  • 最后调用 toMap() 可以将 MutableMap 转为不可变 Map。

✅ 适合需要控制每一步操作的场景。


6. 处理重复值的情况

到目前为止,我们假设原始 Map 中的值都是唯一的。但现实中,可能存在多个键对应相同值的情况。

示例:

val priceMap2 = mapOf(
    "Apple" to 1.99,
    "Orange" to 3.49,
    "Milk" to 1.79,
    "Pizza" to 4.99,
    "Egg" to 1.99,
    "Strawberry" to 3.49,
    "Chicken" to 4.99,
    "Grape" to 4.99,
)

此时,如果直接使用上面的方法反转,后面的键会覆盖前面的键


6.1. 使用 associate() 时的覆盖行为

val result = priceMap2.entries.associate { (k, v) -> v to k }
val overwrittenMap = mapOf(
    1.99 to "Egg",
    3.49 to "Strawberry",
    1.79 to "Milk",
    4.99 to "Grape",
)
assertEquals(overwrittenMap, result)

⚠️ 注意:这种方式会丢失部分原始数据。


6.2. 创建 reverse() 扩展函数返回 Result<Map>

为了防止数据丢失,我们可以封装一个扩展函数,当检测到重复值时抛出异常:

fun <K, V> Map<K, V>.reverse(): Result<Map<V, K>> {
    return runCatching {
        this.entries.associate { (k, v) -> v to k }
            .also { reversedMap ->
                require(this.size == reversedMap.size) {
                    "Reversing failed, the map contains duplicated values"
                }
            }
    }
}

使用示例:

val result1 = priceMap.reverse()
assertTrue(result1.isSuccess)
assertEquals(expectedMap, result1.getOrThrow())

val result2 = priceMap2.reverse()
assertTrue(result2.isFailure)
assertThrows<IllegalArgumentException> { result2.getOrThrow() }

✅ 这种方式适用于不允许丢失数据的场景。


6.3. 将重复值的键分组为 List

如果我们希望保留所有原始键,可以将值映射为键对应的 List<K>

val expectedMap2 = mapOf(
    1.99 to listOf("Apple", "Egg"),
    3.49 to listOf("Orange", "Strawberry"),
    1.79 to listOf("Milk"),
    4.99 to listOf("Pizza", "Chicken", "Grape"),
)

实现方式如下:

val result = priceMap2.toList().groupBy { it.second }
    .mapValues { entry -> entry.value.map { it.first } }
assertEquals(expectedMap2, result)

实现步骤:

  1. toList():将 Map 转为 List<Pair<K, V>>
  2. groupBy():按值分组。
  3. mapValues():将每组的键提取为 List

✅ 适用于需要保留所有原始键的场景。


7. 总结

本文介绍了在 Kotlin 中反转 Map 的几种常见方式:

方法 是否处理重复值 适用场景
map() + toMap() 简单反转,值唯一
associate() / associateBy() 简洁,值唯一
MutableMap + put() 控制反转过程
reverse() 扩展函数 ✅(抛异常) 不允许数据丢失
groupBy() + mapValues() ✅(分组) 保留所有键

根据业务需求选择合适的方法,避免数据丢失或逻辑错误。

完整代码示例可在 GitHub 获取。


原始标题:Reversing a Map in Kotlin