里氏替换原则(lsp)要求子类对象可无损替换父类对象:运行结果必须完全一致,不报错、不崩溃、不改返回值、不增删异常、不破坏状态逻辑,本质是严格遵守行为契约而非语法兼容。

里氏替换原则(LSP)不是“子类能编译通过就行”,而是要求:只要代码里用的是父类类型,换成任意子类对象,运行结果必须一模一样——不报错、不崩溃、不改返回值、不增删异常、不破坏状态逻辑。它检验的不是语法兼容,而是行为契约是否被严格遵守。
关键看三点:输入、输出、副作用
父类方法对外承诺了什么,子类就得照做,不能悄悄加料或缩水:
- 前置条件不能收紧:父类允许传 null 或负数,子类就不能突然要求“必须 > 0”;否则调用方原有安全输入,在子类下直接失败
- 后置条件不能放松:父类文档写明“返回非空 List”,子类就不能返回 Collections.unmodifiableList() 或 null;下游 add() 或 size() 就会崩
- 副作用和异常必须守约:父类 save() 保证事务原子性且不抛 IOException,子类若拆成两步写库 + 异步刷缓存,或新增 throws SQLException,就已越界
重写非抽象方法是高危操作
父类已提供具体实现的方法,客户端代码很可能依赖其行为细节。一旦子类 @Override,就极大概率违反 LSP:
- 比如父类 withdraw(BigDecimal amount) 返回成功码 "0000",子类改成先查信用额度再冻结账户,抛出 InsufficientCreditException —— 调用方根本没 catch 这个异常
- 又如父类 setWidth(int w) 和 setHeight(int h) 隐含“彼此独立”的契约,子类 Square 把二者绑死,UI 布局循环调用 setWidth(200) 时,高度也被拉成 200,界面错乱
- 甚至重写 toString() 或 equals() 也很危险:父类按 id 比较,子类多加 name 字段,HashSet 查不到对象、Map get() 返回 null
防范策略:从设计源头堵漏洞
不靠事后测试补救,而是在建模阶段规避风险:
- 优先用抽象类或接口代替具体父类继承:把“四边形”定义为 Shape 接口,Rectangle 和 Square 各自实现 getWidth()/getHeight(),不共享 setWidth() 行为
- 共用逻辑抽离为工具类或策略对象:比如 discount 计算交给 DiscountCalculator 策略,PaymentProcessor 持有它,而非让子类重写 calculateTotal()
- 用委派(delegation)替代继承:Square 内部持有一个 Rectangle 实例,需要面积时调用其 getArea(),但 setWidth() 自己控制逻辑,不承担“可独立设宽高”的义务
- 给父类写契约式测试套件:覆盖正常输入、边界值、非法参数、并发调用等场景,所有子类必须 100% 通过同一组测试,CI 流水线自动拦截 LSP 破坏
快速自查:三句话拷问继承关系
不用翻源码,只看调用侧:
- 把某处 new Parent() 直接换成 new Child(),不改任何调用代码,所有功能是否仍正确?
- 子类有没有重写父类的非 abstract 方法?如果有,它的输入范围、返回值类型、异常列表、对象状态变化,是否和父类文档/测试完全一致?
- 有没有地方出现 if (obj instanceof Child) { ((Child)obj).specialMethod(); }?这种类型检查本身就是 LSP 失效的信号











