“is-a” 关系是继承的一个明显特征。
注释: 我们使用员工和经理的例子,不过这个例子要有所保留。在真实的世界里,员工也可能会成为经理,所以你建模时可能希望经理也是员工,而不是员工的一个子类。不过,在例子中,假设公司只有两类人:一些人永远是员工,另一些人一直是经理。
Employee 类:
import java.time.LocalDate;import java.util.Objects;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;}@Overridepublic String toString() {String str = \"%s[name=%s, salary=%.2f, hireDay=%s]\";return String.format(str, getClass(), name, this.getSalary(), hireDay);}}
Manager 类:
public class Manager extends Employee {private double bonus;public Manager(String name, double salary, int year, int month, int day) {super(name, salary, year, month, day);this.bonus = 0;}public void setBonus(double bonus) {this.bonus = bonus;}@Overridepublic double getSalary() {double baseSalary = super.getSalary();return baseSalary + bonus;}}
1. 定义子类
继承 Employee 类来定义 Manager 类,使用关键字 extends 表示继承。
public class Manager extends Employee {// added methods and fields}
在 Java 中,所有的继承都是公共继承。
关键字 extends 表明正在构造的新类派生于一个已存在的类。已存在的类称为超类(superclass)、基类(base class)或父类(parent class);新类称为子类(subclass)、派生类(derived class)或孩子类(child class)。超类和子类是 Java 程序员最常用的两个术语,而了解其他语言的程序员可能更加偏爱使用父类和孩子类,这可能很贴切地体现了“继承”。
尽管 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);
由于 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 dobule getSalary() {...}}
应该如何实现这个方法呢?乍看起来似乎很简单,只要返回 salary 和 bonus 字段的总和就可以了:
public double getSalary() {return salary + bonus; // 不能运行}
不过,这样做是不行的。回想一下,只有 Employee 方法能直接访问 Employee 类的私有字段。这意味着,Manager 类的 getSalary 方法不能直接访问 salary 字段。如果 Manager 类的方法想要访问那些私有字段,就要像所有其他方法一样使用公共接口,在这里就是要使用 Employee 类中的公共方法 getSalary。
现在,再试一下。你需要调用 getSalary 方法而不是直接访问 salary 字段:
public double getSalary() {double baseSalary = getSalary(); // 仍然不能运行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(name, salary, year, month, day);
是 “调用超类中含有 name、 salary、year、month 和 day 参数的构造器” 的简写形式。
由于 Manager 类的构造器不能访问 Employee 类的私有字段,所以必须通过一个构造器来初始化这些私有字段。可以利用特殊的 super 语法调用这个构造器。使用 super 调用构造器的语句必须是子类构造器的第一条语句。
如果子类的构造器没有显式地调用超类的构造器,将自动地调用超类的无参数构造器。如果超类没有无参数构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,则 Java 编译器将报告错误。
this 关键字有两个含义:
- 指示隐式参数的引用。
- 调用该类的其他构造器。
super 关键字有两个含义:
- 调用超类的方法。
- 调用超类的构造器。
在调用构造器的时候,this 和 super 这两个关键字紧密相关。调用构造器的语句只能作为另一个构造器的第一条语句出现。构造器参数可以传递给当前类(this)的另一个构造器,也可以传递给超类(super)的构造器。
重新定义 Manager 对象的 getSalary 方法之后,奖金将会自动添加到经理的薪水中。
下面给出一个例子说明这个类的使用。我们要创建一个新经理,并设置他的奖金:
Manager boss = new Manager(\"Carl Cracker\", 75000, 1987, 12, 15);boss.setBonus(500000);
下面定义一个包含 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(e);}
运行这条循环语句将会输出下列数据:
Carl Cracker 575000.0Harry Hacker 50000.0Tommy Tester 40000.0
这里的 staff[1] 和 staff[2] 仅输出了基本薪水,这是因为它们是 Employee 对象,而 staff[0] 是一个 Manager 对象,它的 getSalary 方法会将奖金和基本薪水相加。
需要提醒大家的是,以下调用
e.getSalay()
能够选出应该执行的正确 getSalary 方法。请注意,尽管这里将 e 声明为 Employee 类型,但实际上 e 即可以引用 Employee 类型的对象,也可以引用 Manager 类型的对象。
当 e 应用 Employee 对象时,e.getSalary() 调用的是 Employee 类中的 getSalary 方法;当 e 引用 Manager 对象时,e.getSalary() 调用的是 Manager 类中的 getSalary 方法。虚拟机知道 e 实际引用的对象类型,因此能够正确地调用相应的方法。
一个对象变量可以指示多种实际类型的现象被称为多态(polymorphism)。在运行时能够自动地选择调用哪个方法的现象称为动态绑定(dynamic binding)。
在 Java 中,动态绑定是默认的行为。如果不希望让一个方法虚拟的,可以将它标记为 final。
4. 继承层次
继承并不仅限于一个层次。例如,可以由 Manager 类派生 Executive 类。
由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy),在继承层次中,从某个特定的类到其祖先的路径称为该类的继承链(inheritance chain)。
通常,一个祖先类可以拥有多个子孙继承链。例如,可以由 Employee 类派生出子类 Programmer 和 Secretary,它们与 Manager 类没有任何关系(它们彼此之间也没有任何关系)。必要的话,可以将这个过程一直延续下去。
Java 不支持多重继承,但提供了一些类似多重继承的功能——接口。