Nedarvning ved pakkesynlighed i Java
Jeg forstår det D.m()
skjuler A.m()
, men en cast til A
skal afsløre den skjulte m()
metode, er det sandt?
Der er ikke noget, der hedder at skjule for eksempel (ikke-statiske) metoder. Her er det et eksempel på skygge. En cast til A
de fleste steder hjælper det bare med at løse tvetydigheden (f.eks. c.m()
som det er, kan henvise til både A#m
og C#m
[som ikke er tilgængelig fra a
]), der ellers ville føre til en kompileringsfejl.
Eller er D.m()
tilsidesætter A.m()
på trods af at B.m()
og C.m()
bryder arvekæden?
b.m()
er et tvetydigt kald, fordi begge A#m
og B#m
er gældende, hvis du sætter synlighedsfaktoren til side. Det samme gælder for c.m()
. ((A)b).m()
og ((A)c).m()
henvise tydeligt til A#m
som er tilgængelig for den, der ringer op.
((A)d).m()
er mere interessant:både A
og D
bor i den samme pakke (altså tilgængelig [som er forskellig fra de to ovenstående tilfælde]) og D
arver indirekte A
. Under dynamisk afsendelse vil Java være i stand til at kalde D#m
fordi D#m
tilsidesætter faktisk A#m
og der er ingen grund til ikke at kalde det (på trods af det rod, der foregår på arvestien [husk at hverken B#m
heller ikke C#m
tilsidesætter A#m
på grund af synlighedsproblemet]).
Endnu værre, den følgende kode viser tilsidesættelse i kraft, hvorfor?
Jeg kan ikke forklare dette, fordi det ikke er den adfærd, jeg forventede.
Jeg tør godt sige, at resultatet af
((A)e).m();
((A)f).m();
skal være identisk med resultatet af
((D)e).m();
((D)f).m();
hvilket er
D
D
da der ikke er nogen måde at få adgang til de pakke-private metoder i b
og c
fra a
.
Interessant spørgsmål. Det tjekkede jeg i Oracle JDK 13 og Open JDK 13. Begge giver det samme resultat, præcis som du skrev. Men dette resultat er i modstrid med Java Language Specification.
I modsætning til klasse D, som er i samme pakke som A, er klasse B, C, E, F anden pakke og på grund af pakkens private erklæring på A.m()
kan ikke se det og kan ikke tilsidesætte det. For klasse B og C fungerer det som specificeret i JLS. Men for klasse E og F gør det ikke. Sagerne med ((A)e).m()
og ((A)f).m()
er bugs i implementeringen af Java compiler.
Hvordan skal arbejde ((A)e).m()
og ((A)f).m()
? Siden D.m()
tilsidesætter A.m()
, bør dette også gælde for alle deres underklasser. Således både ((A)e).m()
og ((A)f).m()
skal være det samme som ((D)e).m()
og ((D)f).m()
, betyder, at de alle skal ringe til D.m()
.
Dette er virkelig en hjernevrider.
Det følgende svar er endnu ikke helt afgørende, men mine resultater af at have et kort kig på dette. Måske bidrager det i det mindste til at finde et entydigt svar. Dele af spørgsmålet er allerede blevet besvaret, så jeg fokuserer på det punkt, der stadig skaber forvirring og endnu ikke er forklaret.
Det kritiske tilfælde kan koges ned til fire klasser:
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"); }
}
(Bemærk, at jeg tilføjede @Override
annoteringer, hvor det var muligt - jeg håbede, at dette allerede kunne give et hint, men jeg var ikke i stand til at drage konklusioner fra det endnu...)
Og hovedklassen:
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();
}
}
Det uventede output her er
((A)d).m();D
((A)e).m();E
((D)d).m();D
((D)e).m();D
Så
- når du caster et objekt af typen
D
tilA
, metoden fra typeD
hedder - når du caster et objekt af typen
E
tilA
, metoden fra typeE
hedder (!) - når du caster et objekt af typen
D
tilD
, metoden fra typeD
hedder - når du caster et objekt af typen
E
tilD
, metoden fra typeD
hedder
Det er nemt at få øje på det mærkelige herude:Man ville naturligvis forvente, at casting af en E
til A
skal forårsage metoden D
at blive kaldt, fordi det er den "højeste" metode i samme pakke. Den observerede adfærd kan ikke let forklares ud fra JLS, selvom man ville være nødt til at genlæse den omhyggeligt , for at være sikker på, at der ikke er en subtil grund til det.
Af nysgerrighed kiggede jeg på den genererede bytekode for Main
klasse. Dette er hele outputtet af javap -c -v Main
(de relevante dele vil blive uddybet nedenfor):
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"
Det interessante er påkaldelsen af metoderne:
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
Bytekoden eksplicit henviser til metoden A.m
i de første to opkald, og eksplicit henviser til metoden D.m
i det andet opkald.
En konklusion, som jeg drager af det:Synderen er ikke compileren, men håndteringen af invokevirtual
instruktion af JVM!
Dokumentationen for invokevirtual
indeholder ingen overraskelser - citerer kun den relevante del her:
Lad C være klassen af objektref. Den faktiske metode, der skal aktiveres, vælges ved følgende opslagsprocedure:
-
Hvis C indeholder en erklæring for en instansmetode m, der tilsidesætter (§5.4.5) den løste metode, så er m metoden, der skal påberåbes.
-
Ellers, hvis C har en superklasse, udføres en søgning efter en erklæring af en instansmetode, der tilsidesætter den løste metode, startende med den direkte superklasse af C og fortsætter med den direkte superklasse af den klasse, og så videre, indtil en tilsidesættende metode findes, eller der findes ingen yderligere superklasser. Hvis der findes en tilsidesættende metode, er det den metode, der skal påberåbes.
-
Ellers, hvis der er nøjagtig én maksimalt specifik metode (§5.4.3.3) i supergrænseflader af C, der matcher den løste metodes navn og deskriptor og ikke er abstrakt, så er det metoden, der skal påberåbes.
Det går angiveligt bare op i hierarkiet, indtil det finder en metode, der (er eller) tilsidesætter metoden, hvor tilsidesættelser (§5.4.5) er defineret som man naturligt ville forvente.
Stadig ingen åbenlys årsag til den observerede adfærd.
Jeg begyndte så at se på, hvad der rent faktisk sker, når en invokevirtual
er stødt på, og boret ned i LinkResolver::resolve_method
funktion af OpenJDK, men på det tidspunkt er jeg ikke helt sikker på, om dette er det rigtige sted at kigge på, og jeg kan i øjeblikket ikke investere mere tid her...
Måske kan andre fortsætte herfra, eller finde inspiration til deres egne undersøgelser. I det mindste det faktum, at kompilatoren gør det rigtige, og særheden ser ud til at være i håndteringen af invokevirtual
, kan være et udgangspunkt.