什么是协变与逆变?
协变是指能够使用比原始指定的派生类型的派生程度更大(更具体的)类型。
逆变是指能够使用比原始指定的派生类型的派生程度更小(不太具体的)类型。
大部分语言是支持协变的,比如 C++,C#,Java。但是 Java 并不支持逆变。
Number num = new Integer(1);
Number[] numbers = new Integer[10];//Java 中数组的协变
Integer[] integers = new Number[10];//编译错误,不允许父类变量赋值给子类变量
Java 泛型与协变和逆变
虽然 Java 是支持协变,而不支持逆变的,但是对于泛型来说需要特殊考虑了。
普通的泛型是不支持协变和逆变的:
List<Object> oList = new ArrayList<String>();//编译错误
List<String> sList = new ArrayList<Object>();//编译错误
但是有时候我们需要用到协变或者逆变的场景,这时候为了实现协变和逆变我们就可以使用通配符 ? 来实现:
< ? extends T > 实现泛型的协变(? extends Object 的含义是:运行 Object 的子类,也包括 Object,作为泛型参数。)
List< ? extends Object > lis = new ArrayList< String >();
< ? super T > 实现泛型的逆变(? super String 的含义是:运行 String 的父类,也包括 String,作为泛型参数。)
List< ? super String > strings = new ArrayList< Object >();
再探泛型边界
extends
List< ? extends Number > list = new ArrayList<>(Arrays.asList(1,2.0));
System.out.println(list.get(0));//输出 1
System.out.println(list.get(1));//输出 2.0
list.add(1);//编译错误
list.add(2.0)//编译错误
注意到上面代码 get 方法可以正确执行,add 方法却编译错误了。
看下 List 源码中的 get 和 add 方法实现:
E get(int index);
boolean add(E e);
可以知道在调用 add 方法的时候,泛型参数 E 变成了 < ? extends Number > 即类型变为了 Number 或者 Number 子类型中的一种,可能是 Number,可能是 Integer,也可能是 Double,并不确定,在编译的时候编译器并不知道具体会传入什么类型,会以一个占位符 CAP#1 来表示这个类型,在传入 Integer 或者其它类型后,编译器并不知道能不能和这个占位符进行匹配,所以就不允许这种情况发生。
编译代码,会报以下错误:
错误: 对于add(Integer), 找不到合适的方法
list.add(new Integer(1));
^
方法 Collection.add(CAP#1)不适用
(参数不匹配; Integer无法转换为CAP#1)
方法 List.add(CAP#1)不适用
(参数不匹配; Integer无法转换为CAP#1)
其中, CAP#1是新类型变量:
CAP#1从? extends Number的捕获扩展Number
至于 get 方法可以正确执行,个人理解是因为上界就是 Number,在访问元素的时候完全可以用 Number 变量来访问 其自身及其子类的对象。
super
List< ? super Number > list = new ArrayList<>();
list.add(1);
list.add(2.0);
list.add(new Object());//编译错误
Integer integer = list.get(0);//编译错误
Object object = list.get(0);//编译成功
super 与 extens 功能正好相反,它确定了 List 持有类型为 Number 或者 Number 父类中的某一类型,所以传入 Number,Integer,Double 等类型完全可以被 Number 变量访问的。所以 add 方法可以正常执行。
get 方法也可以正常执行,但是因为不知道其类型究竟是哪个基类,所以在获取后,不能用其自身或者子类变量来访问,只能赋值给 Object 变量,因为 Object 类所有类的基类。
PECS总结(使用 extends 和 super 的场景判断):
要从泛型类取数据时,用extends;
要往泛型类写数据时,用super;
既要取又要写,就不用通配符(即extends与super都不用)。
参考:
https://docs.microsoft.com/zh-cn/dotnet/standard/generics/covariance-and-contravariance
https://www.cnblogs.com/en-heng/p/5041124.html
http://swiftlet.net/archives/1950