Java 中包可见性的继承

Inheritance at package visibility in Java

我正在寻找以下行为的解释:

这里是 classes:

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"); }
}

主要 class 在 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。


编辑:第一个问题由 How to override a method with default (package) visibility scope?

回答

An instance method mD declared in or inherited by class D, overrides from D another method mA declared in class A, iff all of the following are true:

  • A is a superclass of D.
  • D does not inherit mA (because crossing package boundaries)
  • The signature of mD is a subsignature (§8.4.2) of the signature of mA.
  • One of the following is true: [...]
    • mA is declared with package access in the same package as D (this case), and either D declares mD or mA is a member of the direct superclass of D. [...]

BUT:第二个问题还没有解决。

I understand that D.m() hides A.m(), but a cast to A should expose the hidden m() method, is that true?

不存在隐藏实例(非静态)方法这样的东西。在这里,它是 shadowing 的示例。在大多数地方转换为 A 只是有助于解决歧义(例如 c.m() 可以指代 A#mC#m [无法从 a]) 否则会导致编译错误。

Or is D.m() overrides A.m() in spite of the fact that B.m() and C.m() breaks the inheritance chain?

b.m() 是一个模棱两可的调用,因为如果您将可见性因素放在一边,A#mB#m 都适用。 c.m() 也是如此。 ((A)b).m()((A)c).m() 明确指的是调用者可以访问的 A#m

((A)d).m() 更有趣:AD 都位于同一个包中(因此,可访问 [与上述两种情况不同])和 D间接继承A。在动态调度期间,Java 将能够调用 D#m 因为 D#m 实际上覆盖了 A#m 并且没有理由不调用它(尽管继承路径上发生了混乱[请记住,由于可见性问题,B#mC#m 都不会覆盖 A#m])。

Even worse, the following code shows overriding in effect, why?

我无法解释,因为这不是我预期的行为。

我敢说

的结果
((A)e).m();
((A)f).m();

应该与

的结果相同
((D)e).m();
((D)f).m();

D
D

因为无法从 a.

访问 bc 中的包私有方法

有趣的问题。我在 Oracle JDK 13 和 Open JDK 13 中检查过。两者都给出相同的结果,与你写的完全一样。但是这个结果与Java Language Specification.

相矛盾

不像classD,它和A在同一个包里,class是B,C,E,F在不同的包里并且由于 A.m() 的包私有声明无法看到它并且无法覆盖它。对于 classes B 和 C,它按照 JLS 中的规定工作。但是对于 classes E 和 F 则不是。 ((A)e).m()((A)f).m() 的情况是 错误 在 Java 编译器的实现中。

应该如何工作 ((A)e).m()((A)f).m()?由于 D.m() 覆盖了 A.m(),这也适用于它们的所有子 class。因此 ((A)e).m()((A)f).m() 应该与 ((D)e).m()((D)f).m() 相同,意味着它们都应该调用 D.m().

这确实是一个脑筋急转弯。

下面的回答还没有完全定论,但是我粗略看了一下这个结果。也许它至少有助于找到一个明确的答案。问题的一部分已经回答了,所以我把重点放在仍然引起混淆且尚未解释的地方。

临界情况可以归结为四个class:

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 注释 - 我希望这已经给出了提示,但我还不能从中得出结论...)

主要class:

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 的方法,因为那是"highest" 方法在同一个包中。观察到的行为不能轻易地从 JLS 中解释,尽管必须重新阅读它,仔细,以确保没有微妙的原因。


出于好奇,我查看了 Main class 生成的字节码。这是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

字节码explicitly在前两次调用中引用方法A.mexplicitly引用方法D.m 在第二次调用中。

我从中得出的一个结论:罪魁祸首是 不是 编译器,而是 JVM 的 invokevirtual 指令的处理!

documentation of invokevirtual 没有任何惊喜 - 此处仅引用相关部分:

Let C be the class of objectref. The actual method to be invoked is selected by the following lookup procedure:

  1. If C contains a declaration for an instance method m that overrides (§5.4.5) the resolved method, then m is the method to be invoked.

  2. Otherwise, if C has a superclass, a search for a declaration of an instance method that overrides the resolved method is performed, starting with the direct superclass of C and continuing with the direct superclass of that class, and so forth, until an overriding method is found or no further superclasses exist. If an overriding method is found, it is the method to be invoked.

  3. Otherwise, if there is exactly one maximally-specific method (§5.4.3.3) in the superinterfaces of C that matches the resolved method's name and descriptor and is not abstract, then it is the method to be invoked.

它应该只是在层次结构中上升,直到它找到一个方法(或)覆盖方法,overrides (§5.4.5) 被定义为人们自然期望的。

观察到的行为仍然没有明显的原因。


然后我开始研究遇到 invokevirtual 时实际发生了什么,并深入研究了 OpenJDK 的 the LinkResolver::resolve_method function,但那时,我不是 完全确定这是否是合适的地方,我目前不能在这里投入更多时间...


也许其他人可以从这里继续,或者为自己的调查寻找灵感。至少 编译器 做了正确的事情,而怪癖似乎在 invokevirtual 的处理中,这可能是一个起点。

我报告了这个问题,并确认它是几个 Java 版本的错误。

Bug report.

我将这个答案标记为解决方案,但还是要感谢大家的所有答案和留言,我学到了很多东西。 :-)