继承
目标
- 了解类分层结构的概念
- 了解实现构造方法的各种方式
- 了解何时和为什么使用抽象类和方法
- 了解如何将一个引用从一个类赋给一个类型属于另一个类的变量。
继承的工作原理
Java 代码中的类存在于分层结构中。分层结构中的给定类上方的类是该类的超类。这个特定的类是分层结构中每个更高层的类的子类。子类继承它的超类。java.lang.Object 类位于类分层结构的顶部 — 所以每个 Java 类是 Object 的子类并继承它。
例如,假设您拥有一个类似清单 1 的 Person 类。
清单 1. 公共 Person 类
public class Person {
public static final String STATE_DELIMITER = "~";
public Person() {
// Default constructor
}
public enum Gender {
MALE,
FEMALE,
UNKNOWN
}
public Person(String name, int age, int height, int weight, String eyeColor, Gender gender) {
this.name = name;
this.age = age;
this.height = height;
this.weight = weight;
this.eyeColor = eyeColor;
this.gender = gender;
}
private String name;
private int age;
private int height;
private int weight;
private String eyeColor;
private Gender gender;
清单 1 中的 Person 类隐式地继承 Object。因为每个类都假定继承 Object,所以您不需要为您定义的每个类键入 extends Object。但说一个类继承它的超类是什么意思?它表示 Person 能够访问它的超类中公开的变量和方法。在本例中,Person 可以查看和使用 Object 的公共和受保护的方法和变量。
定义类分层结构
现在假设您有一个继承 Person 的 Employee 类。Employee 的类定义将类似于这样:
public class Employee extends Person {
private String taxpayerIdentificationNumber;
private String employeeNumber;
private BigDecimal salary;
// . . .
}
Employee 与它的所有超类的继承关系(它的继承图)暗示,Employee 能够访问 Person 中的所有公共和受保护变量和方法(因为 Employee 直接继承 Person),以及 Object 中的公共和受保护变量和方法(因为 Employee 实际上也继承了 Object,尽管是间接继承)。但是,因为 Employee 和 Person 位于同一个包中,所以 Employee 也能访问 Person 中的包私有(有时称为友好)变量和方法。
要进入类分层结构的更深处,您可创建第三个继承 Employee 的类:
public class Manager extends Employee {
// . . .
}
在 Java 语言中,任何类都可拥有至多 1 个超类,但一个类可拥有任意多个子类。这是关于 Java 语言中的继承分层结构要记住的最重要一点。
单一与多重继承
C++ 等语言支持多重继承 的概念:在分层结构中的任一点,一个类都可直接继承一个或多个类。Java 语言仅支持单一继承,这意味着您只能对一个类使用 extends 关键字。所以任何 Java 类的类分层结构始终包含一直连接到 java.lang.Object 的一条直线。但是,您会在 第 17 单元:接口 中了解到,Java 语言支持在单个类中实现多个接口,为您提供单一继承问题的解决办法。
构造方法和继承
构造方法不是完整的面向对象成员,所以它们不是继承的;必须在子类中显式实现它们。在介绍该主题之前,我将回顾一下一些有关如何定义和调用构造方法的基本规则。
构造方法基础知识 请记住,构造方法始终与使用它构造的类同名,而且它没有返回类型。例如:
public class Person {
public Person() {
}
}
每个类都拥有至少一个构造方法,而且如果您没有显式为您的类定义构造方法,编译器会为您生成一个(称为默认构造方法)。前面的类定义和这个类定义具有相同的功能:
public class Person {
}
调用超类构造方法
要调用超类构造方法,而不是默认构造方法,也必须显式这么做。例如,假设 Person 拥有一个仅接受所创建的 Person 对象名称的构造方法。从 Employee 的默认构造方法,可以调用 Person 构造方法,如清单 2 所示:
清单 2. 初始化新 Employee
public class Person {
private String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
}
// Meanwhile, in Employee.java
public class Employee extends Person {
public Employee() {
super("Elmer J Fudd");
}
}
但是,您或许绝不希望以这种方式初始化新 Employee 对象。一般而言,在更加熟悉面向对象的概念和 Java 语法之前,如果您确定需要超类构造方法,一个不错的想法是在子类中实现它们。清单 3 在 Employee 中定义了一个与 Person 中的构造方法相似的方法,以便它们匹配。从维护角度讲,此方法简单易懂得多。
清单 3. 调用超类
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
// Meanwhile, in Employee.java
public class Employee extends Person {
public Employee(String name) {
super(name);
}
}
声明构造方法
构造方法做的第一件事是调用其直接超类的默认构造方法,除非您(在构造方法的第一行代码上)调用一个不同的构造方法。例如,下面两个声明具有相同的功能:
public class Person {
public Person() {
}
}
// Meanwhile, in Employee.java
public class Employee extends Person {
public Employee() {
}
}
public class Person {
public Person() {
}
}
// Meanwhile, in Employee.java
public class Employee extends Person {
public Employee() {
super();
}
}
无参数构造方法
如果您提供了一个替代性构造方法,必须显式提供默认构造方法;否则它将不可用。例如,以下代码会得到一个编译错误:
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
// Meanwhile, in Employee.java
public class Employee extends Person {
public Employee() {
}
}
这个例子中的 Person 类没有默认构造方法,因为它提供了一个替代性构造方法而没有显式包含默认构造方法。
构造方法如何调用构造方法 构造方法可通过 this 关键字和一个参数列表来调用同一个类中的另一个构造方法。像 super() 一样,this() 调用必须是构造方法中的第一行,就像这个示例中一样:
public class Person {
private String name;
public Person() {
this("Some reasonable default?");
}
public Person(String name) {
this.name = name;
}
}
您经常会看到此用法。一个构造方法委托给另一个构造方法,如果调用该构造方法,则会传入一个默认值。此技术也是向一个类添加一个新构造方法,同时最小化对已使用旧构造方法的代码的影响的好方法。
构造方法访问级别
构造方法可拥有您想要的任何访问级别,而且会应用一些可视性规则。表 1 总结了构造方法访问规则。
表 1. 构造方法访问规则
构造方法访问修饰符 | 描述 |
---|---|
public | 构造方法可由任何类调用。 |
protected | 构造方法仅能由同一个包中的类或任何子类调用。 |
无修饰符(包私有) | 构造方法可由同一个包内的任何类调用。 |
private | 构造方法仅能由定义它的类调用。 |
您可能想到了将构造方法声明为 protected 或者甚至包私有的用例,不过 private 构造方法有何用处?如果我不想在实现工厂模式时允许通过 new 关键字直接创建对象,就可以使用私有构造方法。在这种情况下,我会使用一个静态方法来创建类的实例,而且该方法(包含在该类中)允许调用这个私有构造方法。
继承和抽象
如果一个子类覆盖了一个超类中的一个方法,该方法实质上是被隐藏了,因为通过对子类的引用来调用它会调用该方法的子类版本,而不是超类版本。但是,超类方法仍可访问。子类可通过在方法名称中添加 super 关键字作为前缀来调用超类方法(而且不像构造方法规则,该操作可从子类方法的任何行中执行,甚至可在不同的方法内执行)。默认情况下,如果子类方法是通过对子类的引用来调用的,Java 程序会调用它。
此功能同样适用于变量,只要调用方能够访问该变量(也即该变量对尝试访问它的代码可见)。随着您逐渐精通 Java 编程,此细节可能给您带来无尽的烦恼。Eclipse 提供了大量警告,例如,提示您隐藏了来自超类的变量,或者方法调用没有调用您认为它将调用的实体。
在 OOP 上下文中,抽象 指的是将数据和行为一般化到继承分层结构中比当前类更高层级的类型。将变量或方法从一个子类移动到一个超类时,就可以说您在抽象化 这些成员。抽象化的主要目的是,通过将通用的代码推送到分层结构中尽可能高的层级来重用它。将通用的代码放在某个更容易维护的位置。
抽象类和方法
有时,您希望创建仅用作抽象的类,而不是创建必须实例化的类。这些类称为抽象类。出于同样的原因,有时需要以不同的方式为每个实现超类的子类实现某些方法。这些方法是抽象方法。以下是抽象类和方法的一些基本规则:
- 任何类都可声明为 abstract。
- 抽象类无法实例化。
- 抽象方法无法包含一个方法主体。
- 任何包含抽象方法的类都必须声明为 abstract。
使用抽象
假设您不想允许直接实例化 Employee 类。使用 abstract 关键字声明该类即可:
public abstract class Employee extends Person {
// etc.
}
如果尝试运行此代码,就会获得一个编译错误:
public void someMethodSomwhere() {
Employee p = new Employee();// compile error!!
}
编译器抱怨 Employee 是抽象的,无法实例化。
抽象的力量
假设您需要一个方法来检查一个 Employee 对象的状态并确保它有效。似乎所有 Employee 对象都存在这一需求,但它完全无法重用,因为它在所有潜在的子类中具有不同的行为。在这种情况下,您将 validate() 方法声明为 abstract(强制所有子类实现它):
public abstract class Employee extends Person {
public abstract boolean validate();
}
Employee 的每个直接子类(比如 Manager)现在需要实现 validate() 方法。但是,一旦某个子类实现了 validate() 方法,它的所有子类都不需要再实现它。
例如,假设您拥有一个继承 Manager 的 Executive 对象。以下定义将是有效的:
public class Executive extends Manager {
public Executive() {
}
}
何时(不)抽象化:两条规则
作为一条经验规则,不要在初始设计中抽象化。在设计过程的早期使用抽象类会迫使您进入一条可能限制您的应用程序的设计路线。您可以始终在继承图中的更高层级上重构常见行为(这是拥有抽象类的唯一理由) — 而且在发现您需要重构后再重构似乎总是更好一些。Eclipse 对重构提供了极好的支持。
第二,尽管抽象类很强大,仍要拒绝使用它们。除非您的超类包含大量相同的行为,而且这些超类本身没有意义,否则保持它们非抽象化。较深的继承图可能使代码维护变得很困难。请考虑太大的类与可维护的代码之间的利弊。
赋值:类
您可将一个引用从一个类赋给一个类型属于另一个类的变量,但要遵守一些规则。看看这个示例:
Manager m = new Manager();
Employee e = new Employee();
Person p = m; // okay
p = e; // still okay
Employee e2 = e; // yep, okay
e = m; // still okay
e2 = p; // wrong!
目标变量必须是属于来源引用的类的超类型,否则编译器会抛出错误。赋值等式的右侧必须是左侧的实体的子类或同一个类。换句话说:子类的用途比超类更加明确,所以可以将子类视为比超类更狭义。超类是更加一般性的,比子类更广义。规则是,绝不执行将缩小引用范围的赋值。
现在考虑这个示例:
Manager m = new Manager();
Manager m2 = new Manager();
m = m2; // Not narrower, so okay
Person p = m; // Widens, so okay
Employee e = m; // Also widens
Employee e = p; // Narrows, so not okay!
尽管 Employee 是一个 Person,但它几乎肯定不是 Manager,而且编译器会执行此区分。