何为里氏替换原则(LSP)

一、一句话理解

里氏替换原则(LSP) 的意思是:

如果你写了一段代码,是基于一个父类/接口 来操作的。那么,当你把这段代码中的父类对象,替换成它的任何一个子类对象时,程序的行为都不应该出问题(比如崩溃、报错、产生意外结果)。

换句话说,子类必须完全遵守父类定下的"契约"或"承诺" 。父类能做的事,子类必须都能做,而且不能做出格的事。


二、一个经典的"反面教材"(违反LSP的例子)

假设我们有一个表示矩形的类,和一个表示正方形的类。从数学上说,正方形"是一个"矩形,所以直觉会让我们用继承。

java

arduino 复制代码
// 父类:矩形
class Rectangle {
    protected int width;
    protected int height;

    public int getWidth() { return width; }
    public void setWidth(int width) { this.width = width; }

    public int getHeight() { return height; }
    public void setHeight(int height) { this.height = height; }

    public int getArea() {
        return width * height;
    }
}

// 子类:正方形 (数学上 is-a 关系,所以继承?)
class Square extends Rectangle {
    // 正方形要求长宽永远相等,所以重写setter方法
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(width); // 设置宽的同时,把高也改了
    }

    @Override
    public void setHeight(int height) {
        super.setHeight(height);
        super.setWidth(height); // 设置高的同时,把宽也改了
    }
}

现在,我们写一个客户端方法,它的逻辑是基于父类 Rectangle 的承诺:"我可以独立地修改宽和高"。

java

arduino 复制代码
public class Test {
    // 这个方法接收一个Rectangle参数,这是基于父类的"契约"在编程
    public static void testArea(Rectangle rect) {
        rect.setWidth(5);
        rect.setHeight(4);
        // 根据矩形的定义,这里面积应该是 5 * 4 = 20
        System.out.println("Expected area: 20, Got area: " + rect.getArea());
        // 断言结果应该是20,否则逻辑错误
        assert rect.getArea() == 20 : "Oops! Area is not 20!";
    }

    public static void main(String[] args) {
        Rectangle rect = new Rectangle();
        testArea(rect); // 输出:Expected area: 20, Got area: 20 ✅ 正确

        // 现在,我们用子类Square来替换父类Rectangle
        Rectangle square = new Square(); // 多态,父类引用指向子类对象
        testArea(square); // 输出:Expected area: 20, Got area: 16 ❌ 错误!
    }
}

发生了什么?

  1. testArea 方法认为它操作的是一个 Rectangle,所以先设宽为5,再设高为4。

  2. 但当传入的是 Square 时:

    • setWidth(5) 被调用,Square 的重写方法将宽和高都设为了5
    • setHeight(4) 被调用,Square 的重写方法又将宽和高都设为了4
  3. 最终,这个正方形的长宽都是4,面积是16,而不是预期的20。

这就是违反了里氏替换原则!
Square 类虽然语法上成功继承了 Rectangle,但它的行为却破坏了父类 Rectangle 的契约 (父类的契约是:宽和高可以独立设置)。导致所有基于 Rectangle 契约编写的代码(如 testArea 方法),在接收到 Square 时都产生了错误的结果。

程序没有崩溃,但逻辑的正确性被破坏了


三、如何遵守LSP?正确的设计应该是怎样的?

这个例子的根源在于, "行为"上的 is-a 关系"数学定义"上的 is-a 关系 更重要。

正方形在数学上是矩形,但在行为上,它并不能替代一个允许独立修改长宽的矩形。因此,它们不应该用继承关系。

解决方案:多用组合,少用继承!

我们可以创建一个共同的抽象,让两者都去实现,或者干脆不要建立继承关系。

java

csharp 复制代码
// 定义一个形状接口,只包含计算面积的行为
interface Shape {
    int getArea();
}

// 矩形类实现Shape接口
class Rectangle implements Shape {
    private int width;
    private int height;
    // ... getters and setters ...
    @Override
    public int getArea() {
        return width * height;
    }
}

// 正方形类也实现Shape接口
class Square implements Shape {
    private int side; // 正方形只有一个边长属性
    public void setSide(int side) { this.side = side; }
    public int getSide() { return side; }
    @Override
    public int getArea() {
        return side * side;
    }
}

现在,我们有一个方法,它只关心计算面积,它基于 Shape 接口的契约:"你一定能告诉我你的面积"。

java

csharp 复制代码
public static void printArea(Shape shape) {
    System.out.println("The area is: " + shape.getArea());
}

这个方法接收任何 ShapeRectangleSquare),都能正常工作,输出正确的面积。它们互相可以替换,而不破坏 printArea 方法的正确性。这就是遵守了LSP。

四、给Java程序员的实践指南

  1. 子类不应该比父类更严格:父类的方法如果允许传入null,子类重写时就不应该禁止null(可以更宽松,但不能更严格)。
  2. 子类不应该改变父类的预期行为:比如父类的一个方法是排序,子类重写后不能变成打乱顺序。
  3. 谨慎重写父类的方法 :如果你发现需要大量重写父类的方法,或者重写后完全改变了其初衷,这很可能意味着继承关系不合理,应该考虑使用组合(比如策略模式)来代替继承。
  4. 用抽象(接口)来定义契约 :就像上面的 Shape 例子,客户端代码基于最抽象的接口编程,具体替换成哪个实现,都不会影响正确性。这是LSP的最高级应用,也是Spring等框架的核心思想。

总结一下:
里氏替换原则(LSP)就是一把"尺子",用来衡量你的继承关系是否设计得合理。 它要求子类必须乖乖遵守父类立下的所有"规矩",不能搞特殊、搞破坏。只有这样,面向对象的多态特性才能真正发挥威力,让你写出可靠、可扩展的代码。

相关推荐
Cache技术分享10 小时前
177. Java 注释 - 重复注释
前端·后端
用户67570498850211 小时前
Git合并选Rebase还是Merge?弄懂这3点,从此不再纠结
后端
码事漫谈11 小时前
现代C++性能陷阱:std::function的成本、异常处理的真实开销
后端
那些无名之辈11 小时前
03 docker搭建
后端
Swot11 小时前
Nuxt3 服务端调用其他 api 的方式
后端
SimonKing11 小时前
Chrome插件千万别乱装!手把手教你从官方渠道安全下载
java·后端·程序员
武子康11 小时前
大数据-84 Spark RDD创建全攻略:从集合、文件到转换操作详解
大数据·后端·spark
软件开发JR11 小时前
基于Spring Boot的社区团购系统的设计与实现
数据库·spring boot·后端·php
小楓120112 小时前
MySQL數據庫開發教學(四) 後端與數據庫的交互
前端·数据库·后端·mysql