5

我有以下代码。

public class Parent {

    @Override
    public int hashCode() {
         return 0;
    }

}

public class Child extends Parent {

    public void test() {
        this.toString();
        this.hashCode();
    }

}

正如您在上面的代码中看到的,Child 从 Object 继承 toString(),从 Parent 继承 hashCode()。Child#test 的字节码操作如下。

ALOAD 0: this
INVOKEVIRTUAL Object.toString() : String
ALOAD 0: this
INVOKEVIRTUAL Child.hashCode() : int
RETURN

我认为如果invokevirtual 调用Object.toString(),它应该调用Parent.hashCode() 以保持一致性。或者,调用 Child.hashCode(),然后调用 Child.toString()。

但是,当且仅当目标方法被 Object 继承时,invokevirtual 才保持其一致性。

只有在这种情况下,invokevirtual 才会调用 Object 中的方法。对于其他情况,invokevirtual 调用当前类中的方法。

我想知道为什么会这样。

4

3 回答 3

5

根据JVM规范p。3.7

编译器不知道类实例的内部布局。相反,它生成对实例方法的符号引用,这些方法存储在运行时常量池中。这些运行时常量池项在运行时解析以确定实际的方法位置。

这意味着所有这些符号Child.hashCode()只是常量,并没有指定JVM如何调用这些方法。似乎,toString()方法编译器知道,该方法在Object类中有其基本实现,因此它将符号常量Object放入常量池中的类 - 这是某种优化,它使编译器适用于 JVM:

  Constant pool:
const #2 = Method   #24.#25;    //  java/lang/Object.toString:()Ljava/lang/String;
...
const #24 = class   #34;    //  java/lang/Object
const #25 = NameAndType #35:#36;//  toString:()Ljava/lang/String;
于 2013-03-02T09:52:14.797 回答
4

您是正确的,编译器的行为不合逻辑。但是此代码的效果与您建议的两种变体的效果相同。所以这可能不是故意的行为,而是编译器代码长期演变的结果。其他编译器可能会产生不同的代码。

于 2013-03-02T08:59:26.827 回答
1

我的理论:toString()使用非常频繁,所以javac使用commonObject.toString()来保存常量池条目。

例如,如果代码包含foo.toString() and bar.toString(),则容器池只需要一个Object.toString,而不是两个条目。Foo.toString and Bar.toString

Javac 可能对这种优化进行了硬编码,而不是分析代码以查看它是否真的需要。

于 2013-03-02T17:32:41.320 回答