21

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

  • 我有 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()hides A.m(),但是演员A应该暴露 hiddenm()方法,这是真的吗?还是尽管打破了继承链,但还是会被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。


编辑:第一个问题由如何覆盖具有默认(包)可见性范围的方法来回答?

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

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

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

4

4 回答 4

7

我知道D.m()hides A.m(),但是演员A应该暴露 hiddenm()方法,是真的吗?

没有隐藏例如(非静态)方法的东西。在这里,它是一个阴影的例子。在大多数地方,强制转换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

于 2019-09-22T21:04:04.233 回答
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()和的情况是Java编译器实现中((A)f).m()错误

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

于 2019-09-22T21:17:03.580 回答
3

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

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

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

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

所以

  • 将 type 的对象转换为 时D,调用typeA的方法D
  • 将 type 的对象转换为 时,调用 typeEA方法E(!)
  • 将 type 的对象转换为 时D,调用typeD的方法D
  • 将 type 的对象转换为 时E,调用typeD的方法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 包含一个实例方法 m 的声明,它覆盖(第 5.4.5 节)已解析的方法,则 m 是要调用的方法。

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

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

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

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


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


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

于 2019-09-23T14:33:29.437 回答
3

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

错误报告

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

于 2019-10-21T12:04:32.103 回答