旧游无处不堪寻
无寻处,惟有少年心
协变还是逆变,这是一个问题

想要说清楚协变、逆变这两个概念,我们必须先理解多态、里氏代换原则和 PECS。

多态

父类指针指向子类实例对象,调用普通重写方法时,会调用父类中的方法。而调用被子类重写虚函数时,会调用子类中实现的方法,这就是多态,也是我认为面向对象三大特性中最重要的一个。

从安全性角度考虑,可以通俗的理解,子类可能含有一些父类没有的成员变量或者方法函数,但是子类肯定继承了父类所有的成员变量和方法函数。 所以用父类指针指向子类时,可以保证安全性。但是如果用子类指针指向父类的话,一旦访问子类特有的方法函数或者成员变量,就会出现非法访问,无法保证安全性。

里氏代换原则

里氏代换原则(Liskov Substitution Principle,LSP)是面向对象设计的基本原则之一,它是指任何基类可以出现的地方,子类一定可以出现。

遵从里氏替换原则:

  1. 子类必须完全实现父类的抽象方法,但不能覆盖父类的非抽象方法
  2. 子类可以实现自己特有的方法
  3. 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松
  4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格
  5. 子类的实例可以替代任何父类的实例,但反之不成立

其中第四点和第五点我们应该仔细考虑,本质还是多态。

PECS(Producer extends Consumer super)

PECS 是从集合的角度来看的。如果只是从通用集合中提取项目,则该集合是生产者,应该使用 extends,如果只是把对象放到集合里,则该集合是一个消费者,应该使用 super。
例如 Collections.copy 方法:

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
// ...
}

拷贝方法是从 src 集合取元素,则 src 集合是生产者,向 dest 集合放元素,则 dest 集合是消费者。

协变逆变

协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型 。

如果 A 和 B 是类型,f 是类型转换,<= 代表子类关系,即 A <= B 意味着 A 是 B 的子类。那么,我们有:

  1. 如果 A <= B 同时 f(A) <= f(B),那么 f 是协变的
  2. 如果 A <= B 同时 f(B) <= f(A),那么 f 是逆变的
  3. 如果以上均不成立,那么 f 是不变的

例如,在 Java 中,Cat 是 Animal 的子类,但是 List<Cat> 不是 List<Animal> 的子类,也就是说 Java 中,List 是不变的。数组 Cat[] 是 Animal[] 的子类,也就是说 Java 中,数组是协变的。

List<Dog> dogs = new ArrayList<Dog>();
List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
animals.add(new Cat());

// All compiles but throws ArrayStoreException at runtime at last line
Dog[] dogs = new Dog[10];
Animal[] animals = dogs; // compiles
animals[0] = new Cat(); // throws ArrayStoreException at runtime