Kotlin中的泛型:协变与逆变
协变与逆变
现在假设存在类A和类B,以及泛型类LIst<A>和泛型类LIst<B>,则协变和逆变的定义如下:
-
协变
如果A是B的子类,且List<A>是List<B>的子类,那么可以说泛型List<T>是协变的
-
逆变
如果A是B的子类,且List<B>是List<A>的子类,那么可以说泛型List<T>是逆变的
Java中的泛型
Java中的泛型不是协变的,即List<String>并不是List<Object>的子类,这样就会给一些操作带来了不必要的麻烦,例如:
List<Object> list = new ArrayList<>();
List<String> strings = new ArrayList<>();
list.addAll(strings)
因为List<String>不是List<Object>的子类,理论上我们调用 addAll 方法是会编译报错的。但实际上并不会,原因是Java使用通配符类型来实现了协变。
查看Collections<E>类的源码可以发现addAll方法的参数类型实际上是Collection<? extends E>,即对于所有E的子类型其对应的泛型类都是Collection<? extends E>的子类型。
上述示例中,因为Collection<String>是Collection<? extends Object>的子类,所以可以成功调用addAll方法。
同理,Java中也可以使用通配符类型实现逆变,对于所有E的父类型其对应的泛型类都是Collection<? super E>的子类型。
Kotlin中的泛型
Kotlin泛型的声明式协变和逆变
假设对于List<E>泛型类,仅包含输出类型E的接口,那么我们实际上输出任何E的子类型的实例都是安全的。例如:
interface List<E> {
fun get(index: Int) : E
}
那么List<String>就是List<Object>的子类型,即可以将List<String>类型的对象赋值给List<Object>类型的变量,因为赋值给List<Object>类型的变量后实际只会输出String类型的实例,而String类型是Object类型的子类,所以是安全的。
上述接口在Kotlin中需要使用 out 关键字来表示该泛型是协变的,如下所示:
interface List<out E> {
fun get(index: Int) : E
}
out 关键字即表示该泛型类型仅仅包含输出类型E的接口。
同理,假设List<E>泛型仅包含输入类型E的接口,那么实际上输入任何类型E的子类型的实例都是安全的,Kotlin中使用 in 关键字来表示该泛型是逆变的,如下所示:
interface List<in E> {
fun add(item: E)
}
此种情况下,List<Object>就是List<String>的子类型,即可以将List<Object>类型的对象赋值给List<String>类型的变量,因为赋值给List<String>类型的变量后只能输入String类型的实例,而String类型是Object类型的子类,所以是安全的。
实际上来说,我们知道List接口不能只支持输入接口或者只支持输出接口,例如List接口需要同时支持get操作和add操作,那么List泛型就不能在类的声明处用in或者out关键字来声明协变或者逆变。对于此种情况,Kotlin中还支持使用处协变和逆变,即类型投影。
Kotlin泛型的类型投影
interface List<E> {
fun add(item: E)
fun get(index: Int) : E
}
fun copy(list1: List<out Any>, list2: List<Any>) {
//copy items from list1 to list2
}
如上所示,copy 函数从 list1 中拷贝所有的元素到 list2 中,对于 list1 列表该函数只需要读取元素,因此可以在参数列表中使用 out 关键字声明 list1 是协变的,即在copy函数中对于 list1的操作只能是读取元素,而不能向 list1 写入任何元素。
同理,如果一个函数只能对某个参数进行写入操作,那么就可以在参数列表中使用 in 关键字来声明。例如:
fun fill(list: List<in String>, str: String) {
//fill list with str
}