为了账号安全,请及时绑定邮箱和手机立即绑定

Java 中包可见性的继承

Java 中包可见性的继承

精慕HU 2023-08-16 18:02:47
我正在寻找以下行为的解释:我有 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"); }}
查看完整描述

3 回答

?
慕标琳琳

TA贡献1830条经验 获得超9个赞

我理解D.m()hides A.m(),但是强制转换A应该暴露隐藏的m()方法,是这样吗?

不存在隐藏实例(非静态)方法之类的事情。这是一个阴影示例。在大多数地方,强制转换A只是有助于解决歧义(例如,c.m()原样可以同时引用A#mC#m[无法从a] 访问),否则会导致编译错误。

或者是不顾事实而覆盖D.m()并打破继承链?A.m()B.m()C.m()

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#m也不C#m覆盖])。A#m

更糟糕的是,下面的代码显示了覆盖的效果,为什么?

我无法解释这一点,因为这不是我期望的行为。

我敢说结果是

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

应该与结果相同

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

这是

D
D

因为无法访问 中bc来自 的包私有方法a


查看完整回答
反对 回复 2023-08-16
?
MMMHUHU

TA贡献1834条经验 获得超8个赞

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

错误报告

我将此答案标记为解决方案,但要感谢大家的所有答案和消息,我学到了很多。:-)


查看完整回答
反对 回复 2023-08-16
?
梦里花落0921

TA贡献1772条经验 获得超6个赞

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


以下答案尚未完全确定,但我对此进行了简短的研究。也许它至少有助于找到明确的答案。问题的部分内容已经得到解答,因此我将重点放在仍然引起混乱且尚未解释的点上。


关键情况可以归结为四类:


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_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第二次调用中的方法。

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

的文档invokevirtual不包含任何意外 - 此处仅引用相关部分:

设 C 为 objectref 的类。实际要调用的方法是通过以下查找过程选择的:

  1. 如果 C 包含覆盖(第 5.4.5 节)已解析方法的实例方法 m 的声明,则 m 是要调用的方法。

  2. 否则,如果 C 有超类,则执行对覆盖已解析方法的实例方法的声明的搜索,从 C 的直接超类开始,继续搜索该类的直接超类,依此类推,直到覆盖方法已找到或不存在进一步的超类。如果找到重写方法,则该方法就是要调用的方法。

  3. 否则,如果 C 的超级接口中恰好有一个最大特定方法(第 5.4.3.3 节)与已解析方法的名称和描述符匹配并且不是抽象的,那么它就是要调用的方法。

据推测,它只是沿着层次结构向上,直到找到一个(或)覆盖该方法的方法,并且覆盖(§5.4.5)被定义为人们自然期望的。

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


然后我开始研究invokevirtual遇到 an 时实际发生的情况,并深入研究OpenJDK的功能,但在这一点上,我不完全LinkResolver::resolve_method确定这是否是正确的地方,而且我目前无法投入更多时间在这里...


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


查看完整回答
反对 回复 2023-08-16
?
心有法竹

TA贡献1866条经验 获得超5个赞

有趣的问题。我在 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()


查看完整回答
反对 回复 2023-08-16
  • 3 回答
  • 0 关注
  • 184 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信