前言作为Java程序员你一定听过SOLID原则。这五个原则是面向对象设计的基石而其中最让人似懂非懂的恐怕就是里氏替换原则Liskov Substitution PrincipleLSP了。网上有很多解释什么“子类型必须能够替换掉它们的父类型”听起来像绕口令背下来容易真正写代码的时候却不知道怎么用。今天这篇文章不讲晦涩的数学定义不用复杂的UML图全程用大白话 生活化例子保证让初学者也能一听就懂摘要里氏替换原则LSP是SOLID中最让初学者困惑的原则。本文用“爸爸能干的儿子都能干且不能干得更差”这一大白话彻底讲清LSP的核心。通过鸟与鸵鸟的经典反例演示了子类抛异常破坏父类契约的典型违规场景并给出用接口抽取“特殊能力”的正确重构方案。同时总结了三条避坑口诀子类不能削弱父类能力、不能增强前置条件、不能弱化后置条件。当代码中出现instanceof判断子类类型时就是重构的信号。本文不用晦涩术语全程生活化案例让零基础小白也能轻松掌握里氏替换原则的本质。目录一、一句话大白话版二、从一个“鸟”的例子说起三、踩坑现场鸵鸟的出现四、为什么会出现这个问题五、正确的设计应该怎么做六、三条“避坑口诀”① 子类有没有削弱父类的能力② 子类有没有增强父类的“前置条件”③ 子类有没有弱化父类的“后置条件”七、如何快速发现代码违规八、经典案例拓展正方形 vs 长方形总结写在最后一、一句话大白话版里氏替换原则的核心就一句话爸爸能干的活儿儿子子类都能干而且干出来的效果不能比爸爸差更不能把事儿搞砸。反过来就不一定成立了——儿子能干的爸爸可能干不了。说白了子类可以做得更多但不能做得更少。就这么简单。记住这句话后面的内容都是围绕它展开的。二、从一个“鸟”的例子说起假设我们有一个Bird鸟父类class Bird { public void fly() { System.out.println(我在天上飞); } }这个类告诉我们所有的鸟都会飞而且fly()方法一定能成功执行。现在我们定义一个Swallow燕子子类继承Birdclass Swallow extends Bird { // 燕子天生会飞啥也不用写 }然后写一个测试方法让鸟飞起来public void letBirdFly(Bird bird) { bird.fly(); } // 调用 letBirdFly(new Swallow()); // 输出我在天上飞燕子飞得好好的父类能飞子类也能飞这叫符合里氏替换原则。三、踩坑现场鸵鸟的出现现在产品经理来了一个新需求鸵鸟。鸵鸟也是鸟但鸵鸟不会飞。于是我们写出了这样的代码class Ostrich extends Bird { Override public void fly() { throw new UnsupportedOperationException(我是鸵鸟我不会飞); } }然后调用letBirdFly(new Ostrich()); // 运行时报错程序崩溃这就是典型的违反里氏替换原则我们来看问题出在哪对比项父类 Bird子类 Ostrichfly()的承诺一定能成功飞起来抛异常飞不起来调用方预期正常执行程序崩溃父类承诺了“fly()一定成功”子类却违背了这个承诺。结果就是所有能用父类的地方不能用子类来替换——因为一替换就出错。四、为什么会出现这个问题问题根源在于我们的设计本身就错了。我们一开始就给Bird类加上了fly()方法默认所有鸟都会飞。但现实世界中并不是所有鸟都会飞鸵鸟、企鹅、几维鸟……。当继承关系不符合现实逻辑时强行用继承就会出问题。五、正确的设计应该怎么做既然“会飞”不是所有鸟的共性那我们就把“飞”这个行为抽出来只给会飞的鸟用。方案用接口替代继承中的“特殊能力”// 定义更抽象的父类只放所有鸟都有的共性 class Bird { // 比如有羽毛、是卵生、有喙…… } // 定义一个会飞的接口 interface Flyable { void fly(); } // 燕子继承鸟 实现飞行接口 class Swallow extends Bird implements Flyable { Override public void fly() { System.out.println(我在天上飞); } } // 鸵鸟只继承鸟不实现飞行接口 class Ostrich extends Bird { // 没有fly()方法完美 }现在测试方法只接收Flyable类型public void letItFly(Flyable flyer) { flyer.fly(); // 能传进来的一定都会飞 } letItFly(new Swallow()); // ✅ 没问题 // letItFly(new Ostrich()); // ❌ 编译都通不过提前规避了错误这样一来燕子能飞→ 实现了Flyable接口 → 可以传入letItFly()鸵鸟不能飞→ 不实现Flyable接口 → 根本无法传入编译器就拦截了编译期报错永远比运行期报错好这就是好的设计带给我们的好处。六、三条“避坑口诀”以后写继承的时候问自己下面三个问题能避开90%的坑① 子类有没有削弱父类的能力场景是否违背父类能飞子类不能飞❌ 违背父类能存数据子类存不进去❌ 违背父类能计算子类算不出来❌ 违背原则子类可以做得更多但不能做得更少。② 子类有没有增强父类的“前置条件”前置条件就是调用方法前必须满足的条件比如参数的要求。场景是否违背父类说“给个正数就行”子类说“必须大于100”❌ 违背父类说“传个字符串就行”子类说“不能为空”❌ 违背原则子类的要求不能比父类更严格否则原来能用的现在不能用了。③ 子类有没有弱化父类的“后置条件”后置条件就是方法执行完后必须保证的结果。场景是否违背父类保证“一定返回结果”子类可能返回null❌ 违背父类保证“不抛异常”子类却抛异常❌ 违背原则子类的输出不能比父类的承诺更差否则调用方会崩。七、如何快速发现代码违规一个最明显的代码坏味道是if (bird instanceof Ostrich) { // 特殊处理不让它飞 } else { bird.fly(); }如果你在父类的方法里写了if (子类 instanceof XXX)或者在使用父类的地方频繁判断具体子类类型说明你的继承设计已经违背了里氏替换原则——该重构了八、经典案例拓展正方形 vs 长方形很多讲里氏替换的文章都会提到这个经典案例我们简单提一下假设有一个Rectangle长方形父类有setWidth()和setHeight()方法。然后Square正方形继承Rectangle但正方形的宽和高必须相等所以重写了setWidth()和setHeight()让它们互相影响。结果就是所有能用长方形的地方不能用正方形替换——因为正方形的行为跟长方形的预期不一致。这就是里氏替换原则被破坏的又一个经典场景。总结要点内容一句话核心爸爸能干的儿子都能干而且不能干得更差判断标准所有使用父类的地方都能无缝替换为子类常见违规子类抛异常、子类增强前置条件、子类削弱父类能力解决思路把“特殊能力”抽成接口而非硬塞进父类重构信号代码中出现instanceof判断子类类型里氏替换原则的本质是要求我们设计继承关系时尊重父类和子类之间的“契约”。父类怎么承诺的子类就怎么兑现甚至可以做得更好比如返回更具体的子类型但绝不能做得更差。写在最后很多初学者害怕设计原则觉得太抽象。但其实这些原则都是从无数实际项目中总结出来的血泪教训。里氏替换原则教会我们一件事不要为了复用代码而滥用继承。当你发现子类需要“阉割”父类的某个方法时说明继承关系已经错了该用接口组合了。如果这篇文章帮到了你欢迎点赞、收藏、评论三连支持一下