Java继承与多态性


问题内容

继承和多态都构成IS-
A关系吗?在运行时发生继承和“重写”多态性而在编译时发生“过载”多态性是真的吗?我之所以这样问,是因为许多论坛似乎给出了相互矛盾且常常令人困惑的答案。

谢谢!


问题答案:

对于问题的第一部分,我认为维基百科提供了一个很好的定义:

在面向对象的程序设计中,子类型多态或包含多态是类型理论中的一个概念,其中名称可以表示许多不同类的实例,只要它们与某个公共超类相关即可。包含多态性通常通过子类型化来支持,即,不同类型的对象可以完全替代另一种类型(其基本类型)的对象,因此可以通过公共接口进行处理。或者,可以通过类型强制(也称为类型转换)来实现包含多态性。

面向对象编程中的另一种称为多态性的 Wikipedia术语似乎很好地回答了您的问题。本文中的第二篇参考文章“
了解类型,数据抽象和多态性”也详细介绍了此问题。

Java中的这种子类型化功能是通过继承类和接口来实现的。尽管从继承的角度来看,Java的子类型功能可能并不总是很明显。以与泛型的协方差和逆方差为例。而且,数组在序列层次结构中的任何地方都不明显,但它们是可序列化和可克隆的。也可以说,通过原始扩展转换,Java中的数字类型也是多态的。并且运算符的行为取决于其操作数。

无论如何,继承在某些这种多态性的实现中起着重要作用。

重载与覆盖

问题的第二部分似乎是关于选择给定方法的实现。显然,如果一个类重写了一个方法,并且您创建了该类的实例,则即使要通过父类的引用访问该对象,也要调用该方法的重写版本。

正如您所指出的那样, 正确 选择方法的 实现
是在运行时完成的,现在,要调用的方法的签名是在编译时确定的。由于重载是关于具有相同名称和不同签名的不同方法的,所以这就是为什么重写方法选择发生在编译时的原因。

编译时的覆盖方法选择

第15.12节“
方法调用表达式
”中的Java语言规范(JLS)详细解释了编译器遵循的过程,以选择正确的方法进行调用。

在那里,您会注意到这是一个 编译时 任务。JLS在15.12.2小节中说:

此步骤使用 方法名称参数表达式的类型
来定位可访问和适用的方法。可能有不止一种这样的方法,在这种情况下,选择了最具体的方法。

要验证其编译时性质,可以执行以下测试。

声明一个这样的类并进行编译。

public class ChooseMethod {
   public void doSomething(Number n){
    System.out.println("Number");
   }
}

声明第二个类,该类调用第一个类的方法并进行编译。

public class MethodChooser {
   public static void main(String[] args) {
    ChooseMethod m = new ChooseMethod();
    m.doSomething(10);
   }
}

如果您调用main,输出将显示Number

现在,向该类中添加第二个更具体的 重载 方法ChooseMethod,然后重新编译它(但不要重新编译另一个类)。

public void doSomething(Integer i) {
 System.out.println("Integer");
}

如果再次运行main,则输出仍为Number

基本上是因为它是在编译时决定的。如果您重新编译MethodChooser该类(带有主类的类),然后再次运行该程序,则输出将为Integer

因此,如果要强制选择重载方法之一,则参数的类型必须在编译时且不仅在运行时与参数的类型相对应。

在运行时覆盖方法选择

同样,方法的签名是在编译时确定的,而实际的实现是在运行时确定的。

声明一个这样的类并进行编译。

public class ChooseMethodA {
   public void doSomething(Number n){
    System.out.println("Number A");
   }
}

然后声明第二个扩展类并进行编译:

public class ChooseMethodB extends ChooseMethodA {  }

在MethodChooser类中,您可以执行以下操作:

public class MethodChooser {
    public static void main(String[] args) {
        ChooseMethodA m = new ChooseMethodB();
        m.doSomething(10);
    }
}

如果您运行它,则会得到输出Number A,这很好,因为该方法尚未被覆盖ChooseMethodB,因此调用的实现是的实现ChooseMethodA

现在,在中添加重写的方法MethodChooserB

public void doSomething(Number n){
    System.out.println("Number B");
}

并重新编译该代码,然后再次运行main方法。

现在,您得到了输出 Number B

这样,实现是在运行时选择的,不需要重新编译MethodChooser该类。