Kotlin 将集合分为可变集合与只读集合。(Kotlin 集合中暂时还没有不可变集合)
1 可变集合
可变集合,就是可以改变的集合。可变集合都会有一个修饰前缀 Mutable,比如 MutableList。这里的改变是指改变集合中的元素。 比如以下可变集合:
val list = mutableListOf(1, 2, 3, 4, 5)
将集合中的第 1 个元素修改为 0:
list[0] = 0 // [0, 2, 3, 4, 5]
2 只读集合
与可变集合相对,只读集合中的元素在一般情况下是不可修改的。
val list = listOf(1, 2, 3, 4, 5)
如果我们想要集合中的第 1 个元素修改为 0:
用上面的方法修改集合中的元素时,实际上就是调用了 set 方法,但是 Kotlin 的只读集合中是没有这个方法的,所以不能修改其中的值。
只读集合中只有一些可以用来“读”的方法,比如获取集合的大小、遍历集合等。这样做的好处是可以使代码更加安全。比如,实现一个将 a 列表中的元素添加到 b 列表中的方法:
fun merge(a: List<Int>, b: MutableList<Int>) {
for (item in a) {
b.add(item)
}
}
a 列表仅仅只是遍历,b 列表发生了变化。这样做的好处是,我们很容易知道函数 mergeList 不会修改 a 列表,因为 a 是只读的,而 b 是是可能发生改变的。
只读列表并不是无法改变的。在 Kotlin 中,将 List 成为只读列表而不是不可变列表是有原因的,因为在某些情况下只读列表确实是可以改变的, 比如:
val writeList: MutableList<Int> = mutableListOf(1, 2, 3, 4)
val readList: List<Int> = writeList
println(readList) // [1, 2, 3, 4]
writeList[0] = 0
println(readList) // [0, 2, 3, 4]
在上面的代码中,首先定义了一个可变列表 writeList,然后又定义了一个只读列表 readList,readList 和 writeList 指向了同一个集合对象,因为 MutableList 是 List 的子类,所以可以这样做。
public interface MutableList<E> : List<E>, MutableCollection<E> { }
在执行完 writeList[0] = 0 之后,readList 也发生了变化,就是说在这种情况下我们是可以修改只读集合的。所以我们只能说只读列表在某些情况下是安全的,但是它不总是安全的。
通过上述代码可以知道,使用集合接口时需要牢记的一个关键点是:只读集合不一定是不可变的。 如果使用的变量引用一个只读接口类型,它可能只是同一个集合的众多引用中的一个。任何其他的引用都可能拥有一个可变接口类型。
两个不同的引用,一个只读,另一个可变,指向同一个集合对象。 如果当作只读集合在使用,在使用的时候被其它的代码修改,这样就会导致 concurrentModificationException 的错误和其他的一些问题。因此,必须了解只读集合并不总是线程安全的。如果在多线程环境下处理数据,就需要保证代码正确地同步了对数据的访问,或者使用支持并发访问的数据结构。
在另外一种情况下,只读集合也是能被修改的。Kotlin 的集合都是基于 Java 来构建的,并且 Kotlin 与 Java 是兼容的。这就意味着我们可以在 Kotlin 的集合操作中调用在 Java 中定义的方法。这样就很容易出现问题了,因为在 Java 中是不区分只读集合和可变集合的。 比如,用 Java 定义了一个集合的操作:
public static List<Integer> foo(List<Integer> list) {
for (int i = 0; i < list.size(); i++) {
list.set(i, list.get(i) * 2);
}
return list;
}
当我们在 Kotlin 中去使用这个方法的时候:
val list = listOf(1, 2, 3);
foo(list)
println(list) // [2, 4, 6]
由于 Java 中不区分只读集合和可变集合,所以 list 会被 foo 方法改变。因此,Java 和 Kotlin 进行相互操作的时候要考虑这种情况。
3 Kotlin 集合和 Java
Kotlin 的集合设计和 Java 不同的另一项重要特质是,它把访问集合数据的接口和修改集合数据的接口分开了。
这种区别在于最基础的使用集合的接口之中:
- 使用 kotlin.collections.Collection 接口,可以遍历结合中的元素、获取集合的大小、判断集合中是否包含某个元素,以及执行其他从该结合中读取数据的操作。但这个接口没有任何添加或移除元素的方法;
- 使用 kotlin.collections.MutableCollection 接口,可以修改集合中的数据。它继承了普通 kotlin.collections.Collection 接口,还提供了方法来添加和移除元素、清空集合等;
就像 val 和 var 之间的分离一样,只读集合接口和可变集合接口的分离能让程序中的数据发生的事情更加容易理解。一般的规则是在代码的任何地方都应该使用只读接口,只在代码需要修改集合的地方使用可变接口的变体。
如果函数接收 Collection 而不是 MutableCollection 作为形参,就知道它不会修改集合,而只是读取集合中的数据。如果函数要求传递给它 MutableCollection,就可以认为它将会修改数据。
如果用了集合作为组件部状态的一部分,可能需要把集合先拷贝一份再传递给这样的函数,这种模式通常称为防御式拷贝/复制。
Kotlin 中的防御性复制指的是在进行对象赋值或传参时,创建该对象的新副本而不直接使用原始对象,这样可以确保修改新副本而不影响到原始对象,从而提高程序的安全性和健壮性。
fun <T> copyElements(source: Collection<T>, target: MutableCollection<T>) {
for (item in source) { // 循环遍历 source 中的所有元素
target.add(item) // 向可变的 target 集合中添加元素
}
}
fun main() {
val source: Collection<Int> = arrayListOf(3, 5, 7)
val target: MutableCollection<Int> = arrayListOf(1)
copyElements(source, target)
println(target)
}
// [1, 3, 5, 7]
在 copyElements 函数仅仅修改了 target 集合,而没有修改 source 集合。
只读集合和可变集合之间的分离是怎么做到的呢?每一个 Kotlin 接口都是其对应 Java 集合接口的一个实例,这种说法并没有错。在 Kotlin 和 Java 之间转移并不需要转换;不需要包装器也不需要拷贝数据。 但是每一种 Java 集合接口在 Kotlin 中都有两种表示:一种是只读的,另一种是可变的:
Java 类 ArrayList 和 HashSet 都继承了 Kotlin 可变接口。
Kotlin 中只读接口可可变接口的基本结构与 java.util 中的 Java 集合接口的结构是平行的。可变接口直接对应 java.util 包中的接口,而它们的只读接口缺少了所有产生改变的方法。
对于 java.util.ArrayList 和 java.util.HashSet,展示了 Kotlin 是如何对待 Java 标准类的。在 Kotlin 看来,它们分别继承自 MutableList 接口和 MutableSet 接口。
除了集合之外,Kotlin 中的 Map 类(它没有继承 Collection 或是 Iterable)也被表示成了两种不同的版本:Map 和 MutableMap。
注意,setOf、mapOf 返回的是 Java 标准类库中类的实例,在底层它们都是可变的。
当使用 java.util.Collection 作为函数形参的 Java 方法,可以把任意 Collection 或 MutableCollection 的值作为实参传递给这个形参。
这对集合的可变性有重要影响。因为 Java 并不会区分只读集合和可变集合,即使 Kotlin 中把集合声明称只读的,Java 代码也可以修改这个集合。 因此,如果我们写了一个 Kotlin 函数,使用了集合并传递给了 Java,我们有责任使用正确的参数类型。
留意此项注意也适用于包括非空类型元素的集合类。如果向 Java 方法传递这样的集合,该方法就可能在其中写入 null 值。Kotlin 没有办法在不影响性能的情况下,禁止它的发生,或者察觉到已经发生的改变。因此,我们在向可以修改集合的 Java 代码传递给集合的时候,需要采取特别的预防措施,来确保 Kotlin 类型正确地反映出集合上所有可能的修改。
// Java
public class CollectionUtils {
public static List<String> uppercaseAll(List<String> items) {
for (int i = 0; i < items.size(); i++) {
items.set(i, items.get(i).toUpperCase());
}
return items;
}
}
在 Kotlin 中使用:
// Kotlin
fun printInUppercase(list: List<String>) { // 声明只读的参数
println(CollectionUtils.uppercaseAll(list)) // 调用可以修改集合的 Java 函数
println(list.first())
}
val list = listOf("a", "b", "c")
printInUppercase(list)
// [A, B, C]
// A