Java中的方法覆盖和重载

时间:2013-05-24 22:33:48

标签: java methods overloading override

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

考虑这些课程

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)bE的一个实例,并且已升级到BB有一个print方法,它接受A类型的参数,但它被类E重载。但是,此方法的参数是b,它会升级到B,因此方法调用与C.print(A a)的签名匹配(因为C是{{1}的超类因此结果是合理的。

  • E:以同样的方式思考,不会在这里解释输出。 c.print(e)c的一个实例,并且会升级到EC有一个C方法,它接受print类型的参数,但它被类A重载。但与上述情况相反,此方法的参数是E,它匹配e的签名。所以通过这个推理,输出应该是E,而不是!

解释2

这里我从第二个方法调用开始,然后推理出来。

  • E.print(E e)c.print(e)c的一个实例,并且已升级到EC有一个C方法,它接受print类型的参数。此方法的参数是A,它是e的一个实例,后者又是E的子类。由于已提升AE.print(E e)隐藏了c。因此,方法调用匹配C.print(A a)的签名,并且此逻辑的输出是合理的。

  • b.print(b)bE的一个实例,并且已升级到BB有一个print方法,它接受A类型的参数(同样它是隐藏的)。因此,方法调用匹配A.print(A a)的签名,并且通过该逻辑,输出应该是A,而不是。

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

4 个答案:

答案 0 :(得分:7)

将通过两个步骤决定调用哪种方法:

  • 在编译时,根据声明的实例类型
  • 决定使用哪种重载方法
  • 在运行时,根据实际实例类型( polymorphism )确定将使用哪种重写方法< / p>

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

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

      • 在运行时,一旦在上一步中选择了重载方法,bE)的实际类型将用于选择print(A a)的{​​{1}}版本将被使用:C.print(A a)超过A.print(A a)

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

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

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

        < / LI>

答案 1 :(得分: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.请注意,在两行中,编译器选择了采用类型A的参数的方法,但这似乎违反了我们的直觉。我们预计第31行会E.print(E),但我们没有得到它。

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

您可能想知道为什么第26行打印“C”,即使字节码表示要调用B.print(A) ...并等待一分钟,B甚至没有声明print(A)方法;它从print(A)继承A。那么为什么第26行的字节码不是// Method Foo$A.print:(LFoo$A;)V

这就是方法重写的地方。在运行时,Java解释器将使用对象的实际类型来确定调用哪个版本的print(A)。由于两个对象都是E类型,而E没有自己的print(A)方法,因此Java最终会调用C.print(A)

答案 2 :(得分: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");
        }
    }
}

事情开始变得更有意义了。我认为你的样本真正令人困惑的是你的方法有相同的名称,但它们的方式并不相同。

如果这说清楚,请告诉我。那两个样本完全一样,唯一的区别是命名更清晰。

答案 3 :(得分:1)

这是relevant documentation,但短篇小说是:

  • 选择正确方法的过程总是由静态(覆盖)和动态(重载)部分组成;如果它在覆盖后停止,它可能会更直观一些;
  • 所以在你的情况下,首先我们有静态分辨率(选择A类的覆盖方法),然后动态部分在运行时选择该方法的最佳重载(E类)。