<? extends T>和<? super T>

先定义如下几个类, 表示继承关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Life{
String name;
}

class Animal extends Life{

}

class Dog extends Animal{

}

class Cat extends Animal{

}

Cat,Dog -> Animal -> Life

协变 <? extends T>

表示泛型可以等于任何 T 的子类或者 T 本身。

上界就是 T 类本身,下界是 T 类最底层的子类。

List<? extends T> list为例:

该语法规则的意思是: list 可以存储 T 类或者某种 T 的子类的集合。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void func(){
/*
* 下面三行add代码全部会报错。编译器只能确定list中的元素是Animal或者是Animal的子类
* 但具体是哪一类,并不能确定,可能是Dog,也可能是Cat。那就不能让某一种具体的类被add进去。
* 所以,以协变作为泛型的集合类,不能写数据,但可以读数据,
* 可以任意用Animal类或者Animal的父类去接收其中的数据。
* 毕竟,子类赋值给父类是再自然不过的事情。
*/
List<? extends Animal> list = new ArrayList<>();// 上界是Animal,下界是Cat & Dog
list.add(new Dog()); // 错误
list.add(new Cat()); // 错误
list.add(new Animal()); // 错误


/*
* 可以读取,因为list里的元素要么是Animal,要么是Animal的子类。
* 子类赋给父类当然没问题
*/
Animal animal = list.get(0);
}

逆变<? super T>

表示泛型可以等于任何 T 的父类或者 T 本身。

下界是 T 类最底层的子类(T 的子类也是 T 类,视作 T 类本身),上界是 T 类最高层的父类:Object

还是以 List<? extends T> list为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
void func(){
/*
* 第三行出错的原因在于:list的元素类型可以是Animal,可以是Life,也可以是Object,
* 编译器又不知道是具体是哪一种。那就不能让某一种具体的类被add进去。
* 但是Animal及其子类,即是Animal类,也是Life类,更是Object类。那当然可以被add。
*/
List<? super Animal> list = new ArrayList<>(); // 上界是Object,下界是Cat & Dog
list.add(new Animal()); // 合法,可以添加Animal类型的数据
list.add(new Dog()); // 合法,可以添加Dog类型的数据
list.add(new Object()); // 编译错误!只能添加Animal或其子类
Object obj = list.get(0); // 合法,只能读取为Object类型
}

泛型擦除

前两部分中都提到了 编译器又不知道是具体是哪一种。那就当然不能让某一种具体的类被add进去。

可是 Java 里明明有泛型擦除啊,为什么不在擦除泛型确定了具体类型之后再编译后面的代码呢?

比如这段代码:

1
2
3
4
5
6
7
void func(List<? super Animal> list){
list.add(new Animal());
list.add(new Dog());
list.add(new Life()); // 编译错误!只能添加Animal或其子类
Object obj = list.get(0);
}

func方法被编译时,由于泛型擦除,绝对是知道泛型的具体类型的,比如确定为 Life 类。那么第三行就不会报错了。可为什么 JDK 没有这么设计呢?

我们再看一段代码:

假设执行这段代码的 JDK 会先执行泛型擦除确定具体类型之后,再编译后面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
void f() {
List<Life> lifeList = new ArrayList<>();
func(lifeList);
List<Animal> animalList = new ArrayList<>();
func(animalList);
}

void func(List<? super Animal> list){
list.add(new Animal());
list.add(new Dog());
list.add(new Life()); // 编译错误!只能添加Animal或其子类
Object obj = list.get(0);
}

编译func(lifeList);时, ? 确定为 Life 类,编译 list.add(new Life()); 不会报错。

但是编译 func(animalList)时,? 被确定为 Animal 类,再去编译 list.add(new Life());呢?

Life 类是 Animal 类吗?能 add 吗?肯定不是也不能啊。

子类可以被赋值给父类,父类不可能被赋值给子类。

毕竟:

于是编译失败。

如果再对字节码做出些修改,运行时来个编译期根本无法预料的情况,比如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
void f() {
List<Life> lifeList = new ArrayList<>();
func(lifeList);
}

void func(List<? super Animal> list){
list.add(new Animal());
list.add(new Dog());
list.add(new Life()); // 编译错误!只能添加Animal或其子类
Object obj = list.get(0);
}

编译当然没问题,run 起来之后做个热部署,添加新方法:

1
2
3
4
5

void ff() {
List<Animal> animalList = new ArrayList<>();
func(animalList);
}

完蛋,执行func方法直接抛出 ClassCastException,因为Life类不是Animal类。

为了避免这种莫名其妙的问题,倒不如一刀切(我猜的)。不管 func 方法中泛型确定为何种类型,方法体里的代码执行写操作(add)时始终把它当做 Animal 类型。

执行读操作时,管你泛型被擦除为了 Animal 的哪个父类,执行读操作(get)统一返回为 Object 类型。

只要代码初始编译能过,后面随便怎么热部署搞破坏,都不会抛出 ClassCastException

总结

协变 <? extends T>中,add 多个子类对象时,由于不能确定泛型被擦除为哪个子类,99%的概率擦除后的类型和 add 的多个子类对象不兼容。比如 T 是 Life 类,泛型擦除为 Dog 类,addAnimal 类对象,必然报错。因为:父类不能当做子类。所以写操作是不可行的。

但是可以读。不管泛型擦除成了哪个子类,都是 Life或者 Object 类 ,Life life = list.get(0);或者 Object obj = list.get(0);准没问题。

因为:子类可以当做父类。

逆变<? super T>中,add 多个父类对象时,由于不能确定泛型被擦除为哪个父类,不加以限制, 99%的概率擦除后的具体类型和 add 的多个 父类对象不兼容。比如 T 是 Dog 类,泛型擦除为 Animal 类,addLife 类对象,必然报错。因为:父类不能当做子类。

但是,加以限制呢?

比如:执行 add 操作时人为限制 add 的父类对象必须是 T 类或者其子类。那么不管泛型被擦除为何种父类,add的对象都是它的子类,编译通过。

读操作呢?

和协变 <? extends T>一样,不管泛型擦除成了哪个子类,都是 Object 类,Object obj = list.get(0);准没问题。

因为:子类可以当做父类。

extends 不能 add 能 get,但一定得 get T 及其父类的对象
super 能 add,但一定得 add T 及其子类的对象 不能完全 get(只能 get Object)