JVM 中的静态类型和实际类型

3

下面用非常清晰、非常标准的 JVM 术语帮你解释一下 静态类型(Static Type)实际类型(Actual Type / Runtime Type) 的概念,并说明它们在 JVM 中扮演什么角色、为什么需要区分它们。


✅ 一、静态类型(Static Type)

静态类型 = 编译阶段看到的类型
也称为:编译时类型、声明类型

在 Java 中,变量、参数、字段的类型都是写在代码里的,例如:

Animal a = new Dog();

其中:

a 的静态类型 = Animal

静态类型在编译期就确定,不会随执行改变

编译器通过静态类型决定:

✔ 能否调用某个方法(方法是否存在)

例如 a.bark() 会报错,因为在静态类型 Animal 中没有 bark 方法。

✔ 方法重载(overload)选择

编译器只根据静态类型选重载版本。
这也是 Java 重载不支持根据运行时类型选择的原因。

✔ 字节码指令生成(特别是 invokevirtual 的符号引用)

比如:

a.run();

编译器会把 run() 解析为一个方法符号引用:

Animal.run()

最终写入字节码的也会是 Animal 的方法符号引用。


✅ 二、实际类型(Actual Type / Runtime Type)

实际类型 = 运行过程中对象真正的类型
也称为:运行时类型、动态类型

在上边例子:

Animal a = new Dog();

实际类型:

a 的实际类型 = Dog

实际类型是在运行时才真正确定的(对象在堆中新建的时候)。

JVM 的运行时方法调用使用实际类型进行动态绑定(Dynamic Dispatch)

Dog.run()

JVM 会根据对象实际类型的 vtable 找到最终执行的方法。


✨ 这样你就能清楚区分:

概念

静态类型(Static Type)

实际类型(Runtime Type)

决定时机

编译期

运行期

决定因素

变量声明

new 出来的对象

存在位置

字节码中(符号引用)

堆中对象 header(vtable 指针)

决定哪些行为

方法重载选择

方法重写调用(多态)

能否改变

不能改变

运行期可能不同对象不同类型


✅ 三、为什么 JVM 必须同时有它们?

✔ 1. 为了支持静态语言的编译检查

静态类型决定你能不能写某段代码,比如能不能调用方法、访问字段。

✔ 2. 为了支持运行时多态、动态分派

实际类型让 JVM 能做到 Java 最重要的特性:虚方法调用(invokevirtual)走重写方法


✅ 四、例子:静态类型与实际类型不一样会怎样?

示例代码

Animal a = new Dog();
a.run();   // Animal.run()? Dog.run()?
  • 编译器通过静态类型 Animal 生成字节码:

    invokevirtual Animal.run
    
  • JVM 执行时根据实际类型 Dog 查找实现:

    Dog.override_run()
    

→ 最终调用的是 Dog.run()(多态)


🔥 五、再来一个更能说明区别的例子:方法重载 vs 重写

重载(静态分派)只看静态类型:

void hello(Animal a)
void hello(Dog d)

Animal a = new Dog();
hello(a);  // 调用 hello(Animal) 而不是 hello(Dog)

因为静态类型是 Animal。

重写(动态分派)只看实际类型:

Animal a = new Dog();
a.run();  // 调用 Dog.run() (实际类型 Dog)

📌 总结一句话

静态类型决定编译行为,实际类型决定运行行为。
编译看左边,运行看右边:

Animal a = new Dog();
  • 编译阶段:看左边(Animal) → 静态类型

  • 运行阶段:看右边(Dog) → 实际类型