Java中的多态性在这种一般情况下(带有参数的方法)如何工作?

时间:2018-10-12 09:23:26

标签: java polymorphism

我有一个一般情况的代码:

public class A {
    public String show(A obj) {
        return ("A and A");
    }
}

public class B extends A {
    public String show(B obj) {
        return ("B and B");
    }

    public String show(A obj) {
        return ("B and A");
    }
}

public class C extends B {

}

public class Test {
    public static void main(String[] args) {
        A a = new B();
        B b = new B();
        C c = new C();

        System.out.println("1--" + a.show(b));
        System.out.println("2--" + a.show(c));     
    }
}

结果是:

1--B and A
2--B and A

我知道Java中有一个从高到低的优先级链:

this.show(O), super.show(O), this.show((super)O), super.show((super)O)

我的理解如下:

在此代码中:

A a = new B()

发生上流。 A是父类参考,而B是子父类参考。编译并运行代码后,子父类引用将确定如何选择方法。在这种情况下,将选择B类中的show(A)

还必须满足多态性的要求: 选择的方法应包含在父类定义中。

有人可以详细说明显示的结果吗?

4 个答案:

答案 0 :(得分:2)

为了弄清楚为什么两次获得结果B and A,您需要知道有两部分:编译和运行时。

编译

遇到语句a.show(b)时,编译器将执行以下基本步骤:

  1. 查看调用了该方法的对象(a)并获取其声明的类型。此类型为A
  2. 在类A及其所有超类型中,列出所有名为show的方法。编译器将仅找到show(A)。它不会查看BC中的任何方法。
  3. 从找到的方法列表中,选择与参数(b)最匹配的方法。 show(A)将接受b,因此选择了此方法。

您通过c的第二个呼叫也会发生相同的情况。前两个步骤相同,第三步将再次找到show(A),因为只有一个,并且它还与参数c相匹配。因此,对于您的两个呼叫,其余过程都是相同的。

一旦编译器确定了所需的方法,它将创建一个字节码指令invokevirtual,并将已解析的方法show(A)设置为应调用的方法(如Eclipse中所示)打开.class):

invokevirtual org.example.A.show(org.example.A) : java.lang.String [35]

运行时

当运行时最终到达invokevirtual时,还需要执行一些步骤。

  1. 获取在其上调用了该方法的对象(到那时该对象已经在堆栈中),即a
  2. 查看此对象的实际 runtime 类型。自a = new B()起,此类型为B
  3. 查找B,然后尝试找到方法show(A)。由于B覆盖了此方法,因此找到了该方法。如果不是这种情况,它将在超类(AObject)中查找,直到找到这样的方法为止。重要的是要注意,它仅考虑show(A)方法,例如从未考虑过show(B)中的B
  4. 现在,运行时将从show(A)调用方法B,从而得到String B and A

有关更多详细信息,请参见spec for invokevirtual

  

如果解析的方法不是签名多态的(第2.9节),那么invokevirtual指令将按以下步骤进行。

     

让C成为objectref的类。通过以下查找过程选择要调用的实际方法:

     

如果C包含一个实例方法m的声明,该声明将覆盖已解析的方法(第5.4.5节),则m是要调用的方法,并且查找过程终止。

     

否则,如果C具有超类,则使用C的直接超类递归地执行相同的查找过程;要调用的方法是此查找过程的递归调用的结果。

     

否则,将引发AbstractMethodError。

例如,objectrefa,其类是B,解析的方法是{{1}中的方法(invokevirtual中的方法{1}})


tl:dr-编译时确定要调用的方法,运行时确定从何处调用它。

答案 1 :(得分:0)

我认为您的问题与另一个主题有关-区分对象 和参考。从认证的SE 8专业程序员II中: 在Java中,所有对象都是通过引用访问的,因此,作为开发人员,您永远无法直接访问 到对象本身的内存中。从概念上讲,您应该考虑对象 作为内存中存在的实体,由Java运行时环境分配。而不管 对象在内存中具有的引用类型,即对象本身 不变例如,由于所有对象都继承java.lang.Object,因此它们都可以是 重新分配给java.lang.Object,如以下示例所示:

Lemur lemur = new Lemur();
Object lemurAsObject = lemur;

即使为狐猴对象分配了其他类型的引用, 该对象本身并没有改变,仍然作为Lemur对象存在于内存中。什么 那么,已经改变了我们使用以下方法访问Lemur类中的方法的能力: lemurAsObject参考。在没有明确转换为狐猴的情况下,您将在下一个中看到 部分,我们将无法再访问该对象的Lemur属性。

我们可以用以下两个规则来总结这一原理:

  1. 对象的类型确定内存中对象中存在哪些属性。
  2. 对象引用的类型确定哪些方法和变量是 Java程序可以访问。

答案 2 :(得分:0)

在您的示例A a = new B()中,a多态引用-可以指向类层次结构中不同对象的引用(在这种情况下,它是对object的引用类型为B的对象,但也可用作对类A的对象的引用,该类在对象层次结构中排在最前)。

关于您要询问的特定行为:

为什么在输出中打印B

将通过引用变量调用哪个特定的show(B obj)方法,取决于在特定时间点对其持有的对象的引用。也就是说:如果它持有对类B的对象的引用,则将调用该类中的方法(在您的情况下),但如果它指向类A的对象,则该对象的引用该对象将被调用。这就解释了为什么在输出中打印B的原因。

层次结构)。

为什么在输出中打印and A

名称相同但签名不同的子类中的

方法称为方法重载。它使用静态绑定,这意味着适当的方法将在编译时绑定。编译器不知道对象的运行时类型。

因此,在这种情况下,show(A obj)类的A 将受到约束。但是,当在运行时中实际调用该方法时,将调用类B中的实现(show(A obj)中类B的实现),因此,您请参见输出中的B and A而不是A and A


Reference for invokevirutal (an JVM instruction called when virtual methods are executed)

  

如果解析的方法不是签名多态的(第2.9节),则   invokevirtual指令的执行过程如下。

     

让C成为objectref的类。实际要调用的方法是   通过以下查找过程选择:

     

如果C包含重写的实例方法m的声明   (第5.4.5节)解析的方法,则m是要调用的方法,并且   查找过程终止。

     

否则,如果C具有超类,则相同的查找过程为   使用C的直接超类递归执行;该方法   被调用是此查找的递归调用的结果   程序。

     

否则,将引发AbstractMethodError。


对于a.show(c),适用于B的相同规则,因为C不会重载任何方法,而直接从B继承。

编辑:

逐步解释a.show(c)为什么打印B and A的原因:

  1. 编译器将对象a识别为类objectref的对象A(编译时)
  2. 因为a的类型为A,所以方法A::show(A obj)被绑定。
  3. 当代码实际执行时(即在对象show()上调用a方法),运行时将识别出引用a多态指向类型为B的对象(即由于A a = new B())(运行时)
  4. 由于C extends B,运行时将a.show(c)视为b.show(c)(或b.show(b)),因此在这种情况下使用B::show(A obj)代替obj使用类型B的对象。这就是为什么要打印“ B and A”的原因。

答案 3 :(得分:0)

您的引用类型为A,而A仅具有一种方法show(A obj),该方法已在B中被覆盖并打印B and A,这就是为什么总是打印B and A