[英]How does polymorphism in Java work for this general case (method with parameter)?
我有一般情况的代码:
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)
。
还需要多态性必须满足:所选方法应包含在父类定义中。
有人可以对显示的结果做出更详细的解释吗?
为了得到结果B and A
两次的原因,你需要知道这有两个部分:编译和运行时。
汇编
遇到语句a.show(b)
,编译器会采取以下基本步骤:
a
)上调用该方法的对象并获取其声明的类型。 这种类型是A
A
类及其所有超类型中,列出名为show
的所有方法。 编译器只会找到show(A)
。 它没有查看B
或C
中的任何方法。 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
时,还需要执行几个步骤。
a
。 a = new B()
,此类型为B
B
并尝试找到方法show(A)
。 由于B
覆盖它,因此找到此方法。 如果不是这种情况,它会查找超类( A
和Object
),直到找到这样的方法。 重要的是要注意它只考虑show(A)
方法,例如。 从未考虑过来自B
show(B)
。 B
调用方法show(A)
,给出String
B and A
作为结果。 有关这方面的更多细节在invokevirtual
的规范中给出:
如果已解析的方法不是签名多态(第2.9节),则invokevirtual指令如下进行。
设C为objectref的类。 要调用的实际方法由以下查找过程选择:
如果C包含一个覆盖(§5.4.5)已解析方法的实例方法m的声明,则m是要调用的方法,并且查找过程终止。
否则,如果C具有超类,则使用C的直接超类递归地执行相同的查找过程; 要调用的方法是递归调用此查找过程的结果。
否则,引发AbstractMethodError。
对于您的示例, objectref
是a
,其类是B
并且已解析的方法是来自invokevirtual
( show(A)
来自A
)
tl:dr - 编译时确定要调用的方法,运行时确定从哪里调用它。
我认为你的问题涉及另一个主题 - 区分对象和参考。 来自Certified Professional SE 8 Programmer II:在Java中,所有对象都通过引用访问,因此作为开发人员,您永远无法直接访问对象本身的内存。 但是,从概念上讲,您应该将对象视为存储在内存中的实体,由Java运行时环境分配。 无论您在内存中对象的引用类型如何,对象本身都不会更改。 例如,由于所有对象都继承java.lang.Object,因此可以将它们全部重新分配给java.lang.Object,如以下示例所示:
Lemur lemur = new Lemur();
Object lemurAsObject = lemur;
尽管已经为Lemur对象分配了具有不同类型的引用,但对象本身并未更改,并且仍然作为内存中的Lemur对象存在。 那么,改变的是我们使用lemurAsObject引用访问Lemur类中的方法的能力。 如果没有明确的回归Lemur,正如您将在下一节中看到的那样,我们无法再访问对象的Lemur属性。
我们可以用以下两个规则来总结这个原则:
在您的示例A a = new B()
, a
是多态引用 - 可以将不同对象指向类层次结构的引用(在这种情况下,它是对类型B
对象的引用,但也可以用作对类A
对象,它是对象层次结构中的最顶层)。
至于你要问的具体行为:
B
在输出中打印? 将通过引用变量调用哪个特定show(B obj)
方法取决于它在某个时间点保持的对象的引用。 也就是说:如果它持有对B
类对象的引用,那么将调用该类的方法(这是你的情况),但如果它指向类A
的对象,则会调用该对象的引用。 这解释了为什么B
在输出中打印。
层次)。
and A
打印在输出中? 具有相同名称但签名不同的子类中的方法称为方法重载 。 它使用静态绑定,这意味着在编译时绑定适当的方法。 编译器不知道对象的运行时类型。
因此,在这种情况下, A
类的 show(A obj)
将受到约束。 然而,当该方法将在运行时实际上是调用,从类它的实现B
将被调用( show(A obj)
从类B
),这就是为什么你看到B and A
,而不是A and A
输出。
invokevirutal
参考(执行虚方法时调用的JVM指令) :
如果已解析的方法不是签名多态(第2.9节),则invokevirtual指令如下进行。
设C为objectref的类。 要调用的实际方法由以下查找过程选择:
如果C包含一个覆盖(§5.4.5)已解析方法的实例方法m的声明,则m是要调用的方法,并且查找过程终止。
否则,如果C具有超类,则使用C的直接超类递归地执行相同的查找过程; 要调用的方法是递归调用此查找过程的结果。
否则,引发AbstractMethodError。
对于a.show(c)
,适用与B
相同的规则,因为C
没有重载任何方法,而是直接从B
继承。
编辑:
逐步解释为什么a.show(c)
打印B and A
:
a
识别为对类A
对象的objectref
(编译时) a
是A
类型,所以绑定方法A::show(A obj)
。 a
)上调用show()
方法,运行时会识别a
多态指向B
类对象的引用(因为A a = new B()
)(运行时) C extends B
,运行时会像处理b.show(c)
(或b.show(b)
a.show(c)
一样处理b.show(c)
),所以在这种情况下使用B::show(A obj)
但是到位的obj
类型的对象B
被使用。 这就是打印“B和A”的原因。 您的参考类型是A
而A
只有一个方法show(A obj)
已在B
重写并打印B and A
,这就是为什么你总是得到B and A
打印。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.