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

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

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

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

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

于是,我仔细研究了一下 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}

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

只读集合(listOf、setOf、mapOf)创建后不能用它的方法修改,可变集合(mutableListOf、mutableSetOf、mutableMapOf)可以。

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

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

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 支持只读和可变两类集合,包括 List、Set、Map。

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

进阶: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 返回 0,isEmpty() 返回 true,get(index) 直接抛 IndexOutOfBoundsException。

一句话总结:

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

一点想法

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

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

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

相关推荐
三少爷的鞋6 小时前
Java 线程自上而下,Kotlin 协程自下而上
android
好的佩奇7 小时前
Dart 之增强枚举
android·flutter·dart
梦影_8 小时前
MySQL进阶教程(五)—— SQL调优(下)
android·大数据·数据库·后端·sql·mysql·database
好的佩奇10 小时前
蓝牙之无线电与通信距离
android·flutter·蓝牙
vilya11 小时前
我把一套完整的 Python Agent 运行时塞进手机 APK:依赖闭包、Rust 轮子和三个真机坑
android
呆呆敲代码的小Y12 小时前
【实战精选面试题】C# 面试题详细解答,按难易程度排序
android·java·游戏·c#·游戏开发·unity面试题·c#面试题
思想觉悟14 小时前
SEAndroid实战
android·linux·c++
思想觉悟14 小时前
Aosp开发磨刀石之ADB速查
android·c++
我是唐青枫15 小时前
Kotlin Inheritance(继承)详解:语法、原理、实战 Demo
开发语言·kotlin
应用市场17 小时前
用本机跑的开源音乐模型,给安卓小游戏做 20 秒无缝循环背景音乐
android·python·游戏开发·soundpool·音频处理