JVM中的静态分派与动态分派

2

下面给你一份JVM 层面 + Java 示例 + 字节码原理都讲清楚的《静态分派与动态分派》讲解。这是理解 Java 多态的核心关键。


🟥 一、什么是静态分派(Static Dispatch)

静态分派 = 编译阶段决定调用哪个方法。

特点:

  • 发生在 编译期

  • 依据 静态类型(变量声明类型)

  • 用于 方法重载(overload)

  • 与对象实际类型完全无关

  • 在字节码中不体现重载逻辑,选择在编译器完成

✔ 静态分派的经典例子(方法重载)

class Human {}
class Man extends Human {}
class Woman extends Human {}

class Hello {
    void say(Human h) { System.out.println("hello human"); }
    void say(Man m) { System.out.println("hello man"); }
    void say(Woman w) { System.out.println("hello woman"); }
}

public static void main(String[] args) {
    Human h = new Man();  // 实际类型 = Man,静态类型 = Human
    Hello hello = new Hello();
    hello.say(h);         // 调用哪一个?
}

输出:

hello human

❗为什么不是 "hello man"?

因为 重载选择完全由静态类型决定

h 的静态类型 = Human

编译器选择:

say(Human)

写入字节码中。

运行期不会重新选择。


🔍 静态分派 = 编译器执行“多次选择”(选择参数类型最合适的版本)

这被经典称为:

多分派(multiple dispatch)在编译期完成

参与分派的因素:

  • 变量的静态类型

  • 对应重载方法的参数类型列表

JVM 字节码中调用重载方法只看到:

invokevirtual # say(LHuman;)V

已经确定了。


🟦 二、什么是动态分派(Dynamic Dispatch)

动态分派 = 运行阶段决定调用哪个方法。

特点:

  • 发生在 运行期

  • 依据 实际类型(对象真正的类型)

  • 用于 方法重写(override)

  • 与静态类型无关(只要静态类型有该方法即可)

  • JVM 通过 虚方法表(vtable) 定位最终执行的方法

✔ 动态分派经典例子(方法重写)

class Human {
    void say() { System.out.println("human"); }
}

class Man extends Human {
    @Override
    void say() { System.out.println("man"); }
}

public static void main(String[] args) {
    Human h = new Man();  // 实际类型 = Man
    h.say();              // 调用哪个 say()?
}

输出:

man

❗为什么?

因为 JVM 会根据对象的 实际类型 Man 所指向的 vtable 映射到 Man.say()。


🔍 动态分派 = 依据实际类型进行虚方法的运行期选择

执行:

invokevirtual Human.say

JVM 执行步骤(简化版):

  1. 根据引用找到实际对象:Man

  2. 根据对象的 klass pointer → vtable

  3. 找到被 override 的方法入口

  4. 调用:Man.say()


🟥 静态分派 vs 🟦 动态分派:对照总结

分派类型

静态分派

动态分派

发生阶段

编译期

运行期

依据

静态类型(变量声明类型)

实际类型(对象真正的类型)

应用

重载 (overload)

重写 (override)

JVM 支持

编译器完成,JVM 不参与

JVM 本地实现(vtable)

对运行时类型敏感?

❌ 不敏感

✔ 敏感

字节码

调用点已确定签名

运行时根据 vtable 查找最终方法


🧠 一个结合两者的经典例子

你可以看到静态和动态分派同时发生:

class Human {
    void say(Human h) { System.out.println("Human say human"); }
    void say(Man m)   { System.out.println("Human say man"); }
    void who()        { System.out.println("I am Human"); }
}

class Man extends Human {
    @Override
    void who()        { System.out.println("I am Man"); }

    void say(Man m)   { System.out.println("Man say man"); }
    void say(Human h) { System.out.println("Man say human"); }
}

public static void main(String[] args) {
    Human h = new Man();   // 静态类型 = Human,实际类型 = Man
    Man m = new Man();

    h.say(m);  // 静态分派决定调用哪个 say?
    h.who();   // 动态分派决定最终执行哪个 who?
}

分析:

① 静态分派

h.say(m)
编译器看到:

h: Human
m: Man

所以重载选择 Human 的:

say(Man)

② 动态分派

实际运行调用的 say() 版本,要看 h 的实际类型:

h 的实际类型 = Man

所以最终调用 Man 的:

Man.say(Man)

③ who()

调用的是运行时的:

Man.who()

🔥 最终总结(最重要的三句话)

1️⃣ 重载(overload)= 静态分派,编译期根据静态类型决定

2️⃣ 重写(override)= 动态分派,运行期根据实际类型决定

3️⃣ 编译看左边(静态类型),运行看右边(实际类型)


如果你愿意,我还能继续给你:

🔹 Java 字节码级别的静态分派示例
🔹 invokevirtual、invokespecial、invokeinterface 的动态分派机制
🔹 JVM vtable / itable 的结构图
🔹 专门可视化“静态分派 vs 动态分派”的流程图

需要哪一部分?