2.4.7 覆写
多态中的override,本书翻译成覆写。如果翻译成重写,那么与重构意思过于接近;如果翻译成覆盖,那么少了“写”这个核心动词。如果父类定义的方法达不到子类的期望,那么子类可以重新实现方法覆盖父类的实现。因为有些子类是延迟加载的,甚至是网络加载的,所以最终的实现需要在运行期判断,这就是所谓的动态绑定。动态绑定是多态性得以实现的重要因素,元空间有一个方法表保存着每个可以实例化类的方法信息,JVM 可以通过方法表快速地激活实例方法。如果某个类覆写了父类的某个方法,则方法表中的方法指向引用会指向子类的实现处。代码通常是用这样的方式来调用子类的方法,通常这也被称作向上转型:
Father father = new Son();
// Son 覆写了此方法
father.doSomething();
向上转型时,通过父类引用执行子类方法时需要注意以下两点:
(1)无法调用到子类中存在而父类本身不存在的方法。
(2)可以调用到子类中覆写了父类的方法,这是一种多态实现。
想成功地覆写父类方法,需要满足以下4 个条件:
(1)访问权限不能变小。访问控制权限变小意味着在调用时父类的可见方法无法被子类多态执行,比如父类中方法是用public 修饰的,子类覆写时变成private。设想如果编译器为多态开了后门,让在父类定义中可见的方法随着父类调用链路下来,执行了子类更小权限的方法,则破坏了封装。如下代码所示,在实际编码中不允许将方法访问权限缩小:
class Father {
public void method() {
System.out.println("Father's method");
}
}
class Son extends Father {
// 编译报错,不允许修改为访问权限更严格的修饰符
@override
private void method() {
System.out.println("Son's method");
}
}
(2)返回类型能够向上转型成为父类的返回类型。虽然方法返回值不是方法签名的一部分,但是在覆写时,父类的方法表指向了子类实现方法,编译器会检查返回值是否向上兼容。注意,这里的向上转型必须是严格的继承关系,数据类型基本不存在通过继承向上转型的问题。比如int 与Integer 是非兼容返回类型,不会自动装箱。再比如,如果子类方法返回int,而父类方法返回long,虽然数据表示范围更大,但是它们之间没有继承关系。返回类型是Object 的方法,能够兼容任何对象,包括class、enum、interface 等类型。
(3)异常也要能向上转型成为父类的异常。异常分为checked 和unchecked 两种类型。如果父类抛出一个checked 异常,则子类只能抛出此异常或此异常的子类。而unchecked 异常不用显式地向上抛出,所以没有任何兼容问题。
(4)方法名、参数类型及个数必须严格一致。为了使编译器准确地判断是否是覆写行为,所有的覆写方法必须加@Override 注解。此时编译器会自动检查覆写方法签名是否一致,避免了覆写时因写错方法名或方法参数而导致覆写失败。例如,AbstractCollection 的clear 方法,当覆写此方法时,写成c1ear,注意是数字的1,这会导致定义了两个不同的方法。此外,@Override 注解还可以避免因权限控制可见范围导致的覆写失败。如图2-7 所示,Father 和Son 属于不同的包,它们的method() 方法无权限控制符修饰,是默认仅包内可见的。Father 的method 的方法在Son 中是不可见的。所以,Son 中定义的method 方法是一个“新方法”,如果加上@Override,则会提示:Method does not override method from its superclass。
图2-7 Father 和Son 的覆写关系
综上所述,方法的覆写可以总结成容易记忆的口诀:“一大两小两同”。
- 一大:子类的方法访问权限控制符只能相同或变大。
- 两小:抛出异常和返回值只能变小,能够转型成父类对象。子类的返回值、抛出异常类型必须与父类的返回值、抛出异常类型存在继承关系。
- 两同:方法名和参数必须完全相同。
根据这个原则,再看一个编译和运行都正确的覆写示例代码:
class Father {
protected Number doSomething(int a, Integer b, Object c) throws
SQLException {
System.out.println("Father's doSomething");
return new Integer(7);
}
}
class Son extends Father {
/**
* 1. 权限扩大,由protected 到public(一大)
* 2. 返回值是父类的Number 的子类 (两小)
* 3. 抛出异常是SQLException 的子类
* 4. 方法名必须严格一致 (两同)
* 5. 参数类型与个数必须严格一致
* 6. 必须加@Override
*/
@Override
public Integer doSomething(int a, Integer b, Object c) throws
SQLClientInfoException {
if(a == 0) {
throw new SQLClientInfoException();
}
return new Integer(17);
}
}
覆写只能针对非静态、非final、非构造方法。由于静态方法属于类,如果父类和子类存在同名静态方法,那么两者都可以被正常调用。如果方法有final 修饰,则表示此方法不可被覆写。
如果想在子类覆写的方法中调用父类方法,则可以使用super 关键字。在上述示例代码中,在Son 的doSomething 方法体里可以使用super.doSomething(a,b,c) 调用父类方法。如果与此同时在父类方法的代码中写一句this.doSomething(),会得出什么样的运行结果呢?
public class Father {
protected void doSomething() {
System.out.println("Father's doSomething");
this.doSomething();
}
public static void main(String[] args) {
Father father = new Son();
father.doSomething();
}
}
class Son extends Father {
@Override
public void doSomething() {
System.out.println("Son's doSomething");
super.doSomething();
}
}
在经过了一系列的父子方法循环调用后,JVM 崩溃了,发生了*Error,如图2-8所示。
图2-8 覆写产生的*Error