3 回答
TA贡献1830条经验 获得超9个赞
我理解
D.m()
hidesA.m()
,但是强制转换A
应该暴露隐藏的m()
方法,是这样吗?
不存在隐藏实例(非静态)方法之类的事情。这是一个阴影示例。在大多数地方,强制转换A
只是有助于解决歧义(例如,c.m()
原样可以同时引用A#m
和C#m
[无法从a
] 访问),否则会导致编译错误。
或者是不顾事实而覆盖
D.m()
并打破继承链?A.m()
B.m()
C.m()
b.m()
是一个不明确的调用,因为如果您将可见性因素放在一边,则 和 都适用A#m
。B#m
也同样如此c.m()
。((A)b).m()
并((A)c).m()
明确指出A#m
调用者可以访问哪些内容。
((A)d).m()
更有趣的是: 和A
都D
位于同一个包中(因此,可访问[这与上面两种情况不同])并且D
间接继承A
. 在动态分派期间,Java 将能够调用D#m
,因为D#m
实际上覆盖了A#m
,并且没有理由不调用它(尽管继承路径上发生了混乱[记住,由于可见性问题,既不覆盖B#m
也不C#m
覆盖])。A#m
更糟糕的是,下面的代码显示了覆盖的效果,为什么?
我无法解释这一点,因为这不是我期望的行为。
我敢说结果是
((A)e).m(); ((A)f).m();
应该与结果相同
((D)e).m(); ((D)f).m();
这是
D D
因为无法访问 中b
和c
来自 的包私有方法a
。
TA贡献1772条经验 获得超6个赞
这确实是一个脑筋急转弯。
以下答案尚未完全确定,但我对此进行了简短的研究。也许它至少有助于找到明确的答案。问题的部分内容已经得到解答,因此我将重点放在仍然引起混乱且尚未解释的点上。
关键情况可以归结为四类:
package a;
public class A {
void m() { System.out.println("A"); }
}
package a;
import b.B;
public class D extends B {
@Override
void m() { System.out.println("D"); }
}
package b;
import a.A;
public class B extends A {
void m() { System.out.println("B"); }
}
package b;
import a.D;
public class E extends D {
@Override
void m() { System.out.println("E"); }
}
(请注意,我@Override在可能的情况下添加了注释 - 我希望这已经可以给出提示,但我还无法从中得出结论......)
和主类:
package a;
import b.E;
public class Main {
public static void main(String[] args) {
D d = new D();
E e = new E();
System.out.print("((A)d).m();"); ((A) d).m();
System.out.print("((A)e).m();"); ((A) e).m();
System.out.print("((D)d).m();"); ((D) d).m();
System.out.print("((D)e).m();"); ((D) e).m();
}
}
这里的意外输出是
((A)d).m();D
((A)e).m();E
((D)d).m();D
((D)e).m();D
所以
当将类型的对象转换
D
为时,会调用A
类型的方法D
当将类型的对象转换
E
为时,会调用A
类型的方法(!)E
当将类型的对象转换
D
为时,会调用D
类型的方法D
当将类型的对象转换
E
为时,会调用D
类型的方法D
很容易发现这里的奇怪之处:人们自然会期望强制转换E
toA
会导致调用 方法D
,因为这是同一包中的“最高”方法。观察到的行为很难从 JLS 中解释,尽管人们必须仔细地重新阅读它,以确保其中没有微妙的原因。
出于好奇,我查看了该类生成的字节码Main
。这是完整的输出javap -c -v Main
(相关部分将在下面充实):
public class a.Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // a/Main
#2 = Utf8 a/Main
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Methodref #3.#9 // java/lang/Object."<init>":()V
#9 = NameAndType #5:#6 // "<init>":()V
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 La/Main;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Class #17 // a/D
#17 = Utf8 a/D
#18 = Methodref #16.#9 // a/D."<init>":()V
#19 = Class #20 // b/E
#20 = Utf8 b/E
#21 = Methodref #19.#9 // b/E."<init>":()V
#22 = Fieldref #23.#25 // java/lang/System.out:Ljava/io/PrintStream;
#23 = Class #24 // java/lang/System
#24 = Utf8 java/lang/System
#25 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = String #29 // ((A)d).m();
#29 = Utf8 ((A)d).m();
#30 = Methodref #31.#33 // java/io/PrintStream.print:(Ljava/lang/String;)V
#31 = Class #32 // java/io/PrintStream
#32 = Utf8 java/io/PrintStream
#33 = NameAndType #34:#35 // print:(Ljava/lang/String;)V
#34 = Utf8 print
#35 = Utf8 (Ljava/lang/String;)V
#36 = Methodref #37.#39 // a/A.m:()V
#37 = Class #38 // a/A
#38 = Utf8 a/A
#39 = NameAndType #40:#6 // m:()V
#40 = Utf8 m
#41 = String #42 // ((A)e).m();
#42 = Utf8 ((A)e).m();
#43 = String #44 // ((D)d).m();
#44 = Utf8 ((D)d).m();
#45 = Methodref #16.#39 // a/D.m:()V
#46 = String #47 // ((D)e).m();
#47 = Utf8 ((D)e).m();
#48 = Utf8 args
#49 = Utf8 [Ljava/lang/String;
#50 = Utf8 d
#51 = Utf8 La/D;
#52 = Utf8 e
#53 = Utf8 Lb/E;
#54 = Utf8 SourceFile
#55 = Utf8 Main.java
{
public a.Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this La/Main;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #16 // class a/D
3: dup
4: invokespecial #18 // Method a/D."<init>":()V
7: astore_1
8: new #19 // class b/E
11: dup
12: invokespecial #21 // Method b/E."<init>":()V
15: astore_2
16: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream;
19: ldc #28 // String ((A)d).m();
21: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
24: aload_1
25: invokevirtual #36 // Method a/A.m:()V
28: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream;
31: ldc #41 // String ((A)e).m();
33: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
36: aload_2
37: invokevirtual #36 // Method a/A.m:()V
40: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream;
43: ldc #43 // String ((D)d).m();
45: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
48: aload_1
49: invokevirtual #45 // Method a/D.m:()V
52: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream;
55: ldc #46 // String ((D)e).m();
57: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
60: aload_2
61: invokevirtual #45 // Method a/D.m:()V
64: return
LineNumberTable:
line 9: 0
line 10: 8
line 11: 16
line 12: 28
line 14: 40
line 15: 52
line 16: 64
LocalVariableTable:
Start Length Slot Name Signature
0 65 0 args [Ljava/lang/String;
8 57 1 d La/D;
16 49 2 e Lb/E;
}
SourceFile: "Main.java"
有趣的是方法的调用:
16: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream;
19: ldc #28 // String ((A)d).m();
21: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
24: aload_1
25: invokevirtual #36 // Method a/A.m:()V
28: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream;
31: ldc #41 // String ((A)e).m();
33: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
36: aload_2
37: invokevirtual #36 // Method a/A.m:()V
40: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream;
43: ldc #43 // String ((D)d).m();
45: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
48: aload_1
49: invokevirtual #45 // Method a/D.m:()V
52: getstatic #22 // Field java/lang/System.out:Ljava/io/PrintStream;
55: ldc #46 // String ((D)e).m();
57: invokevirtual #30 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
60: aload_2
61: invokevirtual #45 // Method a/D.m:()V
字节码显式A.m
引用前两次调用中的方法,并显式引用D.m
第二次调用中的方法。
我从中得出的一个结论是:罪魁祸首不是编译器,而是invokevirtual
JVM 指令的处理!
的文档invokevirtual
不包含任何意外 - 此处仅引用相关部分:
设 C 为 objectref 的类。实际要调用的方法是通过以下查找过程选择的:
如果 C 包含覆盖(第 5.4.5 节)已解析方法的实例方法 m 的声明,则 m 是要调用的方法。
否则,如果 C 有超类,则执行对覆盖已解析方法的实例方法的声明的搜索,从 C 的直接超类开始,继续搜索该类的直接超类,依此类推,直到覆盖方法已找到或不存在进一步的超类。如果找到重写方法,则该方法就是要调用的方法。
否则,如果 C 的超级接口中恰好有一个最大特定方法(第 5.4.3.3 节)与已解析方法的名称和描述符匹配并且不是抽象的,那么它就是要调用的方法。
据推测,它只是沿着层次结构向上,直到找到一个(是或)覆盖该方法的方法,并且覆盖(§5.4.5)被定义为人们自然期望的。
观察到的行为仍然没有明显的原因。
然后我开始研究invokevirtual
遇到 an 时实际发生的情况,并深入研究OpenJDK的功能,但在这一点上,我不完全LinkResolver::resolve_method
确定这是否是正确的地方,而且我目前无法投入更多时间在这里...
也许其他人可以从这里继续,或者为自己的调查找到灵感。至少编译器做了正确的事情,并且怪癖似乎存在于 的处理中invokevirtual
,这一事实可能是一个起点。
TA贡献1866条经验 获得超5个赞
有趣的问题。我在 Oracle JDK 13 和 Open JDK 13 中检查过这一点。两者给出的结果相同,与您所写的完全相同。但这个结果与Java语言规范相矛盾。
与类 D 与 A 位于同一包中不同,类 B、C、E、F 位于不同的包中,并且由于包私有声明,A.m()
无法看到它也无法覆盖它。对于 B 类和 C 类,它按照 JLS 中的规定工作。但对于 E 类和 F 类则不然。((A)e).m()
带有和 的情况((A)f).m()
是Java 编译器实现中的错误。
应该如何工作((A)e).m()
和((A)f).m()
?由于D.m()
overrides A.m()
,这也应该适用于它们的所有子类。因此,((A)e).m()
和应该与和((A)f).m()
相同,意味着它们都应该调用。((D)e).m()
((D)f).m()
D.m()
添加回答
举报