Kotlin 集合:只读不等于不可变

我最近一直在用 Kotlin 的 explicit backing field 特性去写代码。

我发现,我的主要目的是缩小外部能力 ------ 暴露给外部的接口一定比内部少,这样可以降低代码风险(也就是 Robust)。

例如我暴露给外部的是一个不可变集合,但实际上内部是一个可变集合。

这在 Java 中很难做到。Java 的 List 接口本身就带有 addremove 方法,如果你想写一个不可变集合,只能在这些方法里抛出异常,非常不友好。

于是,我仔细研究了一下 Kotlin 中的集合......

Kotlin 中有哪些集合类型

Kotlin 的集合类型分两种:只读集合和可变集合。

只读集合创建后不能改,可变集合可以增删改。搞清楚这两类,才能在不同场景下选对集合。

只读集合

只读集合只能访问和查询,创建后内容不能改。Kotlin 用这种方式保证数据不被意外修改。

主要的只读集合类型:

List:有序集合,用下标访问元素。

kotlin 复制代码
val readOnlyList = listOf("关注", "rockbyte", "公众号")
println(readOnlyList[0]) // 输出: 关注

Set:元素不重复的集合。

kotlin 复制代码
val readOnlySet = setOf("kotlin", "java", "kotlin")
println(readOnlySet) // 输出: [kotlin, java]

Map:键值对集合,键不能重复。

kotlin 复制代码
val readOnlyMap = mapOf("language" to "kotlin", "platform" to "android")
println(readOnlyMap["language"]) // 输出: kotlin

对了,这里建议各位看下这篇文章 ------ Kotlin 准备引入 1,2,3 集合字面量,后续我们声明集合,不用这么麻烦了(我指的是写 xxxOf)。

可变集合

可变集合能增删改元素,是在只读集合接口基础上扩展出来的。

可变集合类型:

MutableList :可变的 List,能添加、删除、更新元素。

kotlin 复制代码
val mutableList = mutableListOf("rockbyte", "kotlin")
mutableList.add("developer")
println(mutableList) // 输出: [rockbyte, kotlin, developer]

MutableSet :可变的 Set,能动态增删元素。

kotlin 复制代码
val mutableSet = mutableSetOf("kotlin", "java")
mutableSet.add("android")
println(mutableSet) // 输出: [kotlin, java, android]

MutableMap :可变的 Map,能修改键值对。

kotlin 复制代码
val mutableMap = mutableMapOf("language" to "kotlin")
mutableMap["platform"] = "android"
println(mutableMap) // 输出: {language=kotlin, platform=android}

理解只读和可变集合的可变性

只读集合(listOfsetOfmapOf)创建后不能用它的方法修改,可变集合(mutableListOfmutableSetOfmutableMapOf)可以。

一般来说,数据不需要变就用只读集合,需要动态变化就用可变集合。

但要注意,只读集合不等于不可变。引用本身是只读的,但它和可变集合指向同一个底层对象,内容还是能改。看这个例子:

kotlin 复制代码
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 支持只读和可变两类集合,包括 ListSetMap

只读集合保护数据不被意外修改,可变集合方便动态更新。选哪个看具体需求:要安全用只读,要灵活用可变。

进阶:listOf() 和 emptyList()

可能很多小伙伴在声明集合的时候,碰到过 listOf()emptyList(),但是这两有什么区别?

listOf()emptyList() 都能创建只读列表,但用法有点不同。

有意思的是,listOf() 不传参数时,底层其实调的就是 emptyList()

listOf()

listOf() 是创建只读列表的通用方法,可以传任意多个参数,返回包含这些元素的列表。不传参数就返回空列表,内部调的是 emptyList()

kotlin 复制代码
val nonEmptyList = listOf("关注", "rockbyte", "公众号")
println(nonEmptyList) // 输出: [关注, rockbyte, 公众号]

val emptyUsingListOf = listOf<String>()
println(emptyUsingListOf) // 输出: []

底层实现上,listOf() 先检查元素数量,大于零就直接返回,否则委托给 emptyList()

kotlin 复制代码
public fun <T> listOf(vararg elements: T): List<T> = 
    if (elements.size > 0) elements.asList() else emptyList()

所以 listOf() 不传参数,效果和 emptyList() 一样。

emptyList()

emptyList() 专门用来创建空的只读列表,不接受参数,返回的是单例空列表,针对空列表场景做了优化。

kotlin 复制代码
val emptyList = emptyList<String>()
println(emptyList) // 输出: []

明确要创建空列表时,用 emptyList() 语义更清晰,代码更好懂。

底层实现上,emptyList() 是个优化过的工厂方法,返回的是一个预先创建好的单例对象 EmptyList,不会每次调用都新建列表:

kotlin 复制代码
public fun <T> emptyList(): List<T> = EmptyList

看看 EmptyList 的实现:

kotlin 复制代码
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 返回 0isEmpty() 返回 trueget(index) 直接抛 IndexOutOfBoundsException

一句话总结:

listOf() 是通用方法,能创建包含元素的列表,不传参数时委托给 emptyList()emptyList() 是专门创建空列表的方法,返回的是优化过的单例实例。要创建空列表,用 emptyList() 语义更清晰,虽然效果和 listOf() 不传参数一样。

一点想法

因为 Kotlin 的只读集合在类型层面就把写操作去掉了。对外暴露 List,对内持有 MutableList,不需要额外包装,也不会像 Java 那样在运行时才抛异常。编译器帮你守住了这道门,调用方想 add 都加不进去。

再配合 explicit backing field,能少写不少样板代码。

这语法糖,喂到嘴里了属于是。

相关推荐
风华圆舞2 小时前
一个 Flutter 项目同时保留 Android、iOS、HarmonyOS 支持的实践
android·flutter·ios
顾林海2 小时前
Android来时路:Android 是什么
android
2501_915921432 小时前
uni-app 上架 iOS 的完整流程(无需依赖 Mac)
android·macos·ios·小程序·uni-app·iphone·webview
Che2n3JigW2 小时前
Now in Android Core 模块分析:共享能力是如何被抽离的?
android·architecture·now in android·modularization·core module
黄林晴2 小时前
绝了!Compose Multiplatform 也能实现 iOS26 液态玻璃的效果了
android·kotlin
2601_961767282 小时前
【分享】云视听快TV 快手电视版 手机电视都可以用
android·智能手机
数智工坊11 小时前
机器人运动控制:采样、优化与学习三大流派深度对比与实战
android·学习·机器人
故渊at13 小时前
第二板块:Android 四大组件标准化学理 | 第八篇:Service 后台执行实体与优先级
android·gitee·service·前台服务·后台服务
会Tk矩阵群控的小木13 小时前
安卓群控系统对于游戏工作室实战教程
android·运维·游戏·adb·开源软件·个人开发