override和overload是非常容易混淆的概念。
一、override
override是“覆写”,是子类实现接口,或者继承父类时,保持方法签名完全相同,实现不同的方法体,是垂直方向上行为的不同实现。
如果父类定义的方法达不到子类的期望,那么子类可以重新实现方法覆盖父类的实现。因为有些子类是延迟加载的,甚至是网络加载的,所以最终的实现需要在运行期判断,这就是所谓的动态绑定。动态绑定是多态性得以实现的重要因素。
通过父类引用执行子类方法时需要注意以下两点:
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、异常也要能向上转型成为父类的异常
4、方法名、参数类型及个数必须严格一致
为了使编译器准确的判断是否是覆写行为,所有的覆写方法必须加@Override注解。此时编译器会自动检查覆写方法签名是否一致,避免了覆写时因写错方法名或方法参数而导致覆写失效。
总结,方法的覆写的口诀是:“一大两小两同”。
- 一大:子类的方法访问权限控制符只能相同或变大;
- 两小:抛出异常和返回值只能变小,能够转型成为父类对象。子类的返回值、抛出异常类型必须与父类的返回值、抛出异常类型存在继承关系;
- 两同:方法名和参数必须完全相同
根据以上原则,下面是一个编译和运行都正确的覆写示例:
@Slf4j
class Father {
protected Number doSomething(int a, Integer b, Object c)throws SQLException{
log.info("Father's method");
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(71);
}
}
覆写只能针对非静态、非final、非构造方法。由于静态方法属于类,如果父类和子类存在同名静态方法,那么两者都可以被正常调用。
如果方法由final修饰,则表示此方法不可被覆写。
如果想在子类覆写的方法中调用父类方法,则可以使用super关键字。
public class Father {
protected void doSomething() {
System.out.println("Father's method");
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();
}
}
在Son的doSomething方法体里可以使用super.doSomething()调用父类方法。如果与此同时在父类方法的代码中写了一句this.doSomething(),会出现以下结果。经过一系列的父子方法循环调用后,JVM崩溃了,发生了.StackOverflowError。
Father's method
Son's doSomething
Father's method
Son's doSomething
Father's method
Exception in thread "main" java.lang.StackOverflowError
at sun.nio.cs.UTF_8$Encoder.encodeLoop(UTF_8.java:691)
at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:579)
at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:271)
二、overload
overload是“重载”,方法名称是相同的,但是参数类型或者参数个数是不相同的,是水平方向上行为的不同实现。
在同一个类中,如果多个方法有相同的名字、不同的参数,即成为重载。比如,一个类中有多个构造方法。String类中的valueOf()是比较著名的重载案例。
在编译器眼里,方法名称+参数类型+参数个数,组成一个唯一键,成为方法签名,JVM通过这个唯一键决定调用哪种重载的方法。
注:方法的返回值并非是这个组合体的一员,所以在重载机制中,不能有两个方法名称完全相同,参数类型和个数也相同,但是返回类型不同的方法。
public class Father {
public void doSomething() {
}
// 编译出错,返回值并不是方法签名的一部分
public int doSomething() {
}
// 编译出错,访问控制符并不是方法签名的一部分
private void doSomething() {
}
// 编译出错,静态标识符并不是方法签名的一部分
public static void doSomething() {
}
// 编译出错,final标识符并不是方法签名的一部分
public final void doSomething() {
}
}
重载有时仅凭肉眼就能知道应调用哪种重载方法,尤其是方法签名比较明显的情况下,假如在方法的签名中只是参数类型不同,那如何抉择呢?如果调用doSomething(7),会调用下面那个方法呢?
@Slf4j
public class OverloadDemo {
public void doSomething() {
log.info("无参方法");
}
// 第二种方法:基本数据类型
public void doSomething(int param) {
log.info("参数为基本类型int的方法");
}
// 第三种方法,包装数据类型
private void doSomething(Integer param) {
log.info("参数为包装类型Integer的方法");
}
// 第四种方法:可变参数,可以接受0~n个Integer对象
public void doSomething(Integer ... param) {
log.info("可变参数方法");
}
// 第五种方法:Object对象
public void doSomething(Object param) {
log.info("参数为Object的方法");
}
}
先看一下这五种方法对应的字节签名有何异同:
// V表示Void返回值
public doSomething()V
// I就是代表int基本数据类型,而非Integer
public doSomething(I)V
// L表示输入参数是对象,然后跟着package+类名
public doSomething(Ljava/lang/Integer;)V
// varargs表示可变参数
public varvargs doSomething([Ljava/lang/Integer;])V
// L同样表示对象参数
public doSomething([Ljava/lang/Object;])V
JVM在重载方法中,选择合适的目标方法的顺序如下:
(1)精确匹配;
(2)如果是基本数据类型,自动转换成更大表示范围的基本类型;
(3)通过自动拆箱与装箱;
(4)通过子类向上转型继承路线依次匹配;
(5)通过可变参数匹配。