Java 中的继承和重载

Inheritance and overloading in Java

我正在备考,我需要一些帮助来理解以下代码片段中发生的事情。

class A {
    public void method1(A X) {
        System.out.println("A");
    }
}
class B extends A {
    public void method2() {
        System.out.println("B");
    }
}
class C extends B {
    public void method1(A x) {
        System.out.println("C");
    }
}
class D extends C {
    public void method1(D x) {
        System.out.println("D");
    }
}
public class test {
    public static void main(String[] args) {
        C c = new D();
        B b = c;
        c.method1(c);  // prints C
        b.method1(b);  // prints C
    }
}

好的,这就是我的想法:c.method1(c) 调用 C 中的 method1 而不是 D 中的 method1,因为 c 被清除为 C,因此 D 中的 method1 不可见。但是 b.method1(b) 比较难。 B 没有 method1,我假设将使用超类中的 method1,但事实并非如此。为什么要使用C中的方法?我在 b 中放入了一个新的 D,但是 D 的特化是不可见的,因为 b 来自类型 B。

引用实例的变量类型与调用的方法无关。

对象的类型定义了调用哪个方法; subclasses overriding 方法来改变实例的 behaviour,同时仍然保持它们对 super 类型变量的可分配性class。这就是 OO 的本质 - 请参阅 Liskov substitution principle.

然而,当涉及到重载方法时,持有参数的 varialbe 的类型控制着调用哪个方法——它所引用的实例的类型是无关紧要的。

你的方法覆盖被破坏了。

Class C 中的方法等同于 Class D 中的方法是不同的。您没有正确输入。签名是不同的。

您可以通过在您的方法上写 @Override 来测试它。

    @Override
    public void method1(D x) {
        System.out.println("D");
    }

由于您在子类中更改了该方法的签名,因此上述代码不会编译并给您一个错误。

让我们看看您的示例代码并评论调用了哪个方法,为什么调用它以及为什么这里的所有内容都不是继承的。

class A {
    public void method1(A X) {
        System.out.println("A");      <--|
    }                                    |
}                                        |
class B extends A {                      |
    public void method2() {              | This is the only overriding happening
        System.out.println("B");         | because they share the signature method1(A)
    }                                    | The other signatures are method1() from class B
}                                        | and method1(D) from class D
class C extends B {                      |
    public void method1(A x) {           |
        System.out.println("C");         |
    }                             -------
}
class D extends C {
    public void method1(D x) {
        System.out.println("D");
    }
}
public class test {
    public static void main(String[] args) {
        C c = new D();
        B b = c;
        c.method1(c);  // prints C
        b.method1(b);  // prints C
    }
}   

由于您是从定义为 C 的对象调用方法 1,因此您将获得结果 C。 除了 Object class 的方法之外,此 class 已知的唯一方法是 method1(A) 和 method1()。由于您的 Class beeing 定义为 C 它将正确调用 class C 中的方法,因为 method1 函数 来自 class D 不会覆盖来自 class C.

的那个

编辑以回答您的评论:

此时您正在调用带有签名 method1(A) 的方法。因为你的 Object 实际上代表了 class D 它会注意到方法 method1(A) 被以前继承的 class C 和因此将打印 C。简而言之,class 的定义定义了允许调用哪些方法(在对象的可见范围内),但它引用的对象的实际类型定义了如何调用方法(如果它在某些时候被覆盖了)。

如果我们看到 class C 中的代码方法 method1 正在被 B 覆盖,B 最终继承自 class A。由于覆盖是运行时多态性,尽管您已经创建了 class D 的实例,但是由于这两种方法不同,它调用 class C method1

如果你想调用D的方法,你需要将方法的参数更改为A

总而言之,每个方法在每个继承级别的可见性如下:

class D:

public void method1(D x) { System.out.println("D"); }
public void method1(A x) { System.out.println("C"); }
public void method2()    { System.out.println("B"); }

class C:

public void method1(A x) { System.out.println("C"); }
public void method2()    { System.out.println("B"); }

class乙:

public void method1(A x) { System.out.println("A"); }
public void method2()    { System.out.println("B"); }

class答:

public void method1(A x) { System.out.println("A"); }

同样重要的是要指出您示例中的变量 'c' 和 'b' 是同一个对象,并且是 D.

的实例

所以...如果你调用c.method1(c);,它会打印"C",因为D是A的一个实例(它实际上是一个D,但它也是一个继承的A),所以你可以调用 method1(A),对于 D,打印 "C"。 (那是满嘴)。为什么它不打印 "D" 你会问?因为变量声明为C,编译器只能link到method1(A).

如果您调用 b.method1(b);,它会打印 "C",因为您的变量 b 实际上是 D 的实例,因为您将其创建为 new D() ].实际上 cb 指向相同类型的对象 D.

调用方法时,JVM 在这种情况下会查看对象的实际类型 D,而不是它声明为 B.

的类型

一个很好的记忆方法是当你有类似

的东西时

B b = new D()

等式的左边部分主要由编译器使用。请记住,method1(A)method1(D) 是两种不同的方法(因为不完全相同的签名,不同的参数类型)。

JVM 在运行时使用等式的右边部分。它定义了该变量 b.

在运行时的实际类型

调用对象方法的原理很简单。系统会从对象的class of instantiation开始查找方法,然后他会向上找它的直母,直到找到为止。例如 : 如果你用 D class 实例化,假设 method1 只存在于 A class 中。 所以系统将完成这个研究步骤:

D(not found) > C(not found) > B(not found) > A(found) 

所以他会使用 A class method

如果我没猜错,你的问题是,为什么你打印的是 "C",而不是 b.method1(b) 的 "A",如你所料。 答案很简单,你在这里处理的是动态分派(动态绑定)和静态分派(静态绑定)的组合。第一个是基于运行时类型完成的,第二个是基于编译时类型的。

注意,顺便说一句,在 D 中你有 两个 方法,具有不同的签名(记住,要覆盖,你需要签名的精确匹配,所以在这种情况下它是重载 == 编译时多态性):

public void method1(D x)
public void method1(A x)

那么,当您执行 b.method1(b).

时,这里发生了什么

您需要先确定要调用的方法签名,然后再找到匹配的方法。第一个是基于编译时类型。您传递的参数是 B 类型(隐式向上转换),因此它选择 method1(A x)method1(D x) 具有不适合的签名)并将向上搜索 method1(A x) 继承树以找到比赛。第一场比赛在 C 中。 有关详细信息,请查看此处:JLS,尤其是。解释:

When a method is invoked (§15.12), the number of actual arguments (and any explicit type arguments) and the compile-time types of the arguments are used, at compile time, to determine the signature of the method that will be invoked (§15.12.2)