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

相关推荐
野犬寒鸦1 小时前
从零起步学习并发编程 || 第四章:synchronized底层源码级讲解及项目实战应用案例
java·服务器·开发语言·jvm·后端·学习·面试
计算机毕设VX:Fegn08959 小时前
计算机毕业设计|基于springboot + vue蛋糕店管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
没差c10 小时前
springboot集成flyway
java·spring boot·后端
三水不滴10 小时前
Redis 过期删除与内存淘汰机制
数据库·经验分享·redis·笔记·后端·缓存
笨蛋不要掉眼泪11 小时前
Spring Boot集成LangChain4j:与大模型对话的极速入门
java·人工智能·后端·spring·langchain
sheji341613 小时前
【开题答辩全过程】以 基于SpringBoot的疗养院管理系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
短剑重铸之日14 小时前
《设计模式》第六篇:装饰器模式
java·后端·设计模式·装饰器模式
码界奇点15 小时前
基于Flask与OpenSSL的自签证书管理系统设计与实现
后端·python·flask·毕业设计·飞书·源代码管理
代码匠心16 小时前
从零开始学Flink:状态管理与容错机制
java·大数据·后端·flink·大数据处理
分享牛16 小时前
LangChain4j从入门到精通-11-结构化输出
后端·python·flask