如何理解 Java 中的多态?
结论:多态(polymorphism)是"同一调用、不同行为":父类引用指向子类对象,调用被重写的方法时,运行期按对象的实际类型决定执行哪个实现。它需要三个条件——继承或实现、方法重写(override)、父类引用指向子类对象。JVM 通过**动态分派(dynamic dispatch)**查方法表完成,而重载(overload)是编译期静态分派,两者不要混淆。
1. 运行期如何找到正确的方法
Animal a = new Dog();
a.speak(); // 字节码:invokevirtual Animal.speak
a(栈上引用)──→ Dog 对象
│ 对象头 klass 指针
▼
Dog.class 的虚方法表(vtable)
┌─────────────────────────┐
│ speak() → Dog.speak │ ← 子类重写,槽位被替换
│ eat() → Animal.eat │ ← 未重写,沿用父类
└─────────────────────────┘
invokevirtual 的执行:取实际类 → 查 vtable 对应槽位 → 调用
vtable(虚方法表)在类加载准备阶段构建,每个类一份,槽位顺序与父类一致,重写只是替换槽位中的方法指针。因此动态分派是一次数组下标访问,开销很小。
2. 重写与重载的本质不同
| 维度 | 重载(overload) | 重写(override) |
|---|---|---|
| 判定依据 | 方法名相同、参数列表不同 | 子类方法签名与父类相同 |
| 分派时机 | 编译期(静态分派) | 运行期(动态分派) |
| 决定因素 | 变量的静态类型 + 参数类型 | 对象的实际类型 |
| 返回类型 | 可以不同 | 必须相同或协变(covariant) |
| 访问权限 | 无约束 | 不能比父类更严格 |
| 异常 | 无约束 | 不能抛出更宽的受检异常 |
3. 代码:三个条件与两个反例
class Animal {
String name = "animal"; // 字段
String speak() { return "..."; }
void eat() { System.out.println("animal eat"); }
static String kind() { return "animal"; } // 静态方法
}
class Dog extends Animal {
@Override String speak() { return "woof"; } // 重写
@Override void eat() { System.out.println("dog eat"); }
}
public class PolyDemo {
static void feed(Animal a) { a.eat(); } // 参数是父类引用
public static void main(String[] args) {
Animal a = new Dog(); // 向上转型
System.out.println(a.speak()); // woof,动态分派
feed(a); // dog eat
}
}
两个反例说明"多态不覆盖什么":
class Cat extends Animal {
String name = "cat"; // 隐藏父类字段
static String kind() { return "cat"; } // 隐藏父类静态方法
}
Animal a = new Cat();
System.out.println(a.name); // animal —— 字段按静态类型取,没有多态
System.out.println(a.kind()); // animal —— 静态方法按静态类型调用
private 方法、final 方法、static 方法都不参与动态分派;构造器也不是多态调用点。
4. 经典陷阱:构造器里调用可重写方法
class Parent {
Parent() { init(); } // 构造期调用可重写方法
void init() { System.out.println("parent init"); }
}
class Child extends Parent {
private final int value;
Child(int value) {
super(); // 先执行父类构造器 → 此时 value 还没赋值
this.value = value;
}
@Override void init() {
System.out.println("child init, value=" + value); // 输出 0
}
}
new Child(42); // child init, value=0
原因:父类构造器执行时子类字段仍是默认值(0),init() 却已经被分派到子类。这类 bug 在框架里很常见,规范做法是构造器只做自身初始化,不要调用可重写方法。
5. 常见追问
| 追问 | 回答要点 |
|---|---|
| 多态的好处是什么? | 面向接口编程、屏蔽实现差异、易于扩展(新增实现不改调用方) |
| 字段为什么没有多态? | 字段访问由编译期静态类型决定,字节码是 getfield 固定偏移 |
static 方法能被"重写"吗? | 不能,只能隐藏(hiding),调用看引用类型 |
| 向上转型与向下转型? | 向上自动、安全;向下需强转,失败抛 ClassCastException,可用 instanceof 先判断 |
| 接口多态和继承多态有区别吗? | 机制相同,invokeinterface 查接口方法表(itable) |
| 多态和反射的区别? | 多态是编译期就确定的合法调用,反射是运行期按名字查找,更灵活但更慢 |
| 为什么重载不能靠返回类型区分? | 调用 f() 时编译器无法从返回值推断选哪个重载,签名只含方法名与参数 |
小结
多态 = 继承/实现 + 重写 + 父类引用指向子类对象,运行期靠 vtable 动态分派。回答时务必区分重写的动态分派与重载的静态分派,再补上"字段、静态方法、private 方法没有多态"这个常见陷阱。