在考虑泛型类之间关系之前,我们需要熟悉以下术语,用于描述程序设计语言的类型关系:

协变(Covariance):使你能够使用比原始指定的类型派生程度更大的类型。你可以向List类型的变量分配List

逆变(Contravariance):使你能够使用比原始指定的类型更泛型(派生程度更小)的类型。你可以向List类型的变量分配List

不变(Invariance):这意味着,你只能使用原始指定的类型。固定泛型类型参数既不是协变类型,也不是逆变类型。你无法向List类型的变量分配List(Of Base)。


1. 数组


       我们早已习惯类的继承关系,对于一个父类B,不管是在赋值语句,还是在容器中,或者是在方法参数上,我们总能B的子类型完成业务逻辑。比如:

//compile success
public static void main(String[] args) {
    Object obj = "";
    funcArg(1);
    List<Object> list = new ArrayList<>();
    list.add(0.5);
}

public static void funcArg(Object param) {}

代码清单1


       对于数组来说,考虑以下代码:

//compile success
Number[] integerArray = new Integer[]{1, 2, 3};
Number[] bigDecimalArray = new BigDecimal[]{new BigDecimal("")};
Number[] numberArray = {1, 0.5, new BigDecimal("")};

代码清单2


       从代码清单2中可以发现,在数组的赋值中,依然遵循普通类的赋值关系,可以将子类型数组赋值给父类数组,这样的关系称为协变。


2. 不变


       但是在泛型中,type argument的类型关系和泛型类的类型关系并不直观。考虑类型List<Number >,在赋值时,我们能给它传递什么呢?考虑如下代码:

//compile success
List<Number> numberList = new ArrayList<>();

//compile fail: require Number, found Integer, type mismatch.
numberList = new ArrayList<Integer>();

代码清单3


       在代码清单3中可以看到,即使type argument存在关系,比如Integer是Number的子类,并不意味着泛型类之间存在关系。 实际上,确定类型的泛型类是不变的,List <Number >和List<Integer>无直接关系,仅存在一个共同父类Object,如下图所示:


c compile

图片来源


       确定类型的泛型类要想建立关系,和普通类一样,必须通过extends或者implements显示地定义类层级。比如JDK集合框架中的例子,ArrayList<String >是Iterable<String >的子类。

       除了与父类拥有一样的type parameter之外,子类还可以额外新增不同的type parameter,考虑官方教程的代码例子:

//compile success
interface PayloadList<E,P> extends List<E> {
    void setPayload(int index, P val);
}

代码清单4


       在代码清单4中,子类PayloadList新增了type parameter P,对于任意的P,比如PayloadList<String,String>,PayloadList<String,Number>,PayloadList<String,Integer>, 都同属于List<String >的子类。类关系如下图所示:


c compile

图片来源


3. 通配符


       虽然type argument的类关系不会延伸到泛型类上,但是我们可以利用通配符实现特定的类型关系。 我们已经知道List <Number >和List<Integer>无直接关系,但是List <Number >和List<Integer>都同属于List <? >的子类。

       注:本节的测试代码不建议在实际业务中使用,晦涩难懂,不利于维护。

3.1 通配符协变

       利用上界通配符可以实现协变关系,考虑以下代码:

//compile success
public static void wildcardCovariance() {
    List<? extends Integer> intList = new ArrayList<>();
    List<? extends Number> numList = intList;
}

代码清单5


       在代码清单5中,将一个上界为Integer的列表,赋值给了上界为Number的列表。

3.2 通配符逆变

       利用下界通配符可以实现逆变关系,考虑以下代码:

//compile success
public static void wildcardContravariance() {
    List<? super Number> numList = new ArrayList<>();
    List<? super Integer> intList = numList;
}

代码清单6


       在代码清单6中,将一个下界为Number的列表,赋值给了下界为Integer的列表。具体类型与通配符之间的关系如下图所示:


c compile

图片来源


延伸

       有关通配符上下界的使用,还可以阅读泛型中<? super T>和<? extends T>的区别Guidelines for Wildcard Use


附件

       本博文所展示的源代码由此获得


参考文献

[1] Josh Juneau.泛型:工作原理及其重要性[EB/OL].https://www.oracle.com/technetwork/cn/articles/java/juneau-generics-2255374-zhs.html,2019-07-02.
[2] The Java™ Tutorials - Generics[EB/OL].https://docs.oracle.com/javase/tutorial/java/generics/index.html,2019-07-02.
[3] The Java® Virtual Machine Specification[EB/OL].https://docs.oracle.com/javase/specs/jvms/se10/html/jvms-4.html#jvms-4.3,2019-07-04.
[4] 泛型中的协变和逆变[EB/OL].https://docs.microsoft.com/zh-cn/dotnet/standard/generics/covariance-and-contravariance,2019-07-16.