继承
在前面的课程中,你已经多次看到了继承,在 Java 语言中,类可以从其他类派生,从而从这些类继承字段和方法。
定义:从另一个类派生的类称为子类(也是派生类,扩展类或子类),派生子类的类称为超类(也是基类或父类)。
除了 Object 没有超类,每个类都有一个且只有一个直接超类(单继承),在没有任何其他显式超类的情况下,每个类都隐式地是 Object 的子类。
类可以从派生自类的类派生的类派生,依此类推,最终派生自最顶层的类,Object,这样的类被称为继承链中所有向后延伸到 Object 的类的子类。
继承的概念很简单但很强大:当你想要创建一个新类并且已经有一个包含你想要的一些代码的类时,你可以从现有类派生你的新类,在这样做时,你可以重用现有类的字段和方法,而无需自己编写(和调试)它们。
子类从其超类继承所有成员(字段、方法和嵌套类),构造函数不是成员,因此它们不是由子类继承的,但是可以从子类调用超类的构造函数。
Java 平台类层次结构
在 java.lang 包中定义的 Object 类定义并实现所有类共有的行为 — 包括你写的那些,在 Java 平台中,许多类直接从 Object 派生,其他类派生自其中一些类,依此类推,形成类的层次结构。
在层次结构的顶部,Object 是所有类中最通用的,层次结构底部附近的类提供更专业的行为。
继承的一个例子
下面是类和对象课程中提供的 Bicycle 类的可能实现的示例代码:
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 来调用超类的构造函数。
本课程的以下部分将扩展这些主题。
超类中的私有成员
子类不继承其父类的 private 成员,但是,如果超类具有访问其私有字段的公共或受保护方法,则子类也可以使用这些方法。
嵌套类可以访问其封闭类的所有私有成员 — 包括字段和方法,因此,子类继承的 public 或 protected 嵌套类可以间接访问超类的所有私有成员。
转换对象
我们已经看到一个对象是实例化它的类的数据类型,例如,如果我们写
public MountainBike myBike = new MountainBike();
那么 myBike 是 MountainBike 类型。
MountainBike 是 Bicycle 和 Object 的后代,因此,MountainBike 是一个 Bicycle 并且也是一个 Object,它可以在需要 Bicycle 或 Object 对象的任何地方使用。
反过来不一定是对的:Bicycle 可能是 MountainBike,但不一定。类似地,Object 可以是 Bicycle 或山 MountainBike,但不一定如此。
转换显示在继承和实现允许的对象中使用一种类型的对象代替另一种类型的对象,例如,如果我们写
Object obj = new MountainBike();
那么 obj 既是 Object 又是 MountainBike(直到 obj 被赋予另一个不是 MountainBike 的对象的时候),这称为隐式转换。
另一方面,如果我们写
MountainBike myBike = 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 编程语言支持多种类型的继承,这是类实现多个接口的能力,对象可以有多种类型:它自己的类的类型以及该类实现的所有接口的类型。这意味着如果将变量声明为接口的类型,则其值可以引用从实现接口的任何类实例化的任何对象,这在将接口用作类型一节中讨论。
与多实现继承一样,一个类可以继承它扩展的接口中定义的方法的不同实现(作为默认或静态),在这种情况下,编译器或用户必须决定使用哪一个。
覆盖和隐藏方法
实例方法
子类中的实例方法的签名(名称,加上其参数的数量和类型)和返回类型与超类中的实例方法相同,将覆盖超类的方法。
子类覆盖方法的能力允许类从行为“足够接近”的超类继承,然后根据需要修改行为,重写方法与它重写的方法具有相同的名称、数量和参数类型,以及返回类型。重写方法还可以返回由被重写方法返回的类型的子类型,此子类型称为协变返回类型。
覆盖方法时,你可能希望使用 @Override 注解来指示编译器你要覆盖超类中的方法,如果由于某种原因,编译器检测到该方法在其中一个超类中不存在,那么它将生成错误,有关 @Override 的更多信息,请参阅注解。
静态方法
如果子类定义的静态方法与超类中的静态方法具有相同的签名,则子类中的方法会隐藏超类中的方法。
隐藏静态方法和覆盖实例方法之间的区别具有重要意义:
被调用的重写的实例方法的版本是子类中的版本。
被调用的隐藏静态方法的版本取决于它是从超类还是从子类调用的。
考虑一个包含两个类的示例,第一个是 Animal,它包含一个实例方法和一个静态方法:
public class Animal {
public static void testClassMethod() {
System.out.println(“The static method in Animal”);
}
public void testInstanceMethod() {
System.out.println(“The instance method in Animal”);
}
}
第二个类是 Animal 的一个子类,叫做 Cat:
public class Cat extends Animal {
public static void testClassMethod() {
System.out.println(“The static method in Cat”);
}
public void testInstanceMethod() {
System.out.println(“The instance method in Cat”);
}
public static void main(String[] args) {
Cat myCat = new Cat();
Animal myAnimal = myCat;
Animal.testClassMethod();
myAnimal.testInstanceMethod();
}
}
Cat 类重写 Animal 中的实例方法,并隐藏 Animal 中的静态方法,此类中的 main 方法创建 Cat 的实例,并在类上调用 testClassMethod() 并在实例上调用 testInstanceMethod()。
该程序的输出如下:
The static method in Animal
The instance method in Cat
正如所承诺的那样,被调用的隐藏静态方法的版本是超类中的版本,被调用的重写实例方法的版本是子类中的版本。
接口方法
接口中的默认方法和抽象方法与实例方法一样是继承的,但是,当类或接口的超类型提供具有相同签名的多个默认方法时,Java 编译器遵循继承规则来解决名称冲突,这些规则由以下两个原则驱动:
实例方法优先于接口默认方法。
考虑以下类和接口:
public class Horse {
public String identifyMyself() {
return “I am a horse.”;
}
}
public interface Flyer {
default public String identifyMyself() {
return “I am able to fly.”;
}
}
public interface Mythical {
default public String identifyMyself() {
return “I am a mythical creature.”;
}
}
public class Pegasus extends Horse implements Flyer, Mythical {
public static void main(String… args) {
Pegasus myApp = new Pegasus();
System.out.println(myApp.identifyMyself());
}
}
方法 Pegasus.identifyMyself 返回字符串 I am a horse。
已经被其他候选者覆盖的方法将被忽略,当超类型共享一个共同的祖先时,就会出现这种情况。
考虑以下接口和类:
public interface Animal {
default public String identifyMyself() {
return “I am an animal.”;
}
}
public interface EggLayer extends Animal {
default public String identifyMyself() {
return “I am able to lay eggs.”;
}
}
public interface FireBreather extends Animal {}
public class Dragon implements EggLayer, FireBreather {
public static void main (String… args) {
Dragon myApp = new Dragon();
System.out.println(myApp.identifyMyself());
}
}
方法 Dragon.identifyMyself 返回字符串 I am able to lay eggs。
如果两个或多个独立定义的默认方法冲突,或者默认方法与抽象方法冲突,则 Java 编译器会产生编译器错误,你必须显式覆盖超类型方法。
考虑一下现在可以飞行的计算机控制汽车的例子,你有两个接口(OperateCar 和 FlyCar)为同一方法提供默认实现(startEngine):
public interface OperateCar {
// …
default public int startEngine(EncryptedKey key) {
// Implementation
}
}
public interface FlyCar {
// …
default public int startEngine(EncryptedKey key) {
// Implementation
}
}
实现 OperateCar 和 FlyCar 的类必须覆盖方法 startEngine,你可以使用 super 关键字调用任何默认实现。
public class FlyingCar implements OperateCar, FlyCar {
// …
public int startEngine(EncryptedKey key) {
FlyCar.super.startEngine(key);
OperateCar.super.startEngine(key);
}
}
super 之前的名称(在此示例中为 FlyCar 或 OperateCar)必须引用定义或继承默认调用方法的直接超接口,这种形式的方法调用不限于区分包含具有相同签名的默认方法的多个已实现接口,你可以使用 super 关键字在类和接口中调用默认方法。
类中的继承实例方法可以覆盖抽象接口方法,考虑以下接口和类:
public interface Mammal {
String identifyMyself();
}
public class Horse {
public String identifyMyself() {
return “I am a horse.”;
}
}
public class Mustang extends Horse implements Mammal {
public static void main(String… args) {
Mustang myApp = new Mustang();
System.out.println(myApp.identifyMyself());
}
}
方法 Mustang.identifyMyself 返回字符串 I am a horse,Mustang 类继承了 Horse 类中的方法 identifyMyself,它覆盖了 Mammal 接口中同名的抽象方法。
注意:接口中的静态方法永远不会被继承。
修饰符
重写方法的修饰符可以允许比被重写方法更多但不是更少的访问,例如,超类中的 protected 实例方法可以在子类中是 public,但不能是 private。
如果尝试将超类中的实例方法更改为子类中的静态方法,则会出现编译时错误,反之亦然。
总结
下表总结了在定义具有与超类中的方法相同的签名的方法时发生的情况。
超类实例方法
超类静态方法
子类实例方法
覆盖
生成编译时错误
子类静态方法
生成编译时错误
隐藏
注意:在子类中,你可以重载从超类继承的方法,这样的重载方法既不隐藏也不覆盖超类实例方法 — 它们是新方法,对于子类是唯一的。
多态性
多态性的字典定义是指生物学中的原理,其中生物体或物种可以具有许多不同的形式或阶段,这个原则也可以应用于面向对象的编程和像 Java 语言之类的语言,类的子类可以定义它们自己的唯一行为,但仍然共享父类的一些相同功能。
可以通过对 Bicycle 类的微小修改来演示多态性,例如,可以将 printDescription 方法添加到显示当前存储在实例中的所有数据的类中。
public void printDescription(){
System.out.println(“\nBike is ” + “in gear ” + this.gear
+ ” with a cadence of ” + this.cadence +
” and travelling at a speed of ” + this.speed + “. “);
}
要演示 Java 语言中的多态特性,请使用 MountainBike 和 RoadBike 类扩展 Bicycle 类,对于 MountainBike,添加一个 suspension 字段,这是一个字符串值,表示自行车是否有前减震器,或者,自行车有一个前后减震器。
这是更新的类:
public class MountainBike extends Bicycle {
private String suspension;
public MountainBike(
int startCadence,
int startSpeed,
int startGear,
String suspensionType){
super(startCadence,
startSpeed,
startGear);
this.setSuspension(suspensionType);
}
public String getSuspension(){
return this.suspension;
}
public void setSuspension(String suspensionType) {
this.suspension = suspensionType;
}
public void printDescription() {
super.printDescription();
System.out.println(“The ” + “MountainBike has a” +
getSuspension() + ” suspension.”);
}
}
请注意重写的 printDescription 方法,除了之前提供的信息之外,还有关于 suspension 的其他数据包含在输出中。
接下来,创建 RoadBike 类,因为公路车或赛车的轮胎很薄,所以添加一个属性来跟踪轮胎的宽度,这是 RoadBike 类:
public class RoadBike extends Bicycle{
// In millimeters (mm)
private int tireWidth;
public RoadBike(int startCadence,
int startSpeed,
int startGear,
int newTireWidth){
super(startCadence,
startSpeed,
startGear);
this.setTireWidth(newTireWidth);
}
public int getTireWidth(){
return this.tireWidth;
}
public void setTireWidth(int newTireWidth){
this.tireWidth = newTireWidth;
}
public void printDescription(){
super.printDescription();
System.out.println(“The RoadBike” + ” has ” + getTireWidth() +
” MM tires.”);
}
}
请注意,再次重写了 printDescription 方法,这次,显示有关轮胎宽度的信息。
总而言之,有三个类:Bicycle、MountainBike 和 RoadBike,这两个子类重写 printDescription 方法并打印唯一信息。
这是一个创建三个 Bicycle 变量的测试程序,每个变量都分配给三个自行车类别中的一个,然后打印每个变量。
public class TestBikes {
public static void main(String[] args){
Bicycle bike01, bike02, bike03;
bike01 = new Bicycle(20, 10, 1);
bike02 = new MountainBike(20, 10, 5, “Dual”);
bike03 = new RoadBike(40, 20, 8, 23);
bike01.printDescription();
bike02.printDescription();
bike03.printDescription();
}
}
以下是测试程序的输出:
Bike is in gear 1 with a cadence of 20 and travelling at a speed of 10.
Bike is in gear 5 with a cadence of 20 and travelling at a speed of 10.
The MountainBike has a Dual suspension.
Bike is in gear 8 with a cadence of 40 and travelling at a speed of 20.
The RoadBike has 23 MM tires.
Java 虚拟机(JVM)为每个变量中引用的对象调用适当的方法,它不会调用由变量类型定义的方法,此行为称为虚方法调用,并演示 Java 语言中重要多态特征的一个方面。
隐藏字段
在类中,与超类中的字段具有相同名称的字段会隐藏超类的字段,即使它们的类型不同,在子类中,超类中的字段不能通过其简单名称引用,相反,必须通过 super 访问该字段,一般来说,我们不建议隐藏字段,因为它使代码难以阅读。
使用 super 关键字
访问超类成员
如果你的方法覆盖了它的一个超类的方法,你可以通过使用关键字 super 来调用被重写的方法,你也可以使用 super 来引用隐藏字段(尽管不鼓励隐藏字段),虑这个类,Superclass:
public class Superclass {
public void printMethod() {
System.out.println(“Printed in Superclass.”);
}
}
这是一个名为 Subclass 的子类,它重写了 printMethod():
public class Subclass extends Superclass {
// overrides printMethod in Superclass
public void printMethod() {
super.printMethod();
System.out.println(“Printed in Subclass”);
}
public static void main(String[] args) {
Subclass s = new Subclass();
s.printMethod();
}
}
在 Subclass 中,简单名称 printMethod() 引用在 Subclass 中声明的名称,它覆盖了 Superclass 中的名称,因此,要引用从 Superclass 继承的 printMethod(),Subclass 必须使用限定名,使用 super,编译和执行 Subclass 将打印以下内容:
Printed in Superclass.
Printed in Subclass
子类构造函数
以下示例说明如何使用 super 关键字来调用超类的构造函数,回想一下 Bicycle 的例子,MountainBike 是 Bicycle 的子类,这是 MountainBike(子类)构造函数,它调用超类构造函数,然后添加自己的初始化代码:
public MountainBike(int startHeight,
int startCadence,
int startSpeed,
int startGear) {
super(startCadence, startSpeed, startGear);
seatHeight = startHeight;
}
调用超类构造函数必须是子类构造函数中的第一行。
调用超类构造函数的语法是:
super();
或
super(parameter list);
使用 super(),可以调用超类无参数构造函数,使用 super(parameter list),将调用具有匹配参数列表的超类构造函数。
注意:如果构造函数没有显式调用超类构造函数,Java 编译器会自动插入对超类的无参数构造函数的调用,如果超类没有无参数构造函数,则会出现编译时错误,Object 确实有这样的构造函数,因此如果 Object 是唯一的超类,则没有问题。
如果子类构造函数显式或隐式调用其超类的构造函数,你可能会认为将调用一整个构造函数链,一直返回 Object 的构造函数,事实上,情况就是这样,它被称为构造函数链,当需要很长的类层次时,你需要意识到这一点。
Object 作为超类
java.lang 包中的 Object 类位于类层次结构树的顶部,每个类都是 Object 类的直接或间接的后代,你使用或编写的每个类都继承 Object 的实例方法,你不需要使用任何这些方法,但是,如果你选择这样做,你可能需要使用特定于你的类的代码重写它们,本节中讨论的从 Object 继承的方法是:
protected Object clone() throws CloneNotSupportedException 创建并返回此对象的副本。
public boolean equals(Object obj) 指示某个其他对象是否“等于”此对象。
protected void finalize() throws Throwable 垃圾回收器在对象上执行垃圾回收时调用。集合确定不再有对该对象的引用。
public final Class getClass() 返回对象的运行时类。
public int hashCode() 返回对象的哈希码值。
public String toString() 返回对象的字符串表示形式。
Object 的 notify、notifyAll 和 wait 方法都在同步程序中独立运行的线程的活动中发挥作用,这将在后面的课程中讨论,这里不讨论,有五个方法:
public final void notify()
public final void notifyAll()
public final void wait()
public final void wait(long timeout)
public final void wait(long timeout, int nanos)
注意:许多方法都有一些微妙的方面,尤其是 clone 方法。
clone() 方法
如果某个类或其中一个超类实现了 Cloneable 接口,则可以使用 clone() 方法从现有对象创建副本,要创建克隆,请编写:
aCloneableObject.clone();
Object 的此方法实现检查调用 clone() 的对象是否实现 Cloneable 接口,如果对象没有,则该方法抛出 CloneNotSupportedException 异常,稍后的课程将介绍异常处理,目前,你需要知道 clone() 必须声明为:
protected Object clone() throws CloneNotSupportedException
或
public Object clone() throws CloneNotSupportedException
如果要编写 clone() 方法来覆盖 Object 中的方法。
如果调用 clone() 的对象确实实现了 Cloneable 接口,对象的 clone() 方法实现创建与原始对象相同的类的对象,并初始化新对象的成员变量,使其具有与原始对象的相应成员变量相同的值。
使类可克隆的最简单方法是将 implements Cloneable 添加到类的声明中,然后你的对象可以调用 clone() 方法。
对于某些类,Object 的 clone() 方法的默认行为可以正常工作,但是,如果一个对象包含对外部对象的引用,比如说 ObjExternal,则可能需要重写 clone() 以获得正确的行为,否则,一个对象对 ObjExternal 所做的更改也将在其克隆中可见,这意味着原始对象及其克隆不是独立的 — 要将它们分离,你必须重写 clone() 以便它克隆对象和 ObjExternal,然后原始对象引用 ObjExternal,并且克隆引用 ObjExternal 的克隆,以便对象及其克隆真正独立。
equals() 方法
equals() 方法将两个对象进行相等性比较,如果它们相等则返回 true,Object 类中提供的 equals() 方法使用标识运算符(==)来确定两个对象是否相等。对于原始数据类型,这会给出正确的结果,但是,对于对象,它没有,Object 提供的 equals() 方法测试对象引用是否相等 — 即,如果比较的对象是完全相同的对象。
要测试两个对象在等价意义上是否相等(包含相同的信息),必须重写 equals() 方法,以下是重写 equals() 的 Book 类的示例:
public class Book {
…
public boolean equals(Object obj) {
if (obj instanceof Book)
return ISBN.equals((Book)obj.getISBN());
else
return false;
}
}
考虑这段代码来测试 Book 类的两个实例是否相等:
// Swing Tutorial, 2nd edition
Book firstBook = new Book(“0201914670”);
Book secondBook = new Book(“0201914670”);
if (firstBook.equals(secondBook)) {
System.out.println(“objects are equal”);
} else {
System.out.println(“objects are not equal”);
}
即使 firstBook 和 secondBook 引用了两个不同的对象,此程序也显示对 objects are equal,它们被认为是相同的,因为比较的对象包含相同的 ISBN 号。
如果标识运算符不适合你的类,则应始终重写 equals() 方法。
注意:如果重写 equals(),则还必须重写 hashCode()。
finalize() 方法
Object 类提供了一个回调方法 finalize(),当它变为垃圾时,可以在对象上调用它,Object 的 finalize() 实现什么也没做 — 你可以重写 finalize() 来执行清理,例如释放资源。
finalize() 方法可以由系统自动调用,但是什么时候调用,或者即使调用,也是不确定的,因此,你不应该依赖此方法为你进行清理。例如,如果在执行 I / O 后没有在代码中关闭文件描述符,并且你希望 finalize() 为你关闭它们,则可能会耗尽文件描述符。
getClass() 方法
你不能重写 getClass。
getClass() 方法返回一个 Class 对象,该对象具有可用于获取有关类的信息的方法,例如其名称(getSimpleName()),其超类(getSuperclass())及其实现的接口(getInterfaces()),例如,以下方法获取并显示对象的类名:
void printClassName(Object obj) {
System.out.println(“The object’s” + ” class is ” +
obj.getClass().getSimpleName());
}
java.lang 包中的 Class 类有很多方法(超过 50 个),例如,你可以测试以查看类是注解(isAnnotation()),接口(isInterface())还是枚举(isEnum()),你可以查看对象的字段是什么(getFields())或其方法是什么(getMethods()),等等。
hashCode() 方法
hashCode() 返回的值是对象的哈希码,它是十六进制的对象的内存地址。
根据定义,如果两个对象相等,则它们的哈希码也必须相等,如果重写 equals() 方法,则更改了两个对象的等效方式,并且 Object 的 hashCode() 实现不再有效,因此,如果重写 equals() 方法,则还必须重写 hashCode() 方法。
toString() 方法
你应该始终考虑重写你的类中的 toString() 方法。
Object 的 toString() 方法返回对象的 String 表示,这对调试非常有用,对象的 String 表示完全取决于对象,这就是你需要在类中重写 toString() 的原因。
你可以使用 toString() 和 System.out.println() 来显示对象的文本表示形式,例如 Book 的实例:
System.out.println(firstBook.toString());
对于正确重写的 toString() 方法,它会打印一些有用的东西,如下所示:
ISBN: 0201914670; The Swing Tutorial; A Guide to Constructing GUIs, 2nd Edition
编写 Final 类和方法
你可以声明一些或所有类的方法为 final,你在方法声明中使用 final 关键字来指示子类不能重写该方法,Object 类这样做 — 它的一些方法是 final。
如果方法具有不应被更改的实现,并且对于对象的一致状态至关重要,则可能希望将方法设为 final,例如,你可能希望在此 ChessAlgorithm 类中生成 getFirstPlayer 方法:
class ChessAlgorithm {
enum ChessPlayer {WHITE, BLACK}
…
final ChessPlayer getFirstPlayer() {
return ChessPlayer.WHITE;
}
…
}
从构造函数调用的方法通常应该声明为 final,如果构造函数调用非 final 方法,子类可能会重新定义该方法,并产生意外或不希望看到的结果。
请注意,你还可以声明整个类 final,声明为 final 的类不能被子类化,这特别有用,例如,在创建 String 类这样的不可变类时。
抽象方法和类
抽象类是一个声明为 abstract 的类 — 它可能包括也可能不包括抽象方法,抽象类无法实例化,但可以进行子类化。
抽象方法是在没有实现的情况下声明的方法(没有大括号,后跟分号),如下所示:
abstract void moveTo(double deltaX, double deltaY);
如果一个类包含抽象方法,那么该类本身必须被声明为 abstract,如:
public abstract class GraphicObject {
// declare fields
// declare nonabstract methods
abstract void draw();
}
当抽象类被子类化时,子类通常为其父类中的所有抽象方法提供实现,但是,如果没有,那么子类也必须声明为 abstract。
注意:未声明为 default 或 static 的接口(参见接口部分)中的方法是隐式抽象的,因此 abstract 修饰符不用于接口方法(可以使用,但不需要)。
抽象类与接口相比
抽象类与接口类似,你不能实例化它们,它们可能包含有或没有实现声明的方法的组合,但是,使用抽象类,你可以声明非静态和 final 的字段,并定义 public、protected 和 private 的具体方法。使用接口,所有字段都自动为 public、static 和 final,并且你声明或定义的所有方法(作为默认方法)都是 public,此外,你只能扩展一个类,无论它是否是抽象的,而你可以实现任意数量的接口。
你应该使用哪个,抽象类或接口?
如果任何这些语句适用于你的情况,请考虑使用抽象类:
你希望在几个密切相关的类之间共享代码。
你希望扩展抽象类的类具有许多公共方法或字段,或者需要非公共的访问修饰符(如 protected 和 private)。
你想声明非静态或非最终字段,这使你能够定义能够访问和修改它们所属的对象的状态的方法。
如果任何这些语句适用于你的情况,请考虑使用接口:
你希望不相关的类将实现你的接口,例如,Comparable 和 Cloneable 接口由许多不相关的类实现。
你希望指定特定数据类型的行为,但不关心谁实现其行为。
你希望利用类型的多重继承。
JDK 中的抽象类的一个示例是 AbstractMap,它是集合框架的一部分,它的子类(包括 HashMap、TreeMap 和 ConcurrentHashMap)共享 AbstractMap 定义的许多方法(包括 get、put、isEmpty、containsKey 和 containsValue)。
JDK 中实现多个接口的类的一个示例是 HashMap,它实现了 Serializable、Cloneable 和 Map<K, V> 接口。通过阅读这个接口列表,你可以推断出 HashMap 的实例(无论是实现该类的开发人员或公司)可以克隆,可序列化(这意味着它可以转换为字节流)。请参阅 Serializable Objects 部分,并具有 map 的功能,此外,Map<K, V> 接口已经增强了许多默认方法,例如 merge 和 forEach,实现此接口的旧类不必定义。
请注意,许多软件库都使用抽象类和接口,HashMap 类实现了几个接口,并且还扩展了抽象类 AbstractMap。
抽象类示例
在面向对象的绘图应用程序中,你可以绘制圆形、矩形、线条、贝塞尔曲线和许多其他图形对象,这些对象都具有某些状态(例如:位置、方向、线条颜色、填充颜色)和行为(例如:moveTo、rotate、resize、draw)。所有图形对象中的一些状态和行为都是相同的(例如:位置、填充颜色和 moveTo),其他需要不同的实现(例如,resize 或 draw)。所有 GraphicObjects 必须能够自己绘制或调整大小,它们只是做的方式不同,这是抽象超类的完美情况。你可以利用相似性并声明所有图形对象从同一个抽象父对象(例如,GraphicObject)继承,如下图所示。
首先,声明一个抽象类 GraphicObject,以提供所有子类完全共享的成员变量和方法,例如当前位置和 moveTo 方法,GraphicObject 还声明了方法的抽象方法,例如 draw 或 resize,它们需要由所有子类实现,但必须以不同的方式实现,GraphicObject 类看起来像这样:
abstract class GraphicObject {
int x, y;
…
void moveTo(int newX, int newY) {
…
}
abstract void draw();
abstract void resize();
}
GraphicObject 的每个非抽象子类(例如 Circle 和 Rectangle)必须提供 draw 和 resize 方法的实现:
class Circle extends GraphicObject {
void draw() {
…
}
void resize() {
…
}
}
class Rectangle extends GraphicObject {
void draw() {
…
}
void resize() {
…
}
}
当抽象类实现接口时
在接口一节中,注意到实现接口的类必须实现所有接口的方法,但是,可以定义一个不实现所有接口方法的类,前提是该类被声明为 abstract,例如:
abstract class X implements Y {
// implements all but one method of Y
}
class XX extends X {
// implements the remaining method in Y
}
在这种情况下,类 X 必须是 abstract,因为它没有完全实现 Y,但实际上,类 XX 实现了 Y。
类成员
抽象类可以具有静态字段和静态方法,你可以像使用任何其他类一样使用带有类引用的静态成员(例如,AbstractClass.staticMethod())。
继承总结
除了 Object 类之外,一个类只有一个直接超类,类继承其所有超类中的字段和方法,无论是直接还是间接,子类可以重写它继承的方法,也可以隐藏它继承的字段或方法(请注意,隐藏字段通常是糟糕的编程习惯)。
“覆盖和隐藏方法”部分中的表显示了使用与超类中的方法相同的签名声明方法的效果。
Object 类是类层次结构的顶部,所有类都是此类的后代,并从中继承方法,从 Object 继承的有用方法包括 toString()、equals()、clone() 和 getClass()。
你可以通过在类的声明中使用 final 关键字来阻止类被子类化,同样,你可以通过将方法声明为 final 方法来防止子类重写该方法。
抽象类只能被子类化,它无法实例化,抽象类可以包含抽象方法 — 声明但未实现的方法,然后,子类提供抽象方法的实现。
上一篇:接口