12.7 覆写与重载
覆写(override)与重载(overload)是Java语言中两个比较容易混淆的概念。覆写是指在子类型中重新定义从父类型中继承的方法的实现。覆写是面向对象中的重要概念,通过与继承关系结合来实现基于运行时类型的动态方法分派。当使用一个子类型的对象引用调用一个方法时,实际执行的是子类型中覆写的方法。重载指的是类中可以包含名称相同但类型签名(signature)不同的方法。重载的作用是允许方法的使用者采用不同的参数来调用相同名称的方法。重载的方法可以看成是一组功能相同的方法,只是对于不同的参数类型进行操作。覆写只发生在子类型和父类型之间。由于子类型会继承父类型中的方法,因此子类型中包含的方法既可以覆写父类型中的方法,也可能重载从父类型继承下的方法。覆写和重载的语义不同,需要正确地区分这两种情况。常见的问题是在需要覆写父类型方法时错误地进行了重载,导致父类型中的对应方法没有被覆写,当使用子类的对象调用方法时,仍然调用的是父类的方法,这会造成难以发现的错误。由于泛型的引入,覆写和重载的区分条件变得更加复杂。
覆写和重载的共同前提条件是方法名称相同。名称不同的方法不可能存在覆写或重载的关系。方法的覆写需要满足的条件包括方法类型签名、返回值类型和抛出的异常类型三个方面。因为代码可能使用父类的对象引用来调用方法,并把方法的调用结果赋值给变量,同时还需要处理方法调用中可能产生的异常,所以如果子类型覆写了父类型中的方法,要保证覆写的方法在使用父类型的对象引用进行调用时也是合法的。这一点是基于运行时类型的动态方法分派的基础。调用者使用父类型的引用来进行操作,而运行时的实际类型可能是该父类型的子类型。这一点对于调用者来说是透明的。
12.7.1 覆写对方法类型签名的要求
方法类型签名包括方法名称和参数类型两个部分。子类型中的方法覆写父类型中方法的条件是两个方法的类型签名是相同的,或者父类型中的方法在类型擦除之后的类型签名与子类型中方法的类型签名相同。这两种情况对于一般类型和参数化类型都适用。代码清单12-19给出了用来测试方法覆写的代码示例。类SuperClass和其子类SubClass中包含名称相同的方法method,通过查看调用子类对象的method方法的执行结果,可以判断实际调用的是SuperClass类中的对应方法还是SubClass类中的对应方法,从而可以知道方法覆写是否生效。代码清单12-19中子类的method方法的类型签名与父类的method方法在类型擦除之后的类型签名相同,因此这是一个正确的覆写操作。
代码清单12-19 方法覆写是否生效的测试代码
class SuperClass{
public void method(List<?>param){
System.out.println("调用父类中的方法。");
}
}
class SubClass extends SuperClass{
public void method(List param){
System.out.println("调用子类中的方法。");
}
}
public class OverrideTest{
public static void main(String[]args){
SubClass subClass=new SubClass();
subClass.method(new ArrayList<String>());//调用子类的方法
}
}
方法的参数类型相同的条件对于一般类型和参数化类型都适用。不管SuperClass类和SubClass类中的method方法的参数是相同的String类型,还是List<?extends Number>类型,都是正确的覆写方式。需要注意子类型和父类型的方法在类型擦除之后类型签名相同但不存在覆写关系的情况。比如,SuperClass类中的method方法的参数类型是List<?extends Number>,而SubClass类中对应方法的参数类型是List<?extends Integer>,会出现编译错误。这是因为子类和父类中的同名方法的类型签名不同,而且父类型的方法在类型擦除之后的类型签名不同于子类型的方法的原始类型签名,因此不存在覆写关系,但是两个方法在类型擦除之后的类型签名都是“method(List)”,编译器无法判断哪个方法应该被调用。
在父类型和子类型中有一个或两个都是泛型类型时,参数类型是否相同的判断变得更加复杂。这是因为在方法类型签名中可能使用泛型类型定义中的形式类型参数。下面按照父类型和子类型是否为泛型类型的4种情况进行讨论。
1.父类型和子类型均为非泛型类型
第一种情况是父类型和子类型都是非泛型类型,即不包含形式类型参数。
对于父类型中的非泛型方法,子类型中类型签名相同的非泛型方法可以进行覆写。但是子类型中的泛型方法无法覆写父类型中的非泛型方法。这是因为这种方式不可能满足前面提到的覆写方法时关于类型签名的两个条件中的任何一个:非泛型方法和泛型方法的类型签名不可能相同,因为泛型方法的类型签名中包含形式类型参数;进行类型擦除也不会使父类型中的非泛型方法的类型签名发生改变。
对于父类型中的泛型方法,子类型中的非泛型方法可以对其进行覆写,只要子类型中的非泛型方法的类型签名与父类方法在类型擦除之后的类型签名相同即可。代码清单12-20给出了子类型中的非泛型方法覆写父类型中的泛型方法的示例。父类型中的method方法的形式类型参数没有上界,因此在类型擦除后的类型为Object,与子类型中方法的参数类型相同。如果把父类型中的方法声明改为“<T extends Number>void method(T obj)”,那么子类型的方法就不再覆写父类型中的方法。这是因为父类型中方法在类型擦除之后的参数类型变为Number,不同于子类型对应方法的参数类型Object。
代码清单12-20 子类型中的非泛型方法覆写父类型中的泛型方法的示例
class SuperClass{
public<T>void method(T obj){
}
}
class SubClass extends SuperClass{
public void method(Object obj){
}
}
对于父类型中的泛型方法,子类型中的泛型方法也可以进行对其进行覆写,要求子类型中的方法与父类型中的方法的类型签名相同。由于形式类型参数只是实例化时使用的实际类型的占位符,在考虑方法类型签名时,要根据形式类型参数可能的取值范围来进行判断,判断的依据是不会破坏类型安全性。代码清单12-21给出了子类型中的泛型方法覆写父类型中的泛型方法的示例。两个方法的类型签名中的形式类型参数都表示的是继承自Object类的所有类型,因此两个类型签名是相同的。包含通配符的形式也可以用类似的方式来进行判断。
代码清单12-21 子类型中的泛型方法覆写父类型中的泛型方法的示例
class SuperClass{
public<T>void method(T obj){
}
}
class SubClass extends SuperClass{
public<S>void method(S obj){
}
}
2.父类型为非泛型类型,子类型为泛型类型
第二种情况是父类型为非泛型类型,子类型为泛型类型。这种情况与第一种情况的差别在于子类型中的方法可以使用在类型定义时声明的形式类型参数。如果子类型中的方法不使用在类型定义时声明的形式类型参数,则与第一种情况相同。如果子类型中的方法使用了在类型定义时声明的形式类型参数,则无法覆写父类型中的对应方法。代码清单12-22中的代码是无法通过编译的,原因在于子类中的method方法并没有覆写父类中的对应方法,但是在类型擦除之后两者的类型签名是相同的。没有进行覆写的原因是子类方法的类型签名是不确定的,并不能保证与父类型中的方法签名保持一致,比如使用GenericeSubClass<String>声明的子类中的method方法的参数类型是String,与父类型中的参数类型Object是不相同的。
代码清单12-22 在泛型类型的方法声明中使用形式类型参数造成无法覆写的示例
class SuperClass{
public void method(Object obj){
}
}
class GenericeSubClass<S>extends SuperClass{
public void method(S obj){
}
}
3.父类型为泛型类型,子类型为非泛型类型
第三种情况是父类型为泛型类型,子类型为非泛型类型。这种情况与第一种情况的不同在于父类型中的泛型方法可能使用在泛型类型定义时声明的形式类型参数。子类型中的非泛型方法可以覆写父类型中的泛型方法,这是因为子类型在继承父类型时需要声明父类型中形式类型参数的实际类型。因此父类型中的方法的类型签名实际上是固定的。代码清单12-23给出了相关的示例。父类型的方法method使用了形式类型参数T,在子类型继承父类型时声明了父类型使用的实际类型是Number,所以在父类型中定义的method方法的实际参数类型是Number。在子类型中定义了参数类型为Number的method方法,因此子类型中的方法覆写了父类型中的对应方法。子类型中的泛型方法无法覆写父类型中使用了泛型类型定义的形式类型参数的方法,这是因为不能保证类型签名总是相同的。
代码清单12-23 子类型中的非泛型方法覆写父类型中的泛型方法的示例
class GenericeSuperClass<T>{
public void method(T obj){
}
}
class SubClass extends GenericeSuperClass<Number>{
public void method(Number obj){
}
}
4.父类型和子类型均为泛型类型
最后一种情况是父类型和子类型都是泛型类型。这种情况的复杂性在于父类型和子类型中的泛型方法都可以使用泛型类型的形式类型参数。由于在实例化时使用的实际类型可能有多种选择,这使对于是否覆写的判断变得复杂。这种情况的解决办法是判断实际类型是否兼容,并且是否会带来类型安全问题。代码清单12-24给出了简单的示例。父类型和子类型中的泛型方法method都使用了类型定义中的形式类型参数。在对子类型实例化时,形式类型参数S的值同时也是父类型中形式类型参数T的值,因此两个类型中的method方法的类型签名实际上是相同的。
代码清单12-24 父子类型都是泛型类型时的方法覆写的示例
class GenericeSuperClass<T>{
public void method(T obj){
}
}
class GenericeSubClass<S>extends GenericeSuperClass<S>{
public void method(S obj){
}
}
由于泛型类型的存在,在有些情况下,子类型和父类型中方法的覆写关系并不是非常清晰的,需要仔细判断。一个典型的情况是泛型类型中的形式类型参数包含上界时。代码清单12-25给出了一个示例。子类型中的method方法的类型签名与父类型中对应方法的签名是不同的,子类型中方法的参数类型固定为Number,而父类型中的参数类型可能是Number类及其子类型,但是子类型中的方法仍然覆写了父类型中的方法,这是因为这种覆写关系不会引起类型安全问题,当使用者通过父类型对象的引用调用method方法时,所有可能的参数对象值对于子类型的方法来说都是合法的。
代码清单12-25 与泛型相关的复杂的方法覆写关系的示例
class GenericeSuperClass<T>{
public void method(T obj){
}
}
class GenericeSubClass<S extends Number>extends GenericeSuperClass<S>{
public void method(Number obj){
}
}