X 里氏替换原则:继承父类而不是改变父类
含义
里氏替换原则(Liskov Substitution Principle)是对子类型的特别定义
一个软件实体如果适用一个父类的话,那一定是适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变
- 子类可以实现父类的抽象方法,不能重写(覆盖)父类的非抽象方法
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的入参)要比父类方法的输入 参数更宽松(范围更广)
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的返回值)要比父类更严格或相等
- 子类可以拥有自己独特的方法或属性,即子类可以扩展父类的功能,但不能改变父类原有的功能
里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对实现抽象化的具体步骤的规范,是对开闭原则的补充也是实现开闭原则的重要方式之一
核心思想
将一个父类对象替换成它的子类对象后,该程序不会发生异常
优点
- 约束继承泛滥,里氏替换原则是实现开闭原则的重要方式之一,是开闭原则的一种体现
- 克服了继承中重写父类造成的可复用性变差的缺点
- 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性
- 加强程序的健壮性,同时变更时也可以做到非常好的兼容性,提高程序的维护性、扩展性。降低需求变更时引入的风险
案例
第一点
在“继承”中会面临的这样一个问题:我们的父类定义好的方法,并不会强制要求其子类必须完全遵守该方法的实现规则。子类是可以修改它继承自父类的任意方法的。里氏替换原则需要严格遵守其核心思想
public class Father {
public void getNum(int a, int b) {
System.out.println("父类getNum()方法计算出的结果为:" + (a + b));
}
}
public class Son extends Father {
@Override
public void getNum(int a, int b) {
System.out.println("子类getNum()方法计算出的结果为:" + (a - b));
}
}
public class Test {
public static void main(String[] args) {
test01();
}
public static void test01(){
Father father = new Father();
father.getNum(5, 10);
father = new Son();
father.getNum(5, 10);
}
}
父类的本意是想要定义一个两数相加的方法,但是子类继承该方法后却修改为减法,并且也成功了。子类这样操作后,会对整个继承体系造成破坏。当你想把使用父类的地方替换为其子类时,会发现原来的正常的功能现在出现问题了。
为了解决此问题,就有了里氏替换原则的第一点:
子类可以实现父类中的抽象方法,但是不能重写(覆盖)父类的非抽象方法
第二点
当我们迫不得已需要在子类中添加一个跟父类方法名称相同的方法时,就需要遵守下面这点。
当子类的方法重载父类的方法时,方法的前置条件(即方法的入参)要比父类方法的输入 参数更宽松(范围更广)
这样做直接的一个目的就是防止我们将父类对象替换为子类对象后,造成方法调用混乱的问题
// 父类
public void method(ArrayList arrayList) {
System.out.println("父类方法执行了!");
}
// 子类
@Override
public void method(ArrayList arrayList) {
System.out.println("子类方法执行了!");
}
public static void test02(){
ArrayList list = new ArrayList();
Father father = new Father();
Son son = new Son();
System.out.print("使用父类对象调用的结果:");
father.method(list);
System.out.print("将父类对象替换成子类对象后的调用结果:");
son.method(list);
}
如果我们把父类方法参数和子类方法参数调换一下
// 父类
public void method(List<String> arrayList) {
System.out.println("父类方法执行了!");
}
// 子类
public void method(ArrayList<String> arrayList) {
System.out.println("子类方法执行了!");
}
我们的本意是希望对象替换后还执行原来的方法的,可结果却发生变化了。这样是不符合里氏替换原则的。所以我们要时刻牢记,子类方法的入参要比父类的入参范围更大,这样才不会造成不必要的错误。
第三点
当我们需要重写或者实现父类方法时,需要遵守下面这一点。
当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的返回值)要比父类更严格或相等
// 父类
public List<String> getList() {
return new ArrayList<>();
}
// 子类
@Override
public ArrayList<String> getList() {
return new ArrayList<>();
}
如果我们试图在子类中放大,重写或实现来自父类方法的返回值时,代码会报错,连基本的编译器都无法通过。
第四点
子类可以拥有自己独特的方法或属性
这句话就更好理解了,它说明子类不光可以拥有从父类继承来的东西,也可以进行自我扩展,编写属于自己的东西。
通过上面的描述相信大家都对里氏替换原则有了一个基本的概念,它就是告诉我们在继承中需要注意什么问题和遵守什么规则。
然而在实际开发中我们在很多时候还是会违背该原则的,虽然表面上没有什么特别大的问题,但是这样做会大大增加代码的出错率。