Наследование при видимости пакета в Java - PullRequest
21 голосов
/ 22 сентября 2019

Я ищу объяснение следующего поведения:

  • У меня есть 6 классов, {aA, bB, cC, aD, bE, cF}, каждый из которых имеет видимый пакет m ()метод, который записывает имя класса.
  • У меня есть класс a.Main с методом main, который выполняет некоторое тестирование этих классов.
  • Вывод, кажется, не соответствует надлежащим правилам наследования.

Вот классы:

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 не наследует mA(потому что пересекают границы пакета)
  • Подпись mD является подписями (§8.4.2) подписи mA.
  • Одно из следующих условий: [...]
    • mA объявляется с доступом к пакету в том же пакете, что и D (в этом случае), и либо D объявляет mD, либо mA является членом прямого суперкласса D. [...]

НО: второй вопрос все еще не решен.

Ответы [ 3 ]

7 голосов
/ 23 сентября 2019

Я понимаю, что D.m() скрывает A.m(), но приведение к A должно раскрыть скрытый метод m(), это правда?

Нет такоговещь, как сокрытие, например, (нестатических) методов.Вот пример shadowing .Приведение к 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, ни * 1044)* переопределяет A#m из-за проблемы с видимостью]).

Еще хуже, следующий код показывает переопределение в действительности, почему?

Я не могу этого объяснитьпотому что это не то поведение, которого я ожидал.

Я смею сказать, что результат

((A)e).m();
((A)f).m();

должен быть идентичен результату

((D)e).m();
((D)f).m();

, который равен

D
D

, поскольку в b и c из a.

нет способа получить доступ к частным методам пакета.
3 голосов
/ 23 сентября 2019

Это действительно дразнилка мозга.

Следующий ответ еще не полностью убедителен, но мои результаты краткого взгляда на это.Может быть, это по крайней мере способствует нахождению определенного ответа.На части вопроса уже дан ответ, поэтому я сосредоточился на том, что все еще вызывает путаницу и пока не объясняется.

Критический случай можно свести к четырем классам:

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.Фактический метод, который должен быть вызван, выбирается следующей процедурой поиска:

  1. Если C содержит объявление для метода экземпляра m, который переопределяет (§5.4.5) разрешенный метод, то mявляется вызываемым методом.

  2. В противном случае, если C имеет суперкласс, выполняется поиск объявления метода экземпляра, который переопределяет разрешенный метод, начиная с прямого суперклассаC и продолжение прямого суперкласса этого класса и т. Д. До тех пор, пока не будет найден переопределяющий метод или не появятся дополнительные суперклассы.Если найден переопределяющий метод, это метод, который нужно вызвать.

  3. В противном случае, если в суперинтерфейсах C есть ровно один максимально специфичный метод (§5.4.3.3), которыйсовпадает с именем и дескриптором разрешенного метода и не является абстрактным, тогда это вызываемый метод.

Предположительно, он просто поднимается вверх по иерархии, пока не найдет метод, который( - это или) переопределяет метод, при этом переопределяет (§5.4.5) , определяемый так, как можно было бы ожидать.

Все еще нетОчевидная причина наблюдаемого поведения.


Затем я начал смотреть на то, что на самом деле происходит, когда встречается invokevirtual, и углубился в функцию LinkResolver::resolve_method OpenJDK., но на данный момент я не полностью уверен, что это правильное место, и я в настоящее время не могу тратить больше времени здесь ...


Может быть, другие могут продолжить отсюда или найти вдохновение для своих собственных исследований.По крайней мере, тот факт, что компилятор делает правильные вещи, и причуды, по-видимому, в обработке invokevirtual, может быть отправной точкой.

3 голосов
/ 23 сентября 2019

Интересный вопрос.Я проверил это в Oracle JDK 13 и Open JDK 13. Оба дают одинаковый результат, точно так же, как вы написали.Но этот результат противоречит Спецификация языка Java .

В отличие от класса D, который находится в том же пакете, что и A, классы B, C, E, F в отличаются *Пакет 1006 * и из-за закрытого объявления пакета 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().

...