静态分派依据参数声明类型在编译期确定重载方法,动态分派依据对象实际类型在运行时通过invokevirtual/invokeinterface查vtable/itable决定重写方法;static、final、private方法不参与动态分派。

静态分派看的是变量声明类型,不是实际对象类型
Java 编译器在编译期就决定调用哪个重载(overload)方法,依据是**参数的声明类型**,而非运行时的真实类型。这意味着哪怕你把子类实例赋给父类引用,编译器也只认声明类型。
常见错误现象:NullPointerException 没抛,但调用的却是“错的”重载方法——不是逻辑写错了,而是你误以为动态类型会影响重载选择。
-
String s = null;和Object o = null;传给同一个重载方法,可能触发完全不同的method(String)或method(Object) - 泛型擦除后,
List和List在重载中都视为List,容易意外匹配到同一签名 - 如果重载方法参数分别是
Parent和Child,而你传入Child实例但用Parent p = new Child(); method(p);,编译器选的是method(Parent),不是method(Child)
动态分派靠 invokevirtual 指令,只对重写(override)生效
运行时 JVM 根据对象的实际类型(即 new 出来的那个类),查该类的方法表(vtable),找到最终执行的方法体。这个过程只发生在 virtual 方法上——也就是非 static、非 final、非 private 的实例方法。
注意:构造器、static 方法、final 方法不参与动态分派;它们要么绑定到类型(static),要么无法被重写(final),要么不可见(private)。
立即学习“Java免费学习笔记(深入)”;
-
private方法看似被“继承”,实则只是编译器把父类私有方法复制进子类字节码,子类调用它走的是invokespecial,与多态无关 -
static方法调用由符号引用在编译期解析为具体类,运行时不会根据对象实例切换目标类 - 接口默认方法(
default)属于动态分派,但invokeinterface查的是接口表(itable),不是 vtable
准确识别 final/static/private 是否参与分派的关键信号
看字节码最直接:用 javap -v 反编译,观察调用指令是 invokevirtual、invokestatic、invokespecial 还是 invokeinterface。只有 invokevirtual 和 invokeinterface 才会触发动态查找。
public class Test {
static void s() {}
final void f() {}
private void p() {}
void v() {}
void test() {
s(); // → invokestatic
f(); // → invokevirtual(但无法重写,所以等价于静态绑定)
p(); // → invokespecial
v(); // → invokevirtual
}
}
-
final方法仍用invokevirtual,是因为语法允许子类继承它,但 JVM 在类加载阶段就禁止子类覆盖,所以实际效果是“伪动态” -
private方法在子类里定义同名方法,不是重写,而是全新方法;两个方法在各自类的常量池中独立存在 - 如果一个方法既是
static又是final,final是冗余的——static方法天然不可重写
为什么泛型和 varargs 容易干扰静态分派判断
泛型擦除和可变参数会改变编译器看到的“参数签名”,从而影响重载解析结果,而且这种行为在不同 JDK 版本间可能有差异(尤其 JDK 7→8 对 varargs 的宽松处理)。
典型场景:你写了两个方法 foo(List 和 foo(Object...),然后传入 Arrays.asList("a"),结果却调到了 foo(Object...) ——因为泛型擦除后前者变成 foo(List),而 Arrays.asList() 返回的是 ArrayList,它实现了 List,但编译器发现 Object... 更“宽泛”,且没有精确匹配,就选了可变参数版本。
- 避免在重载中混用泛型边界(如
)和原始类型参数,编译器可能无法区分 -
varargs是最后兜底选项,只要其他重载不匹配,就倾向走它;加@SafeVarargs不影响分派逻辑 - JDK 9+ 对
varargs重载做了更严格的歧义检查,老代码升级后可能编译失败










