这是一个非常好的问题,但只是更一般问题的一个实例。虽然@TheLostMind在4年前已经回答了这个问题(从实现的角度考虑),但从JVM规则/限制的角度来看,问题仍然存在:为什么JVM提出这些规则?必须有一个合理的原因。我们必须从更抽象的层面(观点)来考虑它。
简短回答:
可能所有这些都与类型安全有关,正如我们所知,Java是一种强类型语言,不允许任何人改变这个事实。
详细回答(带一些上下文以便于大家理解):
整个故事始于静态和动态绑定。
子类型多态性所赋予的灵活性使得对象的声明(静态)类型通常与其运行时(动态)类型不同。
运行时类型通常是静态类型的子类型。
例如:
AClass a = new AClass();
AsubClass b = new AsubClass(); // AsubClass is derived from AClass
a=b;
a的静态类型是AClass,在赋值a=b; 后它的运行时类型变成了AsubClass。这对于执行消息时选择最合适的方法有影响。
现在考虑下面给出的Vehicle类。
public class Vehicle {
private int VIN;
private String make;
public boolean equals(Object x) {
return (VIN == (Vehicle)x.VIN);
}
}
根类java.lang.Object中的equals方法被定义为对象标识测试。
这是一般情况下定义对象相等的唯一有意义的方式。也就是说,如果两个对象具有相同的标识,则它们是相等的。
在特定的类中,更适合的相等含义可能更为恰当。在上述类中,如果两辆车的VIN(车辆识别号码)相等,则认为它们是相等的。
因此,在类Vehicle中,方法equals相应地被重新定义。这个继承方法的重定义称为覆盖。
请注意,子类中继承的方法参数的签名需要保持不变,根据function subtyping rule。
这会创建一个尴尬的情况,因为在类Vehicle中,我们想引用参数的VIN字段,而Object没有这样的字段。这就是为什么类型转换(Vehicle)x指定意图将x视为Vehicle。没有办法静态验证这个转换,因此编译器会生成一个动态检查。这是dynamic type checking.的一个例子。
为了使覆盖正常工作,要调用的方法是由接收对象的动态类型确定的(也称为dynamic dispatch (selection) of methods,是面向对象语言中最重要的动态绑定案例)。
例如:
Object a = new Object();
Object b = new Object();
Vehicle aV = new Vehicle();
Vehicle bV = new Vehicle();
a=aV;
b=bV;
. . .
a.equals(b)
. . .
响应消息a.equals(b)的调用方法将是在类Vehicle中重写的equals方法,因为a的运行时类型是Vehicle。
有些情况下,重写方法可能会有问题,不应该允许。一个很好的例子是Java.lang.Object的getClass()方法。这个方法在底层虚拟平台中有一个特定的实现,保证调用此方法将确实返回方法接收者的类对象。
允许重写将对该方法的预期语义产生严重影响,在动态类型检查中创建非常规问题。这可能就是为什么getClass()被声明为final的原因。
public class Object {
public final Class getClass();
....
}
最后,Java中的类Class是final的,即不能被扩展,因此它的所有方法都不能被覆盖。由于类Class只有内省方法,这保证了运行时类型系统的安全性,即类型信息不能在运行时改变。
再深入一点...
基于接收对象类型的动态调度(选择)方法是面向对象语言中的基本技术。它带来了使整个面向对象范例工作的灵活性。
通过继承将新类型添加到已编译和运行的应用程序中,只需要编译和链接新引入的类型,而无需重新编译现有应用程序。然而,这种灵活性会带来一些效率上的惩罚,因为方法选择的决定被推迟到运行时。现代语言具有有效的动态方法调度技术,但某些语言如C++和C#则通过提供静态绑定(方法选择)选项来避免相关成本。在C#中,除非明确声明为虚拟的,否则方法是静态绑定的。
public class Object {
public virtual boolean equals(Object x);
}
在C#中覆盖此方法将使用显式关键字
override指示。
例如。
public class Vehicle {
private int VIN;
private String make;
public override boolean equals(Object x) {
return (VIN == (Vehicle)x.VIN);
}
}
接收器为类对象的方法始终静态绑定。原因是该类的所有对象只有一个类对象。由于在编译时已知接收器,因此无需将方法选择推迟到运行时。这些方法因此被声明为静态,以表示它们属于类本身。
一个例子是Vehicle类的numberOfVehicles方法。车辆数量不是单个车辆对象的属性。它是Vehicle类的所有对象的属性,因此它属于类本身。
例如:
public class Vehicle {
public static int numberOfVehicles();
}
我们可以将以上讨论总结如下:
- 面向对象语言中选择执行消息的方法(方法调度)的基本机制是动态的。它基于接收器对象的运行时类型。
- 静态(即类)方法的接收器是类对象。由于给定类型只有一个类对象,因此选择静态方法是静态的。
- 一些语言(如C++和C#)允许选择静态或动态方法调度。虽然这样做是出于效率的考虑,但已经证明,当程序中同时使用两种调度机制时,可能会使程序的含义变得模糊。