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
2
3
4
5
6
class Fruit{}       // base

class Apple extends Fruit{}

class Lemon extends Fruit{}
class Eureka extends Lemon{}

数组是协变的

1
2
3
4
5
6
7
8
9
10
11
12
13
Fruit[] fruit = new Lemon[20];

fruit[0] = new Lemon();
fruit[1] = new Eureka();

try {
fruit[2] = new Fruit();
}catch (Exception e){
System.out.println(e);
}

运行结果:
java.lang.ArrayStoreException: Baisc.type.generic.Fruit

首先,创建的数组为 Lemon 数组,同时在栈中创建一个 fruit 的引用指向 lemon[]。因为实际数组为 Lemon Class,所以我们可以放入 Lemon 及子类 Eureka,而当我们将 Fruit 基类放入时,排除类型异常,因为并不是所有 Fruit 都属于 Lemon。

那么,为什么编译器不会发现问题呢?因为编译器会将在存储表中标识fruit是Fruit[]类型,所以编译期间通过,但在运行中才会去判断数组元素的类型约束。

泛型

为了解决这中问题,Java从引入泛型去解决编译期间的类型转换问题。但事实上,Java中的泛型不像 C++中的 模板泛型 一样,是真实的模板实例,十分灵活易于拓展。相反,而是一种语法糖,在编译期间会进行 类型擦除,最终都会替换成 非泛型上界

1
2
3
List<Lemon> list = new ArrayList<>();

在编译期间都会进行类型擦除,最终都会转为 class java.util.ArrayList 这样无类型的集合类

So,泛型是不变的

1
2
3
4

List<Fruit> list = new ArrayList<Apple>(); // 编译错误

正因为泛型在编译期间进行了类型擦除,所以在编译期间会统一类型,所以会在编译期间提示错误。

那么,如果我想表示这种类型转换的话,那该怎么办?这时就需要通配符。

泛型中的通配符和边界

  • < ? extend T >: 上界通配符 ( Upper Bounds Wildcards )
  • < ? super T >: 下界通配符 ( Lower Bounds Wildcards )

上界

1
2

List<? extends Fruit> = new ArrayList<Lemon>(); // 编译成功

为什么说是上界通配符呢?

我们把之前列出的几个类通过一颗继承关系树表示,将会得到下面的结果:

<? extend Fruit> 指明了泛型的上界为Fruit,在上面的例子中,< ? extends Fruit > 表示了一个能装水果或者属于水果的盘子。即放得下 List< Fruit > 以及 List< Lemon >的基类。

下界

1
2

List<? super Fruit> list = new ArrayList<>();

下界表示的是一个相反的概念,表示的是当前的 list能存放的是 Fruit的基类。

PECS 原则

producer extends,consumer super —《Effective Java》

1
2
3
4
5
6
7
8
9

List<? extends Fruit> list = new ArrayList<Lemon>();

Object object = list.get(0);
Fruit fruit = list.get(1);
Lemon lemon = list.get(2); // 1 编译错误

list.add(new Lemon()); // 2 编译错误
list.add(new Fruit()); // 3 编译错误

< ? 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
2
3
4
5
6
7
8
9
10

List<? super Fruit> list = new ArrayList<>();
list.add(new Eureka());
list.add(new Lemon());
list.add(new Fruit());

Lemon lemon = list.get(0); // 编译失败
Fruit fruit = list.get(0); // 编译失败

Object obj = list.get(0);
  • get ( ) : 下界规定了 List 存放的 元素的最小粒度的下限,即元素既然是 Fruit的基类,那么往里面放力度比 Fruit的都可以。
  • set ( ) : 由于类型丢失,导致存放的时候只有 基类 Object才能放下。