使用的教材是java核心技术卷1,我将跟着这本书的章节同时配合视频资源来进行学习基础java知识。
day026 继承(定义子类、覆盖方法、子类构造器)
前面学习了类和对象的概念,从今天开始学习面向对象程序设计的另外一个基本概念:继承(inheritance)。利用继承,人们可以基于已存在的类构造一个新类。继承已存在的类就是复用(继承)这些类的方法和域。在此基础上,还可以添加一些新的方法和域,以满足新的需求。这是Java程序设计中的一项核心技术。
先来学习类、超类、子类的有关关知识。
回忆一下之前的Employee类。假设你在某个公司工作,这个公司中经理的待遇与普通雇员的待遇存在着一些差异。不过,他们之间也存在着很多相同的地方,例如,他们都领取薪水。只是普通雇员在完成本职任务之后仅领取薪水,而经理在完成了预期的业绩之后还能得到奖金。这种情形就需要使用继承。这是因为需要为经理定义一个新类Manager,以便增加一些新功能。但可以重用Employee类中已经编写的部分代码,并将其中的所有域保留下来。从理论上讲,在Manager与Employee之间存在着明显的“is-a”(是)关系,每个经理都是一名雇员:“is-a”关系是继承的一个明显特征。
在下面的例子中,假设公司里只有两类人:一些人一直是员工,另一些人一直是经理。
1.定义子类
下面是由继承Employee类来定义Manager类的格式,关键字extends表示继承。
public class Magager extends Employee
{
添加方法和域
}
关键字extends表明正在构造的新类派生于一个已存在的类。已存在的类称为超类(superclass)、基类(base class)或父类(parent class);新类称为子类(subclass)、派生类(derived class)或孩子类(child class)。
尽管Employee类是一个超类,但并不是因为它优于子类或者拥有比子类更多的功能。实际上恰恰相反,子类比超类拥有的功能更加丰富。例如,读过Manager类的源代码之后就会发现,Manager类比超类Employee封装了更多的数据,拥有更多的功能。
在Manager类中,增加了一个用于存储奖金信息的域,以及一个用于设置这个域的新方法:
public class Manager extends Employee
{
private double bonus;
...
public void setBonus(double b)
{
this.bonus = bonus;
}
}
这里定义的方法和域并没有什么特别之处。如果有一个Manager对象,就可以使用setBonus方法。
Manager boss = ...;
boss.setBomus(5000);
当然,由于setBonus方法不是在Employee类中定义的,所以属于Employee类的对象不能使用它。
然而,尽管在Manager类中没有显式地定义getName和getHireDay等方法,但属于Manager类的对象却可以使用它们,这是因为Manager类自动地继承了超类Employee中的这些方法。
同样,从超类中还继承了name、salary和hireDay这3个域。这样一来,每个Manager类对象就包含了4个域:name、salary、hireDay和bonus。
在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。因此在设计类的时候,应该将通用的方法放在超类中,而将具有特殊用途的方法放在子类中,这种将通用的功能放到超类的做法,在面向对象程序设计中十分普遍。
2.覆盖方法
然而,超类中的有些方法对子类Manager并不一定适用。具体来说,Manager类中的getSalary方法应该返回薪水和奖金的总和。为此,需要提供一个新的方法来覆盖(override)超类中的这个方法:
public class Manager extends Employee
{
...
public double getSalary()
{
...
}
...
}
应该如何实现这个方法呢?乍看起来似乎很简单,只要返回salary和bonus域的总和就可以了:
public double getSalary()
{
return salary+bonus;//won't work
}
然而,这个方法并不能运行。这是因为Manager类的getSalary方法不能够直接地访问超类的私有域。也就是说,尽管每个Manager对象都拥有一个名为salary的域,但在Manager类的getSalary方法中并不能够直接地访问salary域。只有Employee类的方法才能够访问私有部分。如果Manager类的方法一定要访问私有域,就必须借助于公有的接口,Employee类中的公有方法getSalary正是这样一个接口。现在,再试一下。将对salary域的访问替换成调用getSalary方法。
public double getSalary()
{
double baseSalary = getSalary();//still won't work
return baseSalary + bonus;
}
上面这段代码仍然不能运行。问题出现在调用getSalary的语句上,这是因为Manager类也有一个getSalary方法(就是正在实现的这个方法),所以这条语句将会导致无限次地调用自己,直到整个程序崩溃为止。
这里需要指出:我们希望调用超类Employee中的getSalary方法,而不是当前类的这个方法。为此,可以使用特定的关键字super解决这个问题:
super.getSalary()
上述语句调用的是Employee类中的getSalary方法。下面是Manager类中getSalary方法的正确书写格式:
public double getSalary()
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
有些人认为super与this引用是类似的概念,实际上,这样比较并不太恰当。这是因为super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。
正像前面所看到的那样,在子类中可以增加域、增加方法或覆盖超类的方法,然而绝对不能删除继承的任何域和方法。
3.子类构造器
在例子的最后,我们来提供一个构造器。
public Manager(String name,double salary,int year,int month,int day)
{
super(name,salary,year,month,day);
bonus = 0;
}
这里的关键字super具有不同的含义。语句
super(n,s,year,month,day);
是“调用超类Employee中含有n、s、yearmonth和day参数的构造器”的简写形式。
由于Manager类的构造器不能访问Employee类的私有域,所以必须利用Employee类的构造器对这部分私有域进行初始化,我们可以通过super实现对超类构造器的调用。使用super调用构造器的语句必须是子类构造器的第一条语句。
如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器’则Java编译器将报告错误。
关键字this有两个用途:一是引用隐式参数,二是调用该类其他的构造器,同样,super关键字也有两个用途:一是调用超类的方法,二是调用超类的构造器。在调用构造器的时候,这两个关键字的使用方式很相似。调用构造器的语句只能作为另一个构造器的第一条语句出现。构造参数既可以传递给本类(this)的其他构造器,也可以传递给超类(super)的构造器。
重新定义Manager对象的getSalary方法之后,奖金就会自动地添加到经理的薪水中。下面给出一个例子,其功能为创建一个新经理,并设置他的奖金:
Manager boss = new Manager ("Carl Cracher",80000,1987,12,15);
boss.setBonus(5000);
下面定义一个包含3个雇员的数组:
Employee[] staff = new Employee[3];
将经理和雇员都放到数组中:
staff[0] = boss;
staff[1] = new Employee("Harry Hacker",50000,1989,10,1);
staff[2] = new Employee("Tommy Tester",40000,1990,3,15);
输出每个人的薪水:
for (Employee e : staff)
{
System.out.println("name="+e.getName()+",salary="+e.getSalary());
}
运行这条循环语句将会输出下列数据:
Carl Cracker 85000.0
Harry Hacker 50000.0
Tommy 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)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamicbinding)。
在Java中,不需要将方法声明为虚拟方法。动态绑定是默认的处理方式。如果不希望让一个方法具有虚拟特征,可以将它标记为final。
用下面的代码测试一下:
//package inheritance;
/**
*@author zzehao
*/
public class ManagerTest
{
public static void main(String[] args)
{
//construct a Manager object
Manager boss = new Manager ("Carl Cracher",80000,1987,12,15);
boss.setBonus(5000);
//fill the staff array with Manager and Employee objects
Employee[] staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employee("Harry Hacker",50000,1989,10,1);
staff[2] = new Employee("Tommy Tester",40000,1990,3,15);
//print out information about all Employee objects
for (Employee e : staff)
{
System.out.println("name="+e.getName()+",salary="+e.getSalary());
}
}
}
//package inheritance;
/**
*@author zzehao
*/
import java.time.*;
public class Employee
{
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String name,double salary,int year,int month,int day)
{
this.name = name;
this.salary = salary;
hireDay = LocalDate.of(year,month,day);
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
}
}
//package inheritance;
/**
*@author zzehao
*/
public class Manager extends Employee
{
private double bonus;
/**
*@param name the employee's name
*@param salary the salary
*@param year the hire year
*@param month the hire month
*@param day the hire day
*/
public Manager(String name,double salary,int year,int month,int day)
{
super(name,salary,year,month,day);
bonus = 0;
}
public double getSalary()
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double b)
{
bonus = b;
}
}
运行的结果: