重写是子类对父类方法的实现过程进行重新编写。重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
从理论上来讲,子类能够继承父类的所有属性和方法。当子类继承了父类的某个方法后,如果发现这个方法并不适合自己,或者是这个方法的算法效率较低,那么对于子类来说,这个从父类继承而来的方法就成为了它体内的“不良基因”。面对从父类中继承过来的“不良基因”,子类是不是只能束手无策,被动接受这样的事实呢?
在Java语言中,提供了一种叫做“重写”的机制,通过这种机制,子类可以重新编写那些不适合自己或是算法效率较低方法,从而去除掉从父类中继承过来的“不良基因”。假如在Person类中定义了一个累加求和的方法sum(),这个方法可以计算从1到n累加求和的结果,其代码如下:
- int sum(int n) {
- int r=0;
- if(n<1) {
- r = n;
- }else {
- for(int i=1;i<=n;i++) {//用循环的方式完成累加求和
- r = r+i;
- }
- }
- System.out.println("父类的sum()方法求和结果:"+r);
- return r;
- }
以上代码所定义的sum()方法中,为了表明这个方法属于父类(Person),在方法中特意添加了一条输出语句,输出“父类的sum()方法求和结果”。该方法中,如果参数n>=1,则使用循环的方式完成从1到n的累加求和。以这种循环的方式完成累加求和,虽然能够计算出正确的结果,但效率偏低。如果参数n的值为10000,就需要虚拟机完成10000次循环。众所周知:完成累加求和可以使用效率更高的等差数列求和公式实现。因此,在子类中可以对该方法进行“重写”操作。所谓“重写”就是子类重新编写父类的某个方法。子类(Student)以等差数列求和公式完成重写sum()方法的代码如下:
- int sum(int n) {
- int r = 0;
- if(n<1) {
- r = n;
- }else {
- r = (1+n)*n/2;//用等差数列求和公式完成累加求和
- }
- System.out.println("子类的sum()方法求和结果:"+r);
- return r;
- }
为了与父类中定义的sum()方法相区分,在子类重写后的sum()方法中添加了一条输出语句,输出“子类的sum()方法求和结果”。从以上代码可以看出,经子类重写后的sum()方法,效率明显得到了提高,在n>=1的情况下,无论n的值是多少,只需要一个简单的四则运算就能完成求和操作。
把上述重写过的sum()方法添加到Student类中之后,运行下面的【例05_04】就能体验重写机制的实际效果:
【例05_04 子类重写父类的方法】
Exam05_04.java
- public class Exam05_04 {
- public static void main(String[] args) {
- Person p = new Person();
- p.sum(100);//调用父类的sum()方法累加求和
- Student s = new Student();
- s.sum(100);//调用子类的sum()方法累加求和
- }
- }
【例05_04】的执行效果如图5-8所示:
图5-8 【例05_04】运行结果
通过图5-8可以看出:子类对象s在调用sum()方法时,并不是调用从父类中继承过来的那个sum(),而是调用了经过自身重写之后的sum()方法。
通过【例05_04】,各位读者已经能够理解重写方法的意义及实现过程。但必须强调:方法的重写是在子类中进行的,并不是直接修改父类中的方法。读者可能会问:既然要被重写的方法来自于父类,为什么不直接去修改父类中方法的源代码以达到重写的目的呢?主要有两方面的原因:
首先,很多情况下程序员根本无法直接修改父类的源代码。例如,程序员所定义的类经常要以Java基础类库中的类作为父类,在这种情况下,程序员根本无法修改基础类库中类的源代码,而只能在自己所定义的类中重写某些方法。
其次,有些情况下,父类所定义的某个方法对大多数子类都是适用的,仅对个别子类不适用,在这种情况下,如果修改了父类方法的源代码会影响到所有子类,因此不能直接修改父类方法的源代码。例如,在Person类中所定义的sayName()方法,该方法用来输出人的真实姓名。该方法对于学生(Student)、教师(Teacher)、工人(Worker)等大多数子类都适用。但如果有一个子类Scout,它表示侦查员。侦查员深入敌人内部工作,不能随意暴露自己的真实姓名,因此从父类继承过来的sayName()方法并不适用于这个子类,这就需要Scout类重写sayName()方法。重写sayName()方法的思路是:在Scout类中定义一个String类型的属性virtualName,它表示侦查员的代号或假名字。在重写后sayName()方法中并不输出Scout对象的name属性(真实名字),而是输出其virtualName属性(假名字)。读者可以尝试自行定义Scout类并完成对sayName()方法的重写。
子类在对父类方法进行重写时需要遵循哪些语法规则呢?重写父类中的方法必须做到“三个相同”:即方法名称相同、参数相同以及返回值类型相同。例如,父类中所定义的方法名称为sum,子类在重写该方法时所定义的方法名也必须是sum,如果方法名称不相同,则编译器会认为子类又扩展出了一个属于自己的新方法。另外,重写父类方法时还需要做到参数相同,如果仅仅是方法名相同而参数不同,编译器会认为子类中定义的方法与父类中所定义的方法形成重载关系。而返回值类型相同也不难理解,即子类方法所定义的返回值必须与父类方法所定义的返回值类型完全相同。但是,Java编译器在判定返回值类型是否相同时允许一种特殊情况的存在:即子类重写后的方法返回值类型可以是父类方法返回值类型的子类。为帮助读者理解这种特殊情况,此处特举【例05_05】进行说明。
【例05_05 重写方法时返回值类型相同的特殊情况】
A.java
- public class A{
-
- }
B.java
- public class B extends A{
-
- }//B是A的子类
SupserClass.java
- public class SuperClass {
- A test() {//父类test()方法返回值类型为A
- A a = new A();
- System.out.println(“父类test()方法返回值类型为A”);
- return a;
- }
- }
SubClass.java
- public class SubClass extends SuperClass {// SubClass是SuperClass的子类
- B test() {//重写后的test()方法返回值类型为B
- B b = new B();
- System.out.println(“重写后的test()方法返回值类型为B”);
- return b;
- }
- }
Exam05_05.java
- public class Exam05_05 {
- public static void main(String[] args) {
- SuperClass sup = new SuperClass();
- sup.test();//调用父类的test()方法
- SubClass sub = new SubClass();
- sub.test();//调用子类的test()方法
- }
- }
【例05_05】共涉及5个类,分别是:A、B、SuperClass、SubClass和Exam05_05。这5个类中有两对类有父子关系:B是A的子类,SubClass是SuperClass的子类。SuperClass类中有一个test()方法,该方法的返回值类型为A,在SubClass中重写SuperClass的test()方法时,其返回值类型不是A,而是A的子类B,这种情况下,编译器也会认为这种重写是合法的,并且程序也能正确运行,这就是所谓的“子类重写后的方法返回值类型可以是父类方法返回值类型的子类”。但是,如果B并非A的子类,那么这种重写方式将导致语法错误。
方法的重写也被称为“覆盖”,英文写法是Override。其实,“重写”和“覆盖”这两个词是从两个不同的角度描述了这种语法机制。“重写”是从编码的角度来说的,它体现了子类“重新编写”了父类的某个方法,因此叫“重写”。而“覆盖”是从代码运行效果的角度来说的,它形象的体现出:当子类重写了父类的某个方法之后,当子类对象调用该方法时,不会调用到父类中定义的那个方法,只能调用到子类中所定义的那个同名方法,父类中的那个方法如同被子类中重写过的方法覆盖住再也看不见一样,因此叫“覆盖”。
子类在重写父类方法的时候,须遵循一个原则:不能更改父类方法的原宗旨。例如:父类Person中的sum()方法是用来求累加之和的,子类Student在重写父类的sum()方法的时候,就不能把sum()方法改成求阶乘的运算。这个原则适用于所有情况的方法重写,请务必牢记。
当子类重写了父类中的某个方法之后,如果从子类内部的其他方法中去调用该方法时,调用到的一定是重写之后的那个方法。例如,在【例05_05】的SubClass类中定义一个call()方法,在该方法中如果直接调用test(),调用到的将是子类重写过的那个test()方法。如果想要调用到父类(SuperClass)中所定义的那个原始的test()方法,需要在方法名称前面加上“super.”。super关键字表示“父类的...”。以下代码展示了如何调用被子类重写过的test()方法以及父类原始的test()方法:
- void call() {
- test();//此处将调用被子类重写过的test()方法
- super.test();//此处将调用父类中所定义的test()方法
- }
读者可以把上面定义的call()方法添加到SubClass类中,并尝试在main()方法中调用它,以观察该方法在执行过程中对两个不同版本的test()方法的调用效果。
子类可以重写父类中的某个方法,但父类也可以阻止子类对其方法进行重写。如果父类不希望子类重写它的某个方法,可以在该方法前面加上final关键字。因此,当子类试图重写父类中的某个方法,而该方法在父类中已经被final关键字所修饰,就会导致程序出现语法错误。