Skip to content

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索引**

Knowledge4J — Java 知识库