重载在编译期确定,因编译器仅依据变量声明类型和参数签名静态匹配;重写在运行时决定,因jvm需根据实际对象类型查虚方法表动态分派。

重载(Overload)为什么在编译期就确定了调用哪个方法
Java 重载的解析发生在编译阶段,靠的是「静态类型 + 方法签名」匹配,跟实际运行时对象是什么类型完全无关。编译器只看变量声明类型(即 ref 的类型),再结合参数个数、顺序、可转换类型(如 int → long)做最精确匹配。
常见错误现象:NullPointerException 不会阻止重载决议;哪怕 ref 是 null,只要声明类型明确,编译照样通过。
- 如果两个重载方法参数分别是
Object和String,传入null,编译器选String(更具体) -
byte、short、char传给重载方法时,优先匹配int形参(自动提升),而不是包装类 - 泛型擦除后,
foo(List<string>)</string>和foo(List<integer>)</integer>编译不通过——它们擦除后都是foo(List),构成重复签名
重写(Override)为什么必须等到运行时才决定调用谁
重写的分派是动态的,JVM 在运行时根据栈帧中实际对象的 getClass() 结果,查该类的虚方法表(vtable),定位最终执行的方法体。这个过程和变量声明类型无关,只认“new 出来的那个类”。
使用场景:多态驱动的框架扩展点(比如 Spring 的 BeanPostProcessor.postProcessBeforeInitialization)、模板方法模式里的钩子方法。
- 父类引用指向子类实例:
Animal a = new Dog(); a.speak();→ 调用Dog.speak() - 子类未重写父类方法,则沿继承链向上找,直到
Object - 被
final、static、private修饰的方法不会进 vtable,无法被重写,也就没有动态分派
静态分派和动态分派怎么共存:一次方法调用里其实发生了两次分派
以 obj.method(arg) 为例:先按 obj 的静态类型 + arg 的编译期类型做静态分派,锁定要调用的“方法签名”;再按 obj 实际类型做动态分派,锁定该签名下的具体实现。
容易踩的坑:误以为重载能配合多态“自动升级”。比如定义了 print(List) 和 print(ArrayList),传入 new ArrayList<string>()</string>,结果总是进 print(List) ——因为变量声明如果是 List 类型,静态分派就止步于此,根本不会考虑运行时是不是 ArrayList。
- 想让重载感知运行时类型?不行。改用
instanceof+ 显式转型,或策略模式 -
toString()、equals(Object)这些在Object中定义的方法,看似“重载”,实则是子类重写——因为参数类型固定为Object,没得重载空间 - 接口默认方法被重写时,也走动态分派;但接口静态方法不参与任何分派,只能通过接口名直接调用
从字节码看分派差异:invokestatic vs invokevirtual
编译后的字节码直接暴露分派机制:invokestatic 用于静态方法、私有方法、构造器,目标方法在编译期绑定;invokevirtual 才是重写方法的载体,依赖运行时对象的实际类型查 vtable。
性能影响:现代 JVM 对 invokevirtual 做了大量优化(单态内联、去虚拟化),只要实际调用路径稳定(比如 99% 都是 Dog),性能几乎等同于直接调用。
-
invokespecial用于super.xxx()、构造器、私有方法——它绕过 vtable,强制调用指定类的版本 -
invokeinterface表面类似invokevirtual,但查找开销略高(接口方法表 itable 更复杂) - 用
javap -c看字节码时,重点盯住这四条指令,就能反推某次调用走的是静态还是动态分派
真正容易被忽略的是:重载决议不看 null 的运行时身份,而重写又完全无视声明类型。这两个机制在同一个点上并存,却各守边界,错一点就会掉进“我以为它该这么走”的陷阱里。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











