我最近一直在用 Kotlin 的 explicit backing field 特性去写代码。
我发现,我的主要目的是缩小外部能力 —— 暴露给外部的接口一定比内部少,这样可以降低代码风险(也就是 Robust)。
例如我暴露给外部的是一个不可变集合,但实际上内部是一个可变集合。
这在 Java 中很难做到。Java 的 List 接口本身就带有 add 和 remove 方法,如果你想写一个不可变集合,只能在这些方法里抛出异常,非常不友好。
于是,我仔细研究了一下 Kotlin 中的集合……
Kotlin 中有哪些集合类型
Kotlin 的集合类型分两种:只读集合和可变集合。
只读集合创建后不能改,可变集合可以增删改。搞清楚这两类,才能在不同场景下选对集合。
只读集合
只读集合只能访问和查询,创建后内容不能改。Kotlin 用这种方式保证数据不被意外修改。
主要的只读集合类型:
List:有序集合,用下标访问元素。
val readOnlyList = listOf("关注", "rockbyte", "公众号")
println(readOnlyList[0]) // 输出: 关注
Set:元素不重复的集合。
val readOnlySet = setOf("kotlin", "java", "kotlin")
println(readOnlySet) // 输出: [kotlin, java]
Map:键值对集合,键不能重复。
val readOnlyMap = mapOf("language" to "kotlin", "platform" to "android")
println(readOnlyMap["language"]) // 输出: kotlin
对了,这里建议各位看下这篇文章 —— Kotlin 准备引入 [1,2,3] 集合字面量,后续我们声明集合,不用这么麻烦了(我指的是写 xxxOf)。
可变集合
可变集合能增删改元素,是在只读集合接口基础上扩展出来的。
可变集合类型:
MutableList:可变的 List,能添加、删除、更新元素。
val mutableList = mutableListOf("rockbyte", "kotlin")
mutableList.add("developer")
println(mutableList) // 输出: [rockbyte, kotlin, developer]
MutableSet:可变的 Set,能动态增删元素。
val mutableSet = mutableSetOf("kotlin", "java")
mutableSet.add("android")
println(mutableSet) // 输出: [kotlin, java, android]
MutableMap:可变的 Map,能修改键值对。
val mutableMap = mutableMapOf("language" to "kotlin")
mutableMap["platform"] = "android"
println(mutableMap) // 输出: {language=kotlin, platform=android}
理解只读和可变集合的可变性
只读集合(listOf、setOf、mapOf)创建后不能用它的方法修改,可变集合(mutableListOf、mutableSetOf、mutableMapOf)可以。
一般来说,数据不需要变就用只读集合,需要动态变化就用可变集合。
但要注意,只读集合不等于不可变。引用本身是只读的,但它和可变集合指向同一个底层对象,内容还是能改。看这个例子:
val mutableList: MutableList<String> = mutableListOf("A", "B", "C")
val readOnlyList: List<String> = mutableList
// 通过只读引用修改会编译报错:
// readOnlyList.add("D") // 不允许
// 但改原始的可变引用,只读引用也会受影响:
mutableList.add("D")
println(readOnlyList) // 输出: [A, B, C, D]
// 甚至可以强转成 MutableList 来改:
(readOnlyList as MutableList<String>).add("E")
println(readOnlyList) // 输出: [A, B, C, D, E]
这里 readOnlyList 虽然是只读的,但 mutableList 一改,它也跟着变了。
要真正不可变,用 kotlinx.collections.immutable 库,或者把内容复制到新集合里。
小结
Kotlin 支持只读和可变两类集合,包括 List、Set、Map。
只读集合保护数据不被意外修改,可变集合方便动态更新。选哪个看具体需求:要安全用只读,要灵活用可变。
进阶:listOf() 和 emptyList()
可能很多小伙伴在声明集合的时候,碰到过 listOf() 和 emptyList(),但是这两有什么区别?
listOf() 和 emptyList() 都能创建只读列表,但用法有点不同。
有意思的是,listOf() 不传参数时,底层其实调的就是 emptyList()。
listOf()
listOf() 是创建只读列表的通用方法,可以传任意多个参数,返回包含这些元素的列表。不传参数就返回空列表,内部调的是 emptyList()。
val nonEmptyList = listOf("关注", "rockbyte", "公众号")
println(nonEmptyList) // 输出: [关注, rockbyte, 公众号]
val emptyUsingListOf = listOf<String>()
println(emptyUsingListOf) // 输出: []
底层实现上,listOf() 先检查元素数量,大于零就直接返回,否则委托给 emptyList():
public fun <T> listOf(vararg elements: T): List<T> =
if (elements.size > 0) elements.asList() else emptyList()
所以 listOf() 不传参数,效果和 emptyList() 一样。
emptyList()
emptyList() 专门用来创建空的只读列表,不接受参数,返回的是单例空列表,针对空列表场景做了优化。
val emptyList = emptyList<String>()
println(emptyList) // 输出: []
明确要创建空列表时,用 emptyList() 语义更清晰,代码更好懂。
底层实现上,emptyList() 是个优化过的工厂方法,返回的是一个预先创建好的单例对象 EmptyList,不会每次调用都新建列表:
public fun <T> emptyList(): List<T> = EmptyList
看看 EmptyList 的实现:
internal object EmptyList : List<Nothing>, Serializable, RandomAccess {
// ... 实现细节 ...
override val size: Int get() = 0
override fun isEmpty(): Boolean = true
override fun contains(element: Nothing): Boolean = false
override fun get(index: Int): Nothing = throw IndexOutOfBoundsException("Empty list doesn't contain element at index $index.")
// ... 其他方法 ...
}
EmptyList 是内部单例对象,整个应用里所有 emptyList() 调用返回的都是同一个实例,非常省内存。
它实现了 List<Nothing> 接口。Nothing 是 Kotlin 的特殊类型,没有任何值,正好适合表示空列表。
因为 List<Nothing> 是任何 List<T> 的子类型,所以 EmptyList 可以安全地转换成任意列表类型(比如 List<String> 或 List<User>)。
EmptyList 的实现就是硬编码成空列表:size 返回 0,isEmpty() 返回 true,get(index) 直接抛 IndexOutOfBoundsException。
一句话总结:
listOf() 是通用方法,能创建包含元素的列表,不传参数时委托给 emptyList()。emptyList() 是专门创建空列表的方法,返回的是优化过的单例实例。要创建空列表,用 emptyList() 语义更清晰,虽然效果和 listOf() 不传参数一样。
一点想法
因为 Kotlin 的只读集合在类型层面就把写操作去掉了。对外暴露 List,对内持有 MutableList,不需要额外包装,也不会像 Java 那样在运行时才抛异常。编译器帮你守住了这道门,调用方想 add 都加不进去。
再配合 explicit backing field,能少写不少样板代码。
这语法糖,喂到嘴里了属于是。