MSDN的dotNET文档中有对协变逆变的定义,截取下来
协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型。 泛型类型参数支持协变和逆变,可在分配和使用泛型类型方面提供更大的灵活性。
泛型这东西特别是通配符那边确实有点绕,不好理解。
Java中数组是协变的,因为它可以这样
1 | Number[] num = new Integer[10]; |
结合最上面的定义,再想想。
但Java的泛型匹配本身前后没有继承关系,既没有协变性也没有逆变性,是不变的关系
所以你不能这样
1 | List<Number> num = new ArrayList<Integer>(); |
更不能这样
1 | List<Integer> num = new ArrayList<Number>(); |
但是有办法实现泛型的协变和逆变,就是使用泛型通配符
1 | // 实现泛型协变 |
这两个List遵循PECS(producer-extends, consumer-super)原则,生产者List使用extends,消费者List使用super。
换句话说,就是上面的test集合add方法受限,get方法不受限但只能用Number类型接收;test1集合可以add,但get方法受限。
1 | import java.util.ArrayList; |
我写这个就是为了记一下这两个到底为什么,说实话有点不好理解,网上说法很多,也说不到点,只能按照自己理解了。
一句话,泛型只存在编译阶段,是给编译器看的,编译通过后就没泛型这个东西了,一切只为类型安全,防止意外。
<? extends E>适合只用来get的场合,属于生产者,为什么不能add。
我们假设现在有A,B,C,D四个类,它们的关系有
1 | class A {} |
extends用来定义泛型上界,这里表示E和所有E的子类范围,现在我定义一个List
1 | List<? extends B> list = new ArrayList<C>(); // 正确 |
可以看到只要是B或B的子类的ArrayList都能声明。假如我定义的是List<? extends B> list = new ArrayList<D>();
,编译器又允许add的话,那我add一个C类型的元素进去怎么办,D类型的怎么去接收他的父类C类型呢?这时ArrayList就没办法安全的接收元素了,而且编译器其实在有些时候没法确定ArrayList的泛型,比如在函数形参上,void function(List<? extends B> list);
,你没调用这个函数时编译器根本不知道你会给他的实参是什么,所以编译器干脆统一不让add了(除了null,因为null代表任何类型),但是get是很安全的,因为至少可以保证所有元素都是B或B的子类,所以里面的元素都统一当作B类型,这样get出的元素只能用B类型的变量来接收。
<? super E>适合只用来add的场合,属于消费者。
super用来定义泛型下界,这里表示E和所有E的基类范围
1 | List<? super C> list = new ArrayList<B>(); // 正确 |
他可以add的范围则是E和E的所有子类,为了类型安全E的父类不会被允许add,比如我这样定义List<? super C> list = new ArrayList<B>();
,那么我规定只能add进C或C的子类,这样才能保证ArrayList能安全接收,因为ArrayList的泛型范围是[C , ?),肯定可以安全接收处在(? , C]类型范围类的元素,所add是被允许的,但是get受限,这样B value = list.get(0);
,这样C value = list.get(0);
都不行,只能Object value = list.get(0);
。
get方法由于类型丢失,所以get出来的元素除了Object类型以外都不能接受(这里不谈强制类型转换,因为这里通配泛型就是为了能扩大接收的类型范围,加入的元素类型是不确定的,如果都确定是某个类型了比如String了,那干嘛不直接声明List
那一个不能正常add一个不能正常get要他有什么用?比方用来当作函数参数,接收数据,并规定类型范围(不像<?>什么都接收)
1 | import java.util.ArrayList; |