Kotlin 朱涛 思维-3 不变性思维 只读集合 val
目录
Kotlin 不变性思维
尽可能地消灭可变性
在满足程序功能可变性需求的同时,应该尽可能地消灭可变性
。
使用条件表达式消灭 var
val i: Int = when (data) {
is Number -> data.toInt()
is String -> data.length
else -> 0
}
使用数据类并消灭可变性
数据类是专门用来存放数据的类,与 Java 当中的Java Bean
类似,优势在于简洁。
class Person {
var name: String? = null
var age: Int? = 0
}
上面的代码等价于下面的形式:
data class Person(var name: String?, var age: Int?) // 如有需要,可使用 var
data class Person(val name: String?, val age: Int?) // 如无需要,尽可能改为 val
改为 val 以后,如果想要修改成员的值,可以借助数据类的 copy
方法,快速创建一份拷贝
并完成对属性的修改。
fun changeName(person: Person, name: String) = person.copy(name = name) // copy
尽可能对外暴露只读集合
- 在 Java 中,List 接口代表的是一个
可变的
列表,接口中有add/remove
等修改集合的方法 - 在 Kotlin 中,List 接口代表的是一个
只读的
列表,接口中没有add/remove
等修改集合的方法
如果想要使用
可变列表
,可以使用MutableList
解决集合的可变性问题
正常情况下,将集合对象暴露出去以后,就没办法阻止外部对集合的修改了。
class Model {
val data: MutableList<String> = mutableListOf() // 可变集合
}
Model().data.add("World") // 类的外部可以修改集合
通过以下三种方案都可以解决这个问题。
属性委托
class Model {
val data: List<String> by ::_data // 对外暴露的是【不可变集合】,外部无法修改
private val _data = mutableListOf<String>() // 委托给【仅内部可访问】的【可变集合】
fun load() = _data.add("Hello") // 外部可通过指定方法修改集合
}
自定义 get 方法
class Model {
val data: List<String> // 对外暴露的是【不可变集合】,外部无法修改
get() = _data // 自定义 get 方法,返回的实际是【可变集合】
private val _data = mutableListOf<String>() // 返回的【可变集合】仅内部可访问
fun load() = _data.add("Hello") // 外部可通过指定方法修改集合
}
自定义对外暴露的方法
class Model {
fun getData(): List<String> = _data // 自定义对外暴露的方法
private val _data = mutableListOf<String>() // 仅内部可访问的【可变集合】
fun load() = _data.add("Hello") // 外部可通过指定方法修改集合
}
使用 toList 解决集合强转问题
以上这三种方式,本质上都是将对外暴露的可变集合【声明为】不可变的集合
。但是,这样存在一定的风险,比如:外部可以进行类型转换:
fun main() {
val model = Model()
val data: List<String> = model.getData() // 返回的是【不可变集合】
(data as? MutableList)?.add("baiqiantao") // 但是可以强转为【可变集合】
println(model.getData()) // 强转后就可以修改集合了
}
使用 toList
将返回的 data 变成真正的 List 类型,即可解决上面提到的风险。
fun getData(): List<String> = data.toList() // 返回的是真正的不可变的 List 类型
当然,toList()
操作可能会造成一定的性能损耗。
Kotlin与Java的集合兼容性问题
只读集合在底层
并非是不可变的,要警惕 Java 代码中对只读集合可能的修改行为。
Kotlin 为了兼容 Java,它的集合类型必须要与 Java 兼容,因此它不能创造出 Java 以外的集合类型,这也就决定了它只能是语法层面的不可变性。
因为 Java 中不存在不可变的集合
的概念,所以,当只读集合
在 Java 中被访问的时候,它的不变性
将会被破坏。
事实上,对于 Kotlin 中的不可变集合 List
来说,在它转换成 Java 字节码以后,在不同的情况下,可能会转换成多种不同的类型,比如 SingletonList
、java.util.Arrays$ArrayList
、java.util.ArrayList
。
因此,当在与 Java 混合编程的时候,Java 里使用 Kotlin 只读集合
的时候一定要足够小心,最好有详细的文档。
java.util.ArrayList
class Model {
private val data = mutableListOf("白乾涛") // 假如只有一个元素
fun getData(): List<String> = data // 对外暴露一个不可变的集合,注意没有调用 toList() 方法
}
class TestJava {
public static void main(String[] args) {
java.util.List<String> data = new Model().getData();
System.out.println(data.getClass().getName()); // java.util.ArrayList
System.out.println(data.get(0)); // 白乾涛
data.set(0, "bqt"); // 修改元素
data.add("bqt2"); // 添加元素
System.out.println(data); // [bqt, bqt2]
}
}
上面集合的实际类型,就是我们平常使用最多的 java.util.ArrayList
,它支持所有增删改查操作。
SingletonList
对上面案例仅修改一行代码:
class Model {
private val data = mutableListOf("白乾涛")
fun getData(): List<String> = data.toList() // 修改点:调用了 toList() 方法
}
class TestJava {
public static void main(String[] args) {
java.util.List<String> data = new Model().getData();
System.out.println(data.getClass().getName()); // java.util.Collections$SingletonList
System.out.println(data.get(0)); // 白乾涛
data.set(0, "bqt"); // 抛异常:UnsupportedOperationException
data.add("bqt2"); // 访问不到
System.out.println(data); // 访问不到
}
}
可以发现,上面集合的实际类型,变成了 java.util.Collections$SingletonList
。
集合 SingletonList
只能保存一个元素,不支持 add/remove/set
等方法,只能通过 Collections
的静态方法创建:
List<String> list = Collections.singletonList("bqt"); // 在初始化需要赋值
list.
set(0,"不能 add/remove/set"); // 提示:Immutable object is modified
java.util.Arrays$ArrayList
对上面案例再修改一行代码:
class Model {
fun getData(): List<String> = listOf("白乾涛", "白乾涛2") // 修改点:不再是仅有一个元素,并且不再通过 MutableList 转换
}
class TestJava {
public static void main(String[] args) {
java.util.List<String> data = new Model().getData();
System.out.println(data.getClass().getName()); // java.util.Arrays$ArrayList
System.out.println(data.get(0)); // 白乾涛
data.set(0, "bqt"); // 抛异常:UnsupportedOperationException
data.add("bqt2"); // 访问不到
System.out.println(data); // 访问不到
}
}
这种情况下,上面集合的实际类型,变成了 java.util.Arrays$ArrayList
。
注意,如果返回的集合是通过 MutableList 转换的,如下代码,则返回集合的实际类型为 java.util.ArrayList
。
class Model {
private val data = mutableListOf("1", "2") // 有多个元素
fun getData(): List<String> = data.toList() // 通过 MutableList 转换而来的
}
val 声明的变量并非绝对不可变
通常来说,用 val
定义的临时变量
,都会被看做是不可变的只读变量
。但是这个结论并不绝对正确。
val 可以理解为:地址不变(地址无法二次赋值,类似 Java 中的 final 修饰的对象),但是值是可变的(就像 final 对象的属性值可变一样)
例如,以下两种方式声明的 val 变量,其值就是可变的。
通过自定义 get 修改可变性
val i: Int
get() = Random.nextInt(100) // 自定义 get() 方法
fun main() {
println("$i - $i - $i") //【5 - 52 - 91】每次访问的值都可能不一样
println("${i == i} - ${i === i}") // false - false
}
通过自定义委托修改可变性
class RandomIntDelegate : ReadOnlyProperty<Any?, Int> {
override operator fun getValue(r: Any?, p: KProperty<*>) = Random.nextInt(100)
}
fun main() {
val i: Int by RandomIntDelegate() // 自定义委托
println("$i - $i - $i") //【5 - 52 - 91】每次访问的值都可能不一样
println("${i == i} - ${i === i}") // false - false
}
小结
所谓 Kotlin 的不变性思维
,就是尽可能消灭代码中非必要的可变性
。
- 使用条件表达式消灭 var
- 使用数据类存储数据,使用 val 消灭数据类属性的可变性
- 对外暴露只读集合
- 只读集合底层不一定不可变,要警惕 Java 代码中的只读集合修改行为
- val 并不意味着绝对的不可变
2017-08-24
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/7423797.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
2016-08-24 Interpreter Expression 解释器模式