协变与逆变

什么是协变与逆变?

协变是指能够使用比原始指定的派生类型的派生程度更大(更具体的)类型。

逆变是指能够使用比原始指定的派生类型的派生程度更小(不太具体的)类型。

大部分语言是支持协变的,比如 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

Comments
Write a Comment