里氏替换原则(Liskov Substitution Principle)是面向对象设计的一个基本原则之一,它由《面向对象分析与设计》一书的作者Barbara Liskov教授在1987年提出。
里氏替换的两层含义
里氏替换原则是针对继承而言的,如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。
如果继承的目的是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,为了符合LSP,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。
不符合LSP的最常见的情况是,父类和子类都是可实例化的非抽象类,且父类的方法被子类重新定义,这一类的实现继承会造成父类和子类间的强耦合,也就是实际上并不相关的属性和方法牵强附会在一起,不利于程序扩展和维护。
遵循里氏替换原则的好处:
提高代码的可复用性和可维护性。通过保持子类的兼容性和可替换性,我们可以更容易地使用和修改代码而不必担心破坏现有的功能。
促进多态性的应用。通过使用抽象和多态的方式组织代码,我们可以在运行时动态地选择适当的子类实现。
减少代码的耦合性。通过遵循里氏替换原则,我们可以降低代码之间的依赖性,使得系统更加灵活和可扩展。
如何遵循里氏替换原则:
子类必须完全实现父类的抽象方法,而不是修改父类已有的方法。
子类可以通过增加自己特有的方法来进行功能扩展,但不能重写或修改父类的非抽象方法。
子类在重写父类方法时,方法的前置条件(参数、返回类型等)应该与父类方法相同或更宽松。
后置条件即输出,假设我们的父类方法约定输出参数要大于0,调用父类方法的程序根据约定对输出参数进行了大于0的验证。而子类在实现的时候却输出了小于等于0的值。此时子类的涉及就违背了里氏替换原则
里氏替换的实现方法
里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
根据上述理解,对里氏替换原则的定义可以总结如下:
子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
子类中可以增加自己特有的方法
当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等
通过重写父类的方法来完成新的功能写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系。
企鹅、鸵鸟和几维鸟从生物学的角度来划分,它们属于鸟类;但从类的继承关系来看,由于它们不能继承“鸟”会飞的功能,所以它们不能定义成“鸟”的子类。同样,由于“气球鱼”不会游泳,所以不能定义成“鱼”的子类;“玩具炮”炸不了敌人,所以不能定义成“炮”的子类等。
里氏替换实例
假设我们有一个 Rectangle(矩形)类和一个 Square(正方形)类,其中 Square 是 Rectangle 的子类。这两个类都有宽度和高度属性,并且都有计算面积的方法。
按照里氏替换原则,我们希望能够将 Square 对象视为 Rectangle 对象的替代品,并且在所有使用 Rectangle 类型的地方,也可以使用 Square 类型的对象。
public class Rectangle {
private int width;
private int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height);
}
}
public class Main {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setWidth(5);
rectangle.setHeight(10);
System.out.println("Rectangle area: " + rectangle.getArea()); // 输出:Rectangle area: 50
Square square = new Square();
square.setWidth(5);
System.out.println("Square area: " + square.getArea()); // 输出:Square area: 25
// 使用 Square 类型的对象替代 Rectangle 类型的对象
Rectangle squareAsRectangle = new Square();
squareAsRectangle.setWidth(5);
squareAsRectangle.setHeight(10);
System.out.println("Square as Rectangle area: " + squareAsRectangle.getArea()); // 输出:Square as Rectangle area: 50
}
}
在上面的例子中,Square 类作为 Rectangle 类的子类,它重写了父类的设置宽度和高度方法,并保持宽度和高度相等。这样,在使用 Square 对象时,我们可以将其视为 Rectangle 对象的替代品,并正确计算出相应的面积。
通过里氏替换原则,我们能够在不影响程序正确性的前提下,将 Square 对象用于任何需要 Rectangle 对象的地方。这增强了代码的灵活性和可复用性。