- 0
- 636 words
经过前三篇的学习,你已经掌握了继承的语法、super 和构造器链、方法重写与 final 关键字的用法。但掌握语法只是第一步——真正到写工程代码的时候,你可能会纠结:
- 「这个场景用继承合适吗?」
- 「两个类有共同代码,我该不该抽一个父类出来?」
- 「怎么设计继承层次才不至于后期改不动?」
这一篇我们就来做「设计层面」的收尾。通过综合实战案例,引出重要的里氏替换原则,并且好好聊聊业界常用的那句话——「优先使用组合而非继承」。
一、里氏替换原则(Liskov Substitution Principle, LSP)
这是面向对象设计五大原则(SOLID)中的 L。Barbara Liskov 在 1987 年提出了它,核心思想用一句话说就是:
任何使用父类对象的地方,都能透明地替换为子类对象,程序的行为不会出错。
听起来很简单?看看这个经典的反面案例:
// ❌ 违反里氏替换原则的经典案例
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 {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // 正方形必须保持边相等
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
看起来挺合理—正方形是特殊的矩形,对吧?但看看这个代码:
void resize(Rectangle r) {
r.setWidth(5);
r.setHeight(10);
// 期望面积 = 50
assert r.getArea() == 50;
}
public class Main {
public static void main(String[] args) {
Rectangle rect = new Rectangle();
resize(rect); // ✅ 面积 = 50
Rectangle square = new Square();
resize(square); // ❌ 面积 = 100!正方形被撑坏了
}
}
Square 替换 Rectangle 后,resize() 方法的行为出错了。这违反了里氏替换原则——因为 Square 改变了 setWidth 和 setHeight 的”语义”,调用者没想到设置宽度还会改变高度。
这也揭示了一个深刻的道理:继承不只是代码复用,更是「is-a」关系的建模。「正方形 is-a 矩形」在数学上成立,但在可变对象的场景下——你要的不是形状的静态关系,而是行为的替换能力。
LSP 的几条判断标准
- 子类不能强化父类的前置条件(比如父类方法接收正整数,子类不能要求必须大于 100)
- 子类不能弱化父类的后置条件(比如父类方法保证返回非空,子类不能返回 null)
- 子类不能改变父类方法的「预期行为」(就像 Square 改了 setWidth 的预期)
二、何时该用继承?
里氏替换原则给了我们一个很好的检验标准:用继承时,问自己三个问题:
- 子类真的是父类的一种吗?(真正的 is-a 关系)
- 子类能完全替代父类吗?(通过 LSP 测试)
- 子类真的需要父类的实现吗?(复用 vs 行为的差异化)
继承的适用场景举例:
- 框架扩展:Spring 中很多类继承框架基类,覆盖特定方法实现定制逻辑
- 模板方法模式:父类定好骨架,子类填充细节(我们上一篇的薪资计算案例)
- 层级清晰的数据模型:动物→哺乳动物→狗,这种层次结构天然稳定
三、继承的缺点
继承不是银弹。不加节制的继承会带来很多问题:
- 强耦合:子类依赖于父类的实现细节,父类一改子类就可能坏
- 脆弱性:父类的修改可能意外影响所有子类的行为
- 层次膨胀:随着需求变化,继承树可能越来越深,越来越难维护
- 不灵活:一个类只能有一个父类(Java 单继承),选择的灵活性不够
- 破坏封装:子类可以访问父类的 protected 成员,继承破坏了封装性
四、组合优先(Composition Over Inheritance)
组合的意思是:一个类包含另一个类的实例,通过委托来实现「复用」,而不是通过继承来「是」。
举个例子:假设我们需要一个「会飞的鸭子」。用继承的方式:
// ❌ 用继承——层级很容易膨胀
class Duck {
public void quack() { System.out.println("嘎嘎"); }
}
class FlyingDuck extends Duck {
public void fly() { System.out.println("飞起来了!"); }
}
class SwimmingDuck extends Duck {
public void swim() { System.out.println("游水中"); }
}
// 需要既能飞又能游的鸭子怎么办?
// 要么多重继承(Java 不支持),要么再加一层... 很快层级就乱套了
用组合的方式:
// ✅ 用组合——灵活、可插拔
interface FlyBehavior {
void fly();
}
interface SwimBehavior {
void swim();
}
class FlyWithWings implements FlyBehavior {
@Override
public void fly() { System.out.println("用翅膀飞"); }
}
class NoFly implements FlyBehavior {
@Override
public void fly() { /* 什么也不做 */ }
}
class SwimNormally implements SwimBehavior {
@Override
public void swim() { System.out.println("正常游泳"); }
}
class Duck {
private FlyBehavior flyBehavior;
private SwimBehavior swimBehavior;
public Duck(FlyBehavior fly, SwimBehavior swim) {
this.flyBehavior = fly;
this.swimBehavior = swim;
}
public void performFly() { flyBehavior.fly(); }
public void performSwim() { swimBehavior.swim(); }
// 运行时还能改行为!
public void setFlyBehavior(FlyBehavior fb) { this.flyBehavior = fb; }
}
public class Main {
public static void main(String[] args) {
Duck mallard = new Duck(new FlyWithWings(), new SwimNormally());
mallard.performFly(); // 用翅膀飞
mallard.performSwim(); // 正常游泳
Duck rubber = new Duck(new NoFly(), new SwimNormally());
rubber.performFly(); // 什么也不做(不会飞的橡皮鸭)
}
}
这就是策略模式(Strategy Pattern)的体现。组合把「行为」独立成接口和实现类,通过构造器或 setter 注入,比继承灵活得多:
- 松耦合:Duck 不依赖具体行为实现,只依赖接口
- 可复用:
FlyWithWings可以被任何需要飞行能力的类复用 - 运行时可变:可以通过
setFlyBehavior()动态改变行为
五、综合案例:快递配送系统
用一个接近真实项目的案例,把继承、LSP、组合都串起来。
import java.util.List;
import java.util.ArrayList;
// ========== 行为接口(组合的部分) ==========
interface ShippingStrategy {
double calculateCost(double weight, double distance);
String getMethodName();
}
interface TrackingCapability {
String track(String orderId);
}
// ========== 策略实现 ==========
class StandardShipping implements ShippingStrategy {
@Override
public double calculateCost(double weight, double distance) {
return weight * 1.5 + distance * 0.5;
}
@Override
public String getMethodName() { return "标准配送"; }
}
class ExpressShipping implements ShippingStrategy {
@Override
public double calculateCost(double weight, double distance) {
return weight * 3.0 + distance * 1.2 + 10; // 额外加急费
}
@Override
public String getMethodName() { return "加急配送"; }
}
class SimpleTracker implements TrackingCapability {
@Override
public String track(String orderId) {
return "订单 " + orderId + ":当前状态——运输中";
}
}
// ========== 继承层次(稳定的 is-a 关系) ==========
abstract class Package {
protected String id;
protected double weight; // kg
protected double distance; // km
protected ShippingStrategy shipping;
public Package(String id, double weight, double distance, ShippingStrategy shipping) {
this.id = id;
this.weight = weight;
this.distance = distance;
this.shipping = shipping;
}
public final String getId() { return id; }
// 模板方法:子类可重写计算细节
public double calculateTotalCost() {
return shipping.calculateCost(weight, distance);
}
public abstract String getDescription();
public String getShippingMethod() {
return shipping.getMethodName();
}
}
// 普通包裹——稳定,不会有奇怪的行为
class RegularPackage extends Package {
public RegularPackage(String id, double weight, double distance, ShippingStrategy shipping) {
super(id, weight, distance, shipping);
}
@Override
public String getDescription() {
return "普通包裹 #" + id + "(" + weight + "kg, " + distance + "km)";
}
}
// 易碎品包裹——可以安全替换 Package
class FragilePackage extends Package {
private static final double FRAGILE_SURCHARGE = 5.0;
public FragilePackage(String id, double weight, double distance, ShippingStrategy shipping) {
super(id, weight, distance, shipping);
}
@Override
public double calculateTotalCost() {
return super.calculateTotalCost() + FRAGILE_SURCHARGE;
}
@Override
public String getDescription() {
return "⚠️ 易碎品 #" + id + "(" + weight + "kg, " + distance + "km)";
}
}
// ========== 配送系统 ==========
class DeliverySystem {
private List packages = new ArrayList();
public void addPackage(Package pkg) {
packages.add(pkg);
}
public void printAllOrders() {
for (Package pkg : packages) {
System.out.println(pkg.getDescription());
System.out.println(" 配送方式:" + pkg.getShippingMethod());
System.out.println(" 总费用:¥" + pkg.calculateTotalCost());
// 有追踪能力的额外输出
if (pkg instanceof TrackingCapability) {
System.out.println(" " + ((TrackingCapability) pkg).track(pkg.getId()));
}
System.out.println();
}
}
}
// ========== 客户端 ==========
public class Main {
public static void main(String[] args) {
DeliverySystem system = new DeliverySystem();
// 满足 LSP:哪里用 Package,就能放 RegularPackage 或 FragilePackage
system.addPackage(new RegularPackage("A001", 2.5, 100, new StandardShipping()));
system.addPackage(new RegularPackage("A002", 10.0, 50, new ExpressShipping()));
system.addPackage(new FragilePackage("A003", 1.2, 200, new StandardShipping()));
// 一个特殊的可追踪包裹——通过组合实现了额外能力
Package tracked = new RegularPackage("A004", 3.0, 80, new ExpressShipping()) {
private TrackingCapability tracker = new SimpleTracker();
@Override
public String getDescription() {
return "📦 可追踪包裹 #" + getId();
}
};
system.addPackage(tracked);
system.printAllOrders();
}
}
运行输出:
普通包裹 #A001(2.5kg, 100km)
配送方式:标准配送
总费用:¥53.75
普通包裹 #A002(10.0kg, 50km)
配送方式:加急配送
总费用:¥100.0
⚠️ 易碎品 #A003(1.2kg, 200km)
配送方式:标准配送
总费用:¥101.8
📦 可追踪包裹 #A004
配送方式:加急配送
总费用:¥115.0
👉 订单 A004:当前状态——运输中
这个案例中你能看到:
- 继承用于稳定层次:
Package → RegularPackage / FragilePackage,这是真正的 is-a 关系,行为一致,满足 LSP - 组合用于可变行为:
ShippingStrategy通过接口注入,配送策略可以自由切换和复用 - 模板方法:
calculateTotalCost()在基类中定义流程,子类可以扩展(如 FragilePackage 加易碎附加费) - final 方法:
getId()被声明为 final,保证标识符不被篡改 - 组合实现额外能力:
TrackingCapability作为接口,任何需要追踪能力的包裹都可以组合进去
六、继承与组合的决策指南
在实际写代码时,可以按下面这个流程做决定:
- 首先问:两个类之间真的是「is-a」关系吗?还是「has-a」关系?
- Dog is-a Animal → 可以考虑继承
- Car has-a Engine → 用组合
- 再问:子类是否能完全替代父类?如果子类会改变父类的预期行为,就不要继承。
- 再问:子类是「扩展」父类,还是「复用」父类代码?
- 如果是扩展行为 → 继承合适
- 如果只是为了复用代码 → 组合更安全
- 最后问:这个继承层次未来会被别人用吗?如果会被外部扩展,确保基类设计稳定。
| 使用继承 | 使用组合 |
|---|---|
| 真正的 is-a 关系 | has-a 关系 |
| 子类能完全替代父类(LSP) | 需要灵活切换行为 |
| 框架要求扩展基类 | 行为需要运行时变更 |
| 层次结构天然稳定不变 | 需要避免强耦合 |
| 模板方法模式 | 策略模式、装饰器模式 |
结语——继承专题回顾
整个继承专题结束了,我们一起来回顾一下这四篇走过的路:
- 第 1 篇:继承基础——extends 语法、父子类关系、继承 vs 组合的初步了解
- 第 2 篇:super 关键字与构造器链——构造器的执行顺序、多层继承、protected
- 第 3 篇:方法重写与 final——Override 规则、动态绑定、final 类/方法/变量
- 第 4 篇:继承体系设计实战——LSP 原则、组合优先、综合案例
继承是理解面向对象的关键枢纽。它连接了封装(通过 protected 控制可见性)、铺垫了多态(通过重写实现动态绑定)。掌握了继承,你就拥有了构建类层次结构的能力——但你也有了「滥用继承」的风险。
下一篇我们将进入面向对象的最后一个大特征——多态(Polymorphism)。实际上你已经在这几篇里接触到了多态的影子:向上转型、动态绑定、重写……下一篇会把多态讲透,让你真正理解 Java 面向对象的灵魂所在。