Java面试基础问题之(七)—— 方法的重载 VS 覆写规则

一. 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】 两同两小一大

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值