里氏替换原则(Liskov Substitution Principle,LSP):如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。
简单来说,替换原则的目的是确保继承关系中的子类能够在不改变程序正确性的前提下,扩展父类的功能。包含以下几个含义:
- 子类可以扩展父类的功能,但不能改变父类原有的功能子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输 出/返回值)要比父类更严格或相等
含义一、含义二详解
在开闭原则的基础上优化:开闭原则
我们可以看下以下代码,HuaWeiDiscount 类继承 HuaWei 并把原本的getPrice() 方法重写,导致会获取不到原本的价格。
含义一就是要保留 getgetPrice() 方法和父类的方法一致,如果需要对价格打折,可以新建一个 getDiscountPrice() 去做打折的逻辑。
那么 getDiscountPrice() 就是子类 HuaWeiDiscount 自己特有的方法,也就是含义二。
public Double getDiscountPrice() {
return super.getPrice() * 0.6;
}
含义三
当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
父类 Father
public class Father {
public void method(HashMap hashMap) {
System.out.println("父类被执行");
}
}
子类 Child
public class Child extends Father {
public void method(Map map) {
System.out.println("子类Map被执行");
}
}
main执行
public class Main {
public static void main(String[] args) {
Child child = new Child();
child.method(new HashMap());
}
}
main 执行结果为:
父类被执行
子类的 method 方法入参范围比父类的大,那么子类的方法永远也不会被执行。这是符合里氏替换原则的,父类和子类互换,程序的执行结果不会发生变化。
以下示例,子类的范围小于父类的范围:
public class Father {
public void method(Map map) {
System.out.println("父类被执行");
}
}
public class Child extends Father {
public void method(HashMap hashMap) {
System.out.println("子类HashMap被执行");
}
}
public class Main {
public static void main(String[] args) {
Child child = new Child();
child.method(new HashMap());
}
}
main 执行结果为:
子类HashMap被执行
可以看到这个时候程序执行了子类的 method 方法,这样就违反了里氏替换原则,在实际的开发过程当中很容易引起业务逻辑的混乱。
含义四
当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或相等
父类 Father
public abstract class Father {
public abstract Map method();
}
子类 Child
public class Child extends Father {
@Override
public HashMap method() {
HashMap hashMap = new HashMap();
System.out.println("子类method方法被执行");
hashMap.put("username", "testname");
return hashMap;
}
}
main执行
public class Main {
public static void main(String[] args) {
Child child = new Child();
child.method();
Father father = new Child();
father.method();
}
}
main 执行结果为:
子类method方法被执行
子类method方法被执行
也就是说在实现父类的抽象方法时,子类的方法返回值范围一定要小于或等于父类方法的返回值范围。
如果换过来,父类保持 Map 返回值不变,子类的返回值改为 Object,编译器会直接报错
Object 是所有类的基类,所以子类返回值大于父类
继承的缺点
- 继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法
- 降低了代码的灵活性增强了耦合性。
- 当父类的常量、变量或者方法被修改时,必需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大片的代码需要重构
里氏替换原则的优点
- 约束继承泛滥(开闭原则的一种体现)
- 加强程序的健壮性,同时变更时也可以做到非常好的兼容性提高程序的维护性,扩展性。降低需求变更时引入的风险。
里氏替换原则反应了基类与子类之间的关系,同时也是对开闭原则的补充以及对实现抽象化的具体步骤的规范。