Java >> Java tutorial >  >> Tag >> package

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

  • når du caster et objekt af typen D til A , metoden fra type D hedder
  • når du caster et objekt af typen E til A , metoden fra type E hedder (!)
  • når du caster et objekt af typen D til D , metoden fra type D hedder
  • når du caster et objekt af typen E til D , metoden fra type D 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:

  1. 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.

  2. 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.

  3. 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.


Java tag