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

相关推荐
苏三说技术几秒前
Kafka已正式接入AI
后端
企业数字化笔记1 分钟前
AI写的系统出现504怎么办?接口超时和数据库慢查询排查
数据库·后端
IT_陈寒7 分钟前
Redis的Set操作居然能把我的服务整挂了?
前端·人工智能·后端
momo0611728 分钟前
Redis新手入门 -- 学习笔记
redis·后端
井云AI34 分钟前
vendor 资源包是什么:井云 DSH 客户端打包为什么离不开它
后端·智能体小程序
重生之我是Java开发战士1 小时前
【Spring Cloud】Nacos注册中心
后端·spring·spring cloud
程序员20071 小时前
AI 编程工具链碎片化?基于多端兼容的统一 Agent Rules 架构实践
后端
程序员20071 小时前
拒绝 AI 代码“暗度陈仓”:工程级 AI 编码边界与防护规则设计
后端
yunwei371 小时前
eBPF 教程:追踪 CUDA GPU 操作
linux·后端·性能优化
搬搬砖得了2 小时前
从“我写的”到“我懂它”——论工程师对代码的心理模型
后端