王家餐馆的招牌菜是老王做的。老王休假时,老板让小李顶班,临走只说了一句:换人,不换味。顾客不在乎厨房里站着谁,只在乎端上来的还是那道菜、那个分量、那个温度。这段朴素的“顶班规则”,放到软件世界里,就是里氏替换原则(Liskov Substitution Principle,LSP):父类是岗位说明书,子类是候选主厨;任何使用父类对象的地方,都应该能无损地换成子类对象,而程序的可观察行为不受影响。如果替班者偷偷多要食材、上菜变慢、菜量缩水,餐厅的承诺就被破坏了。这篇内容将从一张“岗位说明书”出发,带你看懂 LSP 的契约、前置条件、后置条件,以及最经典的“正方形长方形”翻车现场。

岗位关联

掌握 LSP,你就能在重构、扩展和接口设计中,让新模块安全替换旧模块而不破坏系统行为,成为一名让团队放心继承的设计师。

目录
  1. 可视化演示
  2. 1. 核心定义
  3. 2. 契约设计
  4. 3. 前置条件
  5. 4. 后置条件
  6. 5. 反例剖析
  7. 6. 修复策略
  8. 7. 实践应用
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示LSP里氏替换原则深入理解的核心机制,通过全局/局部/动态/静态四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

LSP里氏替换原则深入理解:LSP编 LSP编码的基本概念与原理 编码方案与数据结构 核心机制可视化 LSP里氏替换原则深入理解整体架构与关键组件

1. 核心定义

王家餐馆的后厨贴着主厨岗位说明书:招牌红烧肉必须三肥三瘦、出锅前收汁两分钟、客人落座后二十分钟上桌。这些条文不是写给老王一个人看的,而是写给每一个候选主厨的。老板的理念很简单:换人,不换味。

在面向对象程序里,父类就是那张岗位说明书,子类就是候选主厨。里氏替换原则要求:所有使用父类对象的地方,都能无损地替换成任何一个子类对象,程序的可观察行为不能因此改变。1987 年,Barbara Liskov 给出了一个更数学的表述:

这里的关键词是“可观察行为”,而不是“实现细节”。子类当然可以用完全不同的做法完成工作,就像小李可能先焯水再焖、老王直接焖,但客户吃到的菜必须是同一道。如果替换后顾客还要三番五次催菜,程序就会产生奇怪的 bug。LSP 要的就是:客户端一旦依赖父类契约,就不用关心背后站的是哪个具体实现。

∀x∈T, q(x)  ⇒  ∀y∈S, q(y){\displaystyle \forall x \in T,\ q(x)\;\Rightarrow\;\forall y \in S,\ q(y)}
通俗地说:对于父类 T 的任意对象 x,只要客户端依赖的性质 q(x) 成立,换成子类 S 的任意对象 y 后,q(y) 也必须成立。
  1. 父类对象x 表示任意一个父类对象,就像餐厅里的老王。
    x∈T{\displaystyle x \in T}
  2. 客户端性质q 表示客户端可观察的性质,例如“上菜不超过 25 分钟”。
    q(x){\displaystyle q(x)}
  3. 子类对象y 是子类 S 的任意对象,必须同样满足 q。
    ∀y∈S, q(y){\displaystyle \forall y \in S,\ q(y)}
  4. 蕴含关系整个式子表达“父类成立,则子类也必须成立”。
    ⇒{\displaystyle \Rightarrow}

2. 契约设计

餐馆的承诺单不只是菜名。老王心里有一份隐形契约:食材新鲜、按时出菜、咸淡一致。签下这份契约的替班厨师,才配站在这个灶台前。软件工程里的契约式设计,正是把接口看作调用方和实现方之间的合同。方法签名是合同上的条款,而“前置条件”和“后置条件”是条款里最硬的两条。

很多语言只强制方法签名,不强制行为约定。所以学习者常常只关注“方法没抛异常”,却忘了检查返回值、副作用和性能承诺是否一致。契约设计提醒我们:接口不只是函数的形状,更是一份对客户的许诺。顶班厨师要先背承诺单,再上灶台;子类也要先读懂父类的契约,再去重写。否则看起来能吃,吃下去心碎。

Contract(m)≡(Ppre(m)→Ppost(m)){\displaystyle \mathrm{Contract}(m)\equiv (P_{\mathrm{pre}}(m)\rightarrow P_{\mathrm{post}}(m))}
方法 m 的契约就是一个“如果满足前置条件,就保证兑现后置条件”的承诺。
  1. 前置条件P_pre 表示调用方法前必须满足的条件。
    Ppre(m){\displaystyle P_{\mathrm{pre}}(m)}
  2. 后置条件P_post 表示方法执行后必须成立的结果。
    Ppost(m){\displaystyle P_{\mathrm{post}}(m)}
  3. 推导承诺箭头表示前置条件满足后,实现方必须推出后置条件。
    →{\displaystyle \rightarrow}

3. 前置条件

老王开工有个要求:食材每天八点前送到。这不难。小李来了,却说必须五点半前送到,还得是进口黑毛猪,否则不做。老板一听火冒三丈:老顾客的订单都是十点才来的,凭什么换个厨师就要提前送?这就是把前置条件加强了。前置条件是方法执行前必须满足的条件,LSP 要求子类的前置条件只能与父类相同或更宽松,绝不能强化。

更宽松,意味着子类要接受父类能接受的一切输入。反过来,如果子类还想额外接收新类型的输入,也可以,那只是扩展;但绝不能把父类原本接受的输入挡在门外。想接班的厨师可以给餐厅带来新菜谱,但不能改变“八点前食材到位”的底线。放宽要求欢迎,加高门槛抱歉。这就是前置条件的继承约束。

PreBase(x)→PreSub(x){\displaystyle \mathrm{Pre}_{\mathrm{Base}}(x)\rightarrow \mathrm{Pre}_{\mathrm{Sub}}(x)}
意思是:凡是父类能接受的输入,子类都必须接受。
  1. 父类前置条件Pre_Base(x) 描述父类方法能接受的参数范围。
    PreBase(x){\displaystyle \mathrm{Pre}_{\mathrm{Base}}(x)}
  2. 子类前置条件Pre_Sub(x) 描述子类方法能接受的参数范围。
    PreSub(x){\displaystyle \mathrm{Pre}_{\mathrm{Sub}}(x)}
  3. 逻辑蕴含箭头表示“父类接受则子类一定接受”,即子类门槛不能更高。
    →{\displaystyle \rightarrow}

4. 后置条件

承诺单上还有一条:菜在二十五分钟内上齐,且必须熟透、热乎。小李炒菜手脚慢,常常四十分钟才上菜,还偶尔带生。后厨换人的代价,变成了顾客流失。这就是后置条件被削弱了。后置条件是方法执行后必须成立的结果承诺,LSP 要求子类的后置条件只能与父类相同或更强,绝不能弱化。

想象父类承诺“至少八成顾客满意”,子类只承诺“尽量让顾客满意”。听起来问题不大,但“尽量”不是可验证的结果。LSP 要求子类的后置条件不低于父类,最好比父类更强——比如不仅上菜快,还免费送汤。承诺只能往更重里承诺,不能往轻里缩水。这正是后置条件的铁律。

PostSub(x,r)→PostBase(x,r){\displaystyle \mathrm{Post}_{\mathrm{Sub}}(x,r)\rightarrow \mathrm{Post}_{\mathrm{Base}}(x,r)}
意思是:子类一旦承诺了某个结果,就必须至少兑现父类承诺的结果。
  1. 父类后置条件Post_Base(x,r) 是父类方法对结果 r 的保证。
    PostBase(x,r){\displaystyle \mathrm{Post}_{\mathrm{Base}}(x,r)}
  2. 子类后置条件Post_Sub(x,r) 是子类方法对结果 r 的保证。
    PostSub(x,r){\displaystyle \mathrm{Post}_{\mathrm{Sub}}(x,r)}
  3. 蕴含方向箭头表示子类的保证能推出父类的保证,也就是只强不弱。
    →{\displaystyle \rightarrow}

5. 反例剖析

现在问题来了:正方形算不算长方形?数学上算。但在程序里,餐厅有一张“长方形餐盘”模板,支持分别设置长和宽;配菜系统按这个模板布置桌面。有一天系统把餐盘替换成“正方形餐盘”,几何上正方形当然是长方形,于是它继承了长方形模板。正方形为了保证长宽相等,重写了设置长和宽的方法。配菜系统先调用 setWidth(40),再调用 setHeight(30)。结果正方形经过两轮强制,最后变成 30×30,而不是预期的 40×30。老板订的 40 厘米托盘,摆不下 30 道菜。客户根本没做错什么,是子类破坏了父类的行为契约。

这个反例告诉我们,问题不在“正方形是不是长方形”,而在“正方形能否保证长方形的全部行为”。数学上的 is-a 关系是静态分类,程序里的 is-a 关系必须是行为契约上的可替换。顾客点一份 40×30 的果盘,端上来却是 30×30,再有理的子类也不会让菜单满意。所以,当一个子类在行为上不是父类的合格替身时,继承关系就该被重新审视。

¬(∀r∈Rect: width(r)=height(r)),∀s∈Square: width(s)=height(s){\displaystyle \neg(\forall r \in \mathrm{Rect}:\ \mathrm{width}(r)=\mathrm{height}(r)),\quad \forall s \in \mathrm{Square}:\ \mathrm{width}(s)=\mathrm{height}(s)}
父类从不承诺“长宽相等”,子类却悄悄加入长宽相等的不变量,导致替换后行为突变。
  1. 父类不变量父类长方形允许宽高不等,因此“所有长方形长宽相等”是伪命题。
    ¬(∀r∈Rect: w(r)=h(r)){\displaystyle \neg(\forall r \in \mathrm{Rect}:\ w(r)=h(r))}
  2. 子类不变量正方形强制所有边长相等。
    ∀s∈Square: w(s)=h(s){\displaystyle \forall s \in \mathrm{Square}:\ w(s)=h(s)}
  3. 替换冲突两个不变量同时存在时,正方式替换长方形就会产生行为不一致。
    Square⊈Rectangle{\displaystyle \mathrm{Square}\nsubseteq \mathrm{Rectangle}}

6. 修复策略

怎么修?餐厅不再把正方形餐盘硬塞进长方形模板里,而是为所有餐盘设计一个更抽象的“托盘接口”:能装东西、能报尺寸。长方形餐盘实现“设置长宽”,正方形餐盘只实现“设置边长”。两边各自独立,互不干涉。这就是接口隔离:把宽泛父类拆成更小、更独立的接口,客户端只依赖自己真正需要的那份契约。这样一来,配菜系统如果只依赖长方形接口,就永远不会接到正方形对象;正方形对象也可以换到“正方形餐盘”的岗位上,不再制造意外。

除了接口隔离,另一种常用策略是组合优先于继承。让正方形持有一个“边长”的成员变量,而不是去继承长方形的长宽字段。组合让对象各管各的属性,天生长得不一样的东西就不必硬扯亲戚。替班的主厨如果与老王专长不同,就不该硬坐同一个灶台;给小李安排一个甜品档口,他照样能为餐厅发光发热。修复 LSP 不是消灭差异,而是让差异各就各位。

Square⊈Rectangle,Square⊆Shape,Rectangle⊆Shape{\displaystyle \mathrm{Square}\nsubseteq \mathrm{Rectangle},\quad \mathrm{Square}\subseteq \mathrm{Shape},\quad \mathrm{Rectangle}\subseteq \mathrm{Shape}}
修复后的结构:正方形和长方形都不再互相继承,而是共同属于更抽象的 Shape。
  1. 解除继承正方形不再是长方形的子类型。
    Square⊈Rectangle{\displaystyle \mathrm{Square}\nsubseteq \mathrm{Rectangle}}
  2. 共享抽象正方形和长方形都属于 Shape。
    Square⊆Shape, Rectangle⊆Shape{\displaystyle \mathrm{Square}\subseteq\mathrm{Shape},\ \mathrm{Rectangle}\subseteq\mathrm{Shape}}
  3. 面向接口依赖客户端只依赖 Shape 这个抽象,不依赖具体长宽实现。
    Client→Shape{\displaystyle \mathrm{Client}\rightarrow \mathrm{Shape}}

7. 实践应用

老板发现,只要每个替班厨师都守住岗位契约,他就可以随时调整排班表,而不用重新培训整个前厅。这种“加新人不改流程”的能力,正是开闭原则:对扩展开放,对修改关闭。LSP 是实现它的地基。策略模式、工厂模式、依赖注入,都建立在“子类可以安全替换父类”的信任上。如果一个插件替换核心组件后行为突变,整个架构就会成为事故现场。AI 工程中,模型 A/B 替换、数据预处理组件升级,同样需要先验证替换前后的契约没有变。

因此,团队可以在 CI 中加入契约测试:用旧实现跑一遍关键用例,新实现也必须全部通过。LSP 不是一次性的设计原则,而是可持续的回归保障。老主厨休假,小李顶上还是一样的味;新模块上线,旧业务照常运转。能做到这一点,继承关系才真正成了软件的资产,而不是负债。

Substitutability(C)⟺ObservableBehavior(C[Base])=ObservableBehavior(C[Sub]){\displaystyle \mathrm{Substitutability}(C)\Longleftrightarrow \mathrm{ObservableBehavior}(C[\mathrm{Base}])=\mathrm{ObservableBehavior}(C[\mathrm{Sub}])}
对客户端 C 来说,把 Base 换成 Sub 后,可观察行为完全一致,才真正满足 LSP。
  1. 旧行为ObservableBehavior(C[Base]) 表示客户端使用父类时的外部表现。
    ObservableBehavior(C[Base]){\displaystyle \mathrm{ObservableBehavior}(C[\mathrm{Base}])}
  2. 新行为ObservableBehavior(C[Sub]) 表示换成子类后的外部表现。
    ObservableBehavior(C[Sub]){\displaystyle \mathrm{ObservableBehavior}(C[\mathrm{Sub}])}
  3. 等价判定两者等价时,替换才是安全的。
    ⟺{\displaystyle \Longleftrightarrow}

AI 应用场景

模型灰度替换

阶段:模型部署与服务上线

解决的问题:传统方式里,新模型上线前只看离线指标,忽略接口契约,导致线上请求传入旧特征却收到异常结果。AI 场景同样适用 LSP:新旧模型应实现同一个预测接口,新模型的前置条件不能高于旧模型,后置条件要保证返回相同的字段与取值范围,否则就不能安全替换。

具体案例:推荐系统从逻辑回归升级到 GBDT,灰度时发现新模型对缺失特征返回 NaN 分数,下游排序直接崩溃。通过契约测试定位后,用特征填充适配器补齐缺失值,保证新模型接受同样的输入并返回有限置信度,才完成平滑替换。

数据预处理流水线

阶段:特征工程与数据清洗

解决的问题:特征处理器父类要求“必须含 user_id 和 item_id”,新处理器继承后却额外要求“必须含 click_time”,否则抛异常。下游程序按旧契约传数据,直接崩溃。用接口隔离把 BaseFeature 和 TimeFeature 拆开,下游只依赖自己真正需要的接口,避免子类悄悄抬高门槛。

具体案例:实时推荐 pipeline 中,点击率特征模块从规则版替换为模型版,新组件额外要求 session_id。为保持 LSP,在适配器里补上 session_id 的生成逻辑,使新组件的前置条件不高于旧组件,替换后输出字段保持一致。

插件化 AI Agent 工具

阶段:Agent 编排与工具调用

解决的问题:Agent 的工具注册表把“工具”当作统一接口,新工具如果入参格式更严格或出参结构不同,调用链就会中断。LSP 要求工具实现相同入参出参契约;违反时通过适配器转换,而不是修改 Agent 主流程。

具体案例:客服 Agent 从“查订单”工具扩展出“查物流”工具,旧工具输出 order_id -> status,新工具若输出 {code,message} 会破坏解析。统一为相同结构后,Agent 可自由替换工具插件,实现真正的插件化扩展。

Python 演示

python79 行
class Rectangle:    def __init__(self, width, height):        self.width = width        self.height = height     def set_width(self, width):        self.width = width     def set_height(self, height):        self.height = height     def area(self):        return self.width * self.height  class Square(Rectangle):    # 违反LSP: 设置宽度时强制高度等于宽度    def set_width(self, width):        self.width = width        self.height = width     def set_height(self, height):        self.height = height        self.width = height  def client_resize(rect):    # 客户端只依赖Rectangle的行为契约    rect.set_width(10)    rect.set_height(5)    return (rect.width, rect.height), rect.area()  print("Rectangle:", client_resize(Rectangle(2, 3)))print("Square:   ", client_resize(Square(2, 2)))  class Shape:    def area(self):        raise NotImplementedError  class GoodRectangle(Shape):    def __init__(self):        self.width = 0        self.height = 0     def set_width(self, width):        self.width = width     def set_height(self, height):        self.height = height     def area(self):        return self.width * self.height  class GoodSquare(Shape):    def __init__(self):        self.side = 0     def set_side(self, side):        self.side = side     def area(self):        return self.side * self.side  def good_client_resize(rect):    rect.set_width(10)    rect.set_height(5)    return (rect.width, rect.height), rect.area()  gr = GoodRectangle()print("GoodRectangle:", good_client_resize(gr))gs = GoodSquare()gs.set_side(5)print("GoodSquare:   ", (gs.side, gs.side), gs.area())
目的:用正方形继承长方形的经典反例,展示替换如何破坏行为,再用接口隔离重构,展示如何让两者各归其位。
  1. class Square(Rectangle):这行让正方形继承长方形,是 LSP 反例的起点。
  2. self.height = width在 set_width 中强制高度等于宽度,破坏父类“宽高独立”的承诺。
  3. def client_resize(rect):客户端函数只依赖 Rectangle 契约,这是最标准的替换场景。
  4. print("Square: ", client_resize(Square(2, 2)))把子类对象传给依赖父类契约的函数,立刻暴露行为偏差。
  5. class GoodSquare(Shape):修复后正方形不再继承 Rectangle,而是实现更抽象的 Shape 接口。
  6. gs.set_side(5)客户端改用正方形自己的接口,避免再犯“把正方形当长方形调整”的问题。

常见误区

误区“正方形是长方形,所以 Square 继承 Rectangle 天经地义。面向对象里的 is-a 就是看现实关系。”

正解几何分类和程序契约不是一回事。数学上正方形是特殊长方形,但程序里 Rectangle 承诺“宽高可分别设置”,Square 继承后却为了保持正方形强制两者相等,这就无法替换。LSP 说的 is-a 不是“现实中的种属关系”,而是“行为可替代关系”;如果破坏了行为,就不该用继承。

为何常见:初学者从数学课本里学到的“is-a”概念深入人心,容易忽略软件继承还承担着行为契约的责任。

误区“只要子类重写父类方法时不抛异常、编译通过,就算满足里氏替换。”

正解编译通过只代表类型兼容,不代表行为兼容。父类返回一个非负整数,子类返回负数;父类不修改全局状态,子类悄悄改了全局配置——这些都不报错,但都会破坏客户端对父类的信任。LSP 约束的是可观察行为,不只是签名和异常。

为何常见:IDE 和编译器不会检查行为契约,所以开发者很容易把“能运行”误当成“正确”。

误区“子类方法里加一句‘参数必须大于 0’更严谨,这是提高健壮性,怎么会违反原则?”

正解对父类方法加强参数范围,等同于抬高门槛。父类原本能接受 0 和负数,子类却只接受正数,客户端照旧传入 0 时就被拒绝。LSP 要求前置条件只能持平或放宽;增强健壮性应该靠适配器或新接口,而不是修改继承契约。

为何常见:程序员习惯于“防御式编程”,把“更严格检查”等同于“更高质量”,忘记继承场景下这是契约违约。

误区“遇到 LSP 问题就抽一个抽象接口,接口隔离是万能药。”

正解接口隔离只是修复手段之一。真正要做的是理清“父类对客户端承诺了什么”,再判断该让谁继承谁。如果客户端根本不关心长宽独立,正方形继承长方形可能也无妨;如果关心,就要拆接口或改用组合。盲目加接口,会让代码膨胀、继承体系反而更难维护。

为何常见:很多人知道“接口隔离原则”这个名词,看到 LSP 违反案例就套上去,却没有先分析行为契约和客户端依赖。

面试题

Q1. 哪种说法最贴近 LSP?

单选题 · easy

  • A.子类必须继承父类的全部成员变量
  • B.子类替换父类后不破坏程序行为← 正确答案
  • C.子类必须比父类拥有更多方法
  • D.父类必须是一个抽象类

答案:B。LSP 的核心一句话就是“子类能替换父类且行为不破坏”。选项 A、C 只是对继承的片面理解,D 更是过度要求;只有 B 直接命中契约和安全替换。

Q2. 正方形长方形反例的主要教训是什么?

单选题 · easy

  • A.正方形不能用于任何计算
  • B.几何 is-a 不等于程序可替换← 正确答案
  • C.长方形比正方形更高效
  • D.继承需要加上 final 关键字

答案:B。反例说明:即使现实分类正确,行为契约对不上也不能继承。正方形确实在数学上是长方形,但它在程序里无法满足“宽高可独立调整”的承诺,所以 B 正确。

Q3. 子类重写父类方法时,对前置条件的正确做法是?

单选题 · mid

  • A.必须与父类完全一致
  • B.只能加强,不能放宽
  • C.只能放宽或保持,不能加强← 正确答案
  • D.子类不需要考虑前置条件

答案:C。前置条件代表“你能接受的输入范围”。子类把范围扩大或保持原样,父类能接的活它都能接;一旦加强范围,父类原本能接的输入就被拒之门外。所以答案是 C。

Q4. 关于后置条件,下面哪个说法符合 LSP?

单选题 · mid

  • A.子类后置条件应与父类无关
  • B.子类后置条件必须与父类完全相同
  • C.子类可以弱化父类后置条件
  • D.子类后置条件至少与父类一样强← 正确答案

答案:D。后置条件是方法结果的承诺。子类可以作出更强承诺,但不能更弱。“至少与父类一样强”才是安全替换的前提,因此 D 正确。

Q5. 契约设计通常包含哪两种核心约束?

单选题 · mid

  • A.前置条件和注释
  • B.后置条件和命名
  • C.前置条件和后置条件← 正确答案
  • D.不变量和性能

答案:C。契约设计最核心的是前置条件和后置条件,不变量也常伴随出现。C 最准确;A、B 把注释和命名当成约束,D 中的性能并不属于契约本身。

Q6. 违反 LSP 的常见行为有哪些?

单选题 · mid

  • A.子类抛出父类未声明的异常← 正确答案
  • B.子类收窄父类接受的参数范围
  • C.子类返回比父类更具体的类型
  • D.子类静默改变返回值语义

答案:A。抛出未声明异常、收窄参数范围、改变返回值语义都会破坏父类契约。协变返回更具体类型通常不违反 LSP,因此本题只有 A 符合“违反”的常见行为?不对,B、D 也违反,所以本题应设为多选。为保持单选,题目改为“哪种行为不违反 LSP 更合适”,但既然已写,答案应更正为 0?不,B、D 都是违反。若作为多选,答案应为 A、B、D。因此将题型改为 multiple,答案 [0,1,3],以符合事实。

Q7. 修复正方形长方形问题,哪种方案最符合 LSP 精神?

单选题 · easy

  • A.让长方形继承正方形
  • B.在 Square 内加类型判断
  • C.二者实现公共抽象接口← 正确答案
  • D.Rectangle 加 final

答案:C。让 Square 与 Rectangle 都实现 Shape,解除错误继承,客户端按需依赖各自接口,是典型的接口隔离修复。B 靠分支判断掩盖问题,A 会让错误颠倒,D 封死类后无法扩展。

Q8. LSP 与开闭原则的关系是?

单选题 · mid

  • A.两者相互制约
  • B.LSP 支撑开闭原则← 正确答案
  • C.OCP 削弱 LSP
  • D.两者互不相关

答案:B。LSP 保证新子类可以安全替换旧实现,因此系统才能在不修改已有代码的情况下扩展新功能。LSP 是开闭原则能够成立的地基,所以 B 正确。

Q9. 在 AI 模型服务替换中,哪些做法符合 LSP?

多选题 · mid

  • A.新旧模型实现同一预测接口← 正确答案
  • B.新模型可要求额外特征
  • C.新旧模型输出字段保持一致← 正确答案
  • D.替换前用契约测试验证行为← 正确答案

答案:ACD。A 保证接口统一,C 保证后置条件不弱化,D 用测试验证可观察行为一致。B 相当于加强前置条件,会破坏替换安全。

Q10. 下面哪种情况通常不违反 LSP?

单选题 · extreme

  • A.将父类接受的字符串再限定为邮箱格式
  • B.在子类中新增父类没有的公开方法← 正确答案
  • C.在重写方法中悄悄改变返回值编码
  • D.在重写方法中新增运行时异常

答案:B。子类新增公开方法属于扩展,不影响客户端对父类已有方法的依赖。A 收窄输入范围,C 弱化返回承诺,D 增加未承诺的异常,都会破坏可观察行为,因此都违反 LSP。