静态分派在编译期根据参数声明类型确定调用方法,如Animal dog = new Dog()时调用sayHello(Animal);动态分派在运行时通过vtable根据对象实际类型查找方法,实现真正多态。

静态分派只看声明类型,编译器当场拍板
Java里方法重载(sayHello(Animal)、sayHello(Dog)、sayHello(Cat))的调用目标,在你敲完代码、javac编译那一刻就锁死了——它不看new Dog()这个实际对象,只认变量声明时写的类型,比如Animal dog = new Dog();中的Animal。
这是因为重载解析发生在编译期,javac根据所有重载方法的参数**静态类型**匹配最精确的那个签名;而字节码里最终生成的是invokevirtual指令,但目标方法符号引用已经写死,比如#sayHello:(Lcom/example/Animal;)V,跟运行时dog到底是不是Dog毫无关系。
- 常见错误现象:
sr.sayHello(dog)没调到sayHello(Dog),却进了sayHello(Animal)——不是JVM“选错了”,是javac根本没把Dog当候选 - 实操建议:想靠重载做运行时多态?别试。重载本质是编译期语法糖,不是多态机制
- 容易踩的坑:给父类引用赋子类实例后还指望重载走子类特化版本,结果全掉进最宽泛的父类参数方法里
动态分派靠虚方法表,运行时才查对象真实类型
方法重写(override)才是Java多态的主力,它的分派发生在运行时,靠的是每个类在加载时构建的虚方法表(vtable)。当你调用obj.doWork(),JVM拿到obj的实际类型(比如Man),再查Man的vtable里doWork指向哪个地址——如果Man重写了该方法,就跳转到Man.doWork;否则沿用父类入口。
对应字节码就是invokevirtual指令:它不写死方法地址,只存符号引用,留到运行时由vtable动态解析。
立即学习“Java免费学习笔记(深入)”;
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
- 使用场景:所有非
static、非private、非final的实例方法,默认都是虚方法,都走动态分派 - 参数差异:动态分派只看**调用者对象的实际类型**,和参数类型无关;而静态分派恰恰相反,只看**参数的声明类型**
- 性能影响:vtable查表是O(1),但比直接跳转(如
invokestatic)多一次间接寻址;不过现代JVM有内联优化,热点路径下几乎无感
哪些方法根本不会分派?它们在类加载时就定死了
静态方法、私有方法、构造器、父类方法(用super.调用)、final方法——这五类方法在类加载的解析阶段,就会把class文件里的符号引用(如#println:(Ljava/lang/String;)V)直接替换成内存里的真实入口地址。它们不进vtable,也不参与任何分派逻辑。
字节码上很干净:invokestatic干静态方法,invokespecial干私有/构造/super调用,invokevirtual干其余虚方法(含final,虽被归为非虚,但因用invokevirtual,仍需vtable支持)。
- 常见错误现象:以为
final方法能被子类“覆盖”——不能,它只是用invokevirtual指令调用,但vtable里该槽位永远指向父类实现 - 实操建议:想彻底禁掉多态?加
final或改private;想确保编译期绑定?用static或super.xxx() - 兼容性注意:
invokedynamic是例外,它连vtable都不走,而是靠BootstrapMethod动态链接,Lambda背后就是它
看字节码才能看清分派本质:javap -v 是你的显微镜
光看Java源码容易误判分派行为,比如你以为obj.method()一定动态分派,但如果method是static或private,字节码里出现的就是invokestatic或invokespecial,跟vtable完全无关。
用javap -v反编译class文件,重点盯三处:Constant pool里方法引用的签名、Code段每条invoke指令的类型、以及Virtual methods列表是否包含该方法(决定它有没有vtable槽位)。
- 实操命令:
javac Test.java && javap -v Test | grep -A 5 "invoke",快速定位调用指令 - 容易踩的坑:看到
invokevirtual就默认是动态分派——错。如果该方法是final或来自Object且未被重写,vtable里还是父类地址,但指令本身不变 - 关键点:分派行为不在Java语法层,而在字节码+JVM运行时协作;脱离字节码谈“静态/动态”容易流于表面
invokevirtual前,就按静态类型挑好了符号引用——这点最容易被忽略。

















