理解Java继承[核心技术卷-阅读笔记]
@
1.1 类、超类和子类
现在让我们回忆一下在前一章中讨论过的 Employee 类。假设你在某个公司工作,这个公司里经理的待遇与普通员工的待遇存在着一些差异。不过,他们之间也存在着很多相同的地方,例如,他们都领取薪水。只是普通员工在完成本职任务之后仅领取薪水,而经理在完成了预期的业绩之后还能得到奖金。这种情形就需要使用继承。为什么呢?因为需要为经理定义一个新类Manager,并增加一些新功能。但可以重用Employee 类中已经编写的部分代码,并保留原来 Employee 类中的所有字段。从理论上讲,在Manager 与 Employee 之间存在着明显的“is-a”(是)关系,每个经理都是一个员工:“is-a” 关系是继承的一个明显特征。
注释:这一章中,我们使用了员工和经理的经典示例,不过必须提醒你的是对这个例子要有所保留。在真实世界里,员工也可能会成为经理,所以你建模时可能希望经理也是员工,而不是员工的一个子类。不过,在我们的例子中,假设公司里只有两类人:一些人永远是员工,另一些人一直是经理。
1.1.1 定义子类
可以如下继承 Employee 类来定义Manager 类,这里使用关键字 extends 表示继承。
public class Manager extends Employee{
add methods and fields
}
C++注释:Java 与 C++定义继承的方式十分相似。Java 用关键宇extends 代替了C++中的冒号(:)。在Java 中,所有的继承都是公共继承,而没有C++ 中的私有继承和保护继承。
关键字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 类中定义的,所以属于 Emnployee 类的对象不能使用它。
然而,尽管在Manager类中没有显式地定义getName 和getHireDay 等方法,但是可以对Manager 对象使用这些方法,这是因为Manager 类自动地继承了超类 Employee 中的这些方法。
类似地,从超类中还继承了name、salary 和 hireDay 这3个字段。这样一来,每个Manager对象就包含了4个字段:name、salary、hireDay 和 bonus。
通过扩展超类定义子类的时候,只需要指出子类与超类的不同之处。因此在设计类的时候,应该将最一般的方法放在超类中,而将更特殊的方法放在子类中,这种将通用功能抽取到超类的做法在面向对象程序设计中十分普遍。
1.1.2 覆盖方法
超类中的有些方法对子类Manager 并不一定适用。具体来说,Manager 类中的 getSalary 方法应该返回薪水和奖金的总和。为此,需要提供一个新的方法来覆盖(override)超类中的这个方法:
public class Manager extends Employee{
...
public double getSalary()
{
...
}
...
}
应该如何实现这个方法呢?乍看起来似乎很简单,只要返回 salary 和bonus 字段的总和就可以了:
public double getSalary()
{
return salary + bonus; // won't work
}
不过,这样做是不行的。回想一下,只有Enployee 方法能直接访问 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 与this 引用是类似的概念,实际上,这样比较并不大恰当。这是因为super 不是一个对象的引用,例如,不能将值super 賦给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。
正像前面所看到的那样,在子类中可以增加字段、增加方法或覆盖超类的方法,不过,继承绝对不会删除任何字段或方法。
C++注释:在Java 中使用关键宇super调用超类的方法,而在C+t中则采用超类名加 :: 操作符的形式。例如,Manager 类的 getSalary 方法要调用Employee::getSalary 而不是super.getSalary。
1.1.3 子类构造器
在例子的最后,我们来提供一个构造器。
public Manager(String name, double salary, int year, int month, int day)
{
super(name, salary, month, day);
bonus = 0;
}
这里的关键字 super 具有不同的含义。语句
super(name, salary, month, day);
是“调用超类Employee 中带有n、s、year、month 和day 参数的构造器”的简写形式。
由于Manager 类的构造器不能访问 Employee 类的私有字段,所以必须通过一个构造器来初始化这些私有字段。可以利用特殊的super 语法调用这个构造器。使用 super 调用构造器的语句必须是子类构造器的第一条语句。
如果子类的构造器没有显式地调用超类的构造器,将自动地调用超类的无参数构造器。如果超类没有无参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器,Java 编译器就会报告一个错误。
注释:回想一下,关键字 this 有两个含义:一是指示隐式参数的引用,二是调用该类的其他构造器。类似地,super 关键字也有两个含义:一是调用超类的方法,二是调用超类的构造器。在调用构造器的时候,this 和super这两个关键字紧密相关。调用构造器的语句只能作为另一个构造器的第一条语句出现。构造器参数可以传递给当前类(this)的另一个构造器,也可以传递给超类(super)的构造器。
C++注释:在C++的构造器中,会使用初始化列表语法调用超类的构造器,而不调用super。在C++中,Manager 的构造器如下所示:
// 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("Ton 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] 和staffI2] 仅输出了基本薪水,这是因为它们是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++注释:在C++中,如果希望实现动态绑定,需要将成员函数声明为 virtual。在Java 中,动态绑定是默认的行为。如果不希望让一个方法是虛拟的,可以将它标记为final(本文稍后将介绍关键宇 final)。
程序清单5-1的程序展示了 Employee对象(见程序清单 5-2)与Manager对象(见程序清单5-3)在薪水计算上的区别。
// 程序清单 5-1 inheritance/ManagerTest.java
package inheritance;
/**
* This program demostrates inheritance
* @version 15.07 2022-07-22
* @author JIeJaitt
*/
public class ManagerTest
{
public static void main(String[] args) {
// construct a Manager object
var boss = new Manager("Carl Cracker",80000,1987,12,15);
boss.setBonus(5000);
var staff = new Employee[3];
// fill the staff array with Manager and Employee objects
staff[0] = boss;
staff[1] = new Employee("Harry Hacker",50000,1989,10,1);
staff[2] = new Employee("Ton 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());
}
}
// 程序清单 5-2 inheritance/Employee.java
package inheritance;
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;
}
}
// 程序清单 5-3 inheritance/Manager.java
package inheritance;
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 getBonus() {
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
public void setBonus(double b) {
bonus = b;
}
}
1.1.4 继承层次
继承并不仅限于一个层次。例如,可以由Manager 类派生 Executive 类。由一个公共超类派生出来的所有类的集合称为继承层次(inheritance hierarchy),如图5-1 所示。在继承层次中,从某个特定的类到其祖先的路径称为该类的继承链 (inheritance chain)。
通常,一个祖先类可以有多个子孙链。例如,可以由Employee 类派生出子类Programmer 和 Secretary,它们与Manager 类没有任何关系(它们彼此之间也没有任何关系)。必要的话,可以将这个过程一直延续下去。
C++注释:在C++ 中,一个类可以有多个超类。Java 不支持多重继承,但提供了一些类似多重继承的功能,有关内容请参看本博客中有关接口的讨论。
1.1.5 多态
1.1.6 理解方法调用
1.1.7 阻止继承:final类和方法
1.1.8 强制类型转换
1.1.9 抽象类
1.1.10 受保护访问
1.2 Object:所有类的超类
1.2.1 Object类型的变量
1.2.2 equals方法
1.2.3 相等测试与继承
1.2.4 hashCode方法
1.2.5 toString方法
1.3 泛型数组列表
1.3.1 声明数组列表
1.3.2 访问数组列表元素
1.3.3 类型化与原始数组列表的兼容性
1.4 对象包装器与自动装箱
1.5 参数数量可变的方法
1.6 枚举类
1.7 反射
1.7.1 Class 类
1.7.2 声明异常入门
1.7.3 资源
1.7.4 利用反射分析类的能力
1.7.5 使用反射在运行时分析对象
1.7.6 使用反射编写泛型数组代码
1.7.7 调用任意方法和构造器
1.8 继承的设计技巧
在本文的最后,我们给出对设计继承很有帮助的一些技巧。
- 将公共操作和字段放在超类中。
这就是将姓名字段放在 Person 类中,而没有将它重复放在 Employee 和 Student 类中的原因。
- 不要使用受保护的宇段。
有些程序员认为,将大多数的实例字段定义为protected 是一个不错的主意,“以防万一”,这样子类就能够在需要的时候访问这些字段。然而,protected 机制并不能够带来更多的保护,这有两方面的原因。第一,子类集合是无限制的,任何一个人都能够由你的类派生一个子类,然后编写代码直接访问 protected 实例字段,从而破坏了封装性。第二,在Java中,在同一个包中的所有类都可以访问proteced 字段,而不管它们是否为这个类的子类。
不过,protected 方法对于指示那些不提供一般用途而应在子类中重新定义的方法很有用。
- 使用继承实现“is-a”关系。
使用继承很容易达到节省代码量的目的,但有时候也会被人们滥用。例如,假设需要定义一个Contractor类。钟点工有姓名和雇佣日期,但是没有工资。他们按小时计薪,并且不会因为拖延时间而获得加薪。这似乎在诱导人们由Employee 派生出子类 Contractor,然后再增加一个 hourlyWage字段。
public class Contractor extends Employee
{
private double hourlyWage;
...
}
不过,这并不是一个好主意。因为这样一来,每个钟点工对象中都同时包含了工资和时薪这两个字段。在实现打印薪水或税单的方法时,这会带来无尽的麻烦。与不采用继承相比,使用继承来实现最后反而会多写很多代码。
钟点工与员工之间不属于“is-a〞 关系。钟点工不是特殊的员工。
- 除非所有继承的方法都有意义,否则不要使用继承。