Java泛型-协变与逆变
概念
协变与逆变 (Covariance and contravariance ) 用来描述具有父/子关系的类型通过类型转换之后的继承关系。
即:如果A、B表示类型,f()表示类型转换, 表示子类与父类之间的继承关系,那么有以下定义:
协变(Covariance):当 A $\subseteq$ B时,f(A) $\subseteq$ f(B)成立;
逆变(contravariance):当A $\subseteq$ B时,f(B) $\subseteq$ f(A)成立;
不变(invariance):当A $\subseteq$ B时,以上均不成立,那么f(A)与f(B)之间不存在继承关系;
先定义几个类之间的继承关系
1 | class Fruit{} // base |
数组是协变的
1 | Fruit[] fruit = new Lemon[20]; |
首先,创建的数组为 Lemon 数组,同时在栈中创建一个 fruit 的引用指向 lemon[]。因为实际数组为 Lemon Class,所以我们可以放入 Lemon 及子类 Eureka,而当我们将 Fruit 基类放入时,排除类型异常,因为并不是所有 Fruit 都属于 Lemon。
那么,为什么编译器不会发现问题呢?因为编译器会将在存储表中标识fruit是Fruit[]类型,所以编译期间通过,但在运行中才会去判断数组元素的类型约束。
泛型
为了解决这中问题,Java从引入泛型去解决编译期间的类型转换问题。但事实上,Java中的泛型不像 C++中的 模板泛型 一样,是真实的模板实例,十分灵活易于拓展。相反,而是一种语法糖,在编译期间会进行 类型擦除,最终都会替换成 非泛型上界。
1 | List<Lemon> list = new ArrayList<>(); |
So,泛型是不变的
1 |
|
那么,如果我想表示这种类型转换的话,那该怎么办?这时就需要通配符。
泛型中的通配符和边界
- < ? extend T >: 上界通配符 ( Upper Bounds Wildcards )
- < ? super T >: 下界通配符 ( Lower Bounds Wildcards )
上界
1 |
|
为什么说是上界通配符呢?
我们把之前列出的几个类通过一颗继承关系树表示,将会得到下面的结果:
<? extend Fruit> 指明了泛型的上界为Fruit,在上面的例子中,< ? extends Fruit > 表示了一个能装水果或者属于水果的盘子。即放得下 List< Fruit > 以及 List< Lemon >的基类。
下界
1 |
|
下界表示的是一个相反的概念,表示的是当前的 list能存放的是 Fruit的基类。
PECS 原则
producer extends,consumer super —《Effective Java》
1 |
|
< ? extends Fruit > 只能存,不能放
- get( ) : extends 规定了容器的上界,所以容器中获取的类型只能是 Fruit 或是 它的基类即 Object。
- set( ) : 由于编译器不知道 List<? extends Fruit> 到底指的是什么类型,有可能是 Apple, 也有可能是 Lemon,所以会先在 List上打上标识符:CAP#1,表示捕获一个 Fruit 或 Fruit的子类,但却没有具体的类型可以与这个 CAP#1 进行匹配,所以在执行这种向上转型的时候,将散失其中传递对象的能力。
类比于数组,当我们将 Lemon[] 向上转型为 Fruit[]的时候,在运行期间往数组中添加 fruit会抛出异常,而泛型的时候,就是将这种类型检查移到编译期间,拒绝一切不安全的类型协变。
< ? super Fruit > 只能放,不能读
1 |
|
- get ( ) : 下界规定了 List 存放的 元素的最小粒度的下限,即元素既然是 Fruit的基类,那么往里面放力度比 Fruit的都可以。
- set ( ) : 由于类型丢失,导致存放的时候只有 基类 Object才能放下。