现在让我们重新回忆一下在前一章中讨论的Employee类。假设你在某个公司工作,这个公司中经理的待遇与普通雇员的待遇存在着一些差异。不过,他们之间也存在着很多相同的地方,例如,他们都领取薪水。只是普通雇员在完成本职任务之后仅领取薪水,而经理在完成了预期的业绩之后还能得到奖金。这种情形就需要使用继承。这是因为需要为经理定义一个新类Manager,以便增加一些新功能。但可以重用Employee类中已将编写的部分代码,并将其中的所有域保留下来。从理论上讲,在Manager与Employee之间存在着明显的“is-a”(是)关系,每个经理都是一名雇员:“is-a”关系是继承的一个明显特征。

    下面是有继承Employee类来定义Manager类的格式,关键字extends表示继承。

 
  1. class Manager extends Employee 
  2.     添加方法和域 

    C++注释:Java与C++定义继承类的方式十分相似。Java用关键字extends代替了C++中的冒号(:)。在Java中,所有的继承都是公有继承,而没有C++中的私有继承和保护继承。

    关键字extends表明正在构造的新类派生于一个已存在的类。已存在的类被称为超类(superclass)、基类(base class)或父类(parent class);新类被称为子类(subclass)、派生类(derived class)或孩子类(child class)。超类和子类是Java程序员最常用的两个术语,而其他语言种类的程序员可能更加偏爱使用父类和孩子类,这些都是继承时使用的术语。

    尽管Employee类是一个超类,但并不是因为它位于子类之上或者拥有比子类更多的功能。恰恰相反,子类比超类拥有的功能更加丰富。例如,读过Manager类的源代码之后就会发现,Manager类比超类Employee封装了更多的数据,拥有更多的功能。

    注释:前缀“超”和“子”来源于计算机科学和数学理论中的集合语言的术语。所有雇员组成的集合包含所有经理组成的集合。可以这样说,雇员集合是经理集合的超集,也可以说,经理集合是雇员集合的子集。

    在Manager类中,增加了一个用于存储奖金信息的域,以及一个用于设置这个域的方法:

 
  1. class Manager extends Employee 
  2.     ... 
  3.  
  4.     public void setBonus(double b) 
  5.     { 
  6.         bonus = b; 
  7.     } 
  8.  
  9.     private double bonus; 

这里定义的方法和域并没有什么特别之处。如果有一个Manager对象,就可以使用setBonus方法。

 
  1. Manager boss = ...; 
  2. boss.setBonus(5000); 

当然,由于setBonus方法不是在Employee类中定义的,所以属于Employee类的对象不能使用它。

    然而,尽管在Manager类中没有显式地定义getName和getHireDay等方法,但属于Manager类的对象却可以使用它们,这是因为Manager类自动地继承了超类Employee类中的这些方法。

    同样,从超类中还继承了name、salary和hireDay这3个域。这样一来,每个Manager类对象就包含了4个域:name、salary、hireDay和bonus。

    在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。因此在设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中,这种将通用的功能放到超类的做法,在面向对象程序设计中十分普遍。

    然而,超类中的有些方法对子类Manager并不一定适用。例如,在Manager类中的getSalary方法应该返回薪水和奖金的总和。为此,需要提供一个新的方法来覆盖(override)超类中的这个方法:

 
  1. class Manager extends Employee 
  2.     .... 
  3.     public double getSalary() 
  4.     { 
  5.         .... 
  6.     } 
  7.     .... 

    应该如何实现这个方法呢?咋看起来似乎很简单,只要返回salary和bonus域的总和就可以了:

 
  1. public double getSalary() 
  2.     return salary + bonus;    //won't work 

然而,这个方法并不能运行。这是因为Manager类的getSalary方法不能够直接地访问超类的私有域。也就是说,尽管每个Manager对象都拥有一个名为salary的域,但在Manager类的getSalary方法中并不能够直接地访问salary域。只有Employee类的方法才能够访问私有部分。如果Manager类的方法一定要访问私有域,就必须借助于公有的接口,Employee类中的公有方法getSalary正是这样一个接口。

    现在,再试一下。将对salary域的访问替换成调用getSalary方法。

 
  1. public double getSalary() 
  2.     double baseSalary = getSalary();    //still won't work 
  3.     return baseSalary + bonus; 

    上面这段代码仍然不能运行。问题出现在调用getSalary的语句上,这是因为Manager类也有一个getSalary方法(就是正在实现的这个方法),所以这条语句将会导致无限次地调用自己,直到整个程序崩溃为止。

    这里需要指出:我们希望调用超类Employee中的getSalary方法,而不是当前类的这个方法。为此,可以使用特定的关键字super解决这个问题:

 
  1. super.getSalary() 

上述语句调用的是Employee类中的getSalary方法。下面是Manager类中getSalary方法的正确书写格式:

 
  1. public double getSalary() 
  2.     double baseSalary = super.getSalary(); 
  3.     return baseSalary + bonus; 

    注释:有些人认为super与this引用是类似的概念,实际上,这样的比较并不太恰当。这是因为super不是一个对象的引用,不能将super赋给一个对象变量,它只是一个指示编译器调用超类方法的特有关键字。

    正像前面所看到的那样,在子类中可以增加域、增加方法或覆盖超类的方法,然而绝对不能删除继承的任何域和方法。

    C++注释:在Java中使用关键字super调用超类的方法,而在C++中则采用超类之名加上::操作符的形式。例如,在Manager类的getSalary方法中,应该将super.getSalary替换为Employee::getSalary。

    最后,看一下super在构造器中的应用。

 
  1. public Manager(String aName, double aSalary, int aYear, int aMonth, int aDay) 
  2.     super(aName, aSalary, aYear, aMonth, aDay); 
  3.     bonus = 0

    这里的关键字super具有不同的含义。语句

 
  1. super(aName, aSalary, aYear, aMonth, aDay); 

是“调用超类Employee中含有aName、aSalary、aYear、aMonth和aDay参数的构造器”的简写形式。

    由于Manager类的构造器不能访问Employee类的私有域,所以必须利用Employee类的构造器对这部分私有域进行初始化,我们可以通过super实现对超类构造器的调用。使用super调用构造器的语句必须是子类构造器的第一条语句。

    如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,则Java编译器将报告错误。

    注释:回忆一下,关键字this有两个用途:一是引用隐式参数,二是调用该类其他的构造器。同样,super关键字也有两个用途:一是调用超类的方法,二是调用超类的构造器。在调用构造器的时候,这两个关键字的使用方式很相似。调用构造器的语句只能作为另一个构造器的第一条语句出现。构造器参数既可以传递给本类(this)的其他构造器,也可以传递给超类(super)的构造器。

    C++注释:在C++的构造函数中,使用初始化列表语法调用超类的构造函数,而不调用super。在C++中,Manager的构造函数如下所示:

 
  1. Manager::Manager(String aName, double aSalary, int aYear, int aMonth, int aDay)    // C++ 
  2. : Employee(aName, aSalary, aYear, aMonth, aDay) 
  3.     bonus = 0; 
  4. }  

重新定义Manager对象的getSalary方法之后,奖金就会自动地添加到经理的薪水中。下面给出一个例子,其功能为创建一个新经理,并设置他的奖金:

 
  1. Manager boss = new Manager("Carl Cracker"8000019871215); 
  2. boss.setBonus(5000); 

下面定义一个包含3个雇员的数组:

 
  1. Employee[] staff = new Employee[3]; 

将经理和雇员都放到数组中:

 
  1. staff[0] = boss; 
  2. staff[1] = new Employee("Harry Hacker"500001989101); 
  3. staff[2] = new Employee("Tony Tester"400001990315); 

输出每个人的薪水:

 
  1. for(Employee e : staff) 
  2.     System.out.println(e.getName() + " " + e.getSalary()); 

运行这条循环语句将会输出下列结果:

 
  1. Carl Cracker 85000.0 
  2. Harry Hacker 50000.0 
  3. Tony Tester 40000.0 

这里的staff[1]和staff[2]仅输出了基本薪水,这是因为他们对应的是Employee对象,而staff[0]对应的是Manager对象,它的getSalary方法将奖金与基本薪水加在了一起。

    需要提到的是,e.getSalary()能够确定应该执行那个getSalary方法。请注意,尽管这里将e声明为Employee类型,但实际上e既可以引用Employee类型的对象,也可以引用Manager类型的对象。

    当e应用Employee对象时,e.getSalary()调用的是Employee类中的getSalary方法;当e引用Manager对象时,e.getSalary()调用的是Manager类中的getSalary方法。虚拟机知道e实际引用的对象类型,因此能够正确地调用相应的类方法。

    一个对象变量(例如,变量e)可以引用多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。在本章中将详细地讨论这两个概念。

    C++注释:在Java中,不需要将方法声明为虚拟方法。动态绑定是默认的处理方式。如果不希望让一个方法具有虚拟特征,可以将它标记为final(稍后将介绍关键字final)。

    例5-1的程序展示了Employee对象与Manager对象在薪水计算上的区别。

例5-1 ManagerTest.java

 
  1. ipmort java.util.*; 
  2.  
  3. /** 
  4.  * This program demonstrates inheritance. 
  5.  * @ version 1.01 2012-11-30 
  6.  * @ author Wcg 
  7.  */ 
  8. public class ManagerTest 
  9.     public static vod main(String[] args) 
  10.     { 
  11.         // construct a Manager object 
  12.         Manager boss = new Manager("Carl Cracker"8000019871215); 
  13.         boss.setBonus(5000); 
  14.  
  15.         Employee[] staff = new Employee[3]; 
  16.  
  17.         // fill the staff array with Manager and Employee objects 
  18.  
  19.         staff[0] = boss; 
  20.         staff[1] = new Employee("Harry Hacker"500001989101); 
  21.         staff[2] = new Employee("Tommy Tester"400001990315); 
  22.  
  23.         // print out information about all Employee objects 
  24.         for(Employee e : staff) 
  25.             System.out.println("nam=" + e.getName() + ",salary=" + e.getSalary()); 
  26.     } 
  27.  
  28. class Employee 
  29.     public Employee(String n, double s, int year, int month, int day) 
  30.     { 
  31.         name = n; 
  32.         salary = s; 
  33.         GregorianCalendar calender = new GregorianCalendar(year, month - 1, day); 
  34.         hireDay = calendar.getTime(); 
  35.     } 
  36.  
  37.     public String getName() 
  38.     { 
  39.         return name; 
  40.     } 
  41.  
  42.     public double getSalary() 
  43.     { 
  44.         return salary; 
  45.     } 
  46.  
  47.     public Date getHireDay() 
  48.     { 
  49.         return hireDay; 
  50.     } 
  51.  
  52.     public void raiseSalary(double byPercent) 
  53.     { 
  54.         double raise = salary * byPercent / 100
  55.         salary += raise; 
  56.     } 
  57.  
  58.     private String name; 
  59.     private double salary; 
  60.     private Date hireDay; 
  61.  
  62. class Manager extends Employee 
  63.     /** 
  64.      * @param n the employee'a name 
  65.      * @param s the salary 
  66.      * @param year the hire year 
  67.      * @param month the hire month 
  68.      * @param day the hire day 
  69.      */ 
  70.     public Manager(String n, double s, int year, int month, int day) 
  71.     { 
  72.         super(n, s, year, month, day); 
  73.         bonus = 0
  74.     } 
  75.  
  76.     public double getSalary() 
  77.     { 
  78.         double baseSalary = super.getSalary(); 
  79.         return baseSalary + bonus; 
  80.     } 
  81.  
  82.     public void setBonus(double b) 
  83.     { 
  84.         bonus = b; 
  85.     } 
  86.  
  87.     private double bonus;