LSP全称
LSP , Liskov Substitution Principle , 里氏替换原则
定义
所有引用基类(父类)的地方必须能透明地使用其子类的对象。通俗讲:子类可以扩展父类的功能,但不能改变父类原有的功能
优点
- 是实现开闭原则的重要方式之一
- 克服了继承中重写父类造成的可用性变差的缺点
- 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性
缺点
- 继承是侵入性的,只要继承就必须拥有父类的所有属性和方法
- 可能造成子类代码冗余,灵活性降低,因为必须拥有父类的属性和方法
实现
- 问题由来: 有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
- 解决方案:
当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。
里氏替换原则的核心原理是抽象,抽象又依赖于继承这个特性
- 子类必须完全实现父类的方法
- 子类可以有自己的修改(重写,重载,添加父类中没有的方法)
- 子类中override的方法,传入参数类型必须是与父类相同类型,或是子类型
- 子类中override的方法,返回类型必须是与父类相同类型,或是子类型
实例
举例说明两个数相减,由A类负责
class A {
public int func1(int a, int b) {
return a-b;
}
}
public class LSPClient {
public static void main(String[] args) {
A a = new A();
System.out.println("5-3=" + a.func1(5, 3));
System.out.println("100-10=" + a.func1(100, 10));
}
}
输出结果:
5 - 3 = 2
100 - 10 = 90
此时需要新加一个功能:两数相加,然后再与100求和,由B类负责
class A {
public int func1(int a, int b) {
return a - b;
}
}
class B extends A {
@Override
public int func1(int a, int b) {
return a + b;
}
public int func2(int a, int b) {
return func1(a, b) + 100;
}
}
public class LSPClient {
public static void main(String[] args) {
B b = new B();
System.out.println("5-3=" + b.func1(5, 3));
System.out.println("100-10=" + b.func1(100, 10));
System.out.println("50+20+100=" + b.func2(50, 20));
}
}
输出结果:
5-3=8
100-10=110
50+20+100=170
此时发现原本运行正常的相减功能发生了错误,原因是子类B重写了父类A中的func1方法,从两数相减变成了两数相加。引用基类A完成的功能,换成子类B之后,发生了异常。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。包含以下4层含义:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。 - 看上去很不可思议,因为我们会发现在自己编程中常常会违反里氏替换原则,程序照样跑的好好的。所以大家都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?后果就是:你写的代码出问题的几率将会大大增加。