​很多初学Java语言的小伙伴,在学到“面向对象”这块内容的时候,都会学到的一个概念,那就是“方法的重写”。重写又叫覆盖,英文名为“Override”。虽然”重写”、 ”覆盖”、“Override”这些名词都很容易记住,但很多人并没有真正理解Java语言为什么要提供“重写”这种编程机制,也不知道什么时候该重写父类中的方法,下面我们通过一篇文章来全面学习一下“方法的重写”。

假设有一个类叫做Father,并且我们假设因为某种原因,我们只能使用这个类,但没有办法修改这个类的源代码。Father类中提供了一个能够求正整数累加之和的方法叫做sum。(所谓“累加”就是从1一直加到某个数,比如1+2+3+...+100)代码如下​

Java千问24:一文读懂Java语言方法的重写(覆盖、Override)_重写

后来,我们又编写了一个类叫做Child,它继承了Father类。其他方面都没什么问题,但Father类所提供的这个用于求整数累加之和的方法sum()效率实在太低了,每做一次累加都要执行多次循环。求整数累加之和明明可以用更高效的方法实现,但出于某种原因,我们无法修改Father类的源代码,难道我们继承了Father类就只能被迫选择使用这个效率很低的sum()方法吗?​

幸好,Java语言提供了“重写”这种编程机制。重写,顾名思义,就是在子类中把继承自父类的某个方法重新写一遍。这样就能在子类中弄出一个同名的、更适合自身或者是效率更高的方法。于是我们就可以在子类中重写了sum()方法,代码如下​

Java千问24:一文读懂Java语言方法的重写(覆盖、Override)_Java_02

通过观察代码我们不难发现,重写之后的sum()方法摒弃了循环求和的算法,而采用了更高效的等差数列求和的方法完成累加的计算。这样明显提高了运算效率。当我们创建一个子类对象,并且调用该对象的sum()方法时,虚拟机将会调用重写之后的sum()方法,而不是父类中那个老的sum()方法。​

但是,如果我们在代码在中,使用了父类的引用去指向子类对象的时候,还能不能调用到那个重写之后的sum()方法呢?看下图​

Java千问24:一文读懂Java语言方法的重写(覆盖、Override)_覆盖_03

从上面的代码中我们可以看到,创建了一个Child类对象,但是指向这个对象的对象的引用f却是一个Father类的引用。那么在这种情况下,当我们通过引用f调用sum()方法的时候,调用到的父类中的sum()方法,还是子类中重写过的sum()方法呢?执行main()方法,运行结果如下​

Java千问24:一文读懂Java语言方法的重写(覆盖、Override)_Override_04

根据方法的运行结果,我们可以看出,即使我们使用父类的引用去指向子类的对象,只要引用实际所指向的对象是子类的对象,那么通过这个引用调用方法的时候,调用到的就是子类的方法,父类的中的那个方法仿佛被屏蔽了,因此方法的重写也叫“覆盖”。​

其实,“重写”和“覆盖”这两个词是从两个不同的角度描述了这种编程机制。“重写”是从编码的角度来说的,它体现了子类“重新编写”了父类的某个方法,因此叫“重写”。而“覆盖”是从代码运行效果的角度来说的,它形象的体现出:当子类重写了父类的某个方法之后,当子类对象通过方法名称调用该方法,不会调用到父类中定义的那个方法,只能调用到子类中所定义的那个同名方法,父类中的那个方法如同被子类中重新定义的同名方法覆盖住不见踪影一样,因此叫“覆盖”。​

通过以上这个小例子,我们能够体会到:Java语言中引入重写机制,为的就是让我们在编码的时不必受限于父类。子类可以继承父类的方法以减少编码量,但是如果认为父类的某个方法不适合自身,或者这个方法效率不高,子类完全可以重新编写一个更加适合自身或效率更高的同名方法去代替它。​

虽然我们已经理解了什么是方法的重写,但很多小伙伴还是不清楚在什么情况下要重写父类的方法,在此我们总结出需要进行方法重写的三种常见情况:​

一、父类要求子类重写

这种情况其实就是指父类无法定义出某个方法的实现过程,于是只能把这个方法定义成抽象方法,从而强制子类去重写这个抽象方法。这个过程虽然被称为“实现”,但它实际上就是对某个方法的重写。因为从本质上来讲,这个过程就是把父类的一个没有实现过程的空方法(即抽象方法)重新编写为一个有具体实现过程的方法。​

二、父类中的方法不适合子类

子类如果继承了父类的某个方法,但发现这个方法并不适合自己,就需要重写这个方法。最典型的例子就是表示字符串的String类继承了Object类的equals()方法。但Object类中的equals()方法是用来比较两个对象是否为同一个对象,String类则希望自己的equals()方法能够比较两个字符串的“内容”是否相同,于是在String类当中就重写了equals()方法。有兴趣的小伙伴可以自己去查看一下这两个类当中的equals()方法源码。​

三、父类中的方法效率较低或算法陈旧

第三种情况就是:由于各种历史问题的原因,导致原先父类中定义的方法存在效率偏低或算法陈旧,以及线程不安全等情况,并且我们还不能修改父类方法的源代码。在这种情况下,子类就可以用更先进的实现过程来重写父类中的方法。刚才我们看到的Father类和Child类的例子就属于这种情况。​

另外,我们还必须要强调一个原则,那就是:子类在重写父类方法的时候,不能更改父类方法的原宗旨。比如说:父类Father中的sum()方法是用来求累加之和的,子类Child在重写父类的sum()方法的时候,就不能把sum()方法改成求阶乘的运算。这个原则适用于所有情况的方法重写,请务必牢记。​

接下来我们再来说说子类在重写父类方法的时候,必须遵守的那些语法规则。子类重写父类的方法,需要遵守“三同不降不多抛”的七字规则。​

所谓“三同”就是指子类重写的方法要与父类中原方法的名称、参数和返回值都相同。如果方法名称不相同,将被编译器视为子类新扩展出的方法。同理,如果方法的参数不同,则被编译器视为子类新增加了一个“重载”关系的方法。如果返回值不同,则被编译器视为违反重写规则。​

但是,关于“返回值相同”,这里需要说的更详细一点。通常情况下,都是父类的原方法声明某种类型的返回值,子类重写的方法也声明相同类型的返回值。但有一种特殊情况,编译器会认为是合法的,那就是:父类原方法声明的返回值是一个父类类型,子类的重写方法声明的是一个子类类型。有的小伙伴可能没理解这句话的意思,我们直接看下图​

Java千问24:一文读懂Java语言方法的重写(覆盖、Override)_Java_05

通过上图我们可以看到,代码中定义了两对父子类,分别是A和B以及SuperClass和SubClass。SuperClass在定义test()方法时声明方法返回A类对象,而SubClass在重写test()方法时声明方法返回B类对象。SuperClass是SubClass的父类,A又是B的父类,它们都有相同的父子继承关系,编译器认为这种重写方式是合法的。但是如果SubClass在重写test()方法时声明方法的返回值为String或者是其他的某个类的对象,只要这个对象不是A类的子类对象,都将被编译器认定为不合法操作。因此,我们要正确理解“返回值相同”这个规则。​

语法规则“三同不降不多抛”中的“不降”是指子类重写父类方法时,不能降低方法的访问度。比如说,父类声明方法的访问度为“public”,子类就不能擅自将方法的访问度降为“protected”或者是更低的访问度,否则将无法通过语法检查。​

接下来说说“不多抛”。所谓“不多抛”是子类重写父类方法时,不能用throws关键字声明抛出更多的异常。这里的“更多”并不是指数量上的多,而是指范围不能扩大。不理解的小伙伴看下图​

Java千问24:一文读懂Java语言方法的重写(覆盖、Override)_覆盖_06

从图中我们可以看出,虽然从数量上来讲,父类的test()方法声明抛出两个异常,子类重写的test()方法只声明抛出一个异常,但子类声明的是Exception,Exception代表了所有的异常,换句话说就是:Exception所能代表的异常的种类更多、范围更大。因此虽然从数量上子类的test()方法没有比父类的test()方法抛出更多异常,但范围却扩大了,这也是不允许的。​

我们在实际开发过程中,有的时候会因为粗心导致子类并没有真正的重写父类的方法。比如说父类定义的方法名为”sum”,而子类中却把这个方法错误的写成了”snm”。程序员可能因为粗心没有发现这个错误,导致自己写了半天代码却没有实现“覆盖”的效果。为了避免这种错误,我们在重写某个方法的时候,可以在方法的上面加上@Override注解。一旦加上这个注解,编译器就知道这个方法是意图覆盖父类中的某个方法,于是就会检查父类中是否有同名方法,如果发现子类中的方法与父类中任何一个方法都不同名,那么就标出语法错误来提示程序员。同时,其他程序员看到@Override注解,也能立刻明白这个方法是重写了父类的某个方法。因此,我们最好在所有重写的方法前面都要加上@Override注解。​

当子类重写了父类中的某个方法之后,如果从子类内部去调用这个方法的时候,调用到的一定是重写之后的那个方法。不理解的同学还是看下图​

Java千问24:一文读懂Java语言方法的重写(覆盖、Override)_Java_07

从图上我们可以看出,在子类的method()方法中去调用test()方法,调用到的是子类重写过的test()方法。但是,如果我们希望在子类内部调用父类中那个被覆盖了的test()方法该怎么办呢?这时候,我们必须在方法的前面加上super关键字,代码如下:​

Java千问24:一文读懂Java语言方法的重写(覆盖、Override)_重写_08

在test()方法的前面加上super关键字,可以从子类的内部调用到那个已经被覆盖了的父类的test()方法。这里插一句:super关键字的用法也有很多,我们会专门写一篇文章介绍super关键字的各种用法。​

有小伙伴会问:以上讲解的都是子类重写父类方法的知识点,那么父类是否真的如同“被宰割的羔羊”一般,任由子类重写它所定义的方法吗?如果我们定义一个类,能否不让子类去重写这个类中的方法呢?当然是可以的,我们只要在某个方法的前面加上一个final关键字,那么子类就无法重写这个方法啦!​

以上讲了这么多,说的都是子类重写(或覆盖)父类的“方法”,那么子类能否同样也覆盖父类的“属性”呢?既然说到这个问题,我们也来总结一下。​

其实子类中确实可以定义一个跟父类同名的属性,并且还能给属性赋一个不同于父类属性的初始值,但这个操作并不叫“重写”或“覆盖”,我们可以把它叫做“屏蔽”。关于屏蔽父类属性这个操作,我们要掌握以下几个知识点:​

一、子类屏蔽父类属性与父类属性的类型及访问修饰符无关

话不多说,直接看下图​

Java千问24:一文读懂Java语言方法的重写(覆盖、Override)_重写_09

父类中定义了一个属性名叫a,类型为int,访问度为public,子类中完全可以再定义一个叫a的属性,这个a属性可以与父类中a属性类型不同、访问度不同、初始值不同,完全不会有任何语法问题,只要属性的名称相同就能实现屏蔽效果。​

二、属性的访问由引用(而非对象)决定

还是看下图:​

Java千问24:一文读懂Java语言方法的重写(覆盖、Override)_Override_10

通过上图我们可以看到,代码中分别创建了三个对象,其中第二条语句是父类的引用指向了子类的对象。在这种情况下,通过父类的引用super2去访问a属性,访问到的是父类(即SuperClass类)中的a,而非子类中的a。这一点与方法的访问效果是不同的,小伙伴们一定要注意这个细节。​

三、通过super关键字访问被屏蔽的属性

继续看图​

Java千问24:一文读懂Java语言方法的重写(覆盖、Override)_Java_11

子类的方法如果直接访问a,那么肯定是访问子类自身的a。如果我们想要访问被屏蔽的父类a属性,只要加上一个super关键字就可以了,这跟访问被子类覆盖了的方法是一样的效果。​

四、final关键字无法阻止屏蔽

继续看图​

Java千问24:一文读懂Java语言方法的重写(覆盖、Override)_Override_12


从图中的代码可以看出,父类的a属性前即使加上了final关键字,子类仍然可以定义一个同名属性来屏蔽父类中的a属性,完全不会出现任何语法问题。有小伙伴会问:既然加上final关键字无法阻止屏蔽,那么这个关键字有什么意义呢?答案是:这个final关键字能够防止父类中的a属性被修改值,使之成为一个常量。​

以上几条就是我们总结的关于子类屏蔽父类属性需要小伙伴们记住的几个关键知识点。​

本文较长,讲了很多知识点,有遗漏和错误之处欢迎小伙伴们留言指出。

如果想系统学习Java编程可以点击这里观看我在本站的视频课程。