继承
继承
在前面的章节中,你已经多次看到继承的提及。在Java语言中,类可以从其他类派生,从而继承这些类的字段和方法。
定义:从一个类派生出来的类称为子类(也称为派生类、扩展类或子类)。被子类所派生的类称为超类(也称为基类或父类)。除Object类外,每个类都拥有且仅拥有一个直接父类(单继承)。在没有其他显式父类的情况下,每个类都隐式地成为Object类(Object)的子类。类可以从派生自其他类的类派生,这些类又可从更上层的类派生,如此递进,最终可追溯至最顶层的类Object。这样的类被称为继承链中所有类(直至Object)的后代。
继承的概念简单而强大:当你需要创建一个新类,而现有类中已包含部分所需代码时,可让新类从现有类派生而来。通过这种方式,你无需亲自编写(更不必调试!)现有类的字段和方法,即可直接复用它们。
子类继承了其父类的所有成员(字段、方法和嵌套类)。构造函数不属于成员,因此不会被子类继承,但子类可以调用父类的构造函数。
Object类定义于java.lang包中,它定义并实现了所有类(包括您编写的类)共有的行为。在Java平台中,许多类直接继承自Object类,其他类则继承自这些类,以此类推,从而形成了一个类层次结构。
在类层次结构的顶端,Object 是所有类中最通用的。位于层次结构底部的类则提供更专业的行为。
继承的例子
以下是类与对象章节中提到的自行车类的一种可能实现方案的示例代码:
public class Bicycle {
// the Bicycle class has three fields
public int cadence;
public int gear;
public int speed;
// the Bicycle class has one constructor
public Bicycle(int startCadence, int startSpeed, int startGear) {
gear = startGear;
cadence = startCadence;
speed = startSpeed;
}
// the Bicycle class has four methods
public void setCadence(int newValue) {
cadence = newValue;
}
public void setGear(int newValue) {
gear = newValue;
}
public void applyBrake(int decrement) {
speed -= decrement;
}
public void speedUp(int increment) {
speed += increment;
}
}
一个继承自 Bicycle 类的 MountainBike 类的声明可能如下所示:
public class MountainBike extends Bicycle {
// the MountainBike subclass adds one field
public int seatHeight;
// the MountainBike subclass has one constructor
public MountainBike(int startHeight,
int startCadence,
int startSpeed,
int startGear) {
super(startCadence, startSpeed, startGear);
seatHeight = startHeight;
}
// the MountainBike subclass adds one method
public void setHeight(int newValue) {
seatHeight = newValue;
}
}
MountainBike 继承了 Bicycle 的所有字段和方法,并新增了 seatHeight 字段及其设置方法。除了构造函数外,这相当于从零开始编写了一个全新的 MountainBike 类,包含四个字段和五个方法。然而你无需亲力亲为完成所有工作。若Bicycle类中的方法较为复杂且耗费大量调试时间,这种继承机制将尤为宝贵。
在子类中可以做什么
子类继承其父类的全部 public 和 protected 成员,无论子类位于哪个包中。若子类与父类位于同一包内,则还会继承父类的包私有成员。你可以直接使用继承的成员,替换它们,隐藏它们,或通过新增成员进行补充:
- 继承的字段可以像其他字段一样直接使用。
- 你可以在子类中声明一个与父类中同名的字段,从而隐藏该字段(不推荐)。
- 你可以在子类中声明父类中不存在的新字段。
- 继承的方法可以直接使用。
- 你可以在子类中编写一个具有与父类相同方法签名的新实例方法,从而覆盖该方法。
- 你可以在子类中编写一个新的静态方法,使其方法签名与父类中的方法相同,从而隐藏该方法。
- 子类中可以声明父类中不存在的新方法。
- 你可以编写一个子类的构造函数,该构造函数可以隐式调用父类的构造函数,也可以通过使用关键字 super 来显式调用。
- 本课后续章节将深入探讨这些主题。
父类的私有成员
子类不会继承父类的私有成员。然而,如果父类提供了访问其私有字段的公用或受保护方法,子类也可以使用这些方法。
嵌套类可以访问其外围类的全部私有成员——包括字段和方法。因此,由子类继承的公共或受保护的嵌套类,能够间接访问超类的全部私有成员。
对象的转换
我们已经看到,一个对象的数据类型与其实例化的类相同。例如,如果我们写
public MountainBike myBike = new MountainBike();
那么 myBike 的类型是 MountainBike。
MoubtainBike 继承自 Bicycle 和 Object。因此,MountainBike 既是 Bicycle,也是 Object,可在需要 Bicycle 或 Object 的任何场景中使用。
反之则未必成立:Bicycle 可能是 MountainBike,但未必如此。同样地,某个 Object 可能是 Bicycle 或 MountainBike,但未必如此。
类型转换指在继承和实现允许的对象范围内,用某种类型的对象替代另一种类型的对象。例如,如果我们写
Object obj = new MountainBike();
那么 obj 既是 Object 也是 MountainBike(直到 obj 被赋予另一个非 MountainBike 的对象为止)。这被称为隐式转换。
另一方面,如果我们写
MountainBike mb = obj;
我们会得到一个编译时错误,因为编译器无法识别 obj 是 MountainBike 类型。然而,我们可以通过显式强制转换来告知编译器,我们承诺将 MountainBike 赋值给 obj:
MountainBike myBike = (MountainBike)obj;
此强制转换添加了运行时检查,确保对象 obj 被赋值为 MountainBike 类型,从而编译器可安全地假定 obj 是 MountainBike 对象。若运行时 obj 并非 MountainBike 类型,则会抛出异常。
注意:您可以使用 instanceof 运算符对特定对象的类型进行逻辑测试。这可以避免因不正确的强制转换而导致的运行时错误。例如:
if (obj instanceof MountainBike) {
MountainBike myBike = (MountainBike)obj;
}
此处的 instanceof 运算符用于验证 obj 是否指向 MountainBike 类型,从而确保在进行类型转换时不会引发运行时异常。
状态、实现与类型的多重继承
类与接口的一个显著区别在于:类可以拥有字段,而接口不能。此外,你可以通过实例化类来创建对象,但接口无法实现这一点。正如”什么是对象?”章节所述,对象将其状态存储在字段中,而字段是在类中定义的。Java语言禁止单个类继承多个父类的原因之一,正是为了规避状态多重继承的问题——即从多个类继承字段的能力。例如,假设允许定义同时继承多个类的子类。当通过实例化该类创建对象时,该对象将继承所有父类的字段。若不同父类的方法或构造器对同一字段进行初始化,该如何处理?优先级如何确定?由于接口不包含字段,因此无需担心状态多重继承引发的问题。
实现的多重继承是指能够从多个类继承方法定义的能力。此类多重继承会引发诸如名称冲突和歧义等问题。当支持此类多重继承的编程语言编译器遇到包含同名方法的超类时,有时无法确定应访问或调用哪个成员或方法。此外,程序员在父类中添加新方法时,可能无意间引发名称冲突。默认方法引入了一种实现多重继承的形式:类可实现多个接口,而这些接口可能包含同名默认方法。Java编译器提供了一套规则来确定特定类应采用哪个默认方法。
Java编程语言支持类型多重继承,即类能够实现多个接口的能力。一个对象可以具有多种类型:其自身类的类型以及该类所实现的所有接口的类型。这意味着,如果变量被声明为接口类型,则其值可以引用任何从实现该接口的类实例化的对象。相关内容详见”将接口用作类型”章节。
与实现的多重继承类似,类可以继承其扩展的接口中定义的方法(作为默认方法或静态方法)的不同实现。在这种情况下,编译器或用户必须决定使用哪个实现。