为什么会有协变和逆变

普通类之间有继承关系比较简单, 如果 AB 的父类, 那么 B 的对象可以直接赋值给 A 类型的引用.

而一旦引入泛型, 事情就会变得复杂. List<A> 并不是 List<B> 的父类, 因此不能将 List<B> 的引用赋值给 List<A>的引用.

为了对泛型类之间实现继承关系进而引入了协变和逆变的概念.

实际上, Java 系的语言在编译过程中会发生类型擦除, 导致所有泛型的类型被抹除. 协变与逆变的概念仅仅存在于编译器语法检查过程中, 因此并不会对编译产物造成影响, 更不会出现在编译产物中.

2022-05-07_10-41.png

协变

泛型类与参数类型保持一致的变化关系叫做协变. 比如 AB 的父类, 那么 List<A> 也是 List<B> 的父类. 即:

A a = new B()List<A> la = new List<B>().

协变的关系比较容易理解, 一般用于我们对一个集合的元素进行抽象, 以便后续取出元素进行使用. 这是典型的消费逻辑, 即只从集合中取元素. 协变的集合不能够动态的向其中插入新元素, 原因在下面 协变的代价 中我们就会讲到.

Untitled

协变的语法

在 Java 中, 通过 ? extends X 表示与 X 的协变关系. 如上例则改为:

A a = new B();

ArrayList<? extends A> la = new ArrayList<B>();     // fine.

协变的代价

协变的泛型类只能调用 get 方法, 不能调用 set 方法. 举个例子, 我们有 A, B, C, D 四个类, 依次继承, 如下图:

public static class A {}
public static class B extends A {}
public static class C extends B {}
public static class D extends C {}

...

ArrayList<? extends B> lb = new ArrayList<C>();

B b = lb.get(0);     // fine.

lb.add(new C());     // error: java: incompatible types: com.walfud.Main.C be 
                               converted capture#1 of ? extends com.walfud.Main.B

2022-05-07_12-24.png

由于 ? extends B 限定了元素一定是 B 的子类, 虽然不知道是哪个子类, 但一定可以通过 B 类型进行引用. 因此 B b = lb.get(0) 是合法的.

set 方法为什么不可以呢? 从 JVM 运行时角度讲由于存在类型擦除, 所有的泛型对象都是 Object 类型, 所以从底层角度讲完全可以实现 lb.add(new C()). 上述的编译错误完全是编译器语法检查阶段人为做的限制. 目的是为了从语法层面避免出现运行时的类型错误. 比如如下代码:

ArrayList<? extends B> lb = new ArrayList<C>();

lb.add(new B());  // if this statment compiled, 
                  // there'll be a potential runtime error!!!

lb 本质是 List<C> 类型的列表, Java 语义保障了 lb 内全部都之 C 或其子类对象. 如果 lb.add 方法编译通过, 那么会导致一个 List<C> 的数组内出现 B 类型的对象实例. 这就违背了 Java 类型保障的基本原则.

2022-05-08_00-12.png

逆变

泛型类与参数类型呈现相反的变化关系叫做逆变. 比如 AB 的父类, 那么 List<A> 却是 List<B> 的子类. 即:

A a = new B()List<B> lb = new List<A>().

初看起来这很违反直觉. 那么逆变有什么作用呢? 其实逆变机制是为了向一个不知道具体类型的集合中安全的插入元素. 我们先来看下语法, 接下来详细讲述逆变的用途和逻辑.

2022-05-07_23-19.png

逆变的语法

在 Java 中, 通过 ? super X 表示与 X 的逆变关系. 如上例则改为:

A a = new B();

ArrayList<? super B> lb = new ArrayList<A>();     // amazing, but fine.

逆变的用途