对于您问题的第一部分,我认为Wikipedia 提供了一个很好的定义:
在面向对象编程中,子类型多态或包含
多态性是类型论中的一个概念,其中名称可以表示
许多不同类的实例,只要它们通过以下方式相关
一些常见的超类。包含多态性一般是
通过子类型支持,即不同类型的对象是
完全可以替代另一种类型的对象(它们的基础
type(s)),因此可以通过通用接口进行处理。
或者,包含多态性可以通过类型来实现
强制转换,也称为类型转换。
另一个名为Polymorphism in object-oriented programming 的维基百科文章似乎很好地回答了您的问题。本文中的第二个参考文献On Understanding Types, Data Abstraction, and Polymorphism 也非常详细地介绍了这个问题。
Java 中的这种子类型化特性是通过类和接口的继承等方式实现的。尽管 Java 的子类型化特性在继承方面可能并不总是很明显。以泛型的协变和逆变的情况为例。此外,数组是可序列化和可克隆的,尽管这在类型层次结构中的任何地方都不明显。也可以说,通过原始扩展转换,Java 中的数值类型也是多态的。运算符的行为取决于其操作数。
无论如何,继承在某些多态性的实现中起着重要作用。
重载与覆盖
您问题的第二部分似乎是关于选择给定方法的实现。显然,如果一个类重写了一个方法并且您创建了该类的一个实例,您希望调用该方法的重写版本,即使您通过父类的引用访问该对象也是如此。
正确的实现方法的选择是在运行时完成的,正如您所指出的,现在要调用的方法的签名是在编译时决定的。由于重载是关于具有相同名称和不同签名的不同方法,这就是为什么说覆盖方法选择发生在编译时。
在编译时覆盖方法选择
15.12 节Method Invocation Expressions 中的Java Language Specification (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类(有main的那个),再次运行程序,输出将是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 类。