定义
从不同出发点,该原则的定义存在以下两种描述方式。定义一从条件出发,得出结论。定义二从结论出发,得出特点。定义一和定义二在描述方向上恰好相反,不过两者的目的都是要强调类间建立继承关系时需要遵循的条件。
- 定义一 描述了构成父子关系需要满足的条件,即类间能够透明替换----->是父子关系。
如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。 - 定义二 描述了构成父子关系的类具备的特点 即类间是父子关系------>可以透明替换
子类对象能够替换父类对象,所有引用父类的地方必须能够透明地使用其子类的对象,而程序逻辑不变。
作用
- 里氏替换原则是实现开闭原则的重要方式之一。
- 开闭原则中,扩展类有继承父类和实现接口两种方式。里氏替换原则是通过约束子类行为(可透明替换父类,不出出现逻辑混乱)。因此可以说里氏替换原则是实现开闭原则的重要方式。
- 减少系统中的继承泛滥问题
- 从定义可知,是否满足里氏替换原则可以作为类间是否能建立继承关系的依据。由于继承会增强类间的耦合性、降低程序可移植性等问题,一般会采用组合、聚合等方式替换继承关系。从这个角度出发,里氏替换原则就是为了提供一个准则,减少软件实践过程中滥用继承。通俗的讲,如果一定要用继承,就必须满足里氏替换原则。否则,就别用继承。
- 减少继承带来的复杂度
- 类中实现的方法实际上是定义了规范和锲约,子类如果重写父类的方法,会破坏继承体系,需要不断地思考不同派生类的实现在语义上的差异,此时继承就会增加软件的复杂度。在必须使用继承的地方,符合里氏替换原则的设计不必考虑派生类对父类的侵入,也就不会增加系统复杂度。
软件的首要技术使命就是管理复杂度。
实现方法
里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。在软件实践中,遵循里氏替换原则首先要确定类间是否能构成继承关系,这需要充分理解业务,将业务抽象为软件实体模型。在确定继承关系之后,再规范具体的技术细节,主要有以下几点:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 对于抽象方法,子类是必须要实现的。对于非抽象方法,子类如果将其覆盖,比如说重写父类某一个非抽象方法逻辑,则在父类出现的位置,很可能子类无法透明的替换父类,系统逻辑发生变化。此时子类侵入了父类逻辑,影响了系统的稳定性。
- 子类中可以增加自己特有的方法
- 子类增加特有方法不会侵入父类的原有内容,子类依然可以透明地替换父类。同时,在子类中增加特有方法遵从了开闭原则,正是系统可扩展性的体现。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
- 因为父类方法的参数类型相对较小,所以当传入父类方法的参数类型,重载的时候优先匹配父类的方法,而子类的重载方法不会匹配,因此仍保证执行父类的方法(子类继承的时候其实操作的是子类中的父类成分),所以业务逻辑不会改变。
- 按照本条正确约束,父类方法F1(HashMap)的入参为HashMap类型,子类重载方法F1(Map)的入参为Map类型,则在客户端中调用F1(HashMap)方法处,将父类对象替换为子类对象。假设客户端向这个方法传递实参类型为HashMap类型,则子类对象依然调用父类的F1(HashMap)方法,而不是子类重载的F1(Map)方法。此时是一种透明替换。
- 举个反例,父类方法F1(Map)的入参为Map类型,子类重载方法F1(HashMap)的入参为HashMap类型,则在客户端中调用F1(Map)方法处,将父类对象替换为子类对象,假设客户端向这个方法传递实参类型为HashMap类型,则子类对象调用的是子类新添加的重载F1(HashMap)方法,而不是父类中的F1(Map)方法,按照透明替换的要求,此处仍然应该调用父类的F1(Map)方法。如果子类的F1(HashMap)方法和父类的F1(Map)方法体的业务逻辑不一致,则系统的逻辑发生变化,此时不是透明替换。
- 理解这一层含义要抓住三个点,“重载”、“替换”、参数为“小”类型。
- 实例参看代码实践小节。
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等
- 按照本条正确的约束,父类方法F1的返回值是Map类型,子类的实现F1方法之后的返回值是HashMap类型。则在客户端中调用父类的F1方法的位置处,替换为子类对应的方法,由于HashMap也是一种Map,即发生了向上转型,此时系统的逻辑不发生变化,这是一种正确的替换。
- 举个反例,父类方法F1的返回值是HashMap类型,子类的实现F1方法之后的返回值是Map类型。则在客户端中调用父类的F1方法的位置处,替换为子类对应的方法,由于Map不是一种HashMap,即发生了向下转型,这种情况无法通过编译器。
代码实践
以下代码主要验证里氏替换原则中“当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松”的含义
- 正常情况,父类参数(HashMap)比子类参数(Map)范围“小”。
//父类
//setList的入参为HashMap
public class Base {
public void setList(HashMap ma){
System.out.println("执行父类方法逻辑");
}
}
//子类
//setList的入参为Map
public class Children extends Base {
public void setList(Map map){
System.out.println("执行子类逻辑");
}
}
//应用客户端
//系统逻辑不变,是透明替换
public class Test {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
//父类调用
Base base = new Base();
base.setList(hashMap); //输出:“执行父类方法逻辑”
//替换为子类对象,仍然调用的父类的setList方法,系统逻辑不变
Children children = new Children();
children.setList(hashMap);//输出:“执行父类方法逻辑”
}
}
- 反例,父类入参(Map)比子类入参(HashMap)范围“大”
//父类
//setList的入参为Map
public class Base {
public void setList(Map ma){
System.out.println("执行父类方法逻辑");
}
}
//子类
//setList的入参为HashMap
public class Children extends Base {
public void setList(HashMap map){
System.out.println("执行子类方法逻辑");
}
}
//应用客户端
//系统逻辑发生变化,不是透明替换
public class Test {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
//父类调用
Base base = new Base();
base.setList(hashMap); //输出:“执行父类方法逻辑”
//替换为子类对象,未调用的父类的setList方法
//调用的是子子类的setList方法,系统逻辑发生变化
Children children = new Children();
children.setList(hashMap);//输出:“执行子类方法逻辑”
}
}
编程知识点
- 重载overload:方法名字必须一样,参数(类型或数量)必须不一样,可以通过方法名和参数确定一个方法。返回值可以一样,也可以不一样,无法根据返回类型区分函数重载。在同一个类中。
- 重写override:比重载条件更严格,方法签名必须一样,包括方法名称、参数类型和数量、返回值类型。在子类中。