Ответ 1
Я сообщил об этой проблеме, и была подтверждена ошибка для нескольких версий Java.
Я отмечаю этот ответ как решение, но хотел бы поблагодарить всех за все ответы и сообщения, я многому научился. :-)
Я ищу объяснение следующего поведения:
Вот классы:
package a;
public class A {
void m() { System.out.println("A"); }
}
// ------
package b;
import a.A;
public class B extends A {
void m() { System.out.println("B"); }
}
// ------
package c;
import b.B;
public class C extends B {
void m() { System.out.println("C"); }
}
// ------
package a;
import c.C;
public class D extends C {
void m() { System.out.println("D"); }
}
// ------
package b;
import a.D;
public class E extends D {
void m() { System.out.println("E"); }
}
// ------
package c;
import b.E;
public class F extends E {
void m() { System.out.println("F"); }
}
Основной класс находится в package a
:
package a;
import b.B;
import b.E;
import c.C;
import c.F;
public class Main {
public static void main(String[] args) {
A a = new A();
B b = new B();
C c = new C();
D d = new D();
E e = new E();
F f = new F();
System.out.println("((A)a).m();"); ((A)a).m();
System.out.println("((A)b).m();"); ((A)b).m();
System.out.println("((A)c).m();"); ((A)c).m();
System.out.println("((A)d).m();"); ((A)d).m();
System.out.println("((A)e).m();"); ((A)e).m();
System.out.println("((A)f).m();"); ((A)f).m();
System.out.println("((D)d).m();"); ((D)d).m();
System.out.println("((D)e).m();"); ((D)e).m();
System.out.println("((D)f).m();"); ((D)f).m();
}
}
А вот и вывод:
((A)a).m();
A
((A)b).m();
A
((A)c).m();
A
((A)d).m();
D
((A)e).m();
E
((A)f).m();
F
((D)d).m();
D
((D)e).m();
D
((D)f).m();
D
А вот и мои вопросы:
1) Я понимаю, что D.m()
скрывает A.m()
, но приведение к A
должно раскрывать скрытый метод m()
, это правда? Или D.m()
переопределяет A.m()
, несмотря на то, что B.m()
и C.m()
разрывает цепочку наследования?
((A)d).m();
D
2) Еще хуже, следующий код показывает переопределение, почему?
((A)e).m();
E
((A)f).m();
F
И почему бы не в этой части:
((A)a).m();
A
((A)b).m();
A
((A)c).m();
A
а этот?
((D)d).m();
D
((D)e).m();
D
((D)f).m();
D
Я использую OpenJDK javac 11.0.2.
ОБНОВЛЕНИЕ: На первый вопрос отвечает Как переопределить метод с областью видимости по умолчанию (пакет)?
Метод экземпляра mD, объявленный или унаследованный классом D, переопределяет из D другой метод mA, объявленный в классе A, если все следующее верно:
- A является суперклассом D.
- D не наследует мА (потому что пересекает границы пакета)
- Подпись mD является подписью (§8.4.2) подписи mA.
- Одно из следующего верно: [...]
- mA объявляется с доступом к пакету в том же пакете, что и D (в этом случае), и либо D объявляет mD, либо mA является членом прямого суперкласса D. [...]
НО: второй вопрос до сих пор не решен.
Я сообщил об этой проблеме, и была подтверждена ошибка для нескольких версий Java.
Я отмечаю этот ответ как решение, но хотел бы поблагодарить всех за все ответы и сообщения, я многому научился. :-)
Я понимаю, что
D.m()
скрываетA.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
нет доступа к частным методам пакета.
Интересный вопрос Я проверил это в 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()
переопределяет A.m()
, это должно выполняться и для всех их подклассов. Таким образом, ((A)e).m()
и ((A)f).m()
должны совпадать с ((D)e).m()
и ((D)f).m()
, это означает, что все они должны вызывать D.m()
.
Это действительно дразнилка мозга.
Следующий ответ еще не полностью окончательный, но мои результаты краткого обзора этого. Может быть, это по крайней мере способствует нахождению определенного ответа. На некоторые части вопроса уже дан ответ, поэтому я сосредоточился на том, что все еще вызывает путаницу и пока не объясняется.
Критический случай можно свести к четырем классам:
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
к 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), который соответствует разрешенному имени и дескриптору метода и не является абстрактным, то это метод, который нужно вызвать.
Предположительно, он просто идет вверх по иерархии, пока не найдет метод, который (is или) переопределяет метод, причем переопределяет (§5.4.5), определяемый так, как можно было бы ожидать.
До сих пор нет очевидной причины наблюдаемого поведения.
Затем я начал смотреть на то, что на самом деле происходит при обнаружении invokevirtual
, и углубился в функцию LinkResolver::resolve_method
OpenJDK, но в этот момент я не совсем уверен, является ли это это подходящее место, и я в настоящее время не могу уделять здесь больше времени...
Может быть, другие могут продолжить отсюда или найти вдохновение для своих собственных исследований. По крайней мере тот факт, что компилятор делает правильные вещи, и причуды, по-видимому, связаны с обработкой invokevirtual
, может быть отправной точкой.