“编译看左边,运行看右边”本质是编译器静态检查与jvm动态分派协作:编译时按声明类型校验方法合法性,运行时通过对象头klass指针查子类vtable调用重写方法;字段、static、private、final方法不参与多态。
这句“编译看左边,运行看右边”不是口诀,而是 java 多态方法调用的真实执行逻辑——它背后是编译器和 jvm 分工协作的结果:编译器只管“能不能写”,jvm 才决定“到底执行谁”。
编译阶段:只认声明类型,不问对象是谁
编译器看到 Animal a = new Dog();,只检查 a.shout() 在 Animal 类中是否存在、参数是否匹配、访问权限是否允许。
- 如果 shout() 在 Animal 中没有定义,哪怕 Dog 里写了十遍,也直接报错;
- 如果调用的是 a.bark()(Animal 没这个方法),编译失败,JVM 根本没机会运行;
- 编译器生成的字节码指令是 invokevirtual Animal.shout,但不指定具体实现类。
运行阶段:靠对象头 + 虚方法表,精准跳转到子类实现
真正执行 a.shout() 时,JVM 才开始“看右边”:它从对象内存头读取 klass 指针,定位到实际类(Dog.class),再查它的虚方法表(vtable)。
- vtable 是类加载时就构建好的固定结构,同一方法签名(如 shout())在父类和子类 vtable 中占据相同槽位;
- 若 Dog 重写了 shout(),该槽位填的就是 Dog.shout() 的入口地址;
- JVM 通过 invokevirtual 指令查表、跳转,整个过程叫动态分派。
哪些情况不走“运行看右边”?它们压根不进虚方法表
字段、static、private、final 方法都不参与多态,因为它们根本不会被放进 vtable。
- 字段访问:编译期就计算好内存偏移量,a.name 永远读 Animal.name,哪怕 Dog 也有同名字段;
- static 方法:指令是 invokestatic,绑定到引用类型,Parent p = new Child(); p.method() 调的是 Parent.method();
- private / final 方法:无法重写,JVM 用 invokespecial 直接调用,连查表都省了。
@Override 不是装饰,是防止重写失效的关键防线
不加 @Override,编译器不会校验你是否真重写了父类方法。
- 比如父类是 void shout(),你误写成 void shout(String s),编译通过,但这只是重载,vtable 槽位没被覆盖;
- 加上后,编译器强制比对方法签名(含参数类型、返回值协变等),签名不符直接报错——守住多态行为的第一道关。











