【问题标题】:Inheritance at package visibility in JavaJava中包可见性的继承
【发布时间】:2020-01-22 22:43:30
【问题描述】:

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

  • 我有 6 个类,{a.A,b.B,c.C,a.D,b.E,c.F},每个类都有一个写出类名的包可见 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。


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

在类 D 中声明或继承的实例方法 mD,如果满足以下所有条件,则从 D 覆盖在类 A 中声明的另一个方法 mA:

  • A 是 D 的超类。
  • D 不继承 mA(因为跨越了封装边界)
  • mD 的签名是 mA 签名的子签名 (§8.4.2)。
  • 下列情况之一为真: [...]
    • mA 在与 D 相同的包中声明为具有包访问权限(本例),并且 D 声明 mD 或 mA 是 D 的直接超类的成员。 [...]

但是:第二个问题仍未解决。

【问题讨论】:

  • 我喜欢这个问题,但是,伙计,你真的需要 六个 类来说明这一点吗?
  • @Andrew Tobilko:有效点,抱歉。我想你不会对最初写的 12 门课感到满意。 :-)
  • @dyukha:我还没有看到建议的重复链接如何解释虚拟调度......但也许只是我。
  • 第一个问题的答案是:假。强制转换不会更改在运行时调用的方法。如果将List 转换为 Object 并调用其 toString 方法,则不会调用 Object 的默认 toString 方法。在我看来,您的第二个问题暴露了一个 Java 错误。永远不可能在另一个包中调用包私有功能。
  • 显然这种混乱始于 20 年前...请参阅错误数据库中的 link1link2link3。例如。如果一个类只是因为抽象包可见方法而成为抽象类,那么从前一个类派生的另一个包中的类是抽象的吗?等等……

标签: java inheritance overriding shadowing package-private


【解决方案1】:

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

Bug report.

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

【讨论】:

    【解决方案2】:

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

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

    危急情况可以归结为四类:

    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 类型的方法

    在这里很容易发现奇怪的:人们自然会认为将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

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

    documentation of invokevirtual 不包含任何意外 - 此处仅引用相关部分:

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

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

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

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

    它应该只是向上层级,直到找到一个(或)覆盖该方法的方法,overrides (§5.4.5)被定义为人们自然期望的.

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


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


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

    【讨论】:

      【解决方案3】:

      有趣的问题。我在 Oracle JDK 13 和 Open JDK 13 中检查了这一点。两者都给出了相同的结果,就像你写的一样。但是这个结果与Java Language Specification相矛盾。

      与与A在同一个包中的D类不同,B、C、E、F类在一个不同包中,并且由于A.m()的包私有声明不能看到它并且不能覆盖它。对于 B 类和 C 类,它按照 JLS 中的规定工作。但对于 E 类和 F 类则不然。 ((A)e).m()((A)f).m()的情况是Java编译器实现中的bugs

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

      【讨论】:

      • 我发现((A)e).m()((A)f).m() 都没有问题——它们应该编译。不过,它们产生的结果很奇怪。
      • 他们会编译。但是是的,不管你在层次结构中的哪个类,如果一个方法是可见的,那么执行结果必须是相同的。
      【解决方案4】:

      我知道D.m() 隐藏了A.m(),但是转换为A 应该暴露隐藏的m() 方法,这是真的吗?

      没有隐藏例如(非静态)方法的东西。这里是shadowing 的示例。在大多数地方对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 和@987654351 @ 覆盖 A#m 由于可见性问题])。

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

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

      我敢说结果

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

      应该与结果相同

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

      这是

      D
      D
      

      因为无法从a 访问bc 中的包私有方法。

      【讨论】:

        猜你喜欢
        • 2012-10-14
        • 2014-04-14
        • 1970-01-01
        • 2020-02-07
        • 2012-11-30
        • 2012-06-17
        • 2016-07-30
        • 2019-06-01
        • 2020-02-11
        相关资源
        最近更新 更多