为什么存在invokeVirtual却需要invokeSpecial

62

有三个操作码可用于调用Java方法。很明显,invokeStatic仅用于静态方法的调用。

据我所知,invokespecial用于调用构造函数和私有方法。那么,在运行时需要区分私有方法和公有方法吗?它可以使用相同的操作码,比如说 invokevirtual 吗?

JVM是否处理私有和公共方法定义?据我所知,public和private关键字只在开发阶段需要进行封装。


根据您的评论,调用私有方法时可能需要使用invokespecial。 - Ahmet Karakaya
3个回答

50

来自本站的内容:

如果仔细阅读Java VM规范,答案可以很容易地找到:

invokespecial和invokevirtual指令的区别在于,invokevirtual基于对象的类调用方法。invokespecial指令用于调用实例初始化方法以及当前类的私有方法和超类的方法。

换句话说,invokespecial用于调用方法而不关心动态绑定,以调用特定类的版本的方法。

有几种情况下,在“不考虑动态绑定”的情况下分派方法调用非常重要:

首先是从一个构造函数链到一个超类构造函数的情况。

其次,当从一个方法调用到超类的实现时,例如super.foo()。如果该类或其任何子类覆盖了该方法,则invokevirtual会跳转到错误的位置。

第三,当一个类想要选择使用哪个方法的default版本时,就像上面代码块中的情况。

interface I { void f(); }
interface J extends I { default void f() { ... } }
interface K extends I { default void f() { ... } }
class C implements J, K {
  @Override public void f() { K.super.f(); }
}

上面的 class C 存在钻石继承问题,因此需要选择要调用哪个默认方法。使用 invokespecial 可以将其分派到 K 的版本,但如果 K.super.f 通过 invokevirtual 分派,调用最终会回到 C.f

6
当然,除非你进行超类调用并且目标类具有ACC_SUPER标志(现代类都具有该标志)。 - Antimony
1
@Antimony...并且Java 8及更高版本甚至在没有它的情况下也会假定。 - Holger
如果我没记错的话,他们也将其回溯到了Java 7u51,但现在这已经不重要了。 - Antimony

34
上面的链接提供了宝贵的例子,清楚地回答了我的问题。
class Superclass {

    private void interestingMethod() {
        System.out.println("Superclass's interesting method.");
    }

    void exampleMethod() {
        interestingMethod();
    }
}

class Subclass extends Superclass {

    void interestingMethod() {
        System.out.println("Subclass's interesting method.");
    }

    public static void main(String args[]) {
        Subclass me = new Subclass();
        me.exampleMethod();
    }
}

如果按照上面定义的方式在子类(Subclass)中调用main()方法,它必须打印出"超类(Superclass)的有趣方法"。如果使用invokevirtual,则会打印出"子类(Subclass)的有趣方法"。为什么?因为虚拟机将根据对象的实际类别(即Subclass)选择要调用的interestingMethod()方法。因此,它将使用Subclass的interestingMethod()方法。另一方面,使用invokespecial时,虚拟机将根据引用类型选择方法,因此将调用Superclass版本的interestingMethod()方法。


4
我认为没有正当的理由使用这样的代码。这是隐藏,如果你这样做,任何好的IDE都会给出警告或错误提示。两个interestingMethod方法签名之间微妙的差异足以使读者对发生了什么感到困惑。 - wchargin
7
例如,当父类是一个你继承的库类时,就可能会发生这种情况。也许你甚至没有它的源代码,你肯定不关心它的私有方法。很可能你在子类中添加了一个与父类私有方法同名的方法。 - jcsahnwaldt Reinstate Monica
2
对于其他读者:请记住,Java 语言 有规则和限制,而 JVM 没有。上面是一个非常好的例子,说明“什么样的源代码可能会产生所讨论的字节码”,但这并不意味着它是良好的软件工程。 :-) 事实上,这样的源代码不必是Java,只需是一种可以编译为JVM字节码的语言即可。 - Ti Strga
1
我曾经也认为当超类从exampleMethod()调用interestingMethod()时,应该使用invokevirtual。但实际上,我通过修改Superclass.class文件中的字节码,将invokespecial替换为invokevirtual进行了实验,运行Java Subclass后输出仍然是“Superclass's interesting method.” 此外,我发现JDK 11直接使用字节码指令invokevirtual编译Superclass!而JDK 8仍然使用invokespecial进行编译。 - Alfred Xiao
所以我认为在这种情况下,使用invokespecialinvokevirtual无关紧要,因为被调用的方法是一个私有函数,JVM将会根据类检查并做出不分派的决定。 - Alfred Xiao

1
谢谢您阅读这篇说明:如果它帮助您识别方法调用期间的汇编指令创建,请不要忘记点赞。 在这里,我将解释静态绑定和动态绑定。
首先,我要告诉您,invokeStatic、invokeSpecial、invokeVirtual、invokeInterface等是编译器在编译过程后生成的汇编指令。 正如我们所知道的那样,在编译后我们得到了一个.class文件格式,无法阅读。但是Java提供了一个名为"javap"的工具来解决这个问题。
我们可以使用javap命令读取我们的.class文件汇编指令。默认情况下,我们无法看到私有方法的汇编指令,因此需要使用-private选项。以下是查看Java编译器生成的汇编指令的命令:
  1. 假设您有一个A.java类

    class A { public void printValue() { System.out.println("Inside A"); }

    public static void callMethod(A a) { a.printValue(); } }

  2. 打开cmd提示符并进入包含java文件A.java的文件夹。

  3. 运行javac A.java。

  4. 现在生成了A.class文件,其中包含汇编指令,但您无法阅读它。

  5. 现在运行javap -c A

  6. 您可以看到方法调用的汇编生成--> a.printValue();

  7. 如果printValue()方法是私有的,则需要使用javap -c -private A。

  8. 您可以将printValue()设置为私有/静态/公共/私有静态两者都可以。

  9. 还要记住的一件事是首先编译器检查调用该方法的对象。然后找到其类类型,并在该类中查找该方法是否可用。

注意:请记住,如果我们调用的方法是静态的,则会生成invokeStatic汇编指令;如果是私有的,则会生成invokeSpecial汇编指令;如果是公共的,则会生成invokeVirtual指令。公共方法并不意味着每次都会生成invokeVirtual指令。在子类B中调用父类A的super.printValue()是一个特殊情况。即如果A是B的父类,并且B包含相同的printValue()方法,则会生成invokeVirtual(动态),但如果B中的printValue()将super.printValue()作为其第一条语句,则会生成invokeStatic,即使A的printValue()是公共的。

让我们也试试这个:

class B extends A
{
public void printValue()
{
super.printValue();// invokeStatic
System.out.println("Inside B");
}

}

public class Test
{
public static void main(String[] arr)
{
    A a = new A();
    B b = new B();
    A.callMethod(a);// invokeVirtual
    A.callMethod(b);// invokeVirtual
}
}

--> 将其保存为Test.java --> 运行javac Test.java --> 运行javap -c -private Test


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接