前言
当我在学到“面向对象”这块内容的时候,学到了一个概念,那就是“方法的重写”。重写又叫覆盖,英文名为“Override”。虽然”重写”、 ”覆盖”、“Override”这些名词都很容易记住,但很多人(包括我)并没有真正理解Java语言为什么要提供“重写”这种编程机制,也不知道什么时候该重写父类中的方法。今天,我打算花费5个小时,写两篇博客,彻底把方法重写和方法重载剖开来分析,我势必要弄懂这些散乱和零碎的知识点。
致读者
对于看到这篇博客的你们,其实不需要刻意的去记这些知识,可以收藏起来,有事没事翻开看看,这些其实看得多了,自然就烂熟于心了,如果你只是刻意的记忆了一遍,那其实你很快就会忘记,即使你当时看完可能觉得懂了,但事实上你很快就会忘了,过几个星期,你根本回忆不起来,感觉好像没看过这篇文章似的。所以说,赶紧先收藏,随时拿出来看,不然真的吃亏。
问题引入
假设有一个类叫做Father,并且我们假设因为某种原因,我们只能使用这个类,但没有办法修改这个类的源代码。Father类中提供了一个能够求正整数累加之和的方法叫做sum。(所谓“累加”就是从1一直加到某个数,比如1+2+3+...+100)代码如下
后来,我们又编写了一个类叫做Child,它继承了Father类。其他方面都没什么问题,但Father类所提供的这个用于求整数累加之和的方法sum()效率实在太低了,每做一次累加都要执行多次循环。求整数累加之和明明可以用更高效的方法实现,但出于某种原因,我们无法修改Father类的源代码,难道我们继承了Father类就只能被迫选择使用这个效率很低的sum()方法吗?
幸好,Java语言提供了“重写”这种编程机制。重写,顾名思义,就是在子类中把继承自父类的某个方法重新写一遍。这样就能在子类中弄出一个同名的、更适合自身或者是效率更高的方法。于是我们就可以在子类中重写了sum()方法,代码如下:
通过观察代码我们不难发现,重写之后的sum()方法摒弃了循环求和的算法,而采用了更高效的等差数列求和的方法完成累加的计算。这样明显提高了运算效率。当我们创建一个子类对象,并且调用该对象的sum()方法时,虚拟机将会调用重写之后的sum()方法,而不是父类中那个老的sum()方法。
但是,如果我们在代码在中,使用了父类的引用去指向子类对象的时候,还能不能调用到那个重写之后的sum()方法呢?看下面的代码:
从上面的代码中我们可以看到,创建了一个Child类对象,但是指向这个对象的对象的引用f却是一个Father类的引用。那么在这种情况下,当我们通过引用f调用sum()方法的时候,调用到的父类中的sum()方法,还是子类中重写过的sum()方法呢?执行main()方法,运行结果如下:
根据方法的运行结果,我们可以看出,即使我们使用父类的引用去指向子类的对象,只要引用实际所指向的对象是子类的对象,那么通过这个引用调用方法的时候,调用到的就是子类的方法,父类的中的那个方法仿佛被屏蔽了,因此方法的重写也叫“覆盖”。
其实,“重写”和“覆盖”这两个词是从两个不同的角度描述了这种编程机制。“重写”是从编码的角度来说的,它体现了子类“重新编写”了父类的某个方法,因此叫“重写”。而“覆盖”是从代码运行效果的角度来说的,它形象的体现出:当子类重写了父类的某个方法之后,当子类对象通过方法名称调用该方法,不会调用到父类中定义的那个方法,只能调用到子类中所定义的那个同名方法,父类中的那个方法如同被子类中重新定义的同名方法覆盖住不见踪影一样,因此叫“覆盖”。
通过以上这个小例子,我们能够体会到:Java语言中引入重写机制,为的就是让我们在编码的时不必受限于父类。子类可以继承父类的方法以减少编码量,但是如果认为父类的某个方法不适合自身,或者这个方法效率不高,子类完全可以重新编写一个更加适合自身或效率更高的同名方法去代替它。
虽然我已经理解了什么是方法的重写,但我其实还是不清楚在什么情况下要重写父类的方法,在此我总结出需要进行方法重写的三种常见情况。
重写的三种常见情况
一、父类要求子类重写
这种情况其实就是指父类无法定义出某个方法的实现过程,于是只能把这个方法定义成抽象方法,从而强制子类去重写这个抽象方法。这个过程虽然被称为“实现”,但它实际上就是对某个方法的重写。因为从本质上来讲,这个过程就是把父类的一个没有实现过程的空方法(即抽象方法)重新编写为一个有具体实现过程的方法。
二、父类中的方法不适合子类
子类如果继承了父类的某个方法,但发现这个方法并不适合自己,就需要重写这个方法。最典型的例子就是表示字符串的String类继承了Object类的equals()方法。但Object类中的equals()方法是用来比较两个对象是否为同一个对象,String类则希望自己的equals()方法能够比较两个字符串的“内容”是否相同,于是在String类当中就重写了equals()方法。有兴趣的小伙伴可以自己去查看一下这两个类当中的equals()方法源码。
三、父类中的方法效率较低或算法陈旧
第三种情况就是:由于各种历史问题的原因,导致原先父类中定义的方法存在效率偏低或算法陈旧,以及线程不安全等情况,并且我们还不能修改父类方法的源代码。在这种情况下,子类就可以用更先进的实现过程来重写父类中的方法。刚才我们看到的Father类和Child类的例子就属于这种情况。
另外,我们还必须要强调一个原则,那就是:子类在重写父类方法的时候,不能更改父类方法的原宗旨。比如说:父类Father中的sum()方法是用来求累加之和的,子类Child在重写父类的sum()方法的时候,就不能把sum()方法改成求阶乘的运算。这个原则适用于所有情况的方法重写,请务必牢记。
接下来我们再来说说子类在重写父类方法的时候,必须遵守的那些语法规则。子类重写父类的方法,需要遵守“三同不降不多抛”的七字规则。
重写的七字规则
所谓“三同”就是指子类重写的方法要与父类中原方法的名称、参数和返回值都相同。如果方法名称不相同,将被编译器视为子类新扩展出的方法。同理,如果方法的参数不同,则被编译器视为子类新增加了一个“重载”关系的方法。如果返回值不同,则被编译器视为违反重写规则。
语法规则“三同不降不多抛”中的“不降”是指子类重写父类方法时,不能降低方法的访问度。比如说,父类声明方法的访问度为“public”,子类就不能擅自将方法的访问度降为“protected”或者是更低的访问度,否则将无法通过语法检查。
接下来说说“不多抛”。所谓“不多抛”是子类重写父类方法时,不能用throws关键字声明抛出更多的异常。这里的“更多”并不是指数量上的多,而是指范围不能扩大。可以看看这幅图帮助理解:
从图中我们可以看出,虽然从数量上来讲,父类的test()方法声明抛出两个异常,子类重写的test()方法只声明抛出一个异常,但子类声明的是Exception,Exception代表了所有的异常,换句话说就是:Exception所能代表的异常的种类更多、范围更大。因此虽然从数量上子类的test()方法没有比父类的test()方法抛出更多异常,但范围却扩大了,这也是不允许的。
我们在实际开发过程中,有的时候会因为粗心导致子类并没有真正的重写父类的方法。比如说父类定义的方法名为”sum”,而子类中却把这个方法错误的写成了”snm”。程序员可能因为粗心没有发现这个错误,导致自己写了半天代码却没有实现“覆盖”的效果。为了避免这种错误,我们在重写某个方法的时候,可以在方法的上面加上@Override注解。一旦加上这个注解,编译器就知道这个方法是意图覆盖父类中的某个方法,于是就会检查父类中是否有同名方法,如果发现子类中的方法与父类中任何一个方法都不同名,那么就标出语法错误来提示程序员。同时,其他程序员看到@Override注解,也能立刻明白这个方法是重写了父类的某个方法。因此,我们最好在所有重写的方法前面都要加上@Override注解。
当子类重写了父类中的某个方法之后,如果从子类内部去调用这个方法的时候,调用到的一定是重写之后的那个方法。不理解的同学还是看下图
从图上我们可以看出,在子类的method()方法中去调用test()方法,调用到的是子类重写过的test()方法。但是,如果我们希望在子类内部调用父类中那个被覆盖了的test()方法该怎么办呢?这时候,我们必须在方法的前面加上super关键字,代码如下:
在test()方法的前面加上super关键字,可以从子类的内部调用到那个已经被覆盖了的父类的test()方法。
有读者会问:以上讲解的都是子类重写父类方法的知识点,那么父类是否真的如同“被宰割的羔羊”一般,任由子类重写它所定义的方法吗?如果我们定义一个类,能否不让子类去重写这个类中的方法呢?当然是可以的,我们只要在某个方法的前面加上一个final关键字,那么子类就无法重写这个方法啦!
参考文献:
如何理解Java中重写(覆盖、Override) - 知乎 (zhihu.com)