元数据卡
- 前置知识:第7章(类与对象、封装)
- 预计时间:45 分钟
- 核心难度:
- 阅读模式: 高度专注
- 完成标志:能用继承消除重复代码,能用多态写出可扩展的设计;理解接口和抽象类的各自用途
本章分层
- 必读:继承的两大目的——代码复用(
extends)与多态替换(super+ 方法重写)、多态的动态绑定机制- 选读:
instanceof与向下转型、模板方法设计模式- 深水区:抽象类的"骨架+填空"设计、接口 vs 抽象类的选择、
Object类的方法本章不会要求你掌握
- 类型转换异常的完整处理——留到集合和泛型之前深入
- 接口默认方法的多继承冲突(Java 8+ 高级话题)
你在哪
窗外传来阿花试喇叭的喊话声,你手里的活停了下来。老陈师傅走过来拍了拍你的肩膀。
你在变量村的铁匠铺里蹲了一个星期了。老陈师傅教了你如何把属性(字段)和行为(方法)打包成"类"这种工具箱,还教会了你用 private 锁住关键零件——你已经有封装的概念了。
但这两天你在做一个麻烦的活儿。
村口需要一套"通信系统":驿站跑马传信、信鸽从空中送信、村头大喇叭喊话。嗯,你手上已经有了几个类——Horse、Pigeon、Loudspeaker。每个类都有自己的 send() 方法,但代码里面有大量重复的东西:
- 都有"消息内容"
- 都要"格式化"
- 都要"记录发送时间"
你复制粘贴了三次,现在 Horse 里改了一行,得去 Pigeon 和 Loudspeaker 里同样改一遍。你已经忘了昨晚改了哪个没改哪个。
"你这孩子,"老陈师傅叹了口气,拿起一块铁胚,"不学会站在别人的基础上造东西,你一辈子都在打一模一样的锄头。"
他把那块铁胚放在你面前:"这叫继承。"
你的任务
旧办法:复制粘贴做三个相似的类。问题在于——改一处要改三处,加一处要加三处,一个 bug 可以潜伏两份你没改的副本里。
这一章要解决的问题很简单:能不能只写一次公共逻辑,让其他类型说"我继承它"?
听起来像复制粘贴的自动化——但远比这深。继承的背后有一个叫"多态"的东西,它让你写出"只依赖抽象、不依赖具体"的代码。那才是真正让你从打铁的变成造桥的飞跃。
看完整章,你不仅能消除重复代码,还能写出能轻松扩展的灵活设计。
遭遇战 → 获得技能
第一幕:extends——站在前人的肩膀上
我们先从最直觉的场景入手。
你有三个类:Horse、Pigeon、Loudspeaker,它们都共享"消息"这个公共概念。你把公共部分抽出来,放到一个叫 MessageSender 的类里:
// 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:
// 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() 调用父类被重写前的方法。
信鸽也是继承,但实现不一样:
// 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 窗口
pythonclass 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++ 窗口
cppclass 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()。
先别用继承,看看老办法有多痛苦:
// 没有多态的情况
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()。
有继承之后,一切都变了:
// DispatchCenter.java — 利用多态
public class DispatchCenter {
public void dispatch(MessageSender sender) { // 👈 参数类型是父类
sender.send(); // 👈 实际调用子类的方法
}
}语言:Java 17+ 如何运行:
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 一行不改
这就是多态——"多种形态"。关键理解:
- 编译时看左边:
MessageSender h = new Horse(...)。编译器知道h是MessageSender类型,所以只允许你调用MessageSender上定义的方法。 - 运行时看右边:当
h.send()执行时,JVM 一看h实际指向Horse对象——它调的是Horse.send(),不是MessageSender.send()。
这叫动态绑定。程序里不需要 if (类型判断) { 转成某类再调用 }——编译器做的就是这个判断,只不过在运行时刻。
C++ 窗口
C++ 的默认行为不一样!没有
virtual关键字时,C++ 做的是"编译时绑定"(静态绑定):cppMessageSender* h = new Horse("敌军"); h->send(); // 🙅 调的是 MessageSender::send(),不是 Horse::send()!要让 C++ 做动态绑定,父类方法必须声明为
virtual:cppclass MessageSender { public: virtual void send() { ... } // 👈 virtual };Java 反之——Java 的方法默认就是 virtual。所有非
private、非static、非final的方法天然可被重写。这是 Java 的设计选择:简单、多态友好,但需要你小心设计继承体系。
Python 窗口
Python 没有编译时检查,所有方法天然可重写,所以多态是无缝的:
pythondef dispatch(sender): # 不需要类型注解 sender.send() # 鸭子类型——你只要会 quack 就是鸭子Python 程序员常说"鸭子类型"(duck typing):如果它走起来像鸭子、叫起来像鸭子,它就是鸭子。传入的对象不需要继承同一个父类——只要它有
send()方法就行。
第三幕:instanceof——我得确认一下
你在调度中心里还要处理一些特殊情况。比如信鸽要加急收费,大喇叭要控制音量,马要登记骑士姓名。
但这些特殊操作只对某个子类有效:
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 类型——返回 true 或 false。它回答:"这个对象的实际类型是 Horse 吗?"
向下转型(从 MessageSender 转成 Horse)需要强制类型转换,就像往小瓶子里倒水一样——你必须确认瓶子够大(实际类型匹配),否则会抛出 ClassCastException。
Java 16 引入了更简洁的写法——模式匹配 instanceof:
// Java 16+
if (sender instanceof Horse horse) { // 👈 匹配成功,horse 自动就绪
System.out.println("登记骑士: " + horse.getRiderName());
}语言:Java 16+ 效果:等价于传统的 instanceof + 强制类型转换,但省去了一行变量声明,且变量作用域限定在 if 块内
常见陷阱
陷阱一:继承滥用——"我是你的子类"不等于"我是你的"
新手最大的坑:为了复用方法而继承。
// 🙅 错误示范
public class Dog extends ArrayList<Toy> { // 狗是一堆玩具???
public void bark() { System.out.println("汪!"); }
}狗有玩具,但狗不是玩具的列表。继承表达的是 "is-a"(是一个)关系,不是 "has-a"(有一个)关系。
正确做法是用组合(composition):
public class Dog {
private List<Toy> toys = new ArrayList<>(); // 👈 狗有玩具
public void bark() { System.out.println("汪!"); }
}组合好过继承?是的。设计模式里有一句经典 ——"Favor composition over inheritance"(优先使用组合而不是继承)。你以后会越来越理解这句话。
陷阱二:深继承树——你的代码变成了干层饼
class A extends B extends C extends D extends E { ... }五层继承。你打开 A 的 someMethod(),读不懂。去 B 里找,没有。去 C 里找,有一点——被 D 重写了?哦等等,E 也定义了一个。
深继承就是干层饼。改一层可能影响所有下层。一个方法在多层中被重写、被覆盖、被调用父类版本——你永远不确定它在执行哪个版本。
最佳实践:继承树深度不超过 3 层。如果超过,考虑用接口 + 组合替代。
陷阱三:方法重写 vs 方法重载——一字之差
| 特性 | 方法重写 (Override) | 方法重载 (Overload) |
|---|---|---|
| 什么时候 | 子类重新定义父类方法 | 同一个类里多个同名方法 |
| 参数列表 | 完全相同 | 必须不同 |
| 关键字 | @Override | 无关键字 |
| 绑定时间 | 运行时(动态绑定) | 编译时 |
// Horse.java
@Override
public void send() { ... } // ✅ 重写——参数、方法名、返回类型完全一样
public void send(String priority) { ... } // 这是重载——参数不同,但不是重写!一个隐蔽的坑:你想重写,但方法签名写错了(比如少了一个参数),没有 @Override 编译器也不会报错——它认为你在定义新方法。所以永远加 @Override。
陷阱四:super 链——构造方法要按顺序来
public class Loudspeaker extends MessageSender {
public Loudspeaker(String message) {
// 如果不写 super(message),编译器会隐式调用 super() —— 但 MessageSender 没有无参构造器
// 编译错误!
}
}规则:子类构造方法必须在第一行调用父类构造方法。如果你不写,编译器会插入 super()(调用父类无参构造)。如果父类没有无参构造,编译报错。
链式调用顺序:当你 new Horse(...) 时,实际调用链是:
MessageSender的构造器先执行- 然后
Horse的构造器再执行
这叫构造链——从最顶层的父类一路往下。
步入高阶:从本节起进入"继承的深化"——抽象类和接口。如果你觉得前半章的继承复用和多态已掌握,可以继续;否则建议回头巩固前四幕再读。
🏔 深入冒险
第四幕:抽象类——故事讲一半
你现在发现了 MessageSender 有一个问题。
它的 send() 方法有一个默认实现——打印时间戳和消息。但你真的想让用户直接 new MessageSender("blah") 吗?一个"消息发送器"的具体形式应该是马、鸽子或喇叭,而不是一个什么都不是的通用发送器。
而且,万一有人继承 MessageSender 但忘了重写 send()——那就只能用父类的默认实现,完全不包含子类特色。这不是你想要的。
解决方案:抽象类。
// 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")——编译器会告诉你出错,因为抽象类不能实例化
做了两个改变:
- 类声明加上了
abstract——不能new了 send()方法去掉了方法体,只留签名,加上abstract——子类必须实现
现在如果子类忘记重写 send():
// 编译错误: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) 就是干这个的。
// Sendable.java — 接口定义
public interface Sendable {
void send(); // 👈 抽象方法,默认 public abstract
}语言:Java 17+
任何类都可以实现这个接口:
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(实现契约)。它只能有一个父类,但可以有多个接口:
public class Scout extends Soldier implements Sendable, Receivable, Trackable {
// ...
}语言:Java 17+
这就是接口的强大之处——抽象了"能做什么",而不是"是什么"。
Python 窗口
Python 没有内置的接口概念,但可以用
abc模块创建抽象基类:pythonfrom 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)就是接口:
cppclass 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 之后,接口可以包含:
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。
public class Horse { ... }
// 实际上等价于:
public class Horse extends java.lang.Object { ... }Object 类提供了每个 Java 对象都有的几个方法:
Object obj = new Horse("敌军", "张三");
obj.toString(); // 👈 "Horse@6d06d69c" —— 类名 + 哈希码
obj.equals(another); // 👈 默认比较引用(==),通常需要重写
obj.hashCode(); // 👈 和 equals 一起重写,以后会讲
obj.getClass(); // 👈 运行时获取实际类型这就是为什么你可以把任意对象传给 System.out.println()——它内部调用的就是 toString():
System.out.println(new Horse("敌军", "张三"));
// 默认输出:Horse@6d06d69c(如果你没重写 toString)重写 toString():
@Override
public String toString() {
return "Horse{" + "message='" + message + "\'" + "}";
}语言:Java 17+ 预期输出:Horse{message='敌军来袭'}你试试:所有类都重写 toString(),这样调试时打印对象状态一目了然
常见陷阱
真实故事:生产环境的 ClassCastException
有次我在生产修复一个 bug,写了这样的代码:
List<MessageSender> senders = getSendersFromConfig();
for (MessageSender s : senders) {
if (s instanceof Horse) {
((Horse) s).assignRider("张三");
}
s.send();
}看起来没问题。但 getSendersFromConfig() 重构后可能返回了一种新的 MailCoach 类型——它也继承 MessageSender。instanceof Horse 判断为 false,正常跳过;但后来有人改了代码逻辑,忘了在 instanceof 里加新类型判断。Bug 就潜伏了。
教训:instanceof + 强制向下转型是脆弱的。加一个新类型就要修改所有 instanceof 检查分支——容易遗漏。更好的方法是:如果子类特有行为必须在父类方法之外处理,考虑在父类中加一个通用钩子方法:
public abstract class AbstractMessageSender {
// 钩子方法——默认什么都不做
public void onBeforeSend() {}
public final void send() { // 👈 final 防止子类重写整个流程
onBeforeSend(); // 子类可以选择重写这个钩子
doSend(); // 子类必须实现
}
protected abstract void doSend();
}语言:Java 17+
这叫模板方法模式——父类定义算法骨架,子类填充具体步骤。你以后在框架代码里会大量遇到。
通关挑战
- 🗡 热身(10 分钟,必做)
- 打开你的 IDE,创建一个
Animal抽象类,包含abstract void makeSound()方法。创建Dog和Cat继承它。再写一个Zoo类接收Animal参数并调用makeSound()——这就是多态。 - 在你的类上重写
toString()和equals()方法。 - 试试把
Animal的某个方法加上final——看看子类还能重写它吗?
- 挑战(30 分钟,选做)
- 设计一个支付系统:创建一个
Payable接口,包含void pay(double amount)方法。然后创建CreditCard、Alipay、WeChat三个类实现它。最后写一个ShoppingCart类,接收Payable参数——让支付方式可插拔。 - 实例判断练习:写一个方法,接收
Object参数,用instanceof判断它的实际类型并打印不同信息。然后改用模式匹配instanceof重写。
验收标准
- 你能解释
extends和implements的区别:一个继承实现,一个遵守契约 - 你能说出
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 与高阶函数),你会学到一个新技巧:把行为本身当成参数传来传去,就像传递消息对象一样传递函数。老陈师傅说,那才是真正的"编程思想"。