1. 概述

在 Kotlin 中,我们可以通过 map[key] 的方式访问 Map 中的值。但该方式返回的是一个可空类型(nullable),这在我们确定值一定非空的情况下会带来不便。

本文将讨论 Kotlin 中 Map 的 get 运算符行为,并探索如何从 Map 中获取非空值。

2. 问题背景

我们先来看一个 Map 的例子:

val inputMap = mapOf(
  1 to "Macbook pro, MacOS",
  2 to "Dell Laptop, Windows10",
  3 to "HP Laptop, Archlinux",
  4 to "Thinkpad Laptop, Ubuntu Linux",
)

这是一个 Map<Int, String>,存储了一些笔记本信息。我们尝试通过 key 获取值并进行判断:

val result = inputMap[3]
assertTrue { result!!.startsWith("HP Laptop") }

这里我们使用了 !! 操作符强制解包,因为 map[key] 返回的是一个可空类型 V?。这是 Kotlin 中 Map 的标准方法:

public operator fun get(key: K): V?

如果我们尝试访问一个不存在的 key,比如:

val nullResult = inputMap[42]
assertNull(nullResult)

它会返回 null。Kotlin 的空安全机制虽然强大,但在我们明确知道 key 一定存在时,频繁使用 !! 会显得啰嗦。

那么,有没有办法让我们在访问 Map 时直接返回非空值呢?我们来看几个解决方案。

3. 使用 getValue() 方法

Kotlin 的 Map 提供了一个 getValue() 方法,用于获取非空值。如果 key 不存在,它会抛出 NoSuchElementException

我们来看一个测试示例:

val result = inputMap.getValue(3)
assertTrue { result.startsWith("HP Laptop") }

assertThrows<NoSuchElementException> { inputMap.getValue(42) }

这次我们无需使用 !!,因为 getValue() 返回的是非空类型。这在某些场景下非常实用。

但问题在于,getValue() 并不是 map[key] 风格的语法,我们是否可以让 map[key] 也返回非空值呢?

答案是肯定的,我们可以通过重载 get 运算符来实现。

4. 使用匿名对象封装 Map

我们可以通过创建一个匿名对象,并重载其 get 运算符,让其返回非空值:

val laptops = object {
    val theMap = inputMap
    operator fun get(key: Int): String = theMap[key]!!
}

然后进行测试:

val result = laptops[3]
assertTrue { result.startsWith("HP Laptop") }
assertThrows<NullPointerException> { laptops[42] }

✅ 优点:我们实现了 map[key] 返回非空值
❌ 缺点:此时 laptops 的类型是 Any,不再是 Map,无法直接使用 Map 的方法,如 laptops.size 会编译失败

因此,我们需要进一步优化。

5. 创建 NotNullMap 类并委托给 Map

我们可以通过 Kotlin 的委托模式创建一个 NotNullMap 类,让它实现 Map 接口并重载 get 运算符:

class NotNullMap<K, V>(private val map: Map<K, V>) : Map<K, V> by map {
    override operator fun get(key: K): V {
        return map[key]!! // 如果 key 不存在,抛出 NullPointerException
    }
}

使用方式如下:

val laptops = NotNullMap(inputMap)
assertTrue { laptops[3].startsWith("HP Laptop") }
assertThrows<NullPointerException> { laptops[42] }

✅ 优点:

  • 保留了 Map 的所有方法
  • map[key] 返回非空值
  • 可以直接使用 sizekeys 等 Map 属性

为了更方便地初始化 NotNullMap,我们可以提供一个工厂函数:

fun <K, V> notNullMapOf(vararg pairs: Pair<K, V>) = NotNullMap(mapOf(*pairs))

使用方式:

val personHobbies = notNullMapOf(
  "Tom" to setOf("Reading", "hiking"),
  "Jackson" to setOf("Reading", "Football"),
  "Anna" to setOf("Singing", "Tennis")
)

assertEquals(2, personHobbies["Tom"].size)
assertThrows<NullPointerException> { personHobbies["Shirley"] }

这个方案已经非常接近我们理想的状态了。

6. 小结

本文我们探讨了如何从 Map 中获取非空值的几种方式:

方法 是否返回非空 是否保留 Map 接口 是否支持 map[key] 抛出异常类型
map[key] 返回 null
getValue() NoSuchElementException
匿名对象封装 NullPointerException
NotNullMap + 委托 NullPointerException

最终推荐使用 NotNullMap + 委托 + 工厂函数 的方式,既保留了 Map 的功能,又支持了非空访问的语法。

完整示例代码已上传至 GitHub


原始标题:Get a Not Null Value From a Map in Kotlin