【问题标题】:How does Java Determine methods call at runtime in polymorphism?Java确定方法如何在多态中在运行时调用?
【发布时间】:2015-12-17 04:35:41
【问题描述】:

虽然多态性的主要原理是用types 来解耦“what from who”,但让我感到困惑的是,方法调用机制是如何在多态性中找出并调用正确的方法体的。

由于在java中所有方法绑定都是late-binding,除非方法是staticfinalprivate,并且后期绑定由JVM完成,它为每个类预先计算method table然后做一个表在运行时在正常的方法调用中查找。

但是在多态性期间也会发生同样的事情。例如

假设我有一个带有ride() 方法的通用类Cycle

class Cycle {

    public void ride(){
        System.out.println("I'm Riding generic Cycle()");
    }

}

我有三个专业类BicycleTricycleUnicycle,它们扩展了通用类Cycle 并覆盖了它的ride() 方法。

class Bicycle extends Cycle {

    public void ride() {
        System.out.println("I'm riding Bicycle");

    }

}

class Tricycle extends Cycle{

    public void ride() {
        System.out.println("I'm riding Tricycle ");

    }

}

class Unicycle extends Cycle {

    public void ride() {
        System.out.println("I'm Riding Unicycle ");

    }

}

这是用于测试上述多态性的TestRide 类。

public class TestRide {

    public static void ride(Cycle c){
        c.ride();
    }

    public static void main(String[] args){

        Cycle Cycling = new Cycle();
        ride(Cycling);

        Bicycle bi = new Bicycle();
        ride(bi);

        Tricycle tri = new Tricycle();
        ride(tri);

        Unicycle uni = new Unicycle();
        ride(uni);
    }

}

输出是

I'm Riding generic Cycle()
I'm riding Bicycle
I'm riding Tricycle 
I'm Riding Unicycle 

字节码:

public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=5, args_size=1
         0: new           #17                 // class com/polymorphism/Cycle
         3: dup
         4: invokespecial #24                 // Method com/polymorphism/Cycle."
<init>":()V
         7: astore_1
         8: aload_1
         9: invokestatic  #25                 // Method ride:(Lcom/polymorphism/
Cycle;)V
        12: new           #27                 // class com/polymorphism/Bicycle
        15: dup
        16: invokespecial #29                 // Method com/polymorphism/Bicycle
."<init>":()V
        19: astore_2
        20: aload_2
        21: invokestatic  #25                 // Method ride:(Lcom/polymorphism/
Cycle;)V
        24: new           #30                 // class com/polymorphism/Tricycle

        27: dup
        28: invokespecial #32                 // Method com/polymorphism/Tricycl
e."<init>":()V
        31: astore_3
        32: aload_3
        33: invokestatic  #25                 // Method ride:(Lcom/polymorphism/
Cycle;)V
        36: new           #33                 // class com/polymorphism/Unicycle

        39: dup
        40: invokespecial #35                 // Method com/polymorphism/Unicycl
e."<init>":()V
        43: astore        4
        45: aload         4
        47: invokestatic  #25                 // Method ride:(Lcom/polymorphism/
Cycle;)V
        50: return

即使在字节码中,它也像往常一样使用invokestaticinvokespecial 调用方法,而我认为它会使用invokedynamic 来确定适合对象实际类型的方法版本。但事实并非如此。

那么当我们在ride() 方法中像ride(bi) 那样在TestRide 类中传递一个向上转换的对象时,Java 是如何计算多态性期间的实际方法调用的呢?

编辑:RIDE 方法字节码

public static void ride(com.polymorphism.Cycle);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #16                 // Method com/polymorphism/Cycle.r
ide:()V
         4: return

【问题讨论】:

  • 可能你的测试用例太简单了。如果 Java 编译器在编译时就知道会调用什么方法,则不需要动态执行任何操作的字节码。
  • invokedynamic 是在 JVM 字节码中引入的,用于遵循 Java 的方法查找的动态方法查找。为什么 Java 编译器会使用它?虚拟方法分派是使用invokevirtualinvokeinterface 完成的。请注意,如果需要,Java 编译器当然可以免费使用invokedynamic。 Java 语言规范没有说明 Java 必须如何编译,甚至根本没有说它必须被编译,它也可以被解释。
  • 您正在发布 main() 的字节码,其中进行了 0 个多态方法调用:只有构造函数调用和静态方法调用(到 ride())。多态方法调用在TestRide.ride()
  • @mastov 没有。它有 4 次对静态方法ride() 的调用静态调用,以 Cycle 作为参数。
  • 基类什么都想不出来。 invokevirtual 具有 Cycle 对象,在该对象上调用ride() 方法作为堆栈上的第一个参数。它获取该 Cycle 对象的 concrete 类(例如,获取 BiCycle 或 TriCycle)。然后它按照docs.oracle.com/javase/specs/jvms/se7/html/… 中描述的过程在该具体类中查找ride() 方法

标签: java jvm polymorphism bytecode


【解决方案1】:

我认为@JBNizet 已经在 cmets 中找到了解决方案(我的猜测结果是错误的)。但是由于他没有将其发布为答案,所以我会这样做:

main 方法不应该显示任何动态行为,因为它总是会调用 one single 方法 TestRide.ride(Cycle c)。没有其他可能的方法,所以没有什么可弄清楚的。

动态方法调用在该方法TestRide.ride(Cycle c) 内。现在您发布了该代码,我们确实看到了使用invokevirtual 的动态方法调度。所以,毕竟,没有惊喜。动态方法调度就在那里。

【讨论】:

    【解决方案2】:

    首先invokedynamic 适用于 Java 8 lambda 和非 Java 代码,因此您可以忘记这一点。

    除此之外,还有四个调用指令(invokespecialinvokestaticinvokevirtualinvokeinterface)。你可以在 JVM sepcification 中看到精确的语义,但底线是 invokevirtualinvokeinterfacevirtual 方法调用,即实际调用的方法是在运行时根据具体类型选择的目标。

    您的代码中唯一的虚拟调用是在 TestRide.ride 中。列出的目标是Cycle.ride:()V。但是,由于它是虚拟调用,JVM 会在运行时检查第一个参数的实际类型,并调用该方法的最派生版本。

    这类似于 C++ 中的虚拟方法调用,只是 JVM 和 JIT 编译的抽象允许实现更优化的潜力。

    还请注意,不要将这与方法重载相混淆,方法重载是编译时多态性的一种形式。对于重载的方法,编译器会根据参数的编译时类型来选择调用哪一个。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-09-11
      • 1970-01-01
      • 1970-01-01
      • 2020-11-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多