浅浅理解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) |