逆变与协变
逆变与协变综述:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类):
- f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
- f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
- f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
协变(Co-variance)
- 父类型->子类型:越来越具体(specific)。
- 在LSP中,返回值和异常的类型:不变或变得更具体 。
逆变(Contra-variance)
- 父类型->子类型:越来越抽象。
- 参数类型:要相反的变化,不变或越来越抽象。
LSP定义
Functions that use pointers or referrnces to base classes must be able to use objects of derived classes without knowing
it.(所有引用基类的地方必须能透明地使用其子类的对象。)
里氏替换原则的主要作用就是规范继承时子类的一些书写规则。其主要目的就是保持父类方法不被覆盖。
含义
- 子类必须完全实现父类的方法
- 子类可以有自己的个性
- 覆盖或实现父类的方法时输入参数可以被放大
- 覆盖或实现父类的方法时输出结果可以被缩小
LSP是子类型关系的一个特殊定义,称为(强)行为子类型化。在编程语言中,LSP依赖于以下限制:
- 前置条件不能强化
- 后置条件不能弱化
- 不变量要保持或增强
- 子类型方法参数:逆变
- 子类型方法的返回值:协变
- 异常类型:协变
各种应用中的LSP:
1.【数组是协变的】
数组是协变的:一个数组T[ ] ,可能包含了T类型的实例或者T的任何子类型的实例
Number[] numbers = new Number[2];
numbers[0] = new Integer(10);
numbers[1] = new Double(3.14);
Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
myNumber[0] = 3.14; //run-time error!
报错的原因是myNumber指向的还是一个Integer[] 而不是Number[]
2.【泛型中的LSP】
Java中泛型是不变的,但可以通过通配符"?"实现协变和逆变:
- <? extends>实现了泛型的协变:
- List<? extends Number> list = new ArrayList();
- <? super>实现了泛型的逆变:
- List<? super Number> list = new ArrayList();
由于泛型的协变只能规定类的上界,逆变只能规定下界,使用时需要遵循PECS(producer–extends, consumer-super):
- 要从泛型类取数据时,用extends;
- 要往泛型类写数据时,用super;
- 既要取又要写,就不用通配符(即extends与super都不用)。
泛型是类型不变的(泛型不是协变的)。举例来说
- ArrayList 是List的子类型
- List不是List的子类型
在代码的编译完成之后,泛型的类型信息就会被编译器擦除。因此,这些类型信息并不能在运行阶段时被获得。这一过程称之为类型擦除(type erasure)。
类型擦除的详细定义:如果类型参数没有限制,则用它们的边界或Object来替换泛型类型中的所有类型参数。因此,产生的字节码只包含普通的类、接口和方法。
类型擦除的结果: 被擦除 T变成了Object