5

当我遇到奇怪的事情时,我正在测试代码,我无法弄清楚为什么会发生这种情况。所以我将举一个简单的例子来说明那里发生的事情。

考虑这些类

public class A {
    public void print(A a) {
        System.out.println("A");
    }
}

public class B extends A {
    public void print() {
        System.out.println("B");
    }
}

public class C extends B {
    public void print(A a) {
        System.out.println("C");
    }
}

public class E extends C {
    public void print(E e) {
        System.out.println("E");
    }
}

现在在我的主要方法中,我有这种实例化:

    B b = new E();
    C c = new E();
    E e = new E();

我调用这些方法并获得以下输出。

    b.print(b);
    c.print(e);

输出:

C
C

我有两种解释,但每一种都与这些方法调用之一相矛盾。也有可能我的两个解释都是完全错误的,所以我在这里没有声称任何东西。

解释1

  • b.print(b):b是 的一个实例,E并且向上转换为B. B有一个print接受类型参数的方法,A但它被 class 重载E。然而,该方法的参数b是向上转换为 的B,因此方法调用与C.print(A a)(因为C是 的超类E)的签名匹配,因此结果是合理的。

  • c.print(e): 和上面的思路一样,这里就不解释输出了。c是 的一个实例E并且向上转换为C. C有一个print接受类型参数的方法,A但它被 class 重载E。但与上述情况相反,此方法的参数eE.print(E e). 因此,根据这种推理,输出应该是 E,但不是!

解释2

在这里,我从第二个方法调用和原因开始。

  • c.print(e):c是 的一个实例,E并且向上转换为C. C有一个print接受类型参数的方法A。这个方法的参数是ewhich 是一个实例,E而它又是 的子类A。由于它已被向上转换,因此E.print(E e)是隐藏的c。因此,方法调用与签名相匹配,C.print(A a)输出是合理的。

  • b.print(b):b是 的一个实例,E并且向上转换为B. B有一个print接受类型参数的方法A(同样它是隐藏的)。因此,方法调用与签名匹配,A.print(A a)并且通过此逻辑,输出应该是 A,但不是。

我真的很困惑这里发生了什么。有人可以解释一下。

4

4 回答 4

7

将调用哪个方法分两步决定:

  • 在编译时,根据声明的实例类型决定将使用哪些重载方法
  • 在运行时,根据实际的实例类型(多态性)决定将使用哪些被覆盖的方法

    1. b.print(b); // B b = new E();

      • 在编译时,由于bis的声明类型B,因此只能使用print接受B(或其超类 ( ))的实例,这意味着:AA.print(A a)

      • 在运行时,一旦在上一步中选择了重载方法,就会使用b( E) 的实际类型来选择print(A a)将要使用的版本:C.print(A a)overA.print(A a)

    2. c.print(e); // C c = new E(); E e = new E();

      • 在编译时,is 的声明类型和cisC的声明类型,e因此E只能使用以下方法:A.print(A a)C.print(A a)

      • 因此,在运行时,实际类型eE选择更具体(意味着在类层次结构中更高)的版本:C.print(A a)

于 2013-05-24T22:37:08.517 回答
4

我并不是要坚持 tieTYT 的答案,但我认为查看您的main方法的字节码可能是有启发性的。Foo我将您的代码包装在一个名为并用于javap -classpath . -c -s Foo反汇编它的外部类中。这是我们得到的:

public static void main(java.lang.String[]);
  Signature: ([Ljava/lang/String;)V
  Code:
     0: new           #2                  // class Foo$E
     3: dup           
     4: invokespecial #3                  // Method Foo$E."<init>":()V
     7: astore_1      
     8: new           #2                  // class Foo$E
    11: dup           
    12: invokespecial #3                  // Method Foo$E."<init>":()V
    15: astore_2      
    16: new           #2                  // class Foo$E
    19: dup           
    20: invokespecial #3                  // Method Foo$E."<init>":()V
    23: astore_3      
    24: aload_1       
    25: aload_1       
    26: invokevirtual #4                  // Method Foo$B.print:(LFoo$A;)V
    29: aload_2       
    30: aload_3       
    31: invokevirtual #5                  // Method Foo$C.print:(LFoo$A;)V
    34: return        

有趣的行是第 26 行和第 31 行。请注意,在这两行中,编译器都选择了接受 type 参数的方法A,但这似乎违反了我们的直觉。我们期待E.print(E)在第 31 行,但我们没有得到它。

发生这种情况是因为 Java 编译器在编译时不知道、和变量的实际类型。它只知道他们声明的类型。在这种情况下,您使用类构造函数来创建对象,但想象一下您是否使用静态工厂方法。这些变量的类型可能因非常复杂的逻辑而异,可能涉及反射。编译器在某些情况下可能能够确定它们的实际类型,但不可能在所有情况下都这样做。结果,编译器必须根据变量的声明类型而不是它们的实际类型来决定调用哪个方法。bce

您可能想知道为什么第 26 行会打印“C”,尽管字节码说要调用B.print(A)... 等一下,B甚至没有声明print(A)方法;它继承print(A)A. 那么为什么第 26 行的字节码不说呢// Method Foo$A.print:(LFoo$A;)V

这就是方法覆盖的用武之地。在运行时,Java 解释器将使用对象的实际类型来确定print(A)调用哪个版本。由于两个对象都是 type E,并且E没有自己的print(A)方法,Java 最终调用C.print(A).

于 2013-05-24T23:36:10.807 回答
3

我已将您的示例代码修改为如下所示:

package com.sandbox;

public class Sandbox {
    public static void main(String[] args) {
        B b = new E();
        C c = new E();
        E e = new E();
        b.print(b); //C
        c.print(e); //C
        e.print(e); //E
        e.print(b); //C
    }


    public static class A {
        public void print(A a) {
            System.out.println("A");
        }
    }

    public static class B extends A {
        public void print() {   //doesn't override or overload anyone
            System.out.println("B");
        }
    }

    public static class C extends B {
        public void print(A a) {    //overrides "A"
            System.out.println("C");
        }
    }

    public static class E extends C {
        public void print(E e) {    //Overloads A's print
            System.out.println("E");
        }
    }
}

由于 E 的方法只是被重载了,你可以这样重命名它:

package com.sandbox;

public class Sandbox {
    public static void main(String[] args) {
        B b = new E();
        C c = new E();
        E e = new E();
        b.print(b); //C
        c.print(e); //C
        e.unrelatedMethod(e); //E
        e.print(b); //C
    }


    public static class A {
        public void print(A a) {
            System.out.println("A");
        }
    }

    public static class B extends A {
        public void print() {   //doesn't override or overload anyone
            System.out.println("B");
        }
    }

    public static class C extends B {
        public void print(A a) {    //overrides "A"
            System.out.println("C");
        }
    }

    public static class E extends C {
        public void unrelatedMethod(E e) {
            System.out.println("E");
        }
    }
}

事情开始变得更有意义。我认为您的示例真正令人困惑的是您的方法具有相同的名称,但它们实际上并不相同。

让我知道这是否清楚。这两个样品完全一样,唯一的区别是命名更清晰。

于 2013-05-24T22:48:35.557 回答
1

这是相关文档,但简短的故事是:

  • 选择正确方法的过程总是由静态(覆盖)和动态(重载)部分组成;如果在覆盖后停止,可能会更直观一些;
  • 因此,在您的情况下,首先我们有静态解决方案(选择 A 类的覆盖方法),然后动态部分在运行时选择此方法的最佳重载(E 类)。
于 2013-05-24T23:03:21.980 回答