想要说清楚协变、逆变这两个概念,我们必须先理解多态、里氏代换原则和 PECS。
多态
父类指针指向子类实例对象,调用普通重写方法时,会调用父类中的方法。而调用被子类重写虚函数时,会调用子类中实现的方法,这就是多态,也是我认为面向对象三大特性中最重要的一个。
从安全性角度考虑,可以通俗的理解,子类可能含有一些父类没有的成员变量或者方法函数,但是子类肯定继承了父类所有的成员变量和方法函数。 所以用父类指针指向子类时,可以保证安全性。但是如果用子类指针指向父类的话,一旦访问子类特有的方法函数或者成员变量,就会出现非法访问,无法保证安全性。
里氏代换原则
里氏代换原则(Liskov Substitution Principle,LSP)是面向对象设计的基本原则之一,它是指任何基类可以出现的地方,子类一定可以出现。
遵从里氏替换原则:
- 子类必须完全实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类可以实现自己特有的方法
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格
- 子类的实例可以替代任何父类的实例,但反之不成立
其中第四点和第五点我们应该仔细考虑,本质还是多态。
PECS(Producer extends Consumer super)
PECS 是从集合的角度来看的。如果只是从通用集合中提取项目,则该集合是生产者,应该使用 extends,如果只是把对象放到集合里,则该集合是一个消费者,应该使用 super。
例如 Collections.copy 方法:
|
拷贝方法是从 src 集合取元素,则 src 集合是生产者,向 dest 集合放元素,则 dest 集合是消费者。
协变逆变
协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型 。
如果 A 和 B 是类型,f 是类型转换,<= 代表子类关系,即 A <= B 意味着 A 是 B 的子类。那么,我们有:
- 如果 A <= B 同时 f(A) <= f(B),那么 f 是协变的
- 如果 A <= B 同时 f(B) <= f(A),那么 f 是逆变的
- 如果以上均不成立,那么 f 是不变的
例如,在 Java 中,Cat 是 Animal 的子类,但是 List<Cat> 不是 List<Animal> 的子类,也就是说 Java 中,List 是不变的。数组 Cat[] 是 Animal[] 的子类,也就是说 Java 中,数组是协变的。
|