浅浅理解Java中的逆变与协变
<? extends T>和<? super T>
先定义如下几个类, 表示继承关系:
1 | class Life{ |
Cat,Dog -> Animal -> Life
协变 <? extends T>
表示泛型可以等于任何 T 的子类或者 T 本身。
上界就是 T 类本身,下界是 T 类最底层的子类。
以 List<? extends T> list为例:
该语法规则的意思是: list 可以存储 T 类或者某种 T 的子类的集合。
如:
1 | void func(){ |
逆变<? super T>
表示泛型可以等于任何 T 的父类或者 T 本身。
下界是 T 类最底层的子类(T 的子类也是 T 类,视作 T 类本身),上界是 T 类最高层的父类:Object。
还是以 List<? extends T> list为例:
1 | void func(){ |
泛型擦除
前两部分中都提到了 编译器又不知道是具体是哪一种。那就当然不能让某一种具体的类被add进去。
可是 Java 里明明有泛型擦除啊,为什么不在擦除泛型确定了具体类型之后再编译后面的代码呢?
比如这段代码:
1 | void func(List<? super Animal> list){ |
func方法被编译时,由于泛型擦除,绝对是知道泛型的具体类型的,比如确定为 Life 类。那么第三行就不会报错了。可为什么 JDK 没有这么设计呢?
我们再看一段代码:
假设执行这段代码的 JDK 会先执行泛型擦除确定具体类型之后,再编译后面的代码
1 | void f() { |
编译func(lifeList);时, ? 确定为 Life 类,编译 list.add(new Life()); 不会报错。
但是编译 func(animalList)时,? 被确定为 Animal 类,再去编译 list.add(new Life());呢?
Life 类是 Animal 类吗?能 add 吗?肯定不是也不能啊。
子类可以被赋值给父类,父类不可能被赋值给子类。
毕竟:

于是编译失败。
如果再对字节码做出些修改,运行时来个编译期根本无法预料的情况,比如下面的代码:
1 | void f() { |
编译当然没问题,run 起来之后做个热部署,添加新方法:
1 |
|
完蛋,执行func方法直接抛出 ClassCastException,因为Life类不是Animal类。
为了避免这种莫名其妙的问题,倒不如一刀切(我猜的)。不管 func 方法中泛型确定为何种类型,方法体里的代码执行写操作(add)时始终把它当做 Animal 类型。
执行读操作时,管你泛型被擦除为了 Animal 的哪个父类,执行读操作(get)统一返回为 Object 类型。
只要代码初始编译能过,后面随便怎么热部署搞破坏,都不会抛出 ClassCastException。
总结
协变 <? extends T>中,add 多个子类对象时,由于不能确定泛型被擦除为哪个子类,99%的概率擦除后的类型和 add 的多个子类对象不兼容。比如 T 是 Life 类,泛型擦除为 Dog 类,add 了 Animal 类对象,必然报错。因为:父类不能当做子类。所以写操作是不可行的。
但是可以读。不管泛型擦除成了哪个子类,都是 Life或者 Object 类 ,Life life = list.get(0);或者 Object obj = list.get(0);准没问题。
因为:子类可以当做父类。
逆变<? super T>中,add 多个父类对象时,由于不能确定泛型被擦除为哪个父类,不加以限制, 99%的概率擦除后的具体类型和 add 的多个 父类对象不兼容。比如 T 是 Dog 类,泛型擦除为 Animal 类,add 了 Life 类对象,必然报错。因为:父类不能当做子类。
但是,加以限制呢?
比如:执行 add 操作时人为限制 add 的父类对象必须是 T 类或者其子类。那么不管泛型被擦除为何种父类,add的对象都是它的子类,编译通过。
读操作呢?
和协变 <? extends T>一样,不管泛型擦除成了哪个子类,都是 Object 类,Object obj = list.get(0);准没问题。
因为:子类可以当做父类。
| extends | 不能 add | 能 get,但一定得 get T 及其父类的对象 |
|---|---|---|
| super | 能 add,但一定得 add T 及其子类的对象 | 不能完全 get(只能 get Object) |









