“编译看左边,运行看右边”精准概括java多态机制:编译阶段依据声明类型(左边)检查方法存在性、参数匹配并生成invokevirtual指令;运行时通过对象实际类型(右边)查vtable动态分派。

这句“编译看左边,运行看右边”不是经验口诀,而是对 Java 多态底层执行逻辑的精准概括——它背后是编译器与 JVM 各司其职:前者做静态检查,后者靠虚方法表(vtable)完成动态分派。
编译阶段只认声明类型(左边)
编译器不关心对象实际是谁,只盯着变量声明的类型(即等号左边的类或接口):
- 检查该类型中是否存在被调用的方法——不存在就直接报错,比如 Animal a = new Dog(); a.bark(); 会编译失败,因为 Animal 没有 bark()
- 确认参数个数、类型、访问权限是否匹配,比如传入 String 却期望 int,也会在编译期拦截
- 把方法调用翻译成 invokevirtual 字节码指令,并绑定到声明类型的符号引用(如 Animal.say),但不决定最终执行哪段代码
运行阶段靠对象真实类型查 vtable(右边)
JVM 执行时才真正“看右边”,但不是凭空猜测,而是通过对象内存结构精确查找:
- 每个 Java 对象头里存有一个 klass 指针,指向该对象所属类的元数据(Klass 结构)
- JVM 根据这个指针定位到 Child.class(比如 Dog.class),再取出它在类加载阶段就构建好的 vtable
- vtable 是一张固定索引的函数地址表,同一方法签名(如 say())在父类和子类 vtable 中占据相同槽位(slot);若子类重写了,该槽位就填入子类方法的入口地址
- invokevirtual 指令按签名查表、跳转执行——所以 a.say() 最终运行的是 Dog.say()
哪些东西不进 vtable,所以不“看右边”
vtable 只收可被重写的实例方法。以下四类方法根本不会参与动态绑定,自然也不符合“运行看右边”:
- 字段(成员变量):访问靠编译期计算的内存偏移量,Animal a = new Dog(); System.out.println(a.name); 输出的是 Animal.name,哪怕 Dog 也定义了同名字段
- static 方法:编译成 invokestatic,直接绑定到声明类型,Parent p = new Child(); p.staticMethod(); 调用的是 Parent.staticMethod
- private / final 方法:编译成 invokespecial,JVM 直接调用,不查表;它们无法被重写,也就没有动态分派的必要
@Override 注解是安全网,不是摆设
它不改变字节码,但能防止重写失效:
- 没加 @Override 时,你可能误写成 void say(String s)(而父类是 void say()),编译器当成重载而非重写,结果 vtable 槽位没被替换,运行时仍调父类方法
- 加上后,编译器强制校验:方法签名必须与父类完全一致,否则报错——这是守住多态行为的第一道防线











