这确实是一个脑筋急转弯。
以下答案尚未完全确定,但我对此进行了简短的了解。也许它至少有助于找到一个明确的答案。部分问题已经得到解答,因此我将重点放在仍然会引起混淆且尚未解释的问题上。
危急情况可以归结为四类:
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
所以
- 将 type 的对象转换为 时
D
,调用typeA
的方法D
- 将 type 的对象转换为 时,调用 type
E
的A
方法E
(!)
- 将 type 的对象转换为 时
D
,调用typeD
的方法D
- 将 type 的对象转换为 时
E
,调用typeD
的方法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 包含一个实例方法 m 的声明,它覆盖(第 5.4.5 节)已解析的方法,则 m 是要调用的方法。
否则,如果 C 有超类,则搜索覆盖已解析方法的实例方法的声明,从 C 的直接超类开始,然后继续该类的直接超类,依此类推,直到覆盖方法找到或不存在进一步的超类。如果找到覆盖方法,它就是要调用的方法。
否则,如果 C 的超接口中恰好有一个最大特定方法(第 5.4.3.3 节)与已解析方法的名称和描述符匹配并且不是抽象的,那么它就是要调用的方法。
据说它只是在层次结构中上升,直到找到一个(是或)覆盖该方法的方法,覆盖(§5.4.5)被定义为人们自然期望的。
观察到的行为仍然没有明显的原因。
然后我开始研究遇到 an 时实际发生的情况invokevirtual
,并深入研究 OpenJDK的LinkResolver::resolve_method
功能,但那时我并不完全确定这是否是正确的地方,我目前无法投入更多时间在这里...
也许其他人可以从这里继续,或者为自己的调查寻找灵感。至少编译器做了正确的事情,并且怪癖似乎在处理 .的事实invokevirtual
可能是一个起点。