Kotlin 朱涛 思维-3 不变性思维 只读集合 val
目录
Kotlin 不变性思维
在 Kotlin 当中,所谓的不变性思维,其实就是在满足程序功能可变性需求的同时,尽可能地消灭可变性
。
使用表达式思维消灭 var
Kotlin 不变性思维的第一条准则:尽可能使用条件表达式消灭 var。
fun testExp(data: Any) {
var i = 0 // 可变性变量
if (data is Number) {
i = data.toInt()
} else if (data is String) {
i = data.length
} else {
i = 0
}
println(i)
}
把整体的赋值逻辑变成一个表达式后,我们就能将 var 的变量改为 val:
fun testExp(data: Any) {
val i = when (data) {
is Number -> data.toInt()
is String -> data.length
else -> 0
}
println(i)
}
使用数据类并消灭可变性
Kotlin 不变性思维的第二条准则:使用数据类来存储数据,同时消灭数据类的可变性。
数据类是专门用来存放数据的类,与 Java 当中的Java Bean
类似,优势在于简洁。
class Person {
var name: String? = null
var age: Int? = 0
}
上面的代码等价于下面的形式:
data class Person(var name: String?, var age: Int?)
// 上面的 var 都可以改为 val
data class Person(val name: String?, val age: Int?)
属性都改为 val 以后,想要修改它的值该怎么办呢?实际上,Kotlin 更推崇
我们用下面的方式来解决:
fun changeName(person: Person, newName: String): Person = person.copy(name = newName) // copy
上面代码中,我们并没有直接修改原对象
的值,而是借助数据类的 copy
方法,快速创建一份拷贝
的同时,还完成了对属性的修改。
对外暴露只读集合
Kotlin 不变性思维的第三条准则:尽可能对外暴露只读集合。
Kotlin 中集合库的设计,与 Java 之类的语言不太一样。
- 在 Java 中,List 接口代表的是一个
可变的
列表,接口中会有 add/remove 等方法 - 在 Kotlin 中,List 接口代表的是一个
不可变
的列表,或者说是只读的列表
,接口中没有 add/remove 等方法 - 在 Kotlin 中,想要使用
可变列表
的时候,必须使用MutableList
正常情况下,当我们将类内部的集合对象暴露给外部以后,我们没办法阻止外部对集合的修改。
class Model {
val data: MutableList<String> = mutableListOf()
}
Model().data.add("World") // 类的外部仍然可以修改data
通过以下三种方案都可以解决这个问题。
- 方案 ① :属性之间的直接委托
class Model {
val data: List<String> by ::_data // 属性之间的直接委托
private val _data: MutableList<String> = mutableListOf()
private fun load() = _data.add("Hello")
}
- 方案 ② :自定义属性的 get 方法
class Model {
val data: List<String>
get() = _data // 自定义属性的 get 方法,返回了 _data 这个集合的值
private val _data: MutableList<String> = mutableListOf()
private fun load() = _data.add("Hello")
}
- 方案 ③ :自定义对外暴露的方法
class Model {
private val data: MutableList<String> = mutableListOf()
private fun load() = data.add("Hello")
fun getData(): List<String> = data // 自定义对外暴露的方法
}
以上这三种方式,本质上都是对外暴露了一个不可变的集合
,它们基本上可以满足大部分的使用需求。但是,这三种方式还是会存在一定的风险,那就是 外部可以进行类型转换,就像下面这样:
fun main() {
val model = Model()
val data = model.getData()
(data as? MutableList)?.add("new data")
println(model.getData()) // [new data]
}
借助 Kotlin 提供的 toList
函数,让 data 变成真正的 List 类型,即可解决上面提到的风险。
fun getData(): List<String> = data.toList() // 让 data 变成真正的 List 类型
不过有一点需要注意:当 data 集合数据量很大的时候,toList()
操作可能会比较 耗时。
只读集合在 Java 中被访问
Kotlin 不变性思维的第四条准则:只读集合底层不一定是不可变的,要警惕 Java 代码中的只读集合访问行为。
Kotlin 为了兼容 Java,它的集合类型必须要与 Java 兼容,因此它不能创造出 Java 以外的集合类型,这也就决定了它只能是语法层面的不可变性。
因为 Java 当中不存在不可变的集合
的概念,所以,当只读集合
在 Java 中被访问的时候,它的不变性
将会被破坏。
事实上,对于 Kotlin 的 List
类型来说,在它转换成 Java 字节码以后,在不同的情况下,可能会变成多种类型,比如 SingletonList
、java.util.Arrays$ArrayList
、java.util.ArrayList
。
因此,当我们在与 Java 混合编程的时候,Java 里使用 Kotlin 只读集合
的时候一定要足够小心,最好有详细的文档。
案例一
class Model {
private val data: MutableList<String> = mutableListOf("hello") // 只能是一个元素
fun getData(): List<String> = data.toList()
}
import java.util.List;
class Test {
public static void main(String[] args) {
List<String> data = new Model().getData();
System.out.println(data.getClass().getName()); // java.util.Collections$SingletonList
System.out.println(data.get(0)); // hello
data.set(0, "bqt"); // UnsupportedOperationException
}
}
SingletonList
是 java.util.Collections
的 private static
的内部类,只能保存一个元素,不支持 add/remove 方法,只能通过 Collections
的静态方法创建:
private static class SingletonList extends AbstractList implements RandomAccess, Serializable
public abstract class AbstractList extends AbstractCollection implements List // ArrayList 的父类
List<String> list = Collections.singletonList("bqt"); // 在初始化需要赋值
案例二
class Model {
val list: List<String> = listOf("hello", "world") // 不能是一个元素
}
import java.util.List;
class Test {
public static void main(String[] args) {
List<String> data = new Model().getList();
System.out.println(data.getClass().getName()); // java.util.Arrays$ArrayList
data.set(0, "bqt"); // 正常执行
System.out.println(data.get(0)); // bqt
try {
data.add("bqt"); // UnsupportedOperationException
} catch (Exception e) {
e.printStackTrace();
}
}
}
这种情况下,Kotlin 的 List 被编译器转换成了 java.util.Arrays$ArrayList
类型。
val 并非绝对的不可变
Kotlin 不变性思维的第五条准则:val 并不意味着绝对的不可变。
通常来说,用 val
定义的临时变量
,都会被看做是不可变的只读变量
。但是这个结论并不绝对正确。
import kotlin.properties.ReadOnlyProperty
import kotlin.random.Random
import kotlin.reflect.KProperty
class TestVal {
val d: Double
get() = Random.nextDouble() // 自定义 get() 方法
}
class RandomDelegate : ReadOnlyProperty<Any?, Double> {
override operator fun getValue(thisRef: Any?, property: KProperty<*>) = Random.nextDouble()
}
fun main() {
val test = TestVal()
val tem: Double by RandomDelegate() // 把局部变量委托给 RandomDelegate
println("${test.d} ${test.d}") // 两次访问【成员属性】所得到的值都不一样
println("$tem $tem") // 两次访问【局部变量】所得到的值都不一样
}
val 可以理解为:地址不变(无法二次赋值,类似 Java 中的 final 修饰的对象),但是值可以改变(就行 final 对象的属性值可变一样)
小结
所谓 Kotlin 的不变性思维
,就是尽可能消灭代码中非必要的可变性
。具体来说,一共有 5 大准则:
- 尽可能使用条件表达式消灭 var
- 使用数据类来存储数据,消灭数据类的可变性
- 尽可能对外暴露只读集合
- 只读集合底层不一定是不可变的,要警惕 Java 代码中的只读集合访问行为
- val 并不意味着绝对的不可变
2017-08-24
相关文章
- Kotlin 朱涛-25 源码 集合操作符 总结
- [Kotlin] Conditional expression (return value from when or if statements)
- 熬夜再战Android之修炼Kotlin-点击事件篇
- Android kotlin实现自定义时钟
- 记一段 Kotlin 处理 HashMap 数据结构的代码
- 《Kotlin 极简教程 》第5章 集合类
- 在 Java 9 的JShell中 跟Kotlin 的REPL中尽情体验函数式编程乐趣吧
- 浅析 Kotlin 中的 synchronized
- 你为什么应该学习 Kotlin ?
- by: kotlin.UninitializedPropertyAccessException: lateinit property xxx(变量名) has not been initialized
- Android Studio 4.2.2 Kotlin(不能进入类)的报错提示:Cannot find declaration to go to
- AS kotlin 导入github里的新项目后运行的报错提示:Could not resolve com.flyco.tablayout:FlycoTabLayout_Lib:3.0.0