Skip to content

元数据卡

  • 前置知识:第7章(类与对象、封装)
  • 预计时间:45 分钟
  • 核心难度:
  • 阅读模式: 高度专注
  • 完成标志:能用继承消除重复代码,能用多态写出可扩展的设计;理解接口和抽象类的各自用途

本章分层

  • 必读:继承的两大目的——代码复用(extends)与多态替换(super + 方法重写)、多态的动态绑定机制
  • 选读instanceof 与向下转型、模板方法设计模式
  • 深水区:抽象类的"骨架+填空"设计、接口 vs 抽象类的选择、Object 类的方法

本章不会要求你掌握

  • 类型转换异常的完整处理——留到集合和泛型之前深入
  • 接口默认方法的多继承冲突(Java 8+ 高级话题)

你在哪

窗外传来阿花试喇叭的喊话声,你手里的活停了下来。老陈师傅走过来拍了拍你的肩膀。

你在变量村的铁匠铺里蹲了一个星期了。老陈师傅教了你如何把属性(字段)和行为(方法)打包成"类"这种工具箱,还教会了你用 private 锁住关键零件——你已经有封装的概念了。

但这两天你在做一个麻烦的活儿。

村口需要一套"通信系统":驿站跑马传信、信鸽从空中送信、村头大喇叭喊话。嗯,你手上已经有了几个类——HorsePigeonLoudspeaker。每个类都有自己的 send() 方法,但代码里面有大量重复的东西:

  • 都有"消息内容"
  • 都要"格式化"
  • 都要"记录发送时间"

你复制粘贴了三次,现在 Horse 里改了一行,得去 PigeonLoudspeaker 里同样改一遍。你已经忘了昨晚改了哪个没改哪个。

"你这孩子,"老陈师傅叹了口气,拿起一块铁胚,"不学会站在别人的基础上造东西,你一辈子都在打一模一样的锄头。"

他把那块铁胚放在你面前:"这叫继承。"

你的任务

旧办法:复制粘贴做三个相似的类。问题在于——改一处要改三处,加一处要加三处,一个 bug 可以潜伏两份你没改的副本里。

这一章要解决的问题很简单:能不能只写一次公共逻辑,让其他类型说"我继承它"?

听起来像复制粘贴的自动化——但远比这深。继承的背后有一个叫"多态"的东西,它让你写出"只依赖抽象、不依赖具体"的代码。那才是真正让你从打铁的变成造桥的飞跃。

看完整章,你不仅能消除重复代码,还能写出能轻松扩展的灵活设计。

遭遇战 → 获得技能

第一幕:extends——站在前人的肩膀上

我们先从最直觉的场景入手。

你有三个类:HorsePigeonLoudspeaker,它们都共享"消息"这个公共概念。你把公共部分抽出来,放到一个叫 MessageSender 的类里:

java
// MessageSender.java — 公共消息发送能力
public class MessageSender {
    String message;
    long timestamp;

    public MessageSender(String message) {
        this.message = message;
        this.timestamp = System.currentTimeMillis();
    }

    public void send() {
        System.out.println("[LOG] 发送时间: " + timestamp);
        System.out.println("消息: " + message);
    }
}

语言:Java 17+ 如何运行:保存为 .java 文件,用 javac 编译后用 java 运行

现在,Horse 不再从零开始写了——它 extends(继承)MessageSender

java
// Horse.java — 继承 MessageSender
public class Horse extends MessageSender {
    private String riderName;

    public Horse(String message, String riderName) {
        super(message);            // 👈 调用父类构造方法,必须先写
        this.riderName = riderName;
    }

    public void send() {
        super.send();              // 👈 先执行父类的公共行为
        System.out.println("骑士 " + riderName + " 策马奔腾!");
    }
}

语言:Java 17+ 预期输出:当调用 new Horse("敌军来袭", "张三").send() 时,会输出时间戳、消息,然后输出"骑士张三策马奔腾!" 你试试:把 super(message) 注释掉,看编译器报什么错

super 就是你喊的那声"爸,干活了"——它引用父类。super(message) 调用父类构造器,super.send() 调用父类被重写前的方法。

信鸽也是继承,但实现不一样:

java
// Pigeon.java — 继承 + 不同的 send 实现
public class Pigeon extends MessageSender {
    private int speedLevel;  // 1 = 普通, 2 = 加急

    public Pigeon(String message, int speedLevel) {
        super(message);
        this.speedLevel = speedLevel;
    }

    @Override   // 👈 告诉编译器:我在重写父类的方法
    public void send() {
        super.send();
        String level = speedLevel == 2 ? "🕊️ 加急!" : "🕊️ 普通";
        System.out.println(level + " 信鸽起飞!");
    }
}

语言:Java 17+ 预期输出new Pigeon("天气转晴", 2).send() → 先打印时间戳和消息,再打印" 加急!信鸽起飞!" 你试试:去掉 @Override,再试试在参数类型上故意写错(比如把 String 写成 int),看 @Override 为什么能帮我们发现错误

这里的关键:@Override 不是功能性的,它是编译器的安全检查。你告诉编译器"我要重写父类的一个方法"——如果父类没有同名方法(你拼错了方法名),编译器会报错,而不是悄悄地创建一个新方法。

你一定注意到了:三个类各自有各自的 send(),但结构相同——先把公共逻辑做完,再干自己的事。继承让公共逻辑只写一次。

Python 窗口

python
class MessageSender:
    def __init__(self, message):
        self.message = message

    def send(self):
        print(f"消息: {self.message}")

class Horse(MessageSender):        # 👈 括号里写父类
    def __init__(self, message, rider):
        super().__init__(message)   # 👈 super() 调用父类
        self.rider = rider

    def send(self):
        super().send()              # 👈 显式调用父类方法
        print(f"骑士 {self.rider} 策马!")

Python 的写法几乎一模一样。不同在于:

  • super() 不需要参数(Python 3 会自动推导)
  • 没有 @Override 注解——Python 默认全部虚方法
  • Python 支持多继承(class A(B, C)),但多继承带来菱形问题,Java 选择了单继承来避免它

C++ 窗口

cpp
class MessageSender {
public:
    void send() { cout << "消息"; }
};

class Horse : public MessageSender {   // 👈 public 表示继承接口
public:
    void send() override {             // 👈 override 是 C++11 的关键字
        MessageSender::send();         // 👈 调用父类方法
        cout << "骑士策马";
    }
};

C++ 的 override 和 Java 的 @Override 一样是安全检查。C++ 支持多继承,但和 Python 一样需要面对菱形问题。

第二幕:多态——写一个调度中心

你写好三个类之后,村口需要一个"调度中心"——它不管你是马、鸽子还是喇叭,只要知道你能 send()

先别用继承,看看老办法有多痛苦:

java
// 没有多态的情况
public class DispatchCenter {
    public void sendByHorse(Horse h) { h.send(); }
    public void sendByPigeon(Pigeon p) { p.send(); }
    public void sendByLoudspeaker(Loudspeaker l) { l.send(); }
    // 每多一种通信方式,就要加一个新方法……
}

太傻了。每一种"能发送消息的东西"都要写一个单独的方法。要加烽火台?又写一个 sendByBeacon()

有继承之后,一切都变了:

java
// DispatchCenter.java — 利用多态
public class DispatchCenter {
    public void dispatch(MessageSender sender) {   // 👈 参数类型是父类
        sender.send();                              // 👈 实际调用子类的方法
    }
}

语言:Java 17+ 如何运行

java
public class Main {
    public static void main(String[] args) {
        DispatchCenter center = new DispatchCenter();

        MessageSender h = new Horse("敌军来袭", "张三");
        MessageSender p = new Pigeon("天气转晴", 2);
        MessageSender l = new Loudspeaker("今晚开村民大会");

        center.dispatch(h);   // 👈 调用 Horse 的 send()
        center.dispatch(p);   // 👈 调用 Pigeon 的 send()
        center.dispatch(l);   // 👈 调用 Loudspeaker 的 send()
    }
}

预期输出(略去时间戳):

消息: 敌军来袭
骑士 张三 策马奔腾!
消息: 天气转晴
🕊️ 加急! 信鸽起飞!
消息: 今晚开村民大会
📢 大喇叭响起!

你试试:加一个 Beacon 类继承 MessageSender——只需要写新类,DispatchCenter 一行不改

这就是多态——"多种形态"。关键理解:

  1. 编译时看左边MessageSender h = new Horse(...)。编译器知道 hMessageSender 类型,所以只允许你调用 MessageSender 上定义的方法。
  2. 运行时看右边:当 h.send() 执行时,JVM 一看 h 实际指向 Horse 对象——它调的是 Horse.send(),不是 MessageSender.send()

这叫动态绑定。程序里不需要 if (类型判断) { 转成某类再调用 }——编译器做的就是这个判断,只不过在运行时刻。

C++ 窗口

C++ 的默认行为不一样!没有 virtual 关键字时,C++ 做的是"编译时绑定"(静态绑定):

cpp
MessageSender* h = new Horse("敌军");
h->send();  // 🙅 调的是 MessageSender::send(),不是 Horse::send()!

要让 C++ 做动态绑定,父类方法必须声明为 virtual

cpp
class MessageSender {
public:
    virtual void send() { ... }   // 👈 virtual
};

Java 反之——Java 的方法默认就是 virtual。所有非 private、非 static、非 final 的方法天然可被重写。这是 Java 的设计选择:简单、多态友好,但需要你小心设计继承体系。

Python 窗口

Python 没有编译时检查,所有方法天然可重写,所以多态是无缝的:

python
def dispatch(sender):    # 不需要类型注解
    sender.send()        # 鸭子类型——你只要会 quack 就是鸭子

Python 程序员常说"鸭子类型"(duck typing):如果它走起来像鸭子、叫起来像鸭子,它就是鸭子。传入的对象不需要继承同一个父类——只要它有 send() 方法就行。

第三幕:instanceof——我得确认一下

你在调度中心里还要处理一些特殊情况。比如信鸽要加急收费,大喇叭要控制音量,马要登记骑士姓名。

但这些特殊操作只对某个子类有效:

java
public class DispatchCenter {
    public void dispatch(MessageSender sender) {
        sender.send();        // 公共逻辑

        // 🤔 如果 sender 是 Horse,需要登记骑士
        if (sender instanceof Horse) {
            Horse horse = (Horse) sender;          // 👈 强制向下转型
            System.out.println("登记骑士: " + horse.getRiderName());
        }
    }
}

语言:Java 17+ 预期输出:只有传入 Horse 对象时,才会打印"登记骑士:……"

instanceof 是一个二元操作符——对象 instanceof 类型——返回 truefalse。它回答:"这个对象的实际类型是 Horse 吗?"

向下转型(从 MessageSender 转成 Horse)需要强制类型转换,就像往小瓶子里倒水一样——你必须确认瓶子够大(实际类型匹配),否则会抛出 ClassCastException

Java 16 引入了更简洁的写法——模式匹配 instanceof

java
// Java 16+
if (sender instanceof Horse horse) {            // 👈 匹配成功,horse 自动就绪
    System.out.println("登记骑士: " + horse.getRiderName());
}

语言:Java 16+ 效果:等价于传统的 instanceof + 强制类型转换,但省去了一行变量声明,且变量作用域限定在 if 块内

常见陷阱

陷阱一:继承滥用——"我是你的子类"不等于"我是你的"

新手最大的坑:为了复用方法而继承。

java
// 🙅 错误示范
public class Dog extends ArrayList<Toy> {   // 狗是一堆玩具???
    public void bark() { System.out.println("汪!"); }
}

狗有玩具,但狗不是玩具的列表。继承表达的是 "is-a"(是一个)关系,不是 "has-a"(有一个)关系。

正确做法是用组合(composition):

java
public class Dog {
    private List<Toy> toys = new ArrayList<>();  // 👈 狗有玩具
    public void bark() { System.out.println("汪!"); }
}

组合好过继承?是的。设计模式里有一句经典 ——"Favor composition over inheritance"(优先使用组合而不是继承)。你以后会越来越理解这句话。

陷阱二:深继承树——你的代码变成了干层饼

java
class A extends B extends C extends D extends E { ... }

五层继承。你打开 AsomeMethod(),读不懂。去 B 里找,没有。去 C 里找,有一点——被 D 重写了?哦等等,E 也定义了一个。

深继承就是干层饼。改一层可能影响所有下层。一个方法在多层中被重写、被覆盖、被调用父类版本——你永远不确定它在执行哪个版本。

最佳实践:继承树深度不超过 3 层。如果超过,考虑用接口 + 组合替代。

陷阱三:方法重写 vs 方法重载——一字之差

特性方法重写 (Override)方法重载 (Overload)
什么时候子类重新定义父类方法同一个类里多个同名方法
参数列表完全相同必须不同
关键字@Override无关键字
绑定时间运行时(动态绑定)编译时
java
// Horse.java
@Override
public void send() { ... }          // ✅ 重写——参数、方法名、返回类型完全一样

public void send(String priority) { ... }  // 这是重载——参数不同,但不是重写!

一个隐蔽的坑:你想重写,但方法签名写错了(比如少了一个参数),没有 @Override 编译器也不会报错——它认为你在定义新方法。所以永远加 @Override

陷阱四:super 链——构造方法要按顺序来

java
public class Loudspeaker extends MessageSender {
    public Loudspeaker(String message) {
        // 如果不写 super(message),编译器会隐式调用 super() —— 但 MessageSender 没有无参构造器
        // 编译错误!
    }
}

规则:子类构造方法必须在第一行调用父类构造方法。如果你不写,编译器会插入 super()(调用父类无参构造)。如果父类没有无参构造,编译报错。

链式调用顺序:当你 new Horse(...) 时,实际调用链是:

  1. MessageSender 的构造器先执行
  2. 然后 Horse 的构造器再执行

这叫构造链——从最顶层的父类一路往下。


步入高阶:从本节起进入"继承的深化"——抽象类和接口。如果你觉得前半章的继承复用和多态已掌握,可以继续;否则建议回头巩固前四幕再读。

🏔 深入冒险

第四幕:抽象类——故事讲一半

你现在发现了 MessageSender 有一个问题。

它的 send() 方法有一个默认实现——打印时间戳和消息。但你真的想让用户直接 new MessageSender("blah") 吗?一个"消息发送器"的具体形式应该是马、鸽子或喇叭,而不是一个什么都不是的通用发送器。

而且,万一有人继承 MessageSender 但忘了重写 send()——那就只能用父类的默认实现,完全不包含子类特色。这不是你想要的。

解决方案:抽象类

java
// AbstractMessageSender.java
public abstract class AbstractMessageSender {      // 👈 abstract 关键字
    protected String message;
    protected long timestamp;

    public AbstractMessageSender(String message) {
        this.message = message;
        this.timestamp = System.currentTimeMillis();
    }

    // 抽象方法——没有方法体,子类必须实现
    public abstract void send();

    // 具体方法——子类可以继承,也可以重写
    public void logTime() {
        System.out.println("[LOG] " + timestamp);
    }
}

语言:Java 17+ 你试试:尝试 new AbstractMessageSender("hello")——编译器会告诉你出错,因为抽象类不能实例化

做了两个改变:

  1. 类声明加上了 abstract——不能 new
  2. send() 方法去掉了方法体,只留签名,加上 abstract——子类必须实现

现在如果子类忘记重写 send()

java
// 编译错误:Pigeon 不是抽象的,且没有实现 AbstractMessageSender 中的 send()
public class Pigeon extends AbstractMessageSender {
    public Pigeon(String message) {
        super(message);
    }
    // 😅 忘了重写 send()
}

语言:Java 17+ 预期结果:编译报错——"Pigeon is not abstract and does not override abstract method send()"

抽象类是"讲故事讲一半"——"我有一个 send 方法,但怎么做具体发送——你(子类)自己定。"

这是继承的进阶:父类不再提供默认实现,而是定义一种契约——"继承我的人,必须有这个方法"。

第五幕:接口——纯契约

抽象类身上还有一个矛盾。

AbstractMessageSender 是一个很好的"发送消息"的契约。但你想想——如果有一天,村口需要一个"侦察兵"也能发消息,但侦察兵已经从 Soldier 那里继承了。

Java 不支持多继承。一个类只能 extends 一个父类。那么 Scout 类就没办法再 extends AbstractMessageSender了。

那能不能把"能发送消息"这个能力从"类"中独立出来,变成一种纯契约——任何类,不管它的父类是谁,只要遵守这个契约,就拥有这个能力?

接口 (interface) 就是干这个的。

java
// Sendable.java — 接口定义
public interface Sendable {
    void send();            // 👈 抽象方法,默认 public abstract
}

语言:Java 17+

任何类都可以实现这个接口:

java
public class Pigeon extends AbstractMessageSender implements Sendable {
    // 已经继承了 AbstractMessageSender,再实现 Sendable
    public Pigeon(String message) {
        super(message);
    }

    @Override
    public void send() {
        logTime();
        System.out.println("🕊️ 信鸽起飞!消息: " + message);
    }
}

Pigeon 同时是 AbstractMessageSender(继承)和 Sendable(实现契约)。它只能有一个父类,但可以有多个接口

java
public class Scout extends Soldier implements Sendable, Receivable, Trackable {
    // ...
}

语言:Java 17+

这就是接口的强大之处——抽象了"能做什么",而不是"是什么"

Python 窗口

Python 没有内置的接口概念,但可以用 abc 模块创建抽象基类:

python
from abc import ABC, abstractmethod

class Sendable(ABC):
    @abstractmethod
    def send(self):
        pass

class Pigeon(Sendable):      # Python 没有"implements"关键字
    def send(self):
        print("🕊️ 信鸽起飞!")

更偏 Pythonic 的方式是直接用鸭子类型——不强制继承,只要对象有 send() 方法就行。 这也反映了静态类型语言(Java)和动态类型语言(Python)在设计哲学上的差异。

C++ 窗口

C++ 没有 "interface" 关键字。纯虚类(pure virtual class)就是接口:

cpp
class Sendable {
public:
    virtual void send() = 0;   // 👈 = 0 表示纯虚函数
    virtual ~Sendable() = default;
};

class Pigeon : public AbstractMessageSender, public Sendable {
public:
    void send() override { ... }
};

Java 和 C++ 在接口设计上部分相似,但因为 C++ 支持多继承,其区分没有 Java 这么严格。

Java 8+ 接口的新能力

接口曾经只有抽象方法。Java 8 之后,接口可以包含:

java
public interface Sendable {
    void send();                        // 抽象方法(必须实现)

    default void logTime() {            // 默认方法——接口里也可以有实现了
        System.out.println("[LOG] " + System.currentTimeMillis());
    }

    static boolean isValidMessage(String msg) {  // 静态方法
        return msg != null && !msg.isEmpty();
    }
}

语言:Java 8+ 你试试:创建一个类实现 Sendable,但不重写 logTime()——看看是否能直接调用默认方法

默认方法解决了"接口升级时所有实现类都要改"的问题。但小心——如果你实现的两个接口有同名默认方法,编译器会报冲突。

🧭 前置预告Object 类的 equals()/hashCode() 以及类型转换异常(ClassCastException)的完整深入, 将在集合和泛型章节(第10章、第12章)之前系统讲解。本节只做概念引入。

第六幕:Object 类——所有类的根

你写的每一个类,即使没有显式写 extends,都隐含地继承了 java.lang.Object

java
public class Horse { ... }
// 实际上等价于:
public class Horse extends java.lang.Object { ... }

Object 类提供了每个 Java 对象都有的几个方法:

java
Object obj = new Horse("敌军", "张三");

obj.toString();             // 👈 "Horse@6d06d69c" —— 类名 + 哈希码
obj.equals(another);         // 👈 默认比较引用(==),通常需要重写
obj.hashCode();              // 👈 和 equals 一起重写,以后会讲
obj.getClass();              // 👈 运行时获取实际类型

这就是为什么你可以把任意对象传给 System.out.println()——它内部调用的就是 toString()

java
System.out.println(new Horse("敌军", "张三"));
// 默认输出:Horse@6d06d69c(如果你没重写 toString)

重写 toString()

java
@Override
public String toString() {
    return "Horse{" + "message='" + message + "\'" + "}";
}

语言:Java 17+ 预期输出Horse{message='敌军来袭'}你试试:所有类都重写 toString(),这样调试时打印对象状态一目了然

常见陷阱

真实故事:生产环境的 ClassCastException

有次我在生产修复一个 bug,写了这样的代码:

java
List<MessageSender> senders = getSendersFromConfig();
for (MessageSender s : senders) {
    if (s instanceof Horse) {
        ((Horse) s).assignRider("张三");
    }
    s.send();
}

看起来没问题。但 getSendersFromConfig() 重构后可能返回了一种新的 MailCoach 类型——它也继承 MessageSenderinstanceof Horse 判断为 false,正常跳过;但后来有人改了代码逻辑,忘了在 instanceof 里加新类型判断。Bug 就潜伏了。

教训instanceof + 强制向下转型是脆弱的。加一个新类型就要修改所有 instanceof 检查分支——容易遗漏。更好的方法是:如果子类特有行为必须在父类方法之外处理,考虑在父类中加一个通用钩子方法

java
public abstract class AbstractMessageSender {
    // 钩子方法——默认什么都不做
    public void onBeforeSend() {}

    public final void send() {     // 👈 final 防止子类重写整个流程
        onBeforeSend();            // 子类可以选择重写这个钩子
        doSend();                  // 子类必须实现
    }

    protected abstract void doSend();
}

语言:Java 17+

这叫模板方法模式——父类定义算法骨架,子类填充具体步骤。你以后在框架代码里会大量遇到。

通关挑战

  • 🗡 热身(10 分钟,必做)
  1. 打开你的 IDE,创建一个 Animal 抽象类,包含 abstract void makeSound() 方法。创建 DogCat 继承它。再写一个 Zoo 类接收 Animal 参数并调用 makeSound()——这就是多态。
  2. 在你的类上重写 toString()equals() 方法。
  3. 试试把 Animal 的某个方法加上 final——看看子类还能重写它吗?
  • 挑战(30 分钟,选做)
  1. 设计一个支付系统:创建一个 Payable 接口,包含 void pay(double amount) 方法。然后创建 CreditCardAlipayWeChat 三个类实现它。最后写一个 ShoppingCart 类,接收 Payable 参数——让支付方式可插拔。
  2. 实例判断练习:写一个方法,接收 Object 参数,用 instanceof 判断它的实际类型并打印不同信息。然后改用模式匹配 instanceof 重写。

验收标准

  • 你能解释 extendsimplements 的区别:一个继承实现,一个遵守契约
  • 你能说出 super 的两种用途:调用父类构造器、调用被重写的父类方法
  • 你知道为什么多态能写出"可扩展"的代码——新的子类不需要改动调用方
  • 你能说清抽象类和接口的核心区别:抽象类可以部分实现,接口是纯契约(Java 8 后增加了默认方法)
  • 你知道 Object 是所有类的根类,以及 toString()equals()hashCode() 这三个基本方法

常见卡点

"我应该在什么时候用抽象类,什么时候用接口?" 经验法则:如果两个类共享状态(字段)和部分实现 → 抽象类。如果只是定义能力契约 → 接口。Java 8 的默认方法模糊了这个边界,但核心区别没变——抽象类可以有构造器和状态,接口不能。

"final 关键字在继承中怎么用?" 三处:final class 不能被继承(如 String);final 方法不能被子类重写;final 变量只能赋值一次。

"为什么说继承破坏了封装?" 子类依赖父类的内部实现。父类修改了一个 protected 字段的名字或含义,子类可能就炸了。所以设计继承体系时要精心选择 protected 暴露的接口——或者干脆用组合。

现在不需要理解

  • 菱形问题(多继承时的二义性)——Java 单继承自然地规避了它,C++ 和 Python 开发者需要面对
  • equals()hashCode() 的协定——集合那一章会彻底讲清
  • dynamic dispatch(虚方法表)的 JVM 实现细节——vol 3 讲

旅人笔记

继承消除重复,多态让调用方不依赖具体类型。 抽象类说"我有骨架,你来填",接口说"我只要你遵守契约"。 所有类都是 Object 的孩子。

下一站预告

继承和多态让你设计出了灵活的通信系统。但你现在只能通过创建类来传递行为——太重了。下一站(Lambda 与高阶函数),你会学到一个新技巧:把行为本身当成参数传来传去,就像传递消息对象一样传递函数。老陈师傅说,那才是真正的"编程思想"。

Built with VitePress | Software Systems Atlas