Java多态依赖类加载时生成的虚方法表(vtable),运行时仅通过对象实际类型查表跳转,而非动态解析;private/static/final方法不入vtable,字段无多态,@Override用于编译期校验重写正确性。

Java多态靠的是虚方法表(vtable),不是编译期决定的
Java里invokevirtual指令执行时,JVM不看变量声明类型,而是查对象实际类型的MethodTable——这个表在类加载阶段就生成了,每个类一份,存着所有可被重写的方法入口地址。
所以哪怕你写Animal a = new Dog(); a.sound();,JVM也会跳转到Dog.sound()的字节码,而不是Animal.sound()。这不是“运行时才找”,而是“编译时留好钩子,运行时查表跳转”。
- 只有
private、static、final方法不进虚方法表,它们走invokespecial或invokestatic,直接绑定 -
interface方法用的是invokeinterface,查的是ITable(接口方法表),结构类似但实现更复杂,涉及默认方法和冲突解决 - 子类新增方法不会覆盖父类表项,只会追加;重写才覆盖对应槽位——这也是为什么向上转型后无法调用子类独有方法
动态绑定发生在方法调用那一刻,但表是静态构建的
很多人以为“动态”=“边跑边算”,其实不是。MethodTable在类初始化完成前就固定了:常量池解析、符号引用转直接引用、方法重写校验都做完,表才封版。之后无论new多少个实例,都共享同一张表。
真正“动态”的只有两件事:确定当前对象的实际类(靠obj->klass指针)、查该类的表中对应方法槽。这两步加起来不到10条CPU指令。
- 如果子类没重写父类方法,它的表里那个槽仍指向父类方法的地址——所以不写
@Override也能多态,只是容易误以为没生效 - 类加载器不同会导致同名类被视为不同类型,各自的
MethodTable互不影响,这是OSGi或热更新出问题的根源之一 - 逃逸分析可能让JVM把虚调用优化成单态内联(monomorphic inline),但那是JIT的事,字节码层面永远是
invokevirtual
为什么@Override注解不是必须的,但强烈建议加上
不加@Override,编译器不会检查你是不是真重写了父类方法。比如父类方法是void run(String s),你写成void run(string s)(参数类型拼错),编译通过,但实际是重载而非重写——MethodTable里根本没覆盖原槽位,多态就断了。
这种错误不会报红,运行时也无异常,只是静默走父类逻辑,极难排查。
-
@Override是编译期契约:告诉javac“我明确要覆盖某个方法”,它会去父类/接口里严格匹配签名(包括返回类型协变) - Lombok的
@SuperBuilder或某些泛型擦除场景下,IDE可能误判可重写性,此时@Override反而能提前暴露问题 - IntelliJ默认警告无
@Override的重写行为,Eclipse需手动开“Missing @Override annotation”检查
final类和private方法为什么不能多态
因为它们压根不参与虚方法分发机制。final class没有子类,JVM知道它绝不会被继承,所有调用直接编译为invokespecial;private方法连子类都看不见,自然不可能被重写,也不进虚方法表。
注意:private方法可以被同名重载(overload),但那只是编译期决议,跟多态无关。
- 把父类方法改成
private,子类里同签名方法就变成独立新方法,向上转型后调不到——这不是“失效”,而是从一开始就没绑定关系 -
static方法属于类,不依赖实例,调用目标由编译时的变量类型决定,比如Animal.staticMethod()永远执行Animal里的,和new Dog()无关 - 字段(field)不存在多态,
Animal.name和Dog.name是两个独立变量,访问哪个只看引用类型,不是实际类型
事情说清了就结束。虚方法表是静态结构,动态绑定是查表动作,这两层分离得特别干净——但人容易混淆“动态”二字,以为运行时还在反复解析符号,其实早就在类加载时定死了。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











