子类重写父类非抽象方法极大概率违反lsp,因客户端依赖父类方法契约而非实现;行为变更(如新增异常、收紧前置条件、改变副作用)会导致调用方逻辑崩溃。

子类重写父类非抽象方法,为什么程序会悄悄出错
直接结论:只要子类覆盖了父类已实现的(非 abstract)方法,就极大概率违反 LSP —— 因为客户端代码依赖的是父类方法的契约,不是它的具体实现。一旦子类偷偷改了行为(比如 withdrawal() 从“只扣款”变成“先校验再冻结账户”),所有用 CashCard 类型声明的地方,换成 CreditCard 实例后,逻辑就可能崩。
- 常见错误现象:
c.withdrawal("1001", new BigDecimal("500"))在父类返回"0000",子类却抛出InsufficientCreditException,而调用方根本没准备捕获这个异常 - 本质是契约断裂:父类方法签名承诺“成功返回码”,子类却引入新失败路径,或改变前置条件(如要求
amount > 0变成amount >= 100) - Java 中最隐蔽的坑:
@Override一个看似无害的toString()或equals(),结果子类用了不同字段参与比较,导致HashSet查不到对象
正方形不能继承长方形?这不是数学问题,是接口语义问题
不是“正方形是不是长方形”的几何争论,而是 Rectangle 的 setWidth() 和 setHeight() 方法隐含了“彼此独立可变”的契约。子类 Square 把二者绑死,就破坏了这个语义——任何依赖“设宽不影响高”的代码,一换就错。
- 典型使用场景:UI 布局引擎接收
Shape列表,循环调用setWidth(200)调整控件宽度,若其中混入Square实例,高度也被强制拉成 200,界面直接错乱 - 正确解法不是“让正方形更像长方形”,而是退一步:用接口(
Shape)或组合(Square持有Rectangle并代理部分行为)代替继承 - 参数差异很关键:父类
setHeight(int h)允许任意整数,子类若只接受偶数,就是前置条件收紧,违反 LSP;反过来,子类接受Number而非int,才叫放宽(合法)
如何快速判断一个继承关系是否踩了 LSP 红线
别靠直觉,用三句话拷问自己:
- 有没有地方把父类对象换成子类后,**不改一行调用代码**,结果就变了(返回值、异常、副作用、执行时间)?
- 子类有没有重写父类的非 abstract 方法?如果有,它是否**完全保持原输入输出规则、不变量、文档承诺**?
- 子类新增的方法,会不会诱使调用方写出“先检查是不是子类,再调用特有方法”的脆弱代码?(这说明继承关系本就不该存在)
工具上,IDE 的 “Find Usages” 配合手动替换测试最有效:把 Rectangle r = new Rectangle(); 改成 Rectangle r = new Square();,跑一遍所有用到 r 的单元测试,挂掉的就是 LSP 雷区。
PHP/Python/Java 中容易被忽略的 LSP 细节
语言特性会放大 LSP 风险,尤其在动态类型或弱契约环境下:
- PHP 中
function setWidth(int $w)和子类function setWidth($w)(去掉类型声明)看似兼容,但实际放宽了输入,如果父类内部做了is_int()校验,子类就可能绕过,造成后续逻辑崩溃 - Python 的鸭子类型不等于 LSP 自动成立:
class Bird有fly(),class Ostrich(Bird)重写为raise NotImplementedError,看似“有方法”,但任何遍历[Bird(), Ostrich()]并调用.fly()的代码都会炸 - Java 的协变返回值(子类方法返回父类方法返回类型的子类型)是安全的,但逆变参数(子类方法参数是父类方法参数的父类型)必须显式声明,否则编译器不会帮你拦住
最常被跳过的点:LSP 不是关于“能不能编译通过”,而是关于“换掉之后,运行时行为是否还稳”。很多团队只测 happy path,漏掉了子类替换后边界条件、并发、异常流的验证。










