“编译看左边,运行看右边”指java多态中编译期按引用类型检查合法性,运行期通过对象实际类型查vtable动态绑定;字段、static、private、final不参与因不进vtable;@override防止重写失败。

“编译看左边,运行看右边”这句口诀,本质是描述 Java 多态中方法调用的**双阶段决策机制**:编译器只认引用类型(左边),JVM 运行时才查实际对象类型(右边)。真正支撑这句话落地的,是 JVM 内部的**虚方法表(vtable)** 和 **invokevirtual 指令的动态分派逻辑**。
编译期:只做“合法性检查”,不决定执行谁
当你写下 Parent p = new Child(); p.say();,javac 在编译时只做三件事:
- 确认 Parent 类中声明了 public say() 方法(否则直接报错)
- 把这行调用翻译成字节码指令 invokevirtual #xx(#xx 是常量池中对 Parent.say 的符号引用)
- 完全不关心 p 实际指向的是 Parent 还是 Child——它甚至不知道 Child 类是否存在
也就是说,“左边”(Parent)决定了代码能不能过编译;但“能不能过编译”和“最终执行谁”,是两回事。
运行期:靠对象头里的 klass 指针查 vtable
程序运行到 p.say() 时,JVM 才真正介入:
- 从对象实例的内存头部读取 klass 指针,定位到该对象的真实类——这里是 Child.class
- 找到 Child 类在类加载阶段就构建好的 vtable(虚方法表)
- 根据方法签名(say())在 vtable 中查找对应槽位(slot)——这个槽位在 Child 的 vtable 中,早已被填入 Child.say() 的字节码入口地址
- 跳转执行,完成动态绑定
注意:vtable 不是运行时临时生成的,而是在类加载的“准备”和“解析”阶段就静态构建完成并固化下来的。所谓“动态”,仅指查表动作发生在运行时,且查的是哪个类的表,取决于对象的实际类型。
为什么字段、static、private、final 不参与这套机制?
因为它们根本不进 vtable:
- 字段(name):访问靠偏移量,编译期就确定好读哪个类里定义的字段(所以 p.name 取的是 Parent.name)
- static 方法(speak):编译期绑定到引用类型,指令是 invokestatic,直接跳转,不查 vtable
- private/final 方法:无法被重写,JVM 用 invokespecial 直接调用,连查表都省了
这些都不是“多态失效”,而是压根没进入多态的通道——vtable 只收可被重写的实例方法。
@Override 注解不是语法必需,却是关键防线
不加 @Override,编译器不会校验你是否真重写了父类方法。比如父类是 void say(String s),你误写成 void say(string s)(参数类型拼错),编译通过,但这只是重载,vtable 槽位没被覆盖。结果就是:看似向上转型,实则调用的还是父类方法——多态“静默失败”。@Override 就是让编译器帮你盯住签名一致性。











