假设您在一家公司工作,经理的待遇与其他员工不同。当然,经理在许多方面都与员工一样。员工和管理人员均获得薪水。但是,虽然期望员工完成分配的任务以获取薪水作为回报,但如果经理实际实现了应做的工作,则经理将获得 奖金。这种情况迫切需要继承。
您需要定义一个新的类Manager,并添加功能。但是您可以保留已经在Employee类中编程的一些内容,并且 可以保留原始类的所有字段。更抽象地讲,经理和员工之间存在明显的“是”关系。每个经理 是 一个员工:这“是一个”(is-a)关系是继承的标志。
1.定义子类
这是定义从Employee类继承的Manager类的方式。使用Java关键字extends表示继承。
public class Manager extends Employee
{
added methods and fields
}
C ++注意:继承在Java和C ++中类似。Java使用extends关键字而不是‘:’符号。Java中的所有继承都是公共继承。没有类似C ++私有继承和保护继承功能。
关键字extends表示您正在建立一个从现有类派生的新类。现有的类称为 超类, 基类或父类。新类称为 subclass, 派生类或 子类。
Employee类是超类,但不是因为它优于其子类或包含更多功能。 实际上,事实恰恰相反:子类比其父类具有 更多的功能。例如,当您浏览其余的Manager类代码时,如您所见,Manager类比其超类Employee封装了更多数据并具有更多功能。
注意:前缀 super和 sub来自理论计算机科学和数学中使用的集合的语言。所有雇员的集合包含所有经理的集合,因此被称为是 经理集合的超集。或者,换句话说,所有经理 的集合是所有雇员的集合的子集。
我们的Manager类有一个用于存储奖金的新字段,以及一个用于设置奖金的新方法:
public class Manager extends Employee
{
private double bonus;
. . .
public void setBonus(double bonus)
{
this.bonus = bonus;
}
}
如果您有一个Manager对象,则可以简单地应用setBonus方法:
Manager boss = . . .;
boss.setBonus(5000);
但是, 可以将诸如getName和getHireDay之类的方法与Manager对象一起使用。
即使没有在Manager类中显式定义这些方法,它们也会自动从Employee超类继承。
同样,字段name,salary和hiredDay均取自超类。每个Manager对象都有四个字段:name,salary,hiredDay和bonus。
通过扩展子类的超类来定义子类时,只需指示 子类和超类之间的区别。在设计类时,将最通用的方法放在超类中,将更专门的方法放在其子类中。通过将通用功能移到超类中来排除通用功能是面向对象编程中的例行程序。
2.覆盖方法
一些超类方法不适用于Manager子类。
特别是,getSalary方法应返回基本薪水和奖金的和。您需要提供一个新方法来 覆盖超类方法:
public class Manager extends Employee
{
. . .
public double getSalary()
{
. . .
}
. . .
}
假设我们通过以下方式实现该方法:
public double getSalary()
{
return salary + bonus; // won't work
}
但是,那是行不通的。回想一下,只有Employee方法可以直接访问Employee类的私有字段。这意味着Manager类的getSalary方法无法直接访问salary字段。如果Manager方法要访问这些私有字段,则它们必须执行其他所有方法所要做的工作-使用公共接口,在这种情况下,这是Employee类的公共getSalary方法。
因此,让我们再试一次。您需要调用getSalary而不是简单地访问salary字段:
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并非对对象的引用。
例如,您不能将值super分配给另一个对象变量。
相反,super是一个特殊的关键字,它指示编译器调用超类方法。
C ++注意: Java使用关键字super来调用超类方法。在C ++中,应将超类的名称与::运算符一起使用。例如,Manager类的getSalary方法将调用Employee :: getSalary而不是super.getSalary()。
3.子类构造函数
public Manager(String name, double salary, int year, int month, int day)
{
super(name, salary, year, month, day);
bonus = 0;
}
在此,关键字super具有不同的含义。指令:
super(name, salary, year, month, day);
是“用n,s,year,month和day作为参数调用Employee超类的构造函数的简写”。
由于Manager构造函数无法访问Employee类的私有字段,因此必须通过构造函数对其进行初始化。子类的构造函数中的第一条语句必须是使用super的调用。
this关键字和super关键字小结
请记住,this关键字有两个含义:表示对隐式参数的引用,并调用同一类的另一个构造函数。
同样,super关键字具有两个含义:调用超类方法和调用超类构造函数。当用于调用构造函数时,this和super关键字密切相关。构造函数调用只能作为另一个构造函数中的第一条语句发生。构造函数参数可以传递给同一类的另一个构造函数(this)或超类的构造函数(super)。
C ++注:在C ++构造函数中,您不调用超类,但使用初始化程序列表语法构造超类。Manager构造函数在C ++中看起来像这样:
// C++
Manager::Manager(String name, double salary, int year, int month, int day)
: Employee(name, salary, year, month, day)
{
bonus = 0;
}
重新定义Manager对象的getSalary方法后,经理将自动将奖金添加到他们的薪水中。
这是工作示例。我们任命一名新经理,并设置该经理的奖金:
Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15); boss.setBonus(5000);
var staff = new Employee[3];
staff[0] = boss;
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tony Tester", 40000, 1990, 3, 15);
for (Employee e : staff)
System.out.println(e.getName() + " " + 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对象时,将改为调用Manager类的getSalary方法。虚拟机知道e所引用的对象的实际类型,因此可以调用正确的方法。
对象变量(例如变量e)可以引用多个实际类型的事实称为 多态性。在运行时自动选择适当的方法称为 动态绑定。
C ++注意:在C ++中,如果要动态绑定,则需要将成员函数声明为虚函数。在Java中,动态绑定是默认行为。如果您不希望方法是虚拟的,则将其标记为final方法。