考虑这个简单的 Java 类:
class MyClass {
public void bar(MyClass c) {
c.foo();
}
}
我想讨论一下 c.foo() 行发生了什么。
原始的,误导性的问题
注意:并非所有这些都发生在每个单独的调用虚拟操作码上。提示:如果你想了解 Java 方法调用,不要只阅读 invokevirtual 的文档!
在字节码级别,c.foo() 的核心将是 invokevirtual 操作码,并且根据 invokevirtual的文档,或多或少会发生以下情况:
- 查找在编译时类 MyClass 中定义的 foo 方法。(这涉及到首先解析 MyClass。)
- 做一些检查,包括: 验证 c 不是初始化方法,并验证调用 MyClass.foo 不会违反任何受保护的修饰符。
- 找出实际调用的方法。特别是查找 c 的运行时类型。如果该类型具有 foo(),则调用该方法并返回。如果不是,则查找 c 的运行时类型的超类;如果该类型有 foo,则调用该方法并返回。如果不是,查找c的运行时类型的超类的超类;如果该类型有 foo,则调用该方法并返回。等等。如果找不到合适的方法,则报错。
单独的步骤#3 似乎足以确定调用哪个方法并验证所述方法是否具有正确的参数/返回类型。所以我的问题是为什么首先要执行步骤#1。可能的答案似乎是:
- 在第 1 步完成之前,您没有足够的信息来执行第 3 步。(乍一看似乎不可信,所以请解释一下。)
- 在 #1 和 #2 中完成的链接或访问修饰符检查对于防止某些坏事发生是必不可少的,并且这些检查必须基于编译时类型而不是运行时类型层次结构来执行。(请解释。)
修改后的问题
c.foo() 行的 javac 编译器输出的核心将是这样的指令:
invokevirtual i
其中 i 是 MyClass 的运行时常量池的索引。该常量池条目将是 CONSTANT_Methodref_info 类型,并且将指示(可能间接地)A)被调用的方法的名称(即 foo),B)方法签名,以及 C)调用该方法的编译时类的名称上(即 MyClass)。
问题是,为什么需要对编译时类型(MyClass)的引用?既然invokevirtual 会在c 的运行时类型上做动态调度,那么存储对编译时类的引用不是多余的吗?