Это действительно дразнилка мозга.
Следующий ответ еще не полностью убедителен, но мои результаты краткого взгляда на это.Может быть, это по крайней мере способствует нахождению определенного ответа.На части вопроса уже дан ответ, поэтому я сосредоточился на том, что все еще вызывает путаницу и пока не объясняется.
Критический случай можно свести к четырем классам:
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
называется
Легко обнаружить нечетное здесь: One woВы, естественно, ожидаете, что приведение E
к A
должно вызвать вызов метода 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) , определяемый так, как можно было бы ожидать.
Все еще нетОчевидная причина наблюдаемого поведения.
Затем я начал смотреть на то, что на самом деле происходит, когда встречается invokevirtual
, и углубился в функцию LinkResolver::resolve_method
OpenJDK., но на данный момент я не полностью уверен, что это правильное место, и я в настоящее время не могу тратить больше времени здесь ...
Может быть, другие могут продолжить отсюда или найти вдохновение для своих собственных исследований.По крайней мере, тот факт, что компилятор делает правильные вещи, и причуды, по-видимому, в обработке invokevirtual
, может быть отправной точкой.