覆盖(overriding)作为实现多态的方式,是面向对象的最重要的概念之一。简单说来,方法覆盖是指子类方法的实现替换父类中同名方法的实现,而且两个方法的签名等价。严格说来,Java 中方法覆盖的定义为:
在满足下面所有条件的情况下,我们可以说一个在类 C 中声明的实例方法 m1覆盖了在类 A 中声明的另一个实例方法 m2:
1. C 是 A 的子类;
2. m1 的签名(signature)是 m2 的签名的子签名(subsignature);
3. 满足其中任意一条:
a) m2 的访问限定符是 public、protected 或者是与 C 在同一个包中被声明而拥有默认的访问权限。
b) m1 覆盖了方法 m3,m3 不同于 m1,m3 也不同于 m2,而且 m3 覆盖了 m2。
而且,如果 m1 不是 abstract 的话,我们就可以说 m1 实现了它所覆盖的所有的抽象方法声明。
方法的覆盖使得我们基于对象的父类型进行操作的时候,实际上使用的是对象本身的类型(子类型)中所定义的实现。方法的覆盖和类的继承相结合,实现了面向对象中最强大的概念:多态(polymorphism)。
进行覆盖的方法可以是 abstract 的,即使被覆盖的方法不是 abstract 所修饰的。进行覆盖的方法也可以是 final 的,但是很明显被覆盖的方法不能是 final 所修饰的。覆盖方法的签名中是否有 final 修饰的参数,则和覆盖规则无关,因为参数的 final 修饰符是和实现的细节相关而和方法的声明无关。
实例方法不能覆盖静态方法(static method),否则在编译时报错。在这一方面,和字段的隐藏规则不一样——实例变量是可以隐藏静态变量的。
被覆盖的方法,可以通过包含 super 关键字的方法调用表达式(method invoking expression)来访问。但是,通过使用限定名(qualified name)或者将对象的类型向上转换为父类的类型,是无法访问被覆盖的方法的。在这一方面,又和字段的隐藏规则不同。
无论有没有 strictfp、synchronized 或者 native 修饰符,对于方法覆盖和抽象方法实现的规则都是绝对没有任何影响的。比方说,一个非 FP-strict 的方法可以覆盖一个 FP-strict 的方法,反过来一个 FP-strict 的方法也可以覆盖一个非 FP-strict 的方法。
覆盖方法的 throws 子句可以和父类中被覆盖的方法不相同,只要子类方法中所抛出的异常在父类方法的 throws 子句中存在或者是其中某个异常的子类型即可。
由于 Java 5 中开始引入范型编程的特型,覆盖的规则随之有所变化。如果覆盖方法或者被覆盖方法其中之一的签名中有原始类型(raw type)的形式参数,并且另外一个方法的签名中的对应形式参数是参数化类型(parameterized type)的话,那么二者的签名可以在此方面有所不同。由于这个规则,我们就可以方便的迁移以前的代码到新版本的 Java 环境中,并享受范型所带来的好处。
覆盖只是针对实例方法(instance method)而言,对于类方法(class method)是没有覆盖一说的——因为类方法只能被隐藏。类方法的隐藏的定义如下:
如果一个类声明了一个 static 的方法 m,且在该类的父类或父接口之中存在一个该类可访问的方法 m’,且 m 的签名是 m’ 签名的子签名,我们就说声明 m 隐藏了方法 m’。
也就说,静态方法只能覆盖静态方法,而不能去覆盖实例方法,否则编译器在编译期会报告错误。被隐藏的类方法,可以通过限定名、包含 super 关键字的方法调用表达式或者转化对象类型为父类类型的方式来访问。这和实例方法的覆盖规则不同,但是和字段隐藏的规则比较类似。
最后需要注意的是,如果一个返回类型为 R1 的方法声明为 d1,覆盖了或者隐藏了一个返回类型为 R2 的方法声明 d2,那么 d1 的返回类型必须可以替代 d2 的返回类型,否则编译期报错。此外,如果 R1 不是 R2 的子类型的话,编译器将会给出一个未检查警告。