里氏替换原则要求子类必须严格遵循父类行为契约,否则应改用组合;接口比抽象类更易满足lsp,因只定义能力而不约束实现;常见崩塌场景包括dao更新逻辑、spring事务丢失、dto字段过滤等。

里氏替换原则不是“能编译通过就行”
它要回答的问题是:把 Rectangle 换成 Square,业务逻辑还对不对?如果面积计算从 5 * 2 == 10 变成 2 * 2 == 4,哪怕语法完全合法,也已经违反了 LSP。
关键不在“能不能继承”,而在“替换了会不会悄悄出错”。很多团队直到上线后才发现:某个子类重写了 save() 方法,却偷偷改了事务边界或缓存策略,上游调用方完全没感知——这就是 LSP 失效的典型后果。
子类重写父类方法时的三条硬约束
不是所有重写都安全。LSP 要求子类方法在行为契约上与父类严格对齐,否则就该考虑用组合代替继承:
-
setHeight()和setWidth()在Rectangle中是正交操作;Square把它们耦合成“设宽即设高”,破坏了前置条件,属于违规 - 父类声明抛出
IOException,子类却抛出更宽泛的Exception,调用方的异常处理逻辑可能崩溃 - 父类
findUserById(int id)保证非空返回,子类却可能返回null,而上游代码没做判空——这属于后置条件违约
接口比抽象类更容易守住 LSP
当使用 Shape 接口,让 Rectangle 和 Square 各自实现 getArea(),就天然规避了“设置宽高”的行为冲突。因为接口只定义“能做什么”,不规定“怎么做”或“怎么被设置”。
立即学习“Java免费学习笔记(深入)”;
真实项目中,LSP 最常在以下场景崩塌:
- DAO 层:父类
BaseMapper的updateById()默认更新全部字段,子类UserMapper却重写为只更新非空字段 → 上游批量修改丢失数据 - Spring Bean:父类
PaymentService声明@Transactional,子类AlipayService重写方法但忘了加注解 → 事务失效 - DTO 转换:父类
toVO()返回完整视图,子类AdminUser.toVO()过滤敏感字段 → 前端突然收不到必要字段
检查 LSP 是否被破坏的两个实操动作
别等线上报错才想起 LSP。日常开发中可以快速验证:
- 看测试用例:父类的单元测试(尤其是边界 case)能否直接跑在子类实例上?如果
assertThat(rectangle.getArea(), is(10))换成square就失败,说明契约已破 - 查方法签名变更:IDE 中右键父类方法 → “Find Usages”,再逐个点开子类实现,确认参数类型、返回类型、异常声明、nullability 注解是否一致
最隐蔽的坑是“父类方法看似没变,但子类悄悄改了副作用”——比如日志格式、缓存 key 构造、线程模型。这些不会让编译器报错,却会让监控指标和排查路径突然失灵。






