Java 泛型里的协变和逆变

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<Car>.copy()函数即可,两个参数from, to均为list,

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

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

  • List<? extents Car>:这个类型集合(List<Car>, List<Benz>)里的元素可以使用替换原则
  • List<? super Car>:这个类型集合(List<Car>,List<Vehicle>)里的元素也可以使用替换原则

都可以使用替换原则了,但是他们有何区别呢?

  • 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<A> ≤ I<B> and I<B> ≤ I<A> at the same time);
  • invariant or nonvariant if neither of these applies.

理解

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

  • 如果集合S<T>里的序关系跟集合T里的序关系一致,那么就说S<T>跟T是协变关系
  • 如果集合S<T>里的序关系跟集合T里的序关系相反,那么就说S<T>跟T是逆变关系

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

  • 对于from参数,? extends T表示跟T满足协变关系的List<T>就可以使用替换原则
  • 对于to参数,? super T表示跟T满足逆变关系的List<T>就可以使用替换原则

这样就不用仅仅局限到List<Car>了。

协变、逆变使用的时机

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

  • 如果只是读取的话,那么满足协变关系的类型可以使用替换原则
  • 如果只是写入的话,那么满足逆变关系的类型可以使用替换原则

比如上面的函数,void copy(List<? super Car> to, List<? extends Car> from);,
从from里面读取数据,则完全可以从List<Car>,List<Benz>里面读取,
而往to里面写入数据,则完全可以往List<Car>,List<Vehicle>里面写入,
所以from使用满足协变关系的类型而to使用满足逆变关系的类型。事实上,在C#,Kotlin里,
直接使用out, in来表示协变关系和逆变关系,比如Kotlin里面这样定义copy函数,
fun copy(to: List<in Car>, from: List<out Car>),然后就可以这样使用了,

  • copy(cars, benzs)
  • copy(cars, cars)
  • copy(vehicles, benzs)
  • copy(vehicles, cars)

by:iGNU