Kotlin 为 Map 提供的那些默认值相关的扩展,你用过吗?

Map 的 Value 类型是一个可空类型,Kotlin 早就想好了怎么帮你优雅地面对它。

Map 是我们经常用到的集合框架的一种,Java 标准库当中提供的 Map 的实现也是比较好用的。不过 Kotlin 为 Map 提供了几处默认值相关的扩展,让 Map 的使用变得更加轻松,不知道大家有没有注意到呢?

1. getOrElse

这个比较简单,我们先来看下它的定义:

1
2
3
4
inline fun <K, V> Map<K, V>.getOrElse(
key: K,
defaultValue: () -> V
): V = get(key) ?: defaultValue()

如果没有元素 key,那么就返回默认值,默认值通过对参数中的 defaultValue 进行求值得到。当然,如果不需要默认值,那么这个求值过程也是不会发生的。

这种情况比较适合空值的语义与默认值相同的情况,例如我用 Map 中的值做为某种配置:

1
2
val config = HashMap<String, Boolean>()
val isEnabled = config.getOrElse("isEnabled", { false })

2. getOrPut

需要注意的是,getOrElse 的调用过程中 Map 没有被修改,即默认值并没有真正成为 Map 的元素。如果我们有下面的需求,那就要考虑使用 getOrPut 了:

我们有一个事件回调接口,不同事件用 eventId 来区分:

1
2
3
interface OnEventListener {
fun onEvent(eventId: String, data: String)
}

我们需要提供对事件回调注册的能力:

1
2
3
4
5
val eventListeners = HashMap<String, ArrayList<OnEventListener>>()

fun addOnEventListener(eventId: String, listener: OnEventListener) {
...
}

添加 listener 的思路也很简单,先看看 eventListeners 当中有没有对应的 eventId 的事件回调 list,如果有,直接添加;如果没有,先构造一个 list 实例,然后再添加。所以最为朴素的实现就是下面这样:

1
2
3
4
5
6
7
8
fun addOnEventListener(eventId: String, listener: OnEventListener) {
var listenerList = eventListeners[eventId]
if (listenerList == null) {
listenerList = ArrayList()
eventListeners[eventId] = listenerList
}
listenerList.add(listener)
}

但这个也太不 Kotlin 了。

好在我们有 getOrPut,它提供了在 Map 中不存在对应的 Key 时返回默认值并将默认值添加到 Map 中的能力,它的实现非常直接:

1
2
3
4
5
6
7
8
9
10
11
12
inline fun <K, V> MutableMap<K, V>.getOrPut(
key: K, defaultValue: () -> V
): V {
val value = get(key)
return if (value == null) {
val answer = defaultValue()
put(key, answer)
answer
} else {
value
}
}

既然如此,我们就可以简化 addOnEventListener 的实现了:

1
2
3
fun addOnEventListener(eventId: String, listener: OnEventListener) {
eventListeners.getOrPut(eventId, defaultCreator).add(listener)
}

3. 隐式默认值

除了在获取时才能确定的默认值以外,还有一个在 Map 定义的时候就可以指定的方式,即:

1
val config = HashMap<String, Boolean>().withDefault { false }

这个也被称为隐式默认值,它的效果与 getOrElse 一致,在获取某一个不存在的 Key 时,直接返回默认值表达式的求值结果,在上面的例子当中就是 { false } 的求值结果了。

不过如果大家用过这个功能的话,应该一开始都会感到比较疑惑,例如:

1
val isEnabled = config["isEnabled"] // null

用于此时我们的 config 只是一个空 Map,因此不存在 isEnabled 这个 Key,按照我们的直觉,这时应该触发默认值的求值过程并返回 false 对吧?但实际上不是这样的,标准库 API 的设计者为了确保对应的实现复合 Map 的接口定义,在我们调用 Map 接口的方法时,行为与普通的 Map 保持一致,因此 config["isEnabled"] 或者说等价的 config.get("isEnabled") 调用并不会触发默认值的求值。

那我想要获得默认值要怎么做呢?

调用另外的一个扩展方法:

1
val isEnabled = config.getValue("isEnabled") // false

4. 默认值的提供方式

前面提到的三种方式中,默认值都是通过一个函数提供的,这样做有什么好处呢?

其实如果 Map 的 Value 类型是不可变的数据类型,那么直接使用一个默认值即可,例如 Boolean、String 这样的基本类型。但对于可变的类型,例如前面例子中的 ArrayList,提供统一的默认值显然是行不通的,对象被不同的 Key 共享必然会造成逻辑的混乱。

还有一个原因,函数可以共享,只需要创建一个统一的对象,每次使用的时候复用即可,默认值本身并不总是需要,自然也并不总是需要创建出来,因此这里采用延迟计算还可以减少对象创建的成本。

5. 小结

好啦,这一篇文章没有什么有难度的地方,源码大家一看就明白,使用时只要注意其中的细节即可。


Kotlin 协程对大多数初学者来讲都是一个噩梦,即便是有经验的开发者,对于协程的理解也仍然是懵懵懂懂。如果大家有同样的问题,不妨阅读一下我的新书《深入理解 Kotlin 协程》,彻底搞懂 Kotlin 协程最难的知识点:

扫描二维码或者点击链接《深入理解 Kotlin 协程》购买本书


如果大家想要快速上手 Kotlin 或者想要全面深入地学习 Kotlin 的相关知识,可以关注我基于 Kotlin 1.3.50 全新制作的新课,课程第一版曾帮助3000多名同学掌握 Kotlin,这次更新回归内容更精彩:

扫描二维码或者点击链接《Kotlin 入门到精通》即可进入课程


Android 工程师也可以关注下《破解Android高级面试》,这门课涉及内容均非浅尝辄止,除知识点讲解外更注重培养高级工程师意识,目前已经有近 1000 位同学在学习:

扫描二维码或者点击链接《破解Android高级面试》即可进入课程