Java泛型里的协变和逆变

时间:2021-03-15 19:23:03

Java泛型里的协变和逆变
通过实例来看问题,

// 定义三个类: Benz -> Car -> Vehicle,它们之间是顺次继承关系

class Vehicle {}
class Car extends Vehicle {}
class Benz extends Car {}

// 定义一个util类,其中用到泛型里的协变和逆变
class Utils<T> {
T get(List<? extends T> list, int i) {
return list.get(i);
}

void put(List<? super T> list, T item) {
list.add(item);
}

void copy(List<? super T> to, List<? extends T> from) {
for(T item : from) {
to.add(item);
}
}
}

// 测试函数

void test() {
List<Vehicle> vehicles = new ArrayList<>();
List<Benz> benzs = new ArrayList<>();
Utils<Car> carUtils = new Utils<>();

carUtils.put(vehicles, new Car());
Car car = carUtils.get(benzs, 0);
carUtils.copy(vehicles, benzs);
}

我们只需关注Utils.copy()函数即可,两个参数from, to均为list,

  • 对from的要求:其中的对象必须是Car或者Car的子类,即可以用Car来引用这些对象
  • 对to的要求:它必须可以保存Car类型的对象,即其元素的类型必须是Car或者Car的父类

接下来看看该函数的使用情况,carUtils.copy(vehicles, benzs);,参数的含义是:

  • List<? extents Car>:这个类型集合(List, List)里的元素可以使用替换原则
  • List<? super Car>:这个类型集合(List,List)里的元素也可以使用替换原则
    都可以使用替换原则了,但是他们有何区别呢?

    • List<? extents Car>List<? extents Car>? extends Car的序关系是一致的
    • List<? super Car>List<? super Car>? super Car的序关系是相反的

其中,? extends Car, ? super Car, List<? extents Car>, List<? super Car>
均为类型集合,序关系小的可以替换序关系大的。其实在类型系统里面,Liskov替换原则可以
进一步推广为: 任何序关系大的类型可以出现的地方,序关系小的类型一定可以出现。
而继承关系是一种特殊的序关系,当然这需要语言的类型系统支持才可以。

协变和逆变
定义(wikipedia)
covariant if it preserves the ordering of types (≤), which orders types from more specific to more generic;
contravariant if it reverses this ordering;
bivariant if both of these apply (i.e., both I ≤ I and I ≤ I at the same time);
invariant or nonvariant if neither of these applies.

理解
设T是一个类型集合(type set),其中的元素是一个个类型,如Vehicle, Car, Benz,
S是一个根据T生成的类型集合(如List),其中的元素也是一个个类型,如S,
S, S,那么我们有如下定义,

如果集合S里的序关系跟集合T里的序关系一致,那么就说S跟T是协变关系
如果集合S里的序关系跟集合T里的序关系相反,那么就说S跟T是逆变关系
然后,根据序关系的大小就可以使用替换原则了。那函数Utils.copy()的参数
为啥要用? extends T,? super T而不直接使用T呢,void copy(List to, List from),
把T替换成Car之后,要使用这个函数就只能使用List了,但是很明显,我们完全可以
将一个List copy 到一个List或者List里面,要怎么解决呢?
当然是使用协变和逆变:

对于from参数,? extends T表示跟T满足协变关系的List就可以使用替换原则
对于to参数,? super T表示跟T满足逆变关系的List就可以使用替换原则
这样就不用仅仅局限到List了。

协变、逆变使用的时机
然后问题又来了,什么时候使用协变,什么时候使用逆变呢?
仔细观察(C#里面已经观察好久了)就会发现,

如果只是读取的话,那么满足协变关系的类型可以使用替换原则
如果只是写入的话,那么满足逆变关系的类型可以使用替换原则
比如上面的函数,void copy(List