但是很抱歉,由于种种原因,Java 并不支持。但是,Java 并不是完全抹杀了泛型的型变特性,Java 提供了 extends T> 和 super T> 使泛型拥有协变和逆变的特性。
extends T> 与 super T>
extends T> 称为上界通配符, super T> 称为下界通配符。使用上界通配符可以使泛型协变,而使用下界通配符可以使泛型逆变。
比如之前举的例子
List l = new ArrayList<>();
List
如果使用上界通配符,
List l = new ArrayList<>();
List extends Object> o = l;// 可以通过编译复制代码
这样,List extends Object> 的类型就大于 List 的类型了,也就实现了协变。这也就是所谓的“子类的泛型是泛型的子类”。
同样,下界通配符 super T> 可以实现逆变,如:
public List super Integer> fun(){
List
上述代码怎么就实现逆变了呢?首先,Object > Integer;另外,从前言我们知道,函数返回值类型必须大于实际返回值类型,在这里就是 List super Integer> > List,和 Object > Integer 刚好相反。也就是说,经过泛型变化后,Object 和 Integer 的类型关系翻转了,这就是逆变,而实现逆变的就是下界通配符 super T>。
从上面可以看出, extends T> 中的上界是 T,也就是说 extends T> 所泛指的类型都是 T 的子类或 T 本身,所以 T 大于 extends T> 。 super T> 中的下界是 T,也就是说 super T> 所泛指的类型都是 T 的父类或 T 本身,所以 super T> 大于 T。
class Container { // (1)
private var item: T? = null
fun get(): T? = item
}
val c: Container = Container()// (2)编译通过,因为 T 是一个 out-参数复制代码
(1) 处直接使用 指定 T 类型只能出现在生产者的位置上。虽然多了一些限制,但是,在 kotlin 编译器在知道了 T 的角色以后,就可以像 (2) 处一样将 Container 直接赋值给 Container,好像泛型直接可以协变了一样,而不需要再使用 Java 当中的通配符 extends String>。
同样的,对于消费者来说,
class Container { // (1)
private var item: T? = null
fun set(t: T) {
item = t
}
}val c: Container = Container() // (2) 编译通过,因为 T 是一个 in-参数复制代码
代码 (1) 处使用 指定 T 类型只能出现在消费者的位置上。代码 (2) 可以编译通过, Any > String,但是 Container 可以被 Container 赋值,意味着 Container 大于 Container ,即它看上去就像 T 直接实现了泛型逆变,而不需要借助 super String> 通配符来实现逆变。如果是 Java 代码,则需要写成 Container super String> c = new Container(); 。
这就是声明处型变,在类声明的时候使用 out 和 in 关键字,在使用时可以直接写出泛型型变的代码。
而 Java 在使用时必须借助通配符才能实现泛型型变,这是使用处型变。
类型投影
有时一个类既可以作生产者又可以作消费者,这种情况下,我们不能直接在 T 前面加 in 或者 out 关键字。比如:
class Container { private var item: T? = null
fun set(t: T?) {
item = t
} fun get(): T? = item
}复制代码
考虑这个函数:
fun copy(from: Container, to: Container) {
to.set(from.get())
}复制代码
当我们实际使用该函数时:
val from = Container()val to = Container()
copy(from, to) // 报错,from 是 Container 类型,而 to 是 Container 类型复制代码
这样使用的话,编译器报错,因为我们把两个不一样的类型做了赋值。用 kotlin 官方文档的话说,copy 函数在”干坏事“, 它尝试写一个 Any 类型的值给 from, 而我们用 Int 类型来接收这个值,如果编译器不报错,那么运行时将会抛出一个 ClassCastException 异常。
所以应该怎么办?直接防止 from 被写入就可以了!
将 copy 函数改为如下所示:
fun copy(from: Container, to: Container) { // 给 from 的类型加了 out
to.set(from.get())
}val from = Container()val to = Container()
copy(from, to) // 不会再报错了复制代码
// Illegal code - because otherwise life would be BadList dogs = new List();
List animals = dogs; // Awooga awoogaanimals.add(new Cat());// (1)Dog dog = dogs.get(0); //(2) This should be safe, right?复制代码
如果上述代码可以通过编译,即 List 可以赋值给 List,List 是协变的。接下来往 List 中 add 一个 Cat(),如代码 (1) 处。这样就有可能造成代码 (2) 处的接收者 Dog dog 和 dogs.get(0) 的类型不匹配的问题。会引发运行时的异常。所以 Java 在编译期就要阻止这种行为,把泛型设计为默认不型变的。
总结
1、Java 泛型默认不型变,所以 List 不是 List 的子类。如果要实现泛型型变,则需要 extends T> 与 super T> 通配符,这是一种使用处型变的方法。使用 extends T> 通配符意味着该类是生产者,只能调用 get(): T 之类的方法。而使用 super T> 通配符意味着该类是消费者,只能调用 set(T t)、add(T t) 之类的方法。
2、Kotlin 泛型其实默认也是不型变的,只不过使用 out 和 in 关键字在类声明处型变,可以达到在使用处看起来像直接型变的效果。但是这样会限制类在声明时只能要么作为生产者,要么作为消费者。
使用类型投影可以避免类在声明时被限制,但是在使用时要使用 out 和 in 关键字指明这个时刻类所充当的角色是消费者还是生产者。类型投影也是一种使用处型变的方法。