Java 中包可见性的继承

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

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

}


精慕HU
浏览 185回答 3
3回答

慕标琳琳

我理解D.m()hides 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。

MMMHUHU

我报告了这个问题,并确认了多个 Java 版本的错误。错误报告。我将此答案标记为解决方案,但要感谢大家的所有答案和消息,我学到了很多。:-)

梦里花落0921

这确实是一个脑筋急转弯。以下答案尚未完全确定,但我对此进行了简短的研究。也许它至少有助于找到明确的答案。问题的部分内容已经得到解答,因此我将重点放在仍然引起混乱且尚未解释的点上。关键情况可以归结为四类: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很容易发现这里的奇怪之处:人们自然会期望强制转换EtoA会导致调用 方法D,因为这是同一包中的“最高”方法。观察到的行为很难从 JLS 中解释,尽管人们必须仔细地重新阅读它,以确保其中没有微妙的原因。出于好奇,我查看了该类生成的字节码Main。这是完整的输出javap -c -v Main(相关部分将在下面充实):public class a.Main  minor version: 0  major version: 52  flags: ACC_PUBLIC, ACC_SUPERConstant 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;)V24: aload_125: invokevirtual #36                 // Method a/A.m:()V28: 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;)V36: aload_237: invokevirtual #36                 // Method a/A.m:()V40: 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;)V48: aload_149: invokevirtual #45                 // Method a/D.m:()V52: 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;)V60: aload_261: invokevirtual #45                 // Method a/D.m:()V字节码显式A.m引用前两次调用中的方法,并显式引用D.m第二次调用中的方法。我从中得出的一个结论是:罪魁祸首不是编译器,而是invokevirtualJVM 指令的处理!的文档invokevirtual不包含任何意外 - 此处仅引用相关部分:设 C 为 objectref 的类。实际要调用的方法是通过以下查找过程选择的:如果 C 包含覆盖(第 5.4.5 节)已解析方法的实例方法 m 的声明,则 m 是要调用的方法。否则,如果 C 有超类,则执行对覆盖已解析方法的实例方法的声明的搜索,从 C 的直接超类开始,继续搜索该类的直接超类,依此类推,直到覆盖方法已找到或不存在进一步的超类。如果找到重写方法,则该方法就是要调用的方法。否则,如果 C 的超级接口中恰好有一个最大特定方法(第 5.4.3.3 节)与已解析方法的名称和描述符匹配并且不是抽象的,那么它就是要调用的方法。据推测,它只是沿着层次结构向上,直到找到一个(是或)覆盖该方法的方法,并且覆盖(§5.4.5)被定义为人们自然期望的。观察到的行为仍然没有明显的原因。然后我开始研究invokevirtual遇到 an 时实际发生的情况,并深入研究OpenJDK的功能,但在这一点上,我不完全LinkResolver::resolve_method确定这是否是正确的地方,而且我目前无法投入更多时间在这里...也许其他人可以从这里继续,或者为自己的调查找到灵感。至少编译器做了正确的事情,并且怪癖似乎存在于 的处理中invokevirtual,这一事实可能是一个起点。

心有法竹

有趣的问题。我在 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()overrides A.m(),这也应该适用于它们的所有子类。因此,((A)e).m()和应该与和((A)f).m()相同,意味着它们都应该调用。((D)e).m()((D)f).m()D.m()
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Java