我最早对"多态"产生"哦我可能真的没懂"的念头,是在一次面试辅导里。当时我问候选人:Java多态的本质是什么?对方答得又快又完整——继承、重写、父类引用指向子类对象,三点齐全。我接着问:那Parent p = new Child(); p.name输出什么?他愣了一下。再问:父类构造器里调用被子类重写的方法,会发生什么?他沉默了。这篇笔记就是为这类问题写的。它不是零基础科普,而是围绕Java多态从运行机制到实战坑点的完整梳理,适合正在准备Java面试、写了一阵代码但对运行时绑定和重写规则还含糊的朋友,也适合想把自己的"八股文"升级成真知识的人。
1. 从一道面试题说起:多态到底解决了什么问题
1.1 一个输出题,90%的人只会背结论
先看这道入门题:
java复制class Animal {
public void sound() {
System.out.println("Animal sound");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal a = new Dog();
a.sound();
}
}
输出是什么?"Dog barks"。大部分人都会做,因为这是多态最经典的例子:编译期a的静态类型是Animal,运行期实际对象是Dog,调用sound()时会动态绑定到Dog的重写方法上。
但只背结论不够。你还需要理解两件事:第一,为什么Java运行时知道要调用Dog的方法;第二,这个机制解决的真实问题是什么。要理解第一点,得往JVM的方法分派机制里钻;要理解第二点,得先看没有多态的世界是什么样。
1.2 没有多态的世界:if-else怎么慢慢失控
假设你要做一个支付模块,支持支付宝、微信、银行卡三种渠道。不用多态,代码会长这样:
java复制public void pay(String type, BigDecimal amount) {
if ("alipay".equals(type)) {
new AlipayService().pay(amount);
} else if ("wechat".equals(type)) {
new WechatPayService().pay(amount);
} else if ("card".equals(type)) {
new CardService().pay(amount);
} else {
throw new UnsupportedOperationException("unsupported type");
}
}
这段代码的问题不在于它长了,而在于"扩展"的成本高。半年后老板说加一个云闪付,你要打开这个方法,再加一个else if分支,然后编译、回归、测试。每次新增渠道都在修改这段"核心逻辑",而不小心改错一行,所有渠道都会受影响。
换用多态以后,这段代码变成一个接口引用加一行调用:
java复制public void pay(Payable payable, BigDecimal amount) {
payable.pay(amount);
}
新增渠道时,你只是新增一个实现Payable接口的类,原来的pay()方法一行都不用动。这就是"对扩展开放,对修改关闭"最直观的体现。你做的事是把"变化"从"分支"转移到了"多态分派"上。
1.3 多态的三大前提与一句话本质
所有的多态使用都逃不开三个前提:
- 要有继承或接口实现关系,没有这个关系,类型体系就不成立。
- 子类要重写父类或接口的方法,否则调用到的还是父类版本。
- 父类引用(或接口引用)指向子类对象。
这三条缺一不可。真正要记住的本质只有一句话:**引用类型决定你能调用哪些方法,对象类型决定你实际执行哪个方法。**前半句是编译器的规则,后半句是JVM的运行时规则。
这个理解会贯穿本文的所有章节,后面的虚方法表、字段隐藏、构造器陷阱,全都是这句话在不同场景下的具体表现。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 底层视角:编译期绑定与运行期动态分派如何配合
2.1 静态分派与动态分派:重载和重写的分水岭
Java里"同名方法"其实走着完全不同两条路。
重载(Overload)走的是静态分派。它在编译期根据"方法参数声明的数量、类型"来确定该调用哪个方法。也就是说,编译器在生成字节码时就已经把调用目标定死了,运行时不参与选择。举个例子:
java复制class Printer {
void print(int n) {
System.out.println("int: " + n);
}
void print(String s) {
System.out.println("String: " + s);
}
}
Printer p = new Printer();
p.print(42); // 编译期就确定为 print(int)
p.print("hello"); // 编译期就确定为 print(String)
重写(Override)走的是动态分派。编译器只负责往字节码里生成一条invokevirtual指令,指明"调
