在考虑泛型类之间关系之前,我们需要熟悉以下术语,用于描述程序设计语言的类型关系:
协变(Covariance):使你能够使用比原始指定的类型派生程度更大的类型。你可以向List
类型的变量分配List 。 逆变(Contravariance):使你能够使用比原始指定的类型更泛型(派生程度更小)的类型。你可以向List
类型的变量分配List 不变(Invariance):这意味着,你只能使用原始指定的类型。固定泛型类型参数既不是协变类型,也不是逆变类型。你无法向List
类型的变量分配List(Of Base)。
1. 数组
我们早已习惯类的继承关系,对于一个父类B,不管是在赋值语句,还是在容器中,或者是在方法参数上,我们总能B的子类型完成业务逻辑。比如:
代码清单1
对于数组来说,考虑以下代码:
代码清单2
从代码清单2中可以发现,在数组的赋值中,依然遵循普通类的赋值关系,可以将子类型数组赋值给父类数组,这样的关系称为协变。
2. 不变
但是在泛型中,type argument的类型关系和泛型类的类型关系并不直观。考虑类型List<Number >,在赋值时,我们能给它传递什么呢?考虑如下代码:
代码清单3
在代码清单3中可以看到,即使type argument存在关系,比如Integer是Number的子类,并不意味着泛型类之间存在关系。 实际上,确定类型的泛型类是不变的,List <Number >和List<Integer>无直接关系,仅存在一个共同父类Object,如下图所示:
确定类型的泛型类要想建立关系,和普通类一样,必须通过extends或者implements显示地定义类层级。比如JDK集合框架中的例子,ArrayList<String >是Iterable<String >的子类。
除了与父类拥有一样的type parameter之外,子类还可以额外新增不同的type parameter,考虑官方教程的代码例子:
代码清单4
在代码清单4中,子类PayloadList新增了type parameter P,对于任意的P,比如PayloadList<String,String>,PayloadList<String,Number>,PayloadList<String,Integer>, 都同属于List<String >的子类。类关系如下图所示:
3. 通配符
虽然type argument的类关系不会延伸到泛型类上,但是我们可以利用通配符实现特定的类型关系。 我们已经知道List <Number >和List<Integer>无直接关系,但是List <Number >和List<Integer>都同属于List <? >的子类。
注:本节的测试代码不建议在实际业务中使用,晦涩难懂,不利于维护。
3.1 通配符协变
利用上界通配符可以实现协变关系,考虑以下代码:
代码清单5
在代码清单5中,将一个上界为Integer的列表,赋值给了上界为Number的列表。
3.2 通配符逆变
利用下界通配符可以实现逆变关系,考虑以下代码:
代码清单6
在代码清单6中,将一个下界为Number的列表,赋值给了下界为Integer的列表。具体类型与通配符之间的关系如下图所示:
延伸
有关通配符上下界的使用,还可以阅读泛型中<? 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.