第五章 继承
人们可以基于已存在的类构造一个新类。继承已存在的类就 是复用(继承)这些类的方法和域。在此基础上,还可以添加一些新的方法和域, 以满足新 的需求。
类、超类和子类
定义子类
下面是由继承 Employee 类来定义 Manager 类的格式,关键字 extends 表示继承。
public class Manager extends Employee { 添加方法和域 }
关键字 extends 表明正在构造的新类派生于一个已存在的类。 已存在的类称为超类 ( superclass)、 基类(base class) 或父类(parent class); 新类称为子类(subclass)、 派生类 (derivedclass) 或孩子类(child class)。
在 Manager类中,增加了一个用于存储奖金信息的域,以及一个用于设置这个域的新方法:
public class Manager extends Employee { private double bonus; ... public void setBonos(double bonus){ this.bonus = bonus; } }
这里定义的方法和域并没有什么特别之处。 如果有一个 Manager 对象, 就可以使用 setBonus 方法。
Manager boss = . . .;
boss.setBonus(5000);
当然, 由于 setBonus 方法不是在 Employee 类中定义的,所以属于 Employee 类的对象不能使 用它。
在通过扩展超类定义子类的时候,仅需要指出子类与超类的不同之处。因此在设计类的时候,应该将通用的方法放在超类中, 而将具有特殊用途的方法放在子类中,这种将通用的 功能放到超类的做法,在面向对象程序设计中十分普遍。
覆盖方法
然而, 超类中的有些方法对子类 Manager 并不一定适用。具体来说, Manager 类中的 getSalary方法应该返回薪水和奖金的总和。为此,需要提供一个新的方法来覆盖(override) 超类中的这个方法: public class Manager
public class Manager extends Employee { ... public double getSalary(){ ... } ... }
应该如何实现这个方法呢? 只要返回 salary 和 bonus 域的总和就 可以了:
public double getSalary() { double baseSalary = super.getSalary(); return baseSalary + bonus; }
在子类中可以增加域、 增加方法或覆盖超类的方法,然而绝对 不能删除继承的任何域和方法。
子类构造器
我们来提供一个构造器。
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、year month 和 day 参数的构造器” 的简写形式。
由于 Manager 类的构造器不能访问 Employee 类的私有域, 所以必须利用 Employee 类 的构造器对这部分私有域进行初始化,我们可以通过 super 实现对超类构造器的调用。使用 super 调用构造器的语句必须是子类构造器的第一条语句。
如果子类的构造器没有显式地调用超类的构造器, 则将自动地调用超类默认(没有参数) 的构造器。 如果超类没有不带参数的构造器, 并且在子类的构造器中又没有显式地调用超类 的其他构造器 ’ 则 Java 编译器将报告错误。
程序清单 5-1 的程序展示了 Employee 对象(程序清单 5-2 ) 与 Manager (程序清单 5-3 )对象在薪水计算上的区別。 程序清单 5-1 inheritance/ManagerTest.java
//程序清单 5-1 inheritance/ManagerTest.java package inheritance; /** * This program demonstrates inheritance. * @version 1.21 2004-02-21 * @author Cay Horstmann */ 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("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()); } } //程序清单 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 getSalary() { double baseSalary = super.getSalary(); return baseSalary + bonus; } public void setBonus(double b) { bonus = b; } }
继承层次
继承并不仅限于一个层次。 例如, 可以由 Manager 类派生 Executive 类。由一个公共超类派生出来的所有类的集合被称为继承层次(inheritance hierarchy), 如图 5-1 所示。在继承 层次中, 从某个特定的类到其祖先的路径被称为该类的继承链 (inheritance chain)。

多 态
有一个用来判断是否应该设计为继承 关系的简单规则, 这就是“ is-a” 规则, 它 表明子类的每个对象也是超类的对象。
“ is-a” 规则的另一种表述法是置换法则。它表明程序中出现超类对象的任何地方都可以 用子类对象置换。
例如, 可以将一个子类的对象赋给超类变量。
Employee e; e = new Employee(. . .); // Employee object expected e = new Manager(. . .); // OK, Manager can be used as well
在 Java程序设计语言中,对象变量是多态的。 一个 Employee 变量既可以引用一个 Employee 类对象, 也可以引用一个 Employee 类的任何一个子类的对象(例如, Manager、 Executive、Secretary 等)。
从程序清单 5-1 中, 已经看到了置换法则的优点:
Manager boss = new Manager(. . .); Employee[] staff = new Employee[3]; staff[0] = boss;
在这个例子中,变量 staff[0] 与 boss 引用同一个对象。但编译器将 staff[0]看成 Employee对象。
这意味着, 可以这样调用
boss.setBonus(5000); // OK
但不能这样调用
staff[0].setBonus(5000); // Error
理解方法调用
下面是调用过程的详细描述:
1 ) 编译器査看对象的声明类型和方法名。假设调用 x.f(param),且隐式参数 x声明为 C 类的对象。需要注意的是:有可能存在多个名字为 f, 但参数类型不一样的方法。
至此, 编译器已获得所有可能被调用的候选方法
2 ) 接下来,编译器将査看调用方法时提供的参数类型。如果在所有名为 f 的方法中存在 一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重栽解析(overloading resolution)。
至此, 编译器已获得需要调用的方法名字和参数类型
3 ) 如果是 private 方法、 static 方法、final 方法(有关 final 修饰符的含义将在下一节讲 述)或者构造器, 那么编译器将可以准确地知道应该调用哪个方法, 我们将这种调用方式称 为静态绑定(static binding)。
4 ) 当程序运行,并且采用动态绑定调用方法时, 虚拟机一定调用与 x 所引用对象的实 际类型最合适的那个类的方法。
Manager方法表稍微有些不同。其中有三个方法是继承而来的,一个方法是重新定义的, 还有一个方法是新增加的。
Manager: getName() -> Employee.getName() getSalary() -> Manager.getSalary() getHireDay() -> Employee.getHireDay() raiseSalary(double) -> Employee.raiseSalary(double) setBonus(double) -> Manager.setBonus(double)
在运行时, 调用 e.getSalaryO 的解析过程为:
1 ) 首先,虚拟机提取 e 的实际类型的方法表。
2 ) 接下来, 虚拟机搜索定义 getSalary 签名的类。
3) 最后,虚拟机调用方法。
阻止继承:final 类和方法
不允许扩展的类被称为 final 类。如果 在定义类的时候使用了 final 修饰符就表明这个类是 final 类。
声明格式如下所示:
public final class Executive extends Manager { . . . }
类中的特定方法也可以被声明为 final。如果这样做,子类就不能覆盖这个方法(final 类中的所有方法自动地成为 final 方法)。例如
public class Employee { ... public final String getName(){ return name; } . . . }
强制类型转换
将一个类型强制转换成另外一个类型的过程被称为类型转换。Java 程 序设计语言提供了一种专门用于进行类型转换的表示法。例如:
double x = 3.405; int nx = (int) x;
将表达式 x 的值转换成整数类型, 舍弃了小数部分。
正像有时候需要将浮点型数值转换成整型数值一样,有时候也可能需要将某个类的对象 引用转换成另外一个类的对象引用。对象引用的转换语法与数值表达式的类型转换类似, 仅 需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。例如:
Manager boss = (Manager) staff[0];
进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。
这个过程简单地使用 instanceof操作符就可以实现。 例如:
if (staff[1 ] instanceof Manager) { boss = (Manager) staff[1 ]: }
如果这个类型转换不可能成功, 编译器就不会进行这个转换。
进行强制类型转换需要注意以下两点:
•只能在继承层次内进行类型转换。
•在将超类转换成子类之前,应该使用 instanceof进行检查。
如果 x 为 null, 进行下列测试 x instanceof C 不会产生异常, 只是返回 false。之所以这样处理是因为 null 没有引用任何对象, 当 然也不会引用 C 类型的对象。
抽象类
如果自下而上在类的继承层次结构中上移,位于上层的类更具有通用性,甚至可能更加抽象。从某种角度看, 祖先类更加通用, 人们只将它作为派生其他类的基类,而不作为想使 用的特定的实例类。例如, 考虑一下对 Employee 类层次的扩展。一名雇员是一个人, 一名学生也是一个人。下面将类 Person 和类 Student 添加到类的层次结构中。图 5-2 是这三个类 之间的关系层次图。

为了提高程序的清晰度, 包含一个或多个抽象方法的类本身必须被声明为抽象的。
public abstract class Person { public abstract String getDescription(); }
除了抽象方法之外,抽象类还可以包含具体数据和具体方法。例如, Person 类还保存着 姓名和一个返回姓名的具体方法。
public abstract class Person { private String name; public Person(String name) { this.name = name; } public abstract String getDescription(); public String getName(){ return name; } }
抽象方法充当着占位的角色, 它们的具体实现在子类中。扩展抽象类可以有两种选择。 一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽 象类;另一种是定义全部的抽象方法,这样一来,子类就不是抽象的了。
类即使不含抽象方法,也可以将类声明为抽象类。 抽象类不能被实例化。
需要注意,可以定义一个抽象类的对象变量, 但是它只能引用非抽象子类的对象。例如,
Person p = new Student("Vinee Vu", "Economics");
这里的 p 是一个抽象类 Person 的变量,Person 引用了一个非抽象子类 Student 的实例。
//程序清单 5-4 abstractClasses/PersonTest.java package abstractClasses; /** * This program demonstrates abstract classes. * @version 1.01 2004-02-21 * @author Cay Horstmann */ public class PersonTest { public static void main(String[] args) { var people = new Person[2]; // fill the people array with Student and Employee objects people[0] = new Employee("Harry Hacker", 50000, 1989, 10, 1); people[1] = new Student("Maria Morris", "computer science"); // print out names and descriptions of all Person objects for (Person p : people) System.out.println(p.getName() + ", " + p.getDescription()); } }
//程序清单 5-5 abstractClasses/Person.java package abstractClasses; public abstract class Person { public abstract String getDescription(); private String name; public Person(String name) { this.name = name; } public String getName() { return name; } }
//程序清单 5-6 abstractClasses/Employee.java package abstractClasses; import java.time.*; public class Employee extends Person { private double salary; private LocalDate hireDay; public Employee(String name, double salary, int year, int month, int day) { super(name); this.salary = salary; hireDay = LocalDate.of(year, month, day); } public double getSalary() { return salary; } public LocalDate getHireDay() { return hireDay; } public String getDescription() { return String.format("an employee with a salary of $%.2f", salary); } public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; } }
//程序清单 5-7 abstractClasses/Student.java package abstractClasses; public class Student extends Person { private String major; /** * @param name the student's name * @param major the student's major */ public Student(String name, String major) { // pass name to superclass constructor super(name); this.major = major; } public String getDescription() { return "a student majoring in " + major; } }
受保护访问
大家都知道,最好将类中的域标记为 private, 而方法标记为 public。任何声明为private 的内容对其他类都是不可见的。前面已经看到, 这对于子类来说也完全适用,即子类也不能 访问超类的私有域。
受保护的方法更具有实际意义。如果需要限制某个方法的使用, 就可以将它声明为 protected。这表明子类(可能很熟悉祖先类)得到信任,可以正确地使用这个方法,而其他 类则不行。
下面归纳一下 Java 用于控制可见性的 4 个访问修饰符:
1 ) 仅对本类可见 private。
2 ) 对所有类可见 public。
3 ) 对本包和所有子类可见 protected。
4 ) 对本包可见— —默认(很遗憾), 不需要修饰符。
Object: 所有类的超类
Object 类是 Java 中所有类的始祖, 在 Java 中每个类都是由它扩展而来的。但是并不需 要这样写:
public class Employee extends Object
如果没有明确地指出超类,Object 就被认为是这个类的超类。
equals 方法
Object 类中的 equals方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这 个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用, 它们一定是相 等的。
例如, 如果两个雇员对象的姓名、 薪水和雇佣日期都一样, 就认为它们是相等的(在实 际的雇员数据库中,比较 ID 更有意义。利用下面这个示例演示 equals 方法的实现机制)。
public class Employee{ ... public boolean equals(Object otherObject) { // a quick test to see if the objects are identical if (this == otherObject) return true; // must return false if the explicit parameter is null if (otherObject == null) return false; // if the classes don't match, they can't be equal if (getClassO != otherObject.getClass()) return false; // now we know otherObject is a non-null Employee Employee other = (Employee) otherObject; // test whether the fields have identical values return name.equals(other.name) && salary = other,salary && hi reDay.equals(other,hi reDay): • } }
getClass方法将返回一个对象所属的类,有关这个方法的详细内容稍后进行介绍。在检 测中, 只有在两个对象属于同一个类时, 才有可能相等。
相等测试与继承
如果发现类不匹配,equals方法就返冋 false: 但是,许多程序员 却喜欢使用 instanceof进行检测:
if (KotherObject instanceof Employee)) return false;
这样做不但没有解决 otherObject 是子类的情况,并且还有可能会招致一些麻烦。这就是建议 不要使用这种处理方式的原因所在。Java语言规范要求 equals 方法具有下面的特性:
1 ) 自反性:对于任何非空引用 x, x.equals(?0应该返回 truec
2 ) 对称性: 对于任何引用 x 和 y, 当且仅当 y.equals(x) 返回 true, x.equals(y) 也应该返 回 true。
3 ) 传递性: 对于任何引用 x、 y 和 z, 如果 x.equals(y) 返N true, y.equals(z)返回 true, x.equals(z) 也应该返回 true。
4 ) 一致性: 如果 x 和 y引用的对象没有发生变化,反复调用 x.eqimIS(y) 应该返回同样 的结果。
5 ) 对于任意非空引用 x, x.equals(null) 应该返回 false。
下面可以从两个截然不同的情况看一下这个问题:
•如果子类能够拥有自己的相等概念, 则对称性需求将强制采用 getClass 进行检测
•如果由超类决定相等的概念,那么就可以使用 imtanceof进行检测, 这样可以在不同 子类的对象之间进行相等的比较。
hashCode 方法
散列码( hash code) 是由对象导出的一个整型值。散列码是没有规律的。如果 x 和 y 是 两个不同的对象, x.hashCode( ) 与 y.hashCode( ) 基本上不会相同。
String 类使用下列算法计算散列码:
int hash = 0;
for (int i = 0; i < length0;i++)
hash = 31 * hash + charAt(i);
由于 hashCode方法定义在 Object 类中, 因此每个对象都有一个默认的散列码,其值为 对象的存储地址。

程序清单 5-8 的程序实现了 Employee 类(程序清单 5-9 ) 和 Manager•类(程序清单 5-10 ) 的 equals、hashCode 和 toString方法。
//程序清单 5-8 equals/EqualsTest.java package equals; /** * This program demonstrates the equals method. * @version 1.12 2012-01-26 * @author Cay Horstmann */ public class EqualsTest { public static void main(String[] args) { var alice1 = new Employee("Alice Adams", 75000, 1987, 12, 15); var alice2 = alice1; var alice3 = new Employee("Alice Adams", 75000, 1987, 12, 15); var bob = new Employee("Bob Brandson", 50000, 1989, 10, 1); System.out.println("alice1 == alice2: " + (alice1 == alice2)); System.out.println("alice1 == alice3: " + (alice1 == alice3)); System.out.println("alice1.equals(alice3): " + alice1.equals(alice3)); System.out.println("alice1.equals(bob): " + alice1.equals(bob)); System.out.println("bob.toString(): " + bob); var carl = new Manager("Carl Cracker", 80000, 1987, 12, 15); var boss = new Manager("Carl Cracker", 80000, 1987, 12, 15); boss.setBonus(5000); System.out.println("boss.toString(): " + boss); System.out.println("carl.equals(boss): " + carl.equals(boss)); System.out.println("alice1.hashCode(): " + alice1.hashCode()); System.out.println("alice3.hashCode(): " + alice3.hashCode()); System.out.println("bob.hashCode(): " + bob.hashCode()); System.out.println("carl.hashCode(): " + carl.hashCode()); } }
//程序清单 5-9 equals/Employee.java package equals; import java.time.*; 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; } public boolean equals(Object otherObject) { // a quick test to see if the objects are identical if (this == otherObject) return true; // must return false if the explicit parameter is null if (otherObject == null) return false; // if the classes don't match, they can't be equal if (getClass() != otherObject.getClass()) return false; // now we know otherObject is a non-null Employee var other = (Employee) otherObject; // test whether the fields have identical values return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay); } public int hashCode() { return Objects.hash(name, salary, hireDay); } public String toString() { return getClass().getName() + "[name=" + name + ",salary=" + salary + ",hireDay=" + hireDay + "]"; } }
//程序清单 5-10 equals/Manager.java package equals; 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); bonus = 0; } public double getSalary() { double baseSalary = super.getSalary(); return baseSalary + bonus; } public void setBonus(double bonus) { this.bonus = bonus; } public boolean equals(Object otherObject) { if (!super.equals(otherObject)) return false; var other = (Manager) otherObject; // super.equals checked that this and other belong to the same class return bonus == other.bonus; } public int hashCode() { return java.util.Objects.hash(super.hashCode(), bonus); } public String toString() { return super.toString() + "[bonus=" + bonus + "]"; } }
泛型数组列表
ArrayList 是一个采用类型参数(type parameter) 的泛型类(generic class)。为了指定数 组列表保存的元素对象类型,需要用一对尖括号将类名括起来加在后面, 例如,ArrayList <Employee>。
下面声明和构造一个保存 Employee 对象的数组列表:
ArrayList<Employee> staff = new ArrayList<Eniployee>0;
两边都使用类型参数 Employee, 这有些繁琐。Java SE 7中, 可以省去右边的类型参数:
ArrayList<Employee> staff = new ArrayListoQ;
这被称为“ 菱形” 语法,因为空尖括号<>就像是一个菱形。
访问数组列表元素
数组列表自动扩展容量的便利增加了访问元素语法的复 杂程度。 其原因是 ArrayList 类并不是 Java 程序设计语言的一部分;它只是一个由某些人编 写且被放在标准库中的一个实用类。
使用 get 和 set 方法实现访问或改变数组元素的操作,而不使用人们喜爱的 [ ]语法格式。 例如,要设置第 i 个元素,可以使用:
staff.set(i, harry):
它等价于对数组 a 的元素赋值(数组的下标从 0开始):
a[i] = harry;
没有泛型类时, 原始的 ArrayList 类提供的 get 方法别无选择只能返回 Object, 因 此, get 方法的调用者必须对返回值进行类型转换:
Employee e = (Eiployee) staff.get(i);
原始的 ArrayList 存在一定的危险性。它的 add 和 set 方法允许接受任意类型的对象。 对于下面这个调用
staff.set(i, "Harry Hacker");
编译不会给出任何警告, 只有在检索对象并试图对它进行类型转换时, 才会发现有 问题。如果使用 ArrayList<Employee>, 编译器就会检测到这个错误。
程序清单 5-11 是对 EmployeeTest 做出修改后的程序。在这里, 将 Employee[ ] 数组替换成了 ArrayList<Employee>。请注意下面的变化:
•不必指出数组的大小。
•使用 add 将任意多的元素添加到数组中。
•使用 size() 替代 length 计算元素的数目。
•使用 a.get(i) 替代 a[i] 访问元素。
//程序清单 5-11 arrayList/ArrayListTestjava package arrayList; import java.util.*; /** * This program demonstrates the ArrayList class. * @version 1.11 2012-01-26 * @author Cay Horstmann */ public class ArrayListTest { public static void main(String[] args) { // fill the staff array list with three Employee objects var staff = new ArrayList<Employee>(); staff.add(new Employee("Carl Cracker", 75000, 1987, 12, 15)); staff.add(new Employee("Harry Hacker", 50000, 1989, 10, 1)); staff.add(new Employee("Tony Tester", 40000, 1990, 3, 15)); // raise everyone's salary by 5% for (Employee e : staff) e.raiseSalary(5); // print out information about all Employee objects for (Employee e : staff) System.out.println("name=" + e.getName() + ",salary=" + e.getSalary() + ",hireDay=" + e.getHireDay()); } }
类型化与原始数组列表的兼容性
假设有下面这个遗留下来的类:
public class EmployeeDB { public void update(ArrayList list) { .. . } public ArrayList find(String query) { ... } }
可以将一个类型化的数组列表传递给 update方法, 而并不需要进行任何类型转换。
ArrayList<Employee〉staff = . . .;
employeeDB.update(staff);
也可以将 staff 对象传递给 update方法。
相反地,将一个原始 ArrayList 赋给一个类型化 ArrayList 会得到一个警告。
ArrayList<Employee> result = employeeDB.find(query); // yields warning
使用类型转换并不能避免出现警告。
mployee> result = (ArrayList<Employee>) employeeDB.find(query); // yields another warning
这
浙公网安备 33010602011771号