×

经过前三篇的学习,你已经掌握了继承的语法、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 改变了 setWidthsetHeight 的”语义”,调用者没想到设置宽度还会改变高度。

这也揭示了一个深刻的道理:继承不只是代码复用,更是「is-a」关系的建模。「正方形 is-a 矩形」在数学上成立,但在可变对象的场景下——你要的不是形状的静态关系,而是行为的替换能力。

LSP 的几条判断标准

  • 子类不能强化父类的前置条件(比如父类方法接收正整数,子类不能要求必须大于 100)
  • 子类不能弱化父类的后置条件(比如父类方法保证返回非空,子类不能返回 null)
  • 子类不能改变父类方法的「预期行为」(就像 Square 改了 setWidth 的预期)

二、何时该用继承?

里氏替换原则给了我们一个很好的检验标准:用继承时,问自己三个问题:

  1. 子类真的是父类的一种吗?(真正的 is-a 关系)
  2. 子类能完全替代父类吗?(通过 LSP 测试)
  3. 子类真的需要父类的实现吗?(复用 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 作为接口,任何需要追踪能力的包裹都可以组合进去

六、继承与组合的决策指南

在实际写代码时,可以按下面这个流程做决定:

  1. 首先问:两个类之间真的是「is-a」关系吗?还是「has-a」关系?
    • Dog is-a Animal → 可以考虑继承
    • Car has-a Engine → 用组合
  2. 再问:子类是否能完全替代父类?如果子类会改变父类的预期行为,就不要继承。
  3. 再问:子类是「扩展」父类,还是「复用」父类代码?
    • 如果是扩展行为 → 继承合适
    • 如果只是为了复用代码 → 组合更安全
  4. 最后问:这个继承层次未来会被别人用吗?如果会被外部扩展,确保基类设计稳定。
使用继承使用组合
真正的 is-a 关系has-a 关系
子类能完全替代父类(LSP)需要灵活切换行为
框架要求扩展基类行为需要运行时变更
层次结构天然稳定不变需要避免强耦合
模板方法模式策略模式、装饰器模式

结语——继承专题回顾

整个继承专题结束了,我们一起来回顾一下这四篇走过的路:

  • 第 1 篇:继承基础——extends 语法、父子类关系、继承 vs 组合的初步了解
  • 第 2 篇:super 关键字与构造器链——构造器的执行顺序、多层继承、protected
  • 第 3 篇:方法重写与 final——Override 规则、动态绑定、final 类/方法/变量
  • 第 4 篇:继承体系设计实战——LSP 原则、组合优先、综合案例

继承是理解面向对象的关键枢纽。它连接了封装(通过 protected 控制可见性)、铺垫了多态(通过重写实现动态绑定)。掌握了继承,你就拥有了构建类层次结构的能力——但你也有了「滥用继承」的风险。

下一篇我们将进入面向对象的最后一个大特征——多态(Polymorphism)。实际上你已经在这几篇里接触到了多态的影子:向上转型、动态绑定、重写……下一篇会把多态讲透,让你真正理解 Java 面向对象的灵魂所在。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

作者

2248768396@qq.com

相关文章

20. 接口基础:语法、特性与抽象类对比

抽象类讲完了。你知道父类可以只声明方法签名,...

读出全部

19. 多态的应用:参数多态化、工厂模式与支付系统实战

上一篇我们拆解了多态的底层机制——动态绑定、...

读出全部

18. 多态的本质:动态绑定、向上转型与向下转型

抽象类学完了,你现在应该知道 「父类只声明、...

读出全部