Mutable & Immutable
谈及到"可变"与"不可变",必然绕不开 var
和 val
这两个关键字,用Java来解释的话,前者是"variable",后者是"final"。final
对于Java开发者来说并不陌生,但也必然说不上"常用",可在Kotlin里其地位却上升为定义变量的"关键词",这也说明"可变"与"不可变"的概念已经深刻在Kotlin的基因里了。
因此在Kotlin里,开发者最惦记的Collections同样划分为"可变集合"(Mutable Collections)和"不可变集合"(Immutable Collections)。但这个Immutable是真的不可变吗?这里以list举一个例子:
代码语言:javascript复制fun main() {
// 注意,这里不能写listof(1),否则会被优化为SingletonList类型,其set方法是没有被实现的
val l = listOf(1, 2)
try {
(l as MutableList).add(0, 2)
} catch (e: Exception) {
println(e)
}
try {
(l as MutableList)[0] = 2
} catch (e: Exception) {
println(e)
}
l.forEach { println(it) }
}
运行结果是:
这里 ImmutableList
可以强转为 MutableList
并修改其中的元素。
Kotlin代码要实现100%兼容Java,则无论穿的衣服是 MutableList
还是 ImmutableList
,卸下伪装后都只能是Java的 List
。
可以验证一下:
代码语言:javascript复制fun main() {
println(TypeIntrinsics.isMutableList(listOf(1, 2)))
println(TypeIntrinsics.isMutableList(mutableListOf(1, 2)))
}
运行结果是
你觉得堂堂JetBrains会没想到去实现一个真正的Immutable Collections?那当然不可能了,毕竟Kotlin被寄予了厚望。这个项目就是kotlinx.collections.immutable
完!剩下的自己去翻文档吧。
真正的Immutable
整个库并不复杂(因为还在开发中),但也不简单,因为相比于"可变","不可变"要考虑的地方更多,暂且抛开这点不谈,先来简单看看库的用法。
由于最新版本
ImmutableList
接口分为ImmutableList
和PersistentList
两个接口,其含义也有所区别,因此以最新版本为准
由于要考虑到易用易理解,其提供的API和"可变集合"高度一致。
代码语言:javascript复制// Group A
val immutableList = (0..5).toImmutableList()
val immutableSet = (0..5).toImmutableSet()
val immutableMap = mapOf(0 to 1, 2 to 3).toImmutableMap()
// Group B
val persistentList =(0..5).toPersistentList()
val persistentSet = (0..5).toPersistentSet()
val persistentMap = mapOf(0 to 1, 2 to 3).toPersistentMap()
val persistentHashMap = mapOf(0 to 1, 2 to 3).toPersistentHashMap()
补充: 还有
PersistentOrderedMap
和PersistentOrderedSet
类,对应Java的LinkedHashMap
和LinkedHashSet
那么A组和B组有什么区别呢?
代码语言:javascript复制// 仍然以list举例
val immutableList2 = immutableList 6
val persistentList2 = persistentList 6
// 输出:
// 8123456
(immutableList2 as MutableList)[0] = 8
immutableList2.forEach { print(it.toString()) }
// 下面这一行会标红(没有set方法)
immutableList2[0] = 8
// 下面这一行运行时会报错,因为不能强转
(persistentList2 as MutableList<Int>)[0] = 8
persistentList2.forEach { println(it.toString()) }
明显库里面没有为 ImmutableList
重载运算符(可能是这个接口后续仍会变动,因为 ImmutableXXOf
等扩展方法已经标记 Deprecated
了),其加法执行运算后返回的是标准的 List
。因此库里A组的方式都应该无视掉…
PersistentList
连元素都不能变了!可…除了加法运算…肯定还支持减法运算。难道只是数组内元素不可变,但数组本身是可变的?下面做一个试验:
// A
val mutableList = (0..5).toMutableList()
Thread {
mutableList.forEach {
println(it.toString())
Thread.sleep(1000)
}
}.start()
Thread {
Thread.sleep(2000)
mutableList.removeAt(0)
}.start()
// B
val persistentList =(0..5).toPersistentList()
Thread {
persistentList.forEach {
println("thread 1: $it")
Thread.sleep(1000)
}
}.start()
Thread {
Thread.sleep(2000)
persistentList.removeAt(0).forEach {
println("thread 2: $it")
}
Thread.sleep(2000)
persistentList.removeAt(0).forEach {
println("thread 2: $it")
}
}.start()
A毫无意外报错了。而B的thread 1输出是012345,thread 2输出的是12345012345。这说明数组也是不可变,在其之上"增删"都会生成新的数组。并且由于数组的"不可变",因此其线程安全。
共享的数据
在翻看源码实现的时候,发现了一个比较奇怪的地方,那就是其数据的保存方式。下面同样以 PersistentVector
为例。
直接上图:
当tail的叶子节点数量超到32个时,则会copy成为root下的叶子节点,root的每一层最大叶子数量也是32个。示意图如下:
注意这里PersistentVector A和PersistentVector B红色框标注部分是共享的数据。这里可以以一个例子证明:
代码语言:javascript复制// 以下代码需要修改源码,暴露相关方法才能运行
// Block A,输出:10086123456789101112131415161718192021222324252627282930313233
val immutableList = (0..32).toPersistentList() as PersistentVector
val root = immutableList.root
val newImmutableList = immutableList.add(33)
root[0] = 10086
newImmutableList.forEach {
print(it.toString())
}
也就是从一份 PersistentVector
不断衍生出来的所有列表,都将有一部分是共享的,这将能节省不少内存,同时能让用户在极小负担下对列表进行修改。
PersistentHashMap
和 PersistentHashSet
等相关实现也有类似的特性。
写在最后
尽管本文介绍的内容都不算太难,但kotlinx.collections.immutable的更新历史却值得仔细琢磨,这部分或许之后能有机会深入探讨。
同时该库仍在开发当中,未来将增加更多的特性,例如:
ImmutableStack
和ImmutableQueue
ImmutableMap
的entries
、key
和value
也是"不可变"
……
Let's Kotlin!