Lenox Enjoy

Everything negative-pressure, challenges-is all an opportunity for me to rise

Covariance and contravariance

09 September 2020

这里讨论一下Java中的泛型(Generics),重点说明协变(covariant)和逆变(contravariance)

首先,在Java中,泛型类型是不变(invariant)的,这意味这List<String>并不是List<Object>的子类。并且使用有界通配符(bounded wildcards)可以增加API的灵活性。 为了方便下面讨论,我们这里定义3个有继承关系的类:Animal,Cat,Dog

1
2
3
class Animal {}
class Cat extends Animal {}
class Dog extends Animal {}

协变(covariant)

首先我们查看一下CollectionaddAll的方法签名:

1
2
3
class Collection<E> {
    boolean addAll(Collection<? extends E> c);
}

addAll的参数类型定义成Collection<? extends E>而非Collection<E>的好处在于可以让这个方法接受类型为EE的子类型的Collection,这样极大的增加了API的灵活性,这也是符合逻辑的,比如说我们定义了Collection<Animal>这样一个动物集合,那其实猫集合狗集合都是可以添加进动物集合的。一个很重要的点是,我们对这样一个带有上边界(extends-bound)的通配符的集合(Collection<? extends Animal>)只能读(read),不能写(write),为什么呢?我们上面说过,addAll支持接受猫集合狗集合等其他动物集合,那作为一个通用方法,如果我们支持写的话,那我们写(write)什么动物呢?写(write)猫,如果传递进来的是狗集合那就会发生类型不匹配,所以,我们仅仅能从Collection<? extends Animal>知道,这个集合里的元素都是Animal,这是一定不会有问题的,所以对于这样类型的参数,我们只支持读(read)

这样我们有个清晰的理解:虽然Collection不是Collection的子类型,但Collection是Collection<? extends Animal>的子类型,换句话说,带有上边界的通配符使这个类型发生了协变(the wildcard with an extends-bound (upper bound) makes the type covariant)。

1
2
3
void covariant(List<? extends Animal> items) {
    Animal first = items.get(0);  
}

逆变(contravariance)

Collection<? super Dog>,这中类型匹配所有泛型类型是Dog及其超类的集合类型,如Collection<Dog>Collection<Animal>Collection<Object>。一个很重要的点是,对于发生逆变(contravariance)的类型,我们既可以对其读(read)也可以对其写(write),但是:读(read)的类型是Object,而不是Dog,因为我们并不知具体是DogAnimal还是Object,我们仅仅知道这些类的共同点,它们有一个共同的超类-Object;写(write)的类型只支持Dog类型,因为我们并不知具体是DogAnimal还是Object,我们仅仅知道这些类的共同点,它们有一个子类-Dog

1
2
3
4
void contravariance(List<? super Dog> items) {
    items.add(new Dog());
    Object first = items.get(0);  
}

PECS

这是Producer-Extends, Consumer-Super的缩写,表示从生产者里读(协变(covariant)),写入到消费者(逆变(contravariance))。

Joshua Bloch recommends:For maximum flexibility, use wildcard types on input parameters that represent producers or consumers, and proposes the following mnemonic: PECS

— Lenox Enjoy