适用读者:语法已通、但"为什么这么设计"说不清的人;准备一面 OOP 追问的人。
前置:S04-J07、B05(封装继承多态入门)。
覆盖旧文:2722–2738(教程第 21–37 章)+2966–2982(同题重复导出)→ 34 篇 / 约 40 万字符。
扩展点:E-S04-024(初始化的真实顺序:静态 → 实例 → 构造)、E-S04-025(重写/重载/隐藏的判定表)、E-S04-026(接口 vs 抽象类的选型判据)。
上一篇:S04-J07 · Java 语法速通:从变量到递归的 24 个坑
下一篇:补编第 3 篇 · 异常体系(待写)。
1. 一句话结论
OOP 的所有细节都归结为两条:成员"什么时候存在"(初始化顺序)和"调用谁"(静态绑定 vs 动态绑定)。 this/super、访问修饰符、重写、多态、内部类,都是这两条的推论。
出处:
A-2722–A-2738、A-2966–A-2982(类与对象、构造、继承、多态、接口、内部类)。
2. 初始化的真实顺序(E-S04-024)
class Parent {
static { System.out.println("1 父静态块"); }
{ System.out.println("3 父实例块"); }
Parent() { System.out.println("4 父构造"); }
}
class Child extends Parent {
static { System.out.println("2 子静态块"); }
{ System.out.println("5 子实例块"); }
Child() { super(); System.out.println("6 子构造"); }
public static void main(String[] a) { new Child(); }
}
输出:1 → 2 → 3 → 4 → 5 → 6
顺序口诀:父静态 → 子静态 →(父实例块 → 父构造)→(子实例块 → 子构造)。
| 追问 | 答 |
|---|---|
| 静态块何时执行 | 类首次主动使用时(new、静态成员访问、反射、子类初始化);只执行一次 |
| 实例块有什么用 | 抽公共初始化逻辑(多个构造器共享),等价于把代码复制进每个构造器 |
super() 在哪 |
编译器插在每个构造器的第一行(除非显式写 this(...)) |
| 字段初始化在哪一步 | 在实例块之前执行(源码顺序),字段声明顺序影响结果 |
坑 1(高频):父构造器里调用被子类重写的方法,会执行子类版本,此时子类字段还是默认值。
class P { P() { print(); } void print() { System.out.println("P"); } }
class C extends P {
int v = 42;
void print() { System.out.println("C v=" + v); } // 输出 "C v=0"
}
结论:构造器里不要调用可被重写的方法(Effective Java 第 19 条)。
3. this / super / 访问修饰符
| 关键字 | 用途 | 边界 |
|---|---|---|
this |
当前对象引用;this(...) 调本类其他构造器(必须第一行) |
静态方法里不能用 |
super |
父类引用;super(...)、super.method() |
只能一层,不能跨代 |
private |
类内可见 | 内部类可访问外部私有成员(编译器生成访问器) |
| 默认(包) | 同包可见 | — |
protected |
同包 + 子类(子类跨包只能访问自身继承来的成员) | 常见误解点 |
public |
全部可见 | — |
坑 2:protected 跨包时,子类不能通过"父类引用"访问——只能通过 this 或子类引用:
// 包 a: public class A { protected void m(){} }
// 包 b: class B extends A { void f(A a){ a.m(); } } // ❌ 编译失败
class B extends A { void f(){ this.m(); } } // ✅
4. 重写 / 重载 / 隐藏:判定表(E-S04-025)
| 现象 | 条件 | 绑定时机 | 例子 |
|---|---|---|---|
| 重载 Overload | 同名、参数列表不同(可同类或父子) | 编译期(静态) | f(int) / f(String) |
| 重写 Override | 子类同名同参数、返回值协变、访问权限不更严 | 运行期(动态) | toString() |
| 隐藏 Hiding | 静态方法同名同参 | 编译期(按引用类型) | static void f() |
| 字段 | 永远按引用类型编译期解析 | 编译期 | 父子同名 v |
class P { String v = "p"; void m() { System.out.println("p"); } }
class C extends P {
String v = "c";
void m() { System.out.println("c"); }
}
P x = new C();
System.out.println(x.v); // p —— 字段不参与多态
x.m(); // c —— 方法参与多态
判据:只有"实例方法"参与多态;字段、静态方法、私有方法都是编译期解析。
重写的三条硬规则:① 访问权限不能变小;② 不能抛更宽的受检异常;③ final/static/private 方法不能被重写。
5. 多态:靠什么实现
编译期:方法调用写成 invokevirtual + 方法符号引用
运行期:vtable(虚方法表)按对象实际类型查表 → 实际方法入口
| 追问 | 答 |
|---|---|
| 为什么需要多态 | 面向接口编程:调用方依赖抽象,实现可替换 |
| 性能代价 | 一次虚表查表 + 无法内联;热点代码 JIT 会做类型特化(去虚化) |
instanceof 与 getClass() |
前者含子类,后者精确类型 |
final 的作用 |
类不可继承 / 方法不可重写 / 变量不可改;用于设计意图与内联优化 |
坑 3:instanceof 对 null 返回 false(不抛异常);强转 (T) null 合法,但调用方法会 NullPointerException。
6. 抽象类 vs 接口(E-S04-026)
| 维度 | 抽象类 | 接口 |
|---|---|---|
| 实例字段 | ✅ 可以有状态 | ❌ 只有 static final 常量 |
| 构造器 | ✅ | ❌ |
| 方法实现 | ✅ 任意 | Java 8+ default/static;Java 9+ private |
| 多继承 | ❌ 单继承 | ✅ 多实现 |
| 语义 | "是什么"(is-a,共享实现) | "能做什么"(can-do,契约) |
选型判据:
① 需要保存状态或构造逻辑 → 抽象类
② 需要多方实现、跨继承树复用能力 → 接口
③ 要"演进而不破坏实现者" → 接口 + default 方法(Java 8+)
④ 需要模板方法控制流程 → 抽象类(模板方法模式)
default 方法的菱形冲突:两个接口都有同名 default → 实现类必须重写,或用 Interface.super.m() 指定。
7. 内部类四种与它们的内存含义
| 类型 | 写法 | 是否持有外部实例 | 关键点 |
|---|---|---|---|
| 成员内部类 | class Inner |
✅ | 需外部实例才能 new |
| 静态嵌套类 | static class Nested |
❌ | 优先选它,无隐式引用 |
| 局部内部类 | 方法内 class L |
✅ | 可访问 effectively final 局部变量 |
| 匿名内部类 | new Runnable(){...} |
✅ | Lambda 出现后多用于需要状态的场景 |
坑 4(内存泄漏):非静态内部类持有外部引用,把它交给长生命周期容器(线程池、缓存)会导致外部对象无法回收。静态嵌套类没有这个问题。
8. 反例:五个"看起来对"的错答
| 说法 | 问题 |
|---|---|
| "构造器会先初始化子类字段再调父构造" | 顺序相反,且父构造里调重写方法会看到默认值 |
| "字段也能多态" | 字段按引用类型编译期解析 |
"protected 就是都能访问" |
跨包时受"必须通过子类自身引用"限制 |
| "重载也是运行时决定" | 重载是编译期按静态类型选 |
| "内部类和静态嵌套类差不多" | 前者隐式持有外部引用,易泄漏 |
9. 自测
- 静态块、实例块、构造器的执行顺序?父与子如何交错?
- 为什么构造器里不要调用可重写方法?
- 字段与方法在"多态"上的差别?
- 抽象类与接口的选型四条判据?
- 非静态内部类为什么容易造成内存泄漏?
答案
- 父静态→子静态→父实例块→父构造→子实例块→子构造。
- 子类字段尚未初始化,重写方法读到默认值。
- 方法动态绑定(invokevirtual),字段编译期按引用类型。
- 需状态/构造→抽象类;多实现/跨树→接口;平滑演进→default;模板方法→抽象类。
- 隐式持有外部实例引用,被长生命周期对象持有时外部无法回收。
10. 下一篇
S04-J09 · 异常体系 —— 受检与非受检的边界、finally 何时不执行、try-with-resources 的抑制异常、以及"吞异常"的三种代价。