一. Java方法声明的众多属性
在讨论重载和覆写的区别之前,先补充一下Java方法的背景知识,看下面最为常见的main方法声明,为了完整介绍,自行加了异常的抛出:
public static void main(String[] args) throws Exception {...}
① ② ③ ④ ⑤ ⑥
各属性说明如下:
①:访问权限(修饰符)
②:static/finally
③:返回值
④:方法名
⑤:参数列表
⑥:异常
值得注意的是,在Java语法层面,只有④⑤(⑤包括了形参的个数,类型,顺序)构成方法签名即方法唯一性标识,换言之,如果一个类中两个方法的方法签名一摸一样,则即使①②③⑥不同(特别是返回值)编译器仍然会认为这两个方法重复了,从而报错。
引申来说,其实在Java字节码层面是支持返回值可以作为方法签名的一部分,这是为了支持其他语言编译成class文件,Java语法层面并不支持。
二. 重载(OverLoad)
再看方法:
public static void main(String[] args) throws Exception {...}
① ② ③ ④ ⑤ ⑥
先对方法重载有个认识,overload其实就是在一个类中写一个名字同已经存在的方法名一样的方法,但又要保证使用不同方式调用方法时JVM能识别出这是两个不同的方法,e.g.
new Student("Tom") VS new Student("Tom", 14)
所以,问题变成:在方法名④相同的前提下,①②③⑤⑥哪些变化能保证JVM能识别出这是不同的方法,哪些又是“无关紧要”——对是否重载没有影响呢?
这就引出了方法重载的充要条件:
方法名④要一样,参数列表⑤的个数,类型和顺序至少有一个不同
除此之外,权限修饰符①,static/final②,返回值③,抛出异常⑥都不影响,e.g.
原方法:public Boolean init(String name,Integer order){...}
合法重载1:public Boolean init(String name){...}(个数不同)
合法重载2:public Boolean init(Integer order,String name){...}(顺序不同)(一般不提倡,因为容易混淆)
合法重载3:public Boolean init(Integer name,Integer order){...}(类型不同)
也可以看几个反例:
不合法重载1:public String init(Integer order,String name){...} (只修改方法返回值类型,NO)
不合法重载2:private Boolean init(String name,Integer order){...} (只修改方法访问权限,NO)
可以这么理解,因为重载就是重新写一个方法,这个方法同已经存在的方法名相同,但是参数列表(个数,类型和顺序)不同。那么,其他的属性①②③⑥现在就是属于这个新方法的属性,和原方法没有一点关系 —— 方法签名④⑤已经是保证了方法是一个新方法,所以①②③⑥随便选的。
另外,一个要注意的问题是,重载的原方法可以来自于本类,也可以来自于父类或者任何祖先类——只要对本类可见即可。因为根据第一节已经探究过的访问修饰符问题,如果对子类可见,即代表子类可以直接操作该方法,就如同父类方法”写在本类的最前方“一样。
三. 覆写(Override)
再看方法:
public static void main(String[] args) throws Exception {...}
① ② ③ ④ ⑤ ⑥
先对方法覆写有个认识,Override就是子类覆盖父类的方法,最实用的目的:多态,e.g.
即,在父类引用指向子类对象后,此时父类引用调用一个被覆写的方法的时,JVM要执行子类的方法,不能执行父类的方法。就好像父类的方法被“覆盖”了——果然是法如其名Override。
这也能看出重载和覆写在行为上的区别——重载要让JVM识别出这是不同的方法,覆写要让JVM识别出这就是父类已有的方法。
回顾重载的充要条件——方法签名不能一样,那么现在保证子类方法不是单独写的方法,而是覆写的父类的方法,就一定要保证方法签名(④⑤)要一模一样。其他还有限制,请看:
public static void main(String[] args) throws Exception {...}
① ② ③ ④ ⑤ ⑥
1)方法名④和参数列表⑤:必须相同
2)权限修饰符①:必须比父类方法大
3)static/final②:static和final方法都是不可以被继承的,因此,想要被覆写的父类方法一定不能被这两个修饰符修饰(上面举的main方法的例子只是拿了一个常见又”齐全“的方法来介绍问题,main方法是不能被继承的)。
4)返回值③:必须比父类方法相同或小,即返回值类型所属的类必须和父类一样,或者是其子类,eg:
父类:private Object init(String name){...}
子类:public String init(String name){...}
5)抛出异常③:子类方法不能抛出比父类方法更多的异常【1】,即子方法抛出的异常就是父方法抛出的异常或者其子类。
可简记为“两同两小一大”【2】
如何理解呢,这和之前说的覆写的目的——多态,有很大的关系,再看例子:
很明显,JVM要成功执行grandpa.print("Sam"),就必须保证子类覆写的print()方法上有一定的限制,“就像”grandpa在调用自己的方法一样,即,保证从外观上,调用子类方法和调用父类方法没有区别(又称为“里氏替换原则(Liskov Substitution principle)”),二者只是方法体中行为的不同,e.g.
对应到之前的种种要求,一一分析:
public static void main(String[] args) throws Exception {...}
① ② ③ ④ ⑤ ⑥
1)”方法名④和参数列表⑤:必须相同 “
保证grandpa.print("Sam")时,不管grandpa的真正指向,调用方式都是一样的
2)”权限修饰符①:必须大于等于父类方法“
grandpa指向父类都可以调用,子类方法权限修饰符必然要大于等于父类,才能保证指向子类时调用方法不会因权限不够而出错,e.g.
Son在”覆写“父类方法print时,将权限修饰符改小了—— public缩小为private,此时在Test的main方法中,f指向父类对象Father()时,f.print()顺利执行,但是指向子类对象Son()时,f.print就会报错——因为此时调用的是Son的print()方法,而Son的print()是private即本类可见的。
但其实上面代码是”纸上谈兵“,因为根本不会等到运行时报错,在Java语法层面即报错(因为覆写规则属于Java语法层面,编写时IDE即能检测),看信息:
print(String)' in 'fatherpackage.Son' clashes with 'print(String)' in 'fatherpackage.Father'; attempting to assign weaker access privileges ('private'); was 'public
给父类public方法指定了更低的权限,❌
可见,子类覆写的方法权限修饰符要大于等于父类。将这点应用到异常同理,具体可见下文5)。
3)”static和final方法都是不可以被继承的,因此,想要被覆写的父类方法一定不能被这两个修饰符修饰(上面举的main方法的例子只是拿了一个常见又”齐全“的方法来介绍问题)“
实际上,private,构造方法也不能被继承,当然地,也不能被覆写,不过他们之间有所区别,这一点在其他专题文章中专门有介绍。
4)"返回值③:必须比父类方法相同或小,即返回值类型所属的类必须和父类一样,或者是其子类。"
这个同样可以用”外观上,用调用父类方法的方式调用子类方法,语法层面不会出错“的思路理解:假设父类方法返回值是String,子类方法返回值是Object,那么假设子类方法体中最终真的return了一个Object(类型 > 父类返回值),或者Integer类型对象引用(类型完全不同于父类返回值类型),像之前的grandpa.print("Tom")就会出现问题一样,调用者从面向接口的思维调用方法,很自然地会这么做:
String str = grandpa.print("Tom");
因为考虑到多态的思想——具体行为由运行时对象控制,但是外观上——返回值都是一样的,在调grandpa.print("Tom")时很自然地认为返回值就是String,这也是多态的意义所在。当然可以去查子类具体的返回值,用对应的类型接收,但是这样就毫无意义了。很明显,用父类的返回值类型(String类型)变量接收调用结果,当子类碰到子类返回值比父类大(Object),或者完全不同时(Integer)—— 即接收父方法的变量接收不了子方法的返回值,肯定会发生错误。
另一方面,如果子类方法返回值类型是父类方法返回值类型的父类(完全不同类型的就不用考虑了),如Object和String,也能通过在子类方法体中return做强制类型转换——如上述错误例子 return (String)XXX来达到统一接口的目的,但是一来这样绕一圈弯子给自己找麻烦毫无意义,二来Java的设计者认为把这个安全性交给客户端程序员自己实现是不安全的,所以直接在语法层面”写死了“—— 子类方法返回值类型只能小于等于父类方法。
5)“异常⑥:子类方法不能抛出比父类方法更多的异常【1】。子方法抛出的异常就是是父方法抛出的异常或者其子类。”
异常的基本知识可见其他专题,这里就用到一点,先看方法:
public static void main(String[] args) throws Exception1, Exception2... {...}
① ② ③ ④ ⑤ ⑥
这个声明的意思是指:main方法没有能力处理方法体中可能出现的类型为Exception1, Exception2...的异常,从而抛出了(throws),交由上一层调用者来捕获处理。当然,调用main方法的方法(调用者)只有两个选择:要么在使用时使用try...catch语句调用main方法,以便捕获处理可能出现的Exception1, Exception2...异常,要么继续抛出即在方法声明时throwsException1, Exception2...。
了解完方法声明中关于异常即⑥的含义,再回顾覆写原则——”外观上,用调用父类方法的方式调用子类方法,语法层面不会出错“,假设一下,如果子方法抛出了与父方法不同的异常,或者是父方法异常的父类,则调用处的处理会出问题——方法调用者还是按处理父方法异常的方法处理子方法。如果子方法抛出了更多的异常,那么调用者对于这些多出来的情况可能没有进行捕获处理(catch),同样的会导致错误。故这一条规定也是符合逻辑的。
四. 总结
Java的方法签名由方法名称+参数列表(个数,类型和顺序)构成的。
重载的目的在于让Java识别出这是不同的方法,故方法签名不同就是充要条件。而覆写的目的在于让Java识别出这是同一个方法,故方法签名一定要相同,同时,为了满足Java多态符合逻辑的实现,覆写对子方法进行了多重限制:访问修饰符扩大,返回值缩小,异常缩小(都是带等号的)。
关于记忆问题,重载不用多说,可以直接用平时最常使用的不同构造方法来记忆,e.g.
覆写,”两同两小一大“原则。
【1】 子类方法返回值应比父类返回值更小或相等
【2】 两同两小一大