关于Kotlin的型变
1.Java对泛型的限制
Java中所有的类都是不变型的
在Java中,List<String>并不是List<Object>的子类型。
如果List<String>并是List<Object>的子类型, 那么就是意味着将一个List<Object>类型的对象指向List<String>不会报错,那么我们就可以对这个List<String>对象添加一些Object对象,比如:
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!!即将来临的问题的原因就在这里。Java 禁止这样!
objs.add(1); // 这里我们把一个整数放入一个字符串列表
String s = strs.get(0); // !!! ClassCastException:无法将整数转换为字符串
这就是在Java中禁止List<String>是List<Object>子类型的原因
然而我们将无法做到一下简单的事情(这是完全安全):
// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from);
// !!!对于这种简单声明的 addAll 将不能编译:
// Collection<String> 不是 Collection<Object> 的子类型
}
如果不引入通配符类型参数?
,那么必须手动的为每一个类型写一个addAll方法
比如:addAll(Collection<A>),addAll(Collection<B>),addAll(Collection<C>),等等
// Java
interface Collection<E> …… {
void addAll(Collection<String> items);
}
然而Collection的addAll()
的实际签名是以下这样:
// Java
interface Collection<E> …… {
void addAll(Collection<? extends E> items);
}
通配符类型参数? extends E
表示此方法接受 E
或者 E 的 一些子类型对象的集合,而不只是 E
自身。 这意味着我们可以安全地从其中(该集合中的元素是 E 的子类的实例)读取 E
,但不能写入。
将一个集合作为参数传递给一个有着? extends E
参数的方法,就说明在该方法中,该集合只会被读取,不会被写入
相反的,将一个集合作为参数传递给一个有着? super E
参数的方法,就说明在该方法中,该集合只会被写入,不会被读取
2.Kotlin相对Java的变化
与Java不同,Kotlin并没有直接使用通配符,而是使用了声明处型变
Joshua Bloch 称那些你只能从中读取的对象为生产者,并称那些你只能写入的对象为消费者。他建议:“为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型”,并提出了以下助记符:
PECS 代表生产者-Extens,消费者-Super(Producer-Extends, Consumer-Super)。
Out修饰符:生产者
当一个类Person的类型参数T
被声明为out时,它就只能出现在Person的成员的输出位置,通常也就是返回位置,可以说Person在参数T
上是协变的,Person是T
的生产者,而不是T
的消费者
这和Java中? extends E
是一致的,凡是用Out修饰符修饰的T只能被读取,无法被消费
In修饰符
当一个类的类型参数被声明为In时,类型参数逆变, 它就只能写入,无法被读取,充当一个消费者的角色
比如Comparable
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
// 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量
val y: Comparable<Double> = x // OK!
}
3.类型投影
将类型参数声明为out
非常方便,而且能够避免使用出子类型化的麻烦,但有些类实际上不能限制只返回T
,有些时候在一个类中T
既有充当生产者的方法,又有充当消费者的方法。
一个很好的例子是Array:
class Array<T>(val size: Int) {
fun get(index: Int): T { …… }
fun set(index: Int, value: T) { …… }
}
该类在T
上既不能是协变的也不能是逆变的。
那么下面的函数:
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}
该函数将一个数组复制到另一个数组。
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any)
// ^ 其类型为 Array<Int> 但此处期望 Array<Any>
由于Array<T>在T
上是不型变的,因此Array<Int>
和Array<Any>
都不是另一个的子类型。因为copy()方法可能会做一些不正确的事情,比如会向from
写入一些不被允许的类。因此,只要确保copy()
的from
参数只会被读取,而to
参数只会被写入
我们可以:
fun copy(from: Array<out Any>, to: Array<Any>) { …… }
这里发生的事情称为类型投影,from
是一个受限制的数组,我们只能调用返回类型为类型参数T
的方法。这就意味着我们只能读取,这就是我们的使用处型变的用法,并且是对应于 Java 的 Array<? extends Object>
、 但使用更简单些的方式。