多态是 Java 和其他面向对象语言里的核心特性之一,通俗地说,它让“同一个方法调用,在不同对象上表现不同”。
🧠 一句话定义多态
多态 = 父类引用指向子类对象 + 方法调用在运行时动态绑定
✅ 多态的三大必要条件
-
有继承(父类 / 接口 + 子类)
-
有方法重写(override)
-
父类引用指向子类对象
🐾 举个最简单的例子
class Animal {
public void speak() {
System.out.println("Animal speaks");
}
}
class Dog extends Animal {
@Override
public void speak() {
System.out.println("Dog barks");
}
}
Animal a = new Dog();
a.speak(); // 输出:Dog barks(而不是 Animal speaks)
编译时:
a
的类型是Animal
运行时:a
实际引用的是Dog
,调用的是Dog.speak()
👉 这就是 运行时多态(动态绑定)
🔬 多态的底层实现原理(深入 JVM)
1. 编译阶段:方法是根据“引用类型”来检查
-
编译器看到
a.speak()
,只要Animal
中有speak()
,就编译通过; -
不会理会实际对象是
Dog
还是Cat
。
2. 运行阶段:方法调用由 JVM 的“动态绑定机制”决定
-
JVM 运行时会根据
a
的实际类型(此处是Dog
),去查找Dog
中是否重写了speak()
; -
有的话就调用子类方法。
🧰 JVM 怎么实现多态?靠的是 虚方法表(vtable)
Java 中所有“非 static、非 final 的方法”都是虚方法。JVM 会为每个类维护一个 虚方法表(vtable),这个表记录了每个方法在内存中的实际地址。
📦 举个比喻:
方法名 | Animal 的 vtable | Dog 的 vtable(继承自 Animal) |
---|---|---|
speak() | Animal.speak() | Dog.speak()(覆盖了父类) |
walk() | Animal.walk() | Dog.walk()(如果有重写) |
🧱 调用过程:
-
JVM 在运行时通过对象引用,找到该对象的实际类型;
-
查到这个类的
vtable
; -
根据方法名找到对应的方法地址;
-
调用!
所以 a.speak()
实际上底层变成了:
-> 找到 a 对象实际类型是 Dog
-> 查找 Dog 的 vtable
-> 定位到 speak() 方法地址
-> 执行 Dog.speak()
🔧 非虚方法不会走多态
比如:
-
static
方法:属于类本身,编译时绑定 -
final
方法:不能被重写,JVM 会直接绑定 -
private
方法:只在类内部使用,也不会多态
这些都属于“静态绑定”,在编译期就决定了。
🔬 多态 + 字节码验证
我们用 javap -v
看下字节码:
javac Main.java
javap -v Main
你会看到类似:
invokevirtual #4 // Method Animal.speak:()V
说明这是个虚方法调用。
如果是 static
方法,就会看到:
invokestatic #x // 静态绑定
✅ 总结一句话:
多态的底层原理:
编译期只检查方法是否存在(基于父类类型)
运行时通过对象实际类型的 vtable 找到真正的方法地址
调用的是子类重写后的方法(实现动态绑定)
⚠️ 面试时常见提问点
-
private
/static
方法能多态吗?❌不能,静态绑定 -
多态和重载的区别?
-
多态是重写(override)+ 运行期决定
-
重载是同名方法参数不同 + 编译期决定
-
-
JVM 如何优化多态?👉 JIT 编译时会做内联优化(HotSpot)
如果你想我带你看看 vtable 模拟实现、或者手撸一个简单多态解释器,我也可以帮你拓展思路 🧠 要不要玩点底层的?