1241 字
约 4 分钟
3
S04-B05 · 封装、继承与多态:到底在解决什么问题
适用读者:背得出"三大特性"但说不出"多态靠什么实现"的人。
前置:S04-B02。
实测环境:OpenJDK 21.0.12.1(2026-11-05)。
覆盖旧文:A-1650(304) A-1652(202) A-1658(181) A-1778(182) = 869 点。
覆盖扩展:E-S04-006(虚方法表与动态分派)。
上一篇:S04-B04 · 数组、static 与初始化顺序
下一篇:S04-B06 · 接口、抽象类与 Lambda。
1. 一句话结论
三大特性各解决一个问题:封装解决"谁能改我的状态"、继承解决"复用与替换"、多态解决"调用方不关心具体实现"。
| 特性 | 解决的问题 | 代价 |
|---|---|---|
| 封装 | 状态被随意修改 | 需要设计接口与不变量 |
| 继承 | 代码重复 | 耦合(父类改动影响子类) |
| 多态 | 调用方与实现解耦 | 需要抽象层与动态分派 |
2. 实测:实例方法与静态方法的分派规则不同
class A { void f() { println("A.f"); } static void g() { println("A.g"); } }
class B extends A { @Override void f() { println("B.f"); } static void g() { println("B.g"); } }
A a = new B();
a.f(); // → B.f
A.g(); // → A.g
B.g(); // → B.g
实测输出:
B.f
A.g
B.g
| 调用 | 绑定时机 | 依据 |
|---|---|---|
实例方法 a.f() |
运行期(动态分派) | 对象的实际类型(B) |
静态方法 A.g() |
编译期(静态分派) | 引用的声明类型(A) |
| 字段访问 | 编译期 | 同上(字段没有多态) |
这张表能解释三个经典 bug:
| bug | 原因 |
|---|---|
| 父类引用调静态方法,执行的是父类的 | 静态方法不参与多态 |
| 父类引用读字段,拿到父类的值 | 字段访问按声明类型 |
@Override 拼错没报错 |
加了注解编译器才会检查 |
3. 多态靠什么实现(E-S04-006)
JVM 为每个类维护方法表(虚方法表):实例方法在表里有槽位,子类重写就替换槽位中的实现指针。
调用 a.f()
① 取 a 的实际类型(B)
② 在 B 的方法表里查 f 的槽位
③ 跳到该槽位的实现
这解释了:
| 现象 | 原因 |
|---|---|
| 动态分派有微小开销 | 一次间接跳转 |
final/private/static 方法不走方法表 |
编译期就能确定,可内联 |
| 接口调用可能更慢(历史上) | 需要接口方法表查找(现代 JVM 有优化) |
4. 重写 vs 重载(A-1778)
| 项 | 重写 Override | 重载 Overload |
|---|---|---|
| 发生位置 | 子类 vs 父类 | 同一个类 |
| 签名 | 必须相同 | 参数列表不同 |
| 绑定 | 运行期 | 编译期 |
| 返回值 | 相同或协变 | 可不同 |
| 异常 | 不能抛出更宽的受检异常 | 无限制 |
重载的坑:f(null) 有多个重载时,编译器选"最具体"的那个,可能不是你以为的。
void f(Object o) {}
void f(String s) {}
f(null); // 选 f(String)——更具体
5. 继承的三个使用判据
| 判据 | 说明 |
|---|---|
| 是不是 is-a | "狗是动物" → 继承;"订单有一个地址" → 组合 |
| 父类是否稳定 | 父类频繁改动会波及所有子类 |
| 是否需要多态替换 | 需要 → 抽象类/接口;不需要 → 组合 |
"优先组合而非继承" 的理由:继承把子类绑定到父类的实现细节(脆弱基类问题)。
6. 五个误区
| 误区 | 真相 |
|---|---|
| "多态对字段也生效" | 字段按声明类型访问 |
| "静态方法能被重写" | 只是隐藏(编译期绑定) |
| "重载是运行期决定" | 编译期按参数类型选 |
| "继承越多越好复用" | 增加耦合,优先组合 |
"@Override 只是装饰" |
它让编译器帮你检查签名 |
7. 自检
- 实例方法与静态方法的分派时机有什么不同?
- 为什么"字段没有多态"?
- 重载为什么是编译期绑定?
f(null)的坑是什么? - 方法表如何解释"动态分派有开销"?
- 什么时候该用组合而不是继承?
8. 下一篇
S04-B06 · 接口、抽象类、Lambda 与枚举
多态讲完,下一篇讲"抽象层的三种形态":接口、抽象类、函数式接口——以及它们各自适合的场景。
本篇属于系列《S04 · Java 语言核心》。旧文来源:A-1650/1652/1658/1778;扩展点 E-S04-006。分派行为为 OpenJDK 21 实测。
S04-B05 · 封装、继承与多态:到底在解决什么问题
https://www.clxhxhhr.top/posts/5220/ 评论
0 条
还没有评论,先写一条吧。