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)
实现步骤:
toList()
:将 Map 转为List<Pair<K, V>>
。groupBy()
:按值分组。mapValues()
:将每组的键提取为List
。
✅ 适用于需要保留所有原始键的场景。
7. 总结
本文介绍了在 Kotlin 中反转 Map 的几种常见方式:
方法 | 是否处理重复值 | 适用场景 |
---|---|---|
map() + toMap() |
❌ | 简单反转,值唯一 |
associate() / associateBy() |
❌ | 简洁,值唯一 |
MutableMap + put() |
❌ | 控制反转过程 |
reverse() 扩展函数 |
✅(抛异常) | 不允许数据丢失 |
groupBy() + mapValues() |
✅(分组) | 保留所有键 |
根据业务需求选择合适的方法,避免数据丢失或逻辑错误。
完整代码示例可在 GitHub 获取。