End

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, newName: String): Person = person.copy(name = newName) // copy

尽可能对外暴露只读集合

  • 在 Java 中,List 接口代表的是一个可变的列表,接口中有 add/remove 等修改集合的方法
  • 在 Kotlin 中,List 接口代表的是一个不可变的列表,或者说是只读的列表,接口中没有 add/remove 等方法
  • 在 Kotlin 中,如果想要使用可变列表,必须使用 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("白乾涛")     // 强转为可变集合
    println(model.getData())                 // [白乾涛]
}

使用 toList 将返回的 data 变成真正的 List 类型,即可解决上面提到的风险。

fun getData(): List<String> = data.toList() // 返回的是真正的不可变的 List 类型

当然,toList() 操作可能会造成一定的性能损耗。

只读集合在 Java 中可被修改

只读集合在底层并非是不可变的,要警惕 Java 代码中对只读集合可能的修改行为。

Kotlin 与 Java 的兼容性问题

Kotlin 为了兼容 Java,它的集合类型必须要与 Java 兼容,因此它不能创造出 Java 以外的集合类型,这也就决定了它只能是语法层面的不可变性

因为 Java 中不存在不可变的集合的概念,所以,当只读集合在 Java 中被访问的时候,它的不变性将会被破坏。

事实上,对于 Kotlin 中的不可变集合 List 来说,在它转换成 Java 字节码以后,在不同的情况下,可能会转换成多种不同的类型,比如 SingletonListjava.util.Arrays$ArrayListjava.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 对象的属性值可变一样)

自定义 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

posted @ 2017-08-24 16:31  白乾涛  阅读(6987)  评论(0编辑  收藏  举报