网站建设找客户企业营销推广怎么做
里氏替换原则(Liskov Substitution Principle, LSP)是面向对象设计的核心原则之一,属于 SOLID 中的 L。其核心思想是:子类必须能够替换父类,且替换后程序的行为不变。简单来说,所有使用父类的地方,都应该能透明地替换成子类,而不会引发错误或意外行为。
核心规则
-
行为一致性:子类的方法行为应与父类一致(或更严格)。
-
契约不变:子类不能修改父类定义的方法参数、返回值类型或抛出的异常范围。
-
前置条件不加强:子类方法的输入条件不能比父类更严格。
-
后置条件不削弱:子类方法的输出结果不能比父类更宽松。
违反 LSP 的经典案例
案例 1:正方形继承长方形
数学中,正方形是特殊的长方形,但在编程中直接继承会违反 LSP。
class Rectangle {protected int width;protected int height;public void setWidth(int width) { this.width = width; }public void setHeight(int height) { this.height = height; }public int getArea() { return width * height; } }class Square extends Rectangle {// 正方形要求宽高始终相等@Overridepublic void setWidth(int width) {super.setWidth(width);super.setHeight(width); // 同时修改高度}@Overridepublic void setHeight(int height) {super.setHeight(height);super.setWidth(height); // 同时修改宽度} }
问题:当程序以父类 Rectangle
的方式使用子类 Square
时,行为会异常:
public static void testArea(Rectangle rect) {rect.setWidth(5);rect.setHeight(4);System.out.println("期望面积 20,实际面积:" + rect.getArea()); }public static void main(String[] args) {testArea(new Rectangle()); // 输出 20(正确)testArea(new Square()); // 输出 16(错误!) }
原因:Square
修改了 setWidth
和 setHeight
的行为,导致结果不符合预期。
改进方案:
避免让 Square
直接继承 Rectangle
,而是通过接口或组合实现共性。
案例 2:企鹅继承鸟类
假设父类 Bird
有 fly()
方法,但子类企鹅不会飞:
class Bird {public void fly() {System.out.println("飞行中...");} }class Penguin extends Bird {@Overridepublic void fly() {throw new UnsupportedOperationException("企鹅不会飞!");} }
问题:当程序调用 Bird
的 fly()
时,替换为 Penguin
会抛出异常:
public static void makeBirdFly(Bird bird) {bird.fly(); // 如果 bird 是企鹅,程序崩溃! }public static void main(String[] args) {makeBirdFly(new Bird()); // 正常makeBirdFly(new Penguin()); // 抛出异常! }
改进方案:
将飞行能力分离为接口,只有会飞的鸟才实现它:
interface Flyable {void fly(); }class Sparrow implements Flyable {public void fly() { /* 实现飞行 */ } }class Penguin extends Bird { // 不实现 Flyable 接口 }
符合 LSP 的案例
案例:支付系统
父类 Payment
定义支付行为,子类 CreditCardPayment
和 WeChatPayment
实现各自逻辑,但保持行为一致。
abstract class Payment {public abstract void pay(int amount); }class CreditCardPayment extends Payment {@Overridepublic void pay(int amount) {System.out.println("信用卡支付:" + amount + "元");} }class WeChatPayment extends Payment {@Overridepublic void pay(int amount) {System.out.println("微信支付:" + amount + "元");} }
使用场景:
无论替换成哪种支付方式,程序行为一致。
public static void processPayment(Payment payment, int amount) {payment.pay(amount); }public static void main(String[] args) {processPayment(new CreditCardPayment(), 100); // 信用卡支付processPayment(new WeChatPayment(), 200); // 微信支付 }
总结
-
里氏替换原则的本质:子类必须保持与父类的契约,确保继承是“is-a”关系(如“企鹅是鸟”不成立,但“麻雀是鸟”成立)。
-
如何遵守 LSP:
-
优先使用组合而非继承。
-
通过接口分离不同行为。
-
子类不重写父类非抽象方法,避免破坏原有逻辑。
-
我们通过具体案例逐一解释里氏替换原则(LSP)的四个核心规则,并说明如何避免违反这些规则。
1. 行为一致性:子类方法的行为应与父类一致(或更严格)
反例:子类修改父类行为
class Bird {// 父类方法:飞行距离为100米public int fly() {return 100;} }class Ostrich extends Bird { // 鸵鸟不会飞@Overridepublic int fly() {return 0; // 直接返回0,违反行为一致性} }
问题:
当程序中依赖 Bird
的 fly()
方法时,替换为 Ostrich
会导致意外结果:
void calculateDistance(Bird bird) {int distance = bird.fly() * 2;System.out.println("飞行总距离:" + distance); }calculateDistance(new Bird()); // 输出 200(正确) calculateDistance(new Ostrich()); // 输出 0(错误!鸵鸟被误用为会飞的鸟)
改进方案:
分离“会飞”和“不会飞”的行为,通过接口实现:
interface Flyable {int fly(); }class Sparrow implements Flyable { public int fly() { return 100; } }class Ostrich extends Bird { // 不实现 Flyable 接口 }
2. 契约不变:子类不能修改父类方法的参数、返回值或异常
反例:子类修改返回值类型
class Database {// 父类方法返回一个可修改的 Listpublic List<String> getData() {return new ArrayList<>(Arrays.asList("A", "B"));} }class ReadOnlyDatabase extends Database {@Overridepublic List<String> getData() {// 返回不可修改的 List(契约改变!)return Collections.unmodifiableList(super.getData());} }
问题:
父类允许修改返回的 List
,但子类返回不可修改的列表,导致客户端代码崩溃:
Database db = new ReadOnlyDatabase(); db.getData().add("C"); // 抛出 UnsupportedOperationException
改进方案:
保持返回值行为一致,或通过新方法明确只读特性:
class ReadOnlyDatabase extends Database {// 新增方法,而非重写 getData()public List<String> getReadOnlyData() {return Collections.unmodifiableList(super.getData());} }
3. 前置条件不加强:子类方法的输入条件不能比父类更严格
反例:子类强化参数校验
class Account {// 父类允许任何金额的取款public void withdraw(int amount) {balance -= amount;} }class SafeAccount extends Account {@Overridepublic void withdraw(int amount) {if (amount > balance) { // 新增前置条件!throw new IllegalArgumentException("余额不足!");}super.withdraw(amount);} }
问题:
当父类允许透支,而子类禁止时,替换子类会破坏原有逻辑:
Account account = new SafeAccount(); account.withdraw(1000); // 如果 balance=500,抛出异常 // 但父类 Account 的设计允许透支
改进方案:
如果父类允许透支,子类不应修改这一行为。可通过新类表示“安全账户”:
class SafeAccount { // 不继承 Accountpublic void safeWithdraw(int amount) {if (amount > balance) throw new IllegalArgumentException();balance -= amount;} }
4. 后置条件不削弱:子类方法的输出结果不能比父类更宽松
反例:子类返回值范围扩大
class Calculator {// 父类保证返回值 >=0public int sqrt(int x) {if (x < 0) throw new IllegalArgumentException();return (int) Math.sqrt(x);} }class BuggyCalculator extends Calculator {@Overridepublic int sqrt(int x) {// 未处理负数输入,可能返回负数(后置条件削弱!)return (int) Math.sqrt(x); } }
问题:
父类保证结果非负,但子类未处理负数输入,导致客户端代码出现意外值:
Calculator calc = new BuggyCalculator(); int result = calc.sqrt(-4); // 返回 NaN 或其他未定义值(违反契约)
改进方案:
子类必须维持父类的后置条件:
class BuggyCalculator extends Calculator {@Overridepublic int sqrt(int x) {if (x < 0) throw new IllegalArgumentException(); // 保持后置条件return super.sqrt(x);} }
总结
规则 | 关键点 | 避免方法 |
---|---|---|
行为一致性 | 子类不改变父类核心逻辑 | 通过接口/组合替代继承 |
契约不变 | 参数、返回值、异常与父类严格一致 | 避免重写非抽象方法 |
前置条件不加强 | 子类不增加输入限制 | 子类复用父类校验逻辑 |
后置条件不削弱 | 子类不放松输出约束 | 子类覆盖时明确校验并抛出异常 |
核心思想:子类必须完全遵守父类的“契约”,才能透明替换父类。
问题背景
假设存在一个报表处理系统,定义了一个通用接口 CommonHandler
,要求所有业务模块实现类完成请求校验(businessCheck
)和业务执行(businessExecute
)功能。以下是两个实现类的关键代码片段:
1. 户用模块策略类(ResidentialStrategy
)
@Service public class ResidentialStrategy implements CommonHandler {@Overridepublic void businessCheck(CommonRequest request) {// 校验报表类型if (null == ReportEnum.getDateTypeEnum(request.getReportType())) {throw new BusinessException("无效的报表类型");}// 新增校验:日期字段必填if (null == request.getDate()) {throw new BusinessException("日期不能为空");}} }
2. 商用模块策略类(CommercialStrategy
)
@Service public class CommercialStrategy implements CommonHandler {@Overridepublic void businessCheck(CommonRequest request) {// 仅校验报表类型if (null == ReportEnum.getDateTypeEnum(request.getReportType())) {throw new BusinessException("无效的报表类型");}} }
接口定义(CommonHandler
)
public interface CommonHandler {void businessCheck(CommonRequest request);CommonResponse businessExecute(CommonRequest request); }
LSP 规则回顾:前置条件不加强
LSP 要求子类(或实现类)方法的输入条件(前置条件)不能比父类(或接口)更严格。若父类未对某个字段进行非空校验,而子类强制校验,则违反了这一规则。
问题分析
在以上代码中:
-
接口契约:
CommonHandler
的businessCheck
方法未明确要求校验date
字段。 -
实现类行为:
-
CommercialStrategy
仅校验reportType
,符合接口契约。 -
ResidentialStrategy
额外校验date
字段,单方面加强前置条件。
-
违反 LSP 的后果
当通过 CommonHandler
接口调用 businessCheck
方法时:
-
替换为
ResidentialStrategy
的实现时,若date
为空,会抛出异常。 -
替换为其他实现类(如
CommercialStrategy
)时,date
为空不会报错。
结果:ResidentialStrategy
无法透明替换 CommonHandler
,破坏了一致性。
改进方案:分层校验设计
通过接口的默认方法(default
)统一基础校验,允许子类在遵守契约的前提下扩展校验逻辑。
1. 修改接口定义
public interface CommonHandler {default void businessCheck(CommonRequest request) {// 基础校验:报表类型必填if (null == ReportEnum.getDateTypeEnum(request.getReportType())) {throw new BusinessException("无效的报表类型");}}CommonResponse businessExecute(CommonRequest request); }
2. 实现类按需扩展校验
@Service public class ResidentialStrategy implements CommonHandler {@Overridepublic void businessCheck(CommonRequest request) {// 调用接口默认的基础校验CommonHandler.super.businessCheck(request);// 扩展校验:日期必填if (null == request.getDate()) {throw new BusinessException("日期不能为空");}} }
改进后的设计解析
-
接口明确基础契约
-
通过
default
方法统一实现reportType
校验,所有实现类必须遵守。 -
子类无需重复编写基础校验逻辑。
-
-
子类灵活扩展
-
子类可调用
super.businessCheck()
后补充额外校验(如date
字段)。 -
附加校验不视为加强前置条件,因为基础契约已通过默认方法满足。
-
-
透明替换性保障
-
所有实现类均通过
CommonHandler
接口暴露行为。 -
调用方无需关心具体实现类的校验差异。
-
结论
在接口设计中遵循里氏替换原则,关键在于:
-
明确契约:通过默认方法或文档定义接口的基础行为约束。
-
分层校验:基础校验由接口统一实现,子类按需扩展而非覆盖。
-
契约优先:实现类的扩展逻辑不应破坏接口的原始语义。
通过这种设计,既能保证系统的行为一致性,又能灵活支持不同业务的个性化需求,最终实现高内聚、低耦合的代码结构。