《On Java 中文版 基础卷》阅读笔记 复用 13
08 复用
"代码复用是面向对象编程里最引人注目的特性之一。"
在像 C 这样的过程型语言中,"复用" 通常意味着 "复制代码",这在任何语言中都很容易做到,但效果并不好。
与 Java 中的所有事物一样,这个问题的解决方案也是围绕着类展开的。你可以通过创建新类来复用代码,直接在新类里使用其他人已经构建和调试过的类,而不必再从头开始编写它们。
这个方法的诀窍在于,如何在不污染现有代码的情况下使用它们。在本章中,你将看到实现此目的的两种方法。
第一种方法很简单:在新类中创建现有类的对象。这称为组合 (composition),因为新类由现有类的对象组合而成。你复用的是代码的功能,而不是其形式。
第二种方法更微妙一些,它创建了一个可以视为现有类类型的新类。甚至可以毫不夸张地说,它直接复制了现有类的形式,然后向其中添加代码,而没有修改现有类。这种技术叫作继承 (inheritance),它的大部分工作是由编译器完成的。继承是面向对象编程的基石之一,我们会在下一章进一步探讨它的含义。
对于组合和继承而言,许多语法和行为都是相似的(这是合理的,因为它们都是从现有类型中创建新类型的方法)。在本章中,你将学习这些代码复用机制。
Java 中代码复用的两种方式,组合和继承,组合是在新创建的类中创建已有类的对象。
8.1 组合语法
我们在前面的示例中已经多次使用了组合,将对象引用放在新类中即可。
例如,假设你想要一个对象,它包含了多个字符串对象、几个基本类型以及另一个类的对象。
对于非基本类型的对象,需要将其引用放入新类中,但基本类型就可以直接定义:
java
package reuse;
// reuse/SprinklerSystem.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Composition for code reuse
class WaterSource {
private String s;
WaterSource() {
System.out.println("WaterSource()");
s = "Constructed";
}
@Override public String toString() { return s; }
}
public class SprinklerSystem {
private String valve1, valve2, valve3, valve4;
private WaterSource source = new WaterSource();
private int i;
private float f;
@Override public String toString() {
return
"valve1 = " + valve1 + " " +
"valve2 = " + valve2 + " " +
"valve3 = " + valve3 + " " +
"valve4 = " + valve4 + "\n" +
"i = " + i + " " + "f = " + f + " " +
"source = " + source; // [1]
}
public static void main(String[] args) {
SprinklerSystem sprinklers = new SprinklerSystem();
System.out.println(sprinklers);
}
}
/* Output:
WaterSource()
valve1 = null valve2 = null valve3 = null valve4 = null
i = 0 f = 0.0 source = Constructed
*/
在上面两个类定义的方法中,有一个是特殊的:toString()。
每个非基本类型的对象都有一个 toString() 方法,它会在一些特殊情况下被调用,
比如当编译器需要一个字符串,但有的是一个对象时。
因此,在代码行 1 中,编译器看到你尝试将 String 类型 ("source=") 和 WaterSource 类型对象 source 相拼接。
因为只能将一个字符串拼接到另一个字符串,所以它表示 "我将通过调用 toString() 方法来将 source 对象变成一个字符串"。
然后它就可以拼接这两个字符串,并将生成的字符串传递给 System.out.println()。
如果想要在自己创建的类中实现这种行为,只需要编写一个 toString() 方法即可。
注解 @Override 用在 toString() 方法上,来让编译器确保我们实现了正确的重写。
@Override 是可选的,但它有助于验证有没有拼写错误(或更详细地说,有没有拼错大小写),或有没有犯一些其他的常见错误。
正如第 3 章所述,类中的基本类型字段会自动初始化为零(数值类型的默认值是 0, boolean 和 char 则是和 0 等价的对应值)。

而对象引用被初始化为 null,
如果试图调用其中任何一个对象的方法,你会得到一个异常 (exception),它表示运行时错误。
不过很方便的是,你仍然可以打印一个 null 引用而不会抛出异常。
编译器并不是简单地为每个引用创建一个默认对象,这一点是合理的,因为这在许多情况下会产生不必要的开销。
初始化引用有下列 4 种方式。
1.在定义对象时。这意味着它们将始终在调用构造器之前被初始化。
2.在该类的构造器中。
3.在对象实际使用之前。这通常称为延迟初始化 (lazy initialization)。在对象创建成本高昂且不需要每次都创建的情况下,它可以减少开销。
4.使用实例初始化。
以下是这 4 种方式的示例:
java
package reuse;
// reuse/Bath.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Constructor initialization with composition
//构造器初始化
class Soap {
private String s;
Soap() {
System.out.println("Soap()");
s = "Constructed";
}
@Override public String toString() { return s; }
}
public class Bath {
//在定义时初始化
private String // Initializing at point of definition:
s1 = "Happy",
s2 = "Happy",
s3, s4;
private Soap castile;
private int i;
private float toy;
public Bath() {
System.out.println("Inside Bath()");
s3 = "Joy";
toy = 3.14f;
castile = new Soap();
}
// Instance initialization:
//实例初始化
{ i = 47; }
@Override public String toString() {
//延迟初始化
if(s4 == null) // Delayed initialization:
s4 = "Joy";
return
"s1 = " + s1 + "\n" +
"s2 = " + s2 + "\n" +
"s3 = " + s3 + "\n" +
"s4 = " + s4 + "\n" +
"i = " + i + "\n" +
"toy = " + toy + "\n" +
"castile = " + castile;
}
public static void main(String[] args) {
Bath b = new Bath();
System.out.println(b);
}
}
/* Output:
Inside Bath()
Soap()
s1 = Happy
s2 = Happy
s3 = Joy
s4 = Joy
i = 47
toy = 3.14
castile = Constructed
*/
在构造器 Bath() 里,任何初始化发生之前会先执行一条语句。
当没有在定义处初始化时,就无法保证在给一个对象引用发送消息之前,它已经执行过任何初始化了
如果尝试调用未初始化的对象引用的方法,它会抛出一个运行时异常。
当调用 toString() 时,它会为 s4 赋值,因此所有字段都会在使用前被正确初始化。
8.2 继承语法
继承是所有面向对象语言不可或缺的一个组成部分。
其实当创建一个类时,总是在继承。
除非明确指定了要继承某个类,否则将隐式继承 Java 的标准根类 Object。
组合的语法直观明了,但继承使用了一种特殊的语法。
当继承时,你是在表示 "这个新类就像那个旧类"。
你需要在类主体的左花括号之前声明这一点,这通过关键字 extends 以及后面跟着的基类名称来实现。
这样做时,你会自动获得基类中的所有字段和方法 。
下面是一个示例:
java
package reuse;
// reuse/Detergent.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Inheritance syntax & properties
class Cleanser {
private String s = "Cleanser";
public void append(String a) { s += a; }
public void dilute() { append(" dilute()"); }
public void apply() { append(" apply()"); }
public void scrub() { append(" scrub()"); }
@Override public String toString() { return s; }
public static void main(String[] args) {
Cleanser x = new Cleanser();
x.dilute(); x.apply(); x.scrub();
System.out.println(x);
}
}
public class Detergent extends Cleanser {
// Change a method:
@Override public void scrub() {
append(" Detergent.scrub()");
super.scrub(); // Call base-class version
}
// Add methods to the interface:
public void foam() { append(" foam()"); }
// Test the new class:
public static void main(String[] args) {
Detergent x = new Detergent();
x.dilute();
x.apply();
x.scrub();
x.foam();
System.out.println(x);
System.out.println("Testing base class:");
Cleanser.main(args);
}
}
/* Output:
Cleanser dilute() apply() Detergent.scrub() scrub()
foam()
Testing base class:
Cleanser dilute() apply() scrub()
*/
这个程序展示了 Java 的许多特性。
首先,在 Cleanser 类的 append() 方法中,我们使用 += 操作符把字符串拼接到 s,
此操作符(连同 +)是 Java 设计者为处理字符串对象而重载的操作符之一。
其次,Cleanser 和 Detergent 都包含一个 main() 方法。可以为每个类都创建一个 main(),来进行简单的测试。
当测试完成后,也不需要删除 main(),可以将其保留以供后续测试。
即使一个程序中有很多类,唯一运行的 main() 也只会是在命令行中调用的那个。
所以在此例中,运行 java Detergent 命令时,会调用 Detergent.main()。
尽管 Cleanser 不是一个公共类,但也可以运行 java Cleanser 来调用 Cleanser.main()。
因此即使一个类只有包访问权限,也可以访问它的 public main() 方法。
运行 Cleanser 提示是否继续,点继续 continue。

在这个示例里,Detergent.main() 显式调用了 Cleanser.main(),并将从命令行获得的参数传递给了它(当然,你也可以传递任何字符串数组)。
Cleanser 中的所有方法都是 public 的。记住,如果省略访问权限修饰符,则该成员的权限默认是包访问权限,仅允许包内的成员进行访问。
因此,在这个包内,如果没有访问权限修饰符,任何人都可以使用这些方法。例如 Detergent 就没有问题。
但是,如果来自其他包的类要继承 Cleanser,那它就只能访问 public 成员。
**因此,考虑到继承,作为一般规则,应该将所有字段设为 private,将所有方法设为 public(稍后你将学到,protected 成员也允许子类访问)**。
在特定情况下,你必须进行调整,但一般来说这是一个有用的指导方针。
Cleanser 在其接口中有一组方法:append()、dilute()、apply()、scrub() 和 toString()。
因为 Detergent 继承了 Cleanser(通过 extends 关键字),所以它的接口就自动获得了这些方法,即使并没有在 Detergent 中显式定义它们。因此,可以将继承视作对类的复用。
正如在 scrub() 中看到的那样,可以使用基类中定义的方法并对其进行修改。

在这个示例中,你可能想从新版本的方法里调用继承来的基类方法。但是在 scrub() 中不能简单地调用 scrub(),因为这会产生递归调用。
为了解决这个问题,Java 提供了 super 关键字,来指代当前类继承的 "超类"(基类)。
因此,表达式 super.scrub() 调用了基类版本的 scrub() 方法。
在继承时,并不局限于只使用基类的方法。你还可以向子类中添加新方法,其方式与在类中添加任意方法一样:只需定义它即可。foam() 方法就是一个例子。

在 Detergent.main() 中可以看到,对于一个 Detergent 对象,你不仅可以调用 Cleanser 的所有可用方法,也可以调用 Detergent 里的所有可用方法(例如 foam())。

初始化基类
现在涉及两个类:基类和子类。想象一下子类产生的对象,这可能会令人困惑。
从外部看,新类与基类具有相同的接口,或许还有一些额外的方法和字段。但是继承不只是复制基类的接口这么简单。
当创建子类对象时,它里面包含了一个基类的子对象 (subobject)。这个子对象与直接通过基类创建的对象是一样的。
只是从外面看,基类的子对象被包裹在了子类的对象中。
正确初始化基类的子对象至关重要,我们只有一种方法可以保证这一点:在子类构造器中调用基类构造器来执行初始化,
它具有执行基类初始化所需的全部信息和权限。
Java 会自动在子类构造器中插入对基类构造器的调用 。
以下示例通过 3 个继承层级展示了这一点:
java
package reuse;
// reuse/Cartoon.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Constructor calls during inheritance
class Art {
Art() {
System.out.println("Art constructor");
}
}
class Drawing extends Art {
Drawing() {
System.out.println("Drawing constructor");
}
}
public class Cartoon extends Drawing {
public Cartoon() {
System.out.println("Cartoon constructor");
}
public static void main(String[] args) {
Cartoon x = new Cartoon();
}
}
/* Output:
Art constructor
Drawing constructor
Cartoon constructor
*/
构造过程是从基类 "向外" 进行的,因此基类在子类构造器可以访问它之前就被初始化了。
即使没有为 Cartoon 创建构造器,编译器也会为它合成一个可以调用基类构造器的无参构造器。
可以试着删除 Cartoon 的构造器来验证是不是这样的。
带参数的构造器
前面的示例里都是无参构造器;也就是说,它们没有参数。编译器很容易调用它们,因为不需要考虑传递什么参数。
如果基类没有无参构造器,或者如果你必须要调用具有参数的基类构造器,那么就要使用 super 关键字和相应的参数列表,来显式调用基类构造器:
java
package reuse;
// reuse/Chess.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Inheritance, constructors and arguments
class Game {
Game(int i) {
System.out.println("Game constructor");
}
}
class BoardGame extends Game {
BoardGame(int i) {
super(i);
System.out.println("BoardGame constructor");
}
}
public class Chess extends BoardGame {
Chess() {
super(11);
System.out.println("Chess constructor");
}
public static void main(String[] args) {
Chess x = new Chess();
}
}
/* Output:
Game constructor
BoardGame constructor
Chess constructor
*/
如果不在 BoardGame 的构造器中显式调用基类的构造器,编译器会报错,表示它找不到形式为 Game() 的构造器。
另外,对基类构造器的调用必须是子类构造器的第一个操作(否则编译器会通过报错来提示)。
8.3 委托
虽然 Java 里没有提供直接支持,但除了组合和继承外,还有第三种关系叫作委托 (delegation)。
它介于继承和组合之间,之所以这么说是因为你将成员对象放在正在构建的类中(类似组合),但同时又在新类里公开了成员对象的所有方法(类似继承)。
例如,一艘太空船需要一个控制模块:
java
package reuse;
// reuse/SpaceShipControls.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
public class SpaceShipControls {
void up(int velocity) {}
void down(int velocity) {}
void left(int velocity) {}
void right(int velocity) {}
void forward(int velocity) {}
void back(int velocity) {}
void turboBoost() {}
}
构造太空船的一种方式是使用继承:
java
package reuse;
// reuse/DerivedSpaceShip.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
public class DerivedSpaceShip extends SpaceShipControls {
private String name;
public DerivedSpaceShip(String name) {
this.name = name;
}
@Override public String toString() {
return name;
}
public static void main(String[] args) {
DerivedSpaceShip protector =
new DerivedSpaceShip("NSEA Protector");
protector.forward(100);
}
}
然而,DerivedSpaceShip 并非真正的 SpaceShipControls 类型,即使你可以 "告诉" 一个 DerivedSpaceShip 调用 forward() 方法。
更准确地说,一艘太空船中包含了 SpaceShipControls,同时 SpaceShipControls 中的所有方法也都在太空船中暴露给了外部。
委托解决了一下难题:基类中方法全部通过子类暴漏出来了,没法进行控制。
java
package reuse;
// reuse/SpaceShipDelegation.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
public class SpaceShipDelegation {
private String name;
private SpaceShipControls controls =
new SpaceShipControls();
public SpaceShipDelegation(String name) {
this.name = name;
}
// Delegated methods:
public void back(int velocity) {
controls.back(velocity);
}
public void down(int velocity) {
controls.down(velocity);
}
public void forward(int velocity) {
controls.forward(velocity);
}
public void left(int velocity) {
controls.left(velocity);
}
public void right(int velocity) {
controls.right(velocity);
}
public void turboBoost() {
controls.turboBoost();
}
public void up(int velocity) {
controls.up(velocity);
}
public static void main(String[] args) {
SpaceShipDelegation protector =
new SpaceShipDelegation("NSEA Protector");
protector.forward(100);
}
}
方法调用被转发到了内部的 controls 对象,因此这里得到的接口与使用继承得到的是相同的。
但是,这里可以更好地控制委托,因为你可以选择仅提供成员对象中的部分方法。

尽管 Java 语言不支持委托,但开发工具通常支持。例如,上面的示例就是用 JetBrains Idea 集成开发工具自动生成的。
8.4 组合与继承相结合
你会经常同时用到组合和继承。以下示例显示了使用组合和继承来创建类,并进行必要的构造器初始化:
java
package reuse;
// reuse/PlaceSetting.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Combining composition & inheritance
class Plate {
Plate(int i) {
System.out.println("Plate constructor");
}
}
class DinnerPlate extends Plate {
DinnerPlate(int i) {
super(i);
System.out.println("DinnerPlate constructor");
}
}
class Utensil {
Utensil(int i) {
System.out.println("Utensil constructor");
}
}
class Spoon extends Utensil {
Spoon(int i) {
super(i);
System.out.println("Spoon constructor");
}
}
class Fork extends Utensil {
Fork(int i) {
super(i);
System.out.println("Fork constructor");
}
}
class Knife extends Utensil {
Knife(int i) {
super(i);
System.out.println("Knife constructor");
}
}
// A cultural way of doing something:
class Custom {
Custom(int i) {
System.out.println("Custom constructor");
}
}
public class PlaceSetting extends Custom {
private Spoon sp;
private Fork frk;
private Knife kn;
private DinnerPlate pl;
public PlaceSetting(int i) {
super(i + 1);
sp = new Spoon(i + 2);
frk = new Fork(i + 3);
kn = new Knife(i + 4);
pl = new DinnerPlate(i + 5);
System.out.println("PlaceSetting constructor");
}
public static void main(String[] args) {
PlaceSetting x = new PlaceSetting(9);
}
}
/* Output:
Custom constructor
Utensil constructor
Spoon constructor
Utensil constructor
Fork constructor
Utensil constructor
Knife constructor
Plate constructor
DinnerPlate constructor
PlaceSetting constructor
*/
尽管编译器会强制你初始化基类,并要求在构造器一开头就执行,但它不会监督你是否的确初始化了成员对象。

请注意这些类分离得多么清晰。你甚至不需要方法的源代码就可以复用它们。
最多只需要导入一个包就可以了(对于继承和组合来说都是如此)。
8.4.1 确保正确的清理
Java 没有 C++ 中析构函数的概念,析构函数会在对象被销毁时自动调用。
之所以没有这个概念,原因可能是在 Java 中,我们一般只是简单地忘记对象而不是销毁它们,从而允许垃圾收集器在需要时回收其内存。
通常这工作得很好,但有时你的类可能会在其生命周期中执行一些需要清理的活动。
第 6 章中解释过,我们不知道垃圾收集器什么时候被调用,甚至它是否被调用。
所以如果想为某个类清理一些东西,你必须明确地编写一个特殊的方法来完成它,并确保客户程序员知晓他们需要调用这个方法。
除此之外------如第 15 章所述------你必须将此类清理活动放在 finally 子句里,以预防异常的出现。
思考如下示例,这里有一个能在屏幕上绘制图像的计算机辅助设计系统:
java
// reuse/CADSystem.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Ensuring proper cleanup
// {java reuse.CADSystem}
package reuse;
class Shape {
Shape(int i) {
System.out.println("Shape constructor");
}
void dispose() {
System.out.println("Shape dispose");
}
}
class Circle extends Shape {
Circle(int i) {
super(i);
System.out.println("Drawing Circle");
}
@Override void dispose() {
System.out.println("Erasing Circle");
super.dispose();
}
}
class Triangle extends Shape {
Triangle(int i) {
super(i);
System.out.println("Drawing Triangle");
}
@Override void dispose() {
System.out.println("Erasing Triangle");
super.dispose();
}
}
class Line extends Shape {
private int start, end;
Line(int start, int end) {
super(start);
this.start = start;
this.end = end;
System.out.println(
"Drawing Line: " + start + ", " + end);
}
@Override void dispose() {
System.out.println(
"Erasing Line: " + start + ", " + end);
super.dispose();
}
}
public class CADSystem extends Shape {
private Circle c;
private Triangle t;
private Line[] lines = new Line[3];
public CADSystem(int i) {
super(i + 1);
for(int j = 0; j < lines.length; j++)
lines[j] = new Line(j, j*j);
c = new Circle(1);
t = new Triangle(1);
System.out.println("Combined constructor");
}
@Override public void dispose() {
System.out.println("CADSystem.dispose()");
// The order of cleanup is the reverse
// of the order of initialization:
t.dispose();
c.dispose();
for(int i = lines.length - 1; i >= 0; i--)
lines[i].dispose();
super.dispose();
}
public static void main(String[] args) {
CADSystem x = new CADSystem(47);
try {
// Code and exception handling...
} finally {
x.dispose();
}
}
}
/* Output:
Shape constructor
Shape constructor
Drawing Line: 0, 0
Shape constructor
Drawing Line: 1, 1
Shape constructor
Drawing Line: 2, 4
Shape constructor
Drawing Circle
Shape constructor
Drawing Triangle
Combined constructor
CADSystem.dispose()
Erasing Triangle
Shape dispose
Erasing Circle
Shape dispose
Erasing Line: 2, 4
Shape dispose
Erasing Line: 1, 1
Shape dispose
Erasing Line: 0, 0
Shape dispose
Shape dispose
*/
这个系统中的一切都是某种 Shape 类型(它本身是一种 Object 类型,因为它隐式继承了这个根类)。
每个类不仅都重写了 Shape 的 dispose() 方法,而且还使用了 super 关键字调用了该方法的基类版本。

特定的Shape类---Circle、Triangle和Line---都有用来"绘制"的构造器,

不过在对象生命周期内调用的任何方法,都可能会做一些需要清理的事情。
每个类都有自己的 dispose() 方法,来将非内存相关的事物恢复到对象存在之前的状态。
在 main() 方法中有两个你没见过的关键字,第 15 章中会对此进行详细解释:try 和 finally。
try 关键字表示后面的代码块(由花括号来限定)是一个保护区域,这意味着它会被特殊处理。
其中一个特殊处理就是,无论 try 代码块如何退出,该保护区域之后的 finally 子句始终都会执行(有了异常处理,就可能以多种非常规的方式退出 try 代码块)。
这里,finally 子句表示,"无论发生了什么,总是为 x 调用 dispose() 方法"。

在清理方法中(本例中为 dispose()),还需要注意基类与成员对象两者之间清理方法的调用顺序,以防止某个子对象依赖于另一个。
首先执行 自己的类 的 所有特定清理工作,其顺序同创建顺序相反。(这通常要求基类元素仍然存活。)然后调用基类的清理方法,
如上述示例所示。

在很多情况下,清理并不是问题,只需要让垃圾收集器完成它的工作就可以了。
但必须执行显式清理时,你就需要多留心和多注意了,因为在垃圾收集方面可以依靠的并不多。
垃圾收集器可能永远不会被调用,如果被调用,它也可以按照自己想要的任何顺序回收对象 。
除了内存回收之外,你不能依赖垃圾收集 。如果想要执行清理,请创建自己的清理方法,不要使用 finalize()。
8.4.2 名称隐藏
如果 Java 基类的方法名称被多次重载,则在子类中重新定义该方法名称不会隐藏任何基类版本。
无论方法是在子类还是在基类中定义,重载都有效:
java
package reuse;
// reuse/Hide.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Overloading a base-class method name in a derived
// class does not hide the base-class versions
class Homer {
char doh(char c) {
System.out.println("doh(char)");
return 'd';
}
float doh(float f) {
System.out.println("doh(float)");
return 1.0f;
}
}
class Milhouse {}
class Bart extends Homer {
void doh(Milhouse m) {
System.out.println("doh(Milhouse)");
}
}
public class Hide {
public static void main(String[] args) {
Bart b = new Bart();
b.doh(1);
b.doh('x');
b.doh(1.0f);
b.doh(new Milhouse());
}
}
/* Output:
doh(float)
doh(char)
doh(float)
doh(Milhouse)
*/
Homer 的所有重载方法都可以在 Bart 中使用,即使 Bart 引入了一个新的重写方法。
正如你将在下一章中看到的那样,使用与基类完全相同的签名和返回类型来重写同名方法更为常见,否则就可能会令人困惑。
Java 5 的 @Override 注解并不是关键字,但可以像关键字一样使用。
当你打算重写一个方法时,可以选择添加这个注解,如果不小心对方法进行了重载而不是重写,编译器就会产生一个错误消息:


这样 @Override 注解就防止了意外重载。