02.1 — 三大特性:封装、继承、多态
定位: 面向对象编程的三大支柱 — 管理代码复杂度的核心思想工具 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: 多态实现原理、继承的利与弊、封装的设计哲学
一、这是什么?为什么需要它?
面向对象 vs 面向过程
面向过程(C 风格):
cook(切菜())
cook(炒菜())
cook(装盘())
→ 数据和处理分离,函数操作全局数据
面向对象(Java 风格):
chef.切菜().炒菜().装盘()
→ 数据和处理聚合在对象中,对象之间协作
为什么面向对象更好?
当代码规模 > 1万行 → 面向过程的全局数据难以维护
面向对象通过"封装"隔离变化,通过"多态"应对扩展封装 — 隐藏实现,暴露接口
为什么需要封装?
java
// ❌ 没有封装
class BankAccount {
int balance = 0; // 可以直接修改!
}
account.balance = -100; // 余额可以为负数!
// ✅ 有封装
class BankAccount {
private int balance = 0; // 隐藏内部状态
public void deposit(int amount) {
if (amount <= 0) throw new IllegalArgumentException();
balance += amount;
}
public void withdraw(int amount) {
if (amount > balance) throw new InsufficientFundsException();
balance -= amount;
}
}封装的好处:
1. 保护不变性 — 阻止非法状态(负数余额)
2. 降低复杂度 — 调用方只需知道公开方法
3. 隔离变化 — 内部实现可随时修改,不影响外部继承 — 代码复用的双刃剑
继承解决的问题:is-a 关系下的代码复用
动物 ← 狗 (Dog is an Animal)
├── eat()
└── sleep()
狗继承 eat() 和 sleep(),只需实现 bark()
继承的代价:
1. 脆弱基类问题 — 修改父类可能破坏子类
2. 继承深度 > 3 层 — 认知负担骤增
3. 菱形问题 — 多继承冲突(Java 不允许类多继承)多态 — 同一接口,不同实现
java
// 没有多态
if (animal instanceof Dog) {
((Dog) animal).bark();
} else if (animal instanceof Cat) {
((Cat) animal).meow();
}
// 每加一种动物就要加 if-else!
// 有多态
animal.makeSound(); // 不管是什么动物,一句话搞定!
// 加新动物:新建类实现 makeSound(),无需修改已有代码!
// 符合"开闭原则":对扩展开放,对修改关闭二、原理拆解
2.1 多态的实现原理:虚方法表
java
class Animal {
void makeSound() { System.out.println("..."); }
}
class Dog extends Animal {
@Override
void makeSound() { System.out.println("Woof!"); }
}
class Cat extends Animal {
@Override
void makeSound() { System.out.println("Meow!"); }
}
Animal a = new Dog();
a.makeSound(); // 输出 Woof! ← 运行时决定编译时(静态类型:Animal):
a.makeSound()
JVM 检查 Animal 类是否有 makeSound() 方法
→ 有,编译通过
运行时(实际类型:Dog):
1. 从 a 的对象头中找到 Klass 指针
2. Klass 指向 Dog 的虚方法表 (vtable)
3. vtable 中 makeSound() → Dog 的实现
4. 执行 Dog 的 makeSound()
虚方法表结构:
┌────────────────┐
│ Dog vtable │
├────────────────┤
│ ... │
│ makeSound() ───┼──→ Dog::makeSound()
│ toString() ────┼──→ Object::toString()
│ hashCode() ────┼──→ Object::hashCode()
└────────────────┘
注意:
- 静态方法不存在 vtable(编译期绑定)
- final 方法不存在 vtable(不能被重写)
- private 方法不存在 vtable(隐式 final)2.2 静态分派 vs 动态分派
静态分派(重载 Overload)— 编译期决定:
void print(String s) { ... }
void print(int i) { ... }
print("hello"); → 编译期根据参数类型选 print(String)
print(42); → 编译期根据参数类型选 print(int)
动态分派(重写 Override)— 运行期决定:
Animal a = new Dog();
a.makeSound(); → 运行期根据实际类型选 Dog::makeSound()2.3 继承的设计原则:组合优于继承
优先使用组合,而不是继承 (Effective Java Item 18)
❌ 继承(脆弱):
class Stack<E> extends Vector<E> { ... }
// Stack 本应是"后进先出",但继承了 Vector 的所有方法
// 用户可以调用不合适的 Vector 方法!
✅ 组合(灵活):
class Stack<E> {
private Vector<E> vector = new Vector<>();
// 只暴露 push/pop,不暴露 Vector 的其他方法
}三、图解全景
┌─────────────────────────────────────────────────────────────┐
│ 多态的方法调用链路 │
│ │
│ 源码: a.makeSound() │
│ │ │
│ ▼ │
│ 编译期: 检查 a 的声明类型 Animal 是否有 makeSound() │
│ ✔ 有 → 编译通过 │
│ ✘ 无 → 编译错误 │
│ │ │
│ ▼ │
│ 运行期: 获取 a 的实际类型 Dog 的 vtable │
│ │ │
│ ▼ │
│ vtable[makeSound_offset] → Dog::makeSound() │
│ │ │
│ ▼ │
│ 执行 Dog 的 makeSound() 方法体 │
│ │
│ 性能影响:vtable 查找 ≈ 1-2 条指令,可忽略 │
│ 这就是为什么"面向接口编程"几乎没有性能代价 │
└─────────────────────────────────────────────────────────────┘四、实战验证
4.1 多态 vs 非多态对比
java
// 非多态版本:每增加一种动物需要修改代码
class Zoo {
void handleDog(Dog d) { d.bark(); }
void handleCat(Cat c) { c.meow(); }
// 加新动物需要加方法
}
// 多态版本:增加动物只需新建类
class Zoo {
void handleAnimal(Animal a) { a.makeSound(); }
// 永不修改!
}4.2 继承 vs 组合对比
java
// 继承
class HashSet<E> extends HashMap<E, Object> {
// 继承了 HashMap 的所有方法(包括不适用的)
}
// 组合
class InstrumentedSet<E> {
private final Set<E> set;
// 只暴露需要的接口
}五、面试视角
| 追问 | 答案要点 |
|---|---|
| 多态的实现原理是什么? | 虚方法表 (vtable),运行时从对象头找到 Klass,通过 vtable 定位方法 |
| 静态方法为什么不能被重写? | 静态方法编译期绑定到类,不存在 vtable 中 |
| 重载和重写的区别? | 重载 = 编译期静态分派(同名不同参);重写 = 运行期动态分派(子类替换父类) |
| 构造方法能否被重写? | 不能。构造方法名 = 类名,子类不可能有同名构造器 |
| 继承和组合怎么选? | is-a 用继承,has-a 用组合;不确定时优先组合 |
📚 相关链接
- **抽象类与接口** — 抽象类和接口如何支持多态
- **内部类与匿名类** — 内部类的特殊多态机制
- **Object类与通用方法** — equals/hashCode 与多态
- ← 返回 **OOP索引**