提交 fc820e26 编写于 作者: W wizardforcel

ch7

上级 e9384feb
......@@ -6,7 +6,7 @@
但这样做也会遇到一个问题,如下例所示(若执行这个程序遇到麻烦,请参考第3章的3.1.2小节“赋值”):
```
//: Music.java
//: Music.java
// Inheritance & upcasting
package c07;
......@@ -14,7 +14,7 @@ class Note {
private int value;
private Note(int val) { value = val; }
public static final Note
middleC = new Note(0),
middleC = new Note(0),
cSharp = new Note(1),
cFlat = new Note(2);
} // Etc.
......@@ -46,21 +46,21 @@ public class Music {
} ///:~
```
其中,方法Music.tune()接收一个Instrument引用,同时也接收从Instrument衍生出来的所有东西。当一个Wind引用传递给tune()的时候,就会出现这种情况。此时没有转换的必要。这样做是可以接受的;Instrument里的接口必须存在于Wind中,因为Wind是从Instrument里继承得到的。从Wind向Instrument的向上转换可能“缩小”那个接口,但不可能把它变得比Instrument的完整接口还要小。
其中,方法`Music.tune()`接收一个`Instrument`引用,同时也接收从`Instrument`衍生出来的所有东西。当一个`Wind`引用传递给`tune()`的时候,就会出现这种情况。此时没有转换的必要。这样做是可以接受的;`Instrument`里的接口必须存在于`Wind`中,因为`Wind`是从`Instrument`里继承得到的。从`Wind``Instrument`的向上转换可能“缩小”那个接口,但不可能把它变得比`Instrument`的完整接口还要小。
7.1.1 为什么要向上转换
这个程序看起来也许显得有些奇怪。为什么所有人都应该有意忘记一个对象的类型呢?进行向上转换时,就可能产生这方面的疑惑。而且如果让tune()简单地取得一个Wind引用,将其作为自己的参数使用,似乎会更加简单、直观得多。但要注意:假如那样做,就需为系统内Instrument的每种类型写一个全新的tune()。假设按照前面的推论,加入Stringed(弦乐)和Brass(铜管)这两种Instrument(乐器):
这个程序看起来也许显得有些奇怪。为什么所有人都应该有意忘记一个对象的类型呢?进行向上转换时,就可能产生这方面的疑惑。而且如果让`tune()`简单地取得一个`Wind`引用,将其作为自己的参数使用,似乎会更加简单、直观得多。但要注意:假如那样做,就需为系统内`Instrument`的每种类型写一个全新的`tune()`。假设按照前面的推论,加入`Stringed`(弦乐)和`Brass`(铜管)这两种`Instrument`(乐器):
```
//: Music2.java
//: Music2.java
// Overloading instead of upcasting
class Note2 {
private int value;
private Note2(int val) { value = val; }
public static final Note2
middleC = new Note2(0),
middleC = new Note2(0),
cSharp = new Note2(1),
cFlat = new Note2(2);
} // Etc.
......@@ -110,7 +110,7 @@ public class Music2 {
} ///:~
```
这样做当然行得通,但却存在一个极大的弊端:必须为每种新增的Instrument2类编写与类紧密相关的方法。这意味着第一次就要求多得多的编程量。以后,假如想添加一个象tune()那样的新方法或者为Instrument添加一个新类型,仍然需要进行大量编码工作。此外,即使忘记对自己的某个方法进行重载设置,编译器也不会提示任何错误。这样一来,类型的整个操作过程就显得极难管理,有失控的危险。
这样做当然行得通,但却存在一个极大的弊端:必须为每种新增的`Instrument2`类编写与类紧密相关的方法。这意味着第一次就要求多得多的编程量。以后,假如想添加一个象`tune()`那样的新方法或者为`Instrument`添加一个新类型,仍然需要进行大量编码工作。此外,即使忘记对自己的某个方法进行重载设置,编译器也不会提示任何错误。这样一来,类型的整个操作过程就显得极难管理,有失控的危险。
但假如只写一个方法,将基础类作为参数使用,而不是使用那些特定的衍生类,岂不是会简单得多?也就是说,如果我们能不顾衍生类,只让自己的代码与基础类打交道,那么省下的工作量将是难以估计的。
......
# 7.10 练习
(1) 创建Rodent(啮齿动物):Mouse(老鼠),Gerbil(鼹鼠),Hamster(大颊鼠)等的一个继承分级结构。在基础类中,提供适用于所有Rodent的方法,并在衍生类中覆盖它们,从而根据不同类型的Rodent采取不同的行动。创建一个Rodent数组,在其中填充不同类型的Rodent,然后调用自己的基础类方法,看看会有什么情况发生。
(1) 创建`Rodent`(啮齿动物):`Mouse`(老鼠),`Gerbil`(鼹鼠),`Hamster`(大颊鼠)等的一个继承分级结构。在基础类中,提供适用于所有`Rodent`的方法,并在衍生类中覆盖它们,从而根据不同类型的`Rodent`采取不同的行动。创建一个`Rodent`数组,在其中填充不同类型的`Rodent`,然后调用自己的基础类方法,看看会有什么情况发生。
(2) 修改练习1,使Rodent成为一个接口。
(2) 修改练习1,使`Rodent`成为一个接口。
(3) 改正WindError.java中的问题。
(3) 改正`WindError.java`中的问题。
(4) 在GreenhouseControls.java中,添加Event内部类,使其能打开和关闭风扇。
\ No newline at end of file
(4) 在`GreenhouseControls.java`中,添加`Event`内部类,使其能打开和关闭风扇。
# 7.2 深入理解
对于Music.java的困难性,可通过运行程序加以体会。输出是Wind.play()。这当然是我们希望的输出,但它看起来似乎并不愿按我们的希望行事。请观察一下tune()方法:
对于`Music.java`的困难性,可通过运行程序加以体会。输出是`Wind.play()`。这当然是我们希望的输出,但它看起来似乎并不愿按我们的希望行事。请观察一下`tune()`方法:
```
public static void tune(Instrument i) {
......@@ -10,19 +10,19 @@ i.play(Note.middleC);
}
```
它接收Instrument引用。所以在这种情况下,编译器怎样才能知道Instrument引用指向的是一个Wind,而不是一个Brass或Stringed呢?编译器无从得知。为了深入了理解这个问题,我们有必要探讨一下“绑定”这个主题。
它接收`Instrument`引用。所以在这种情况下,编译器怎样才能知道`Instrument`引用指向的是一个`Wind`,而不是一个`Brass``Stringed`呢?编译器无从得知。为了深入了理解这个问题,我们有必要探讨一下“绑定”这个主题。
7.2.1 方法调用的绑定
将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。若在程序运行以前执行绑定(由编译器和链接程序,如果有的话),就叫作“早期绑定”。大家以前或许从未听说过这个术语,因为它在任何程序化语言里都是不可能的。C编译器只有一种方法调用,那就是“早期绑定”。
上述程序最令人迷惑不解的地方全与早期绑定有关,因为在只有一个Instrument引用的前提下,编译器不知道具体该调用哪个方法。
上述程序最令人迷惑不解的地方全与早期绑定有关,因为在只有一个`Instrument`引用的前提下,编译器不知道具体该调用哪个方法。
解决的方法就是“后期绑定”,它意味着绑定在运行期间进行,以对象的类型为基础。后期绑定也叫作“动态绑定”或“运行期绑定”。若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。
Java中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成final。这意味着我们通常不必决定是否应进行后期绑定——它是自动发生的。
Java中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成`final`。这意味着我们通常不必决定是否应进行后期绑定——它是自动发生的。
为什么要把一个方法声明成final呢?正如上一章指出的那样,它能防止其他人覆盖那个方法。但也许更重要的一点是,它可有效地“关闭”动态绑定,或者告诉编译器不需要进行动态绑定。这样一来,编译器就可为final方法调用生成效率更高的代码。
为什么要把一个方法声明成`final`呢?正如上一章指出的那样,它能防止其他人覆盖那个方法。但也许更重要的一点是,它可有效地“关闭”动态绑定,或者告诉编译器不需要进行动态绑定。这样一来,编译器就可为`final`方法调用生成效率更高的代码。
7.2.2 产生正确的行为
......@@ -30,58 +30,58 @@ Java中绑定的所有方法都采用后期绑定技术,除非一个方法已
在面向对象的程序设计中,有一个经典的“形状”例子。由于它很容易用可视化的形式表现出来,所以经常都用它说明问题。但很不幸的是,它可能误导初学者认为OOP只是为图形化编程设计的,这种认识当然是错误的。
形状例子有一个基础类,名为Shape;另外还有大量衍生类型:Circle(圆形),Square(方形),Triangle(三角形)等等。大家之所以喜欢这个例子,因为很容易理解“圆属于形状的一种类型”等概念。下面这幅继承图向我们展示了它们的关系:
形状例子有一个基础类,名为`Shape`;另外还有大量衍生类型:`Circle`(圆形),`Square`(方形),`Triangle`(三角形)等等。大家之所以喜欢这个例子,因为很容易理解“圆属于形状的一种类型”等概念。下面这幅继承图向我们展示了它们的关系:
![](7-1.gif)
向上转换可用下面这个语句简单地表现出来:
```
Shape s = new Circle();
```
在这里,我们创建了Circle对象,并将结果引用立即赋给一个Shape。这表面看起来似乎属于错误操作(将一种类型分配给另一个),但实际是完全可行的——因为按照继承关系,Circle属于Shape的一种。因此编译器认可上述语句,不会向我们提示一条出错消息。
在这里,我们创建了`Circle`对象,并将结果引用立即赋给一个`Shape`。这表面看起来似乎属于错误操作(将一种类型分配给另一个),但实际是完全可行的——因为按照继承关系,`Circle`属于`Shape`的一种。因此编译器认可上述语句,不会向我们提示一条出错消息。
当我们调用其中一个基础类方法时(已在衍生类里覆盖):
```
s.draw();
```
同样地,大家也许认为会调用Shape的draw(),因为这毕竟是一个Shape引用。那么编译器怎样才能知道该做其他任何事情呢?但此时实际调用的是Circle.draw(),因为后期绑定已经介入(多态性)。
同样地,大家也许认为会调用`Shape``draw()`,因为这毕竟是一个`Shape`引用。那么编译器怎样才能知道该做其他任何事情呢?但此时实际调用的是`Circle.draw()`,因为后期绑定已经介入(多态性)。
下面这个例子从一个稍微不同的角度说明了问题:
```
//: Shapes.java
// Polymorphism in Java
class Shape {
class Shape {
void draw() {}
void erase() {}
void erase() {}
}
class Circle extends Shape {
void draw() {
System.out.println("Circle.draw()");
void draw() {
System.out.println("Circle.draw()");
}
void erase() {
System.out.println("Circle.erase()");
void erase() {
System.out.println("Circle.erase()");
}
}
class Square extends Shape {
void draw() {
System.out.println("Square.draw()");
void draw() {
System.out.println("Square.draw()");
}
void erase() {
System.out.println("Square.erase()");
void erase() {
System.out.println("Square.erase()");
}
}
class Triangle extends Shape {
void draw() {
System.out.println("Triangle.draw()");
void draw() {
System.out.println("Triangle.draw()");
}
void erase() {
void erase() {
System.out.println("Triangle.erase()");
}
}
......@@ -107,11 +107,11 @@ public class Shapes {
} ///:~
```
针对从Shape衍生出来的所有东西,Shape建立了一个通用接口——也就是说,所有(几何)形状都可以描绘和删除。衍生类覆盖了这些定义,为每种特殊类型的几何形状都提供了独一无二的行为。
针对从`Shape`衍生出来的所有东西,`Shape`建立了一个通用接口——也就是说,所有(几何)形状都可以描绘和删除。衍生类覆盖了这些定义,为每种特殊类型的几何形状都提供了独一无二的行为。
在主类Shapes里,包含了一个static方法,名为randShape()。它的作用是在每次调用它时为某个随机选择的Shape对象生成一个引用。请注意向上转换是在每个return语句里发生的。这个语句取得指向一个Circle,Square或者Triangle的引用,并将其作为返回类型Shape发给方法。所以无论什么时候调用这个方法,就绝对没机会了解它的具体类型到底是什么,因为肯定会获得一个单纯的Shape引用。
在主类`Shapes`里,包含了一个`static`方法,名为`randShape()`。它的作用是在每次调用它时为某个随机选择的`Shape`对象生成一个引用。请注意向上转换是在每个`return`语句里发生的。这个语句取得指向一个`Circle``Square`或者`Triangle`的引用,并将其作为返回类型`Shape`发给方法。所以无论什么时候调用这个方法,就绝对没机会了解它的具体类型到底是什么,因为肯定会获得一个单纯的`Shape`引用。
main()包含了Shape引用的一个数组,其中的数据通过对randShape()的调用填入。在这个时候,我们知道自己拥有Shape,但不知除此之外任何具体的情况(编译器同样不知)。然而,当我们在这个数组里步进,并为每个元素调用draw()的时候,与各类型有关的正确行为会魔术般地发生,就象下面这个输出示例展示的那样:
`main()`包含了`Shape`引用的一个数组,其中的数据通过对`randShape()`的调用填入。在这个时候,我们知道自己拥有`Shape`,但不知除此之外任何具体的情况(编译器同样不知)。然而,当我们在这个数组里步进,并为每个元素调用`draw()`的时候,与各类型有关的正确行为会魔术般地发生,就象下面这个输出示例展示的那样:
```
Circle.draw()
......@@ -125,16 +125,16 @@ Square.draw()
Square.draw()
```
当然,由于几何形状是每次随机选择的,所以每次运行都可能有不同的结果。之所以要突出形状的随机选择,是为了让大家深刻体会这一点:为了在编译的时候发出正确的调用,编译器毋需获得任何特殊的情报。对draw()的所有调用都是通过动态绑定进行的。
当然,由于几何形状是每次随机选择的,所以每次运行都可能有不同的结果。之所以要突出形状的随机选择,是为了让大家深刻体会这一点:为了在编译的时候发出正确的调用,编译器毋需获得任何特殊的情报。对`draw()`的所有调用都是通过动态绑定进行的。
7.2.3 扩展性
现在,让我们仍然返回乐器(Instrument)示例。由于存在多态性,所以可根据自己的需要向系统里加入任意多的新类型,同时毋需更改true()方法。在一个设计良好的OOP程序中,我们的大多数或者所有方法都会遵从tune()的模型,而且只与基础类接口通信。我们说这样的程序具有“扩展性”,因为可以从通用的基础类继承新的数据类型,从而新添一些功能。如果是为了适应新类的要求,那么对基础类接口进行操纵的方法根本不需要改变,
现在,让我们仍然返回乐器(I`nstrument`)示例。由于存在多态性,所以可根据自己的需要向系统里加入任意多的新类型,同时毋需更改`true()`方法。在一个设计良好的OOP程序中,我们的大多数或者所有方法都会遵从`tune()`的模型,而且只与基础类接口通信。我们说这样的程序具有“扩展性”,因为可以从通用的基础类继承新的数据类型,从而新添一些功能。如果是为了适应新类的要求,那么对基础类接口进行操纵的方法根本不需要改变,
对于乐器例子,假设我们在基础类里加入更多的方法,以及一系列新类,那么会出现什么情况呢?下面是示意图:
![](7-2.gif)
所有这些新类都能与老类——tune()默契地工作,毋需对tune()作任何调整。即使tune()位于一个独立的文件里,而将新方法添加到Instrument的接口,tune()也能正确地工作,不需要重新编译。下面这个程序是对上述示意图的具体实现:
所有这些新类都能与老类——`tune()`默契地工作,毋需对`tune()`作任何调整。即使`tune()`位于一个独立的文件里,而将新方法添加到`Instrument`的接口,`tune()`也能正确地工作,不需要重新编译。下面这个程序是对上述示意图的具体实现:
```
//: Music3.java
......@@ -216,8 +216,8 @@ public class Music3 {
} ///:~
```
新方法是what()和adjust()。前者返回一个String引用,同时返回对那个类的说明;后者使我们能对每种乐器进行调整。
新方法是`what()``adjust()`。前者返回一个`String`引用,同时返回对那个类的说明;后者使我们能对每种乐器进行调整。
main()中,当我们将某样东西置入Instrument3数组时,就会自动向上转换到Instrument3
`main()`中,当我们将某样东西置入`Instrument3`数组时,就会自动向上转换到`Instrument3`
可以看到,在围绕tune()方法的其他所有代码都发生变化的同时,tune()方法却丝毫不受它们的影响,依然故我地正常工作。这正是利用多态性希望达到的目标。我们对代码进行修改后,不会对程序中不应受到影响的部分造成影响。此外,我们认为多态性是一种至关重要的技术,它允许程序员“将发生改变的东西同没有发生改变的东西区分开”。
可以看到,在围绕`tune()`方法的其他所有代码都发生变化的同时,`tune()`方法却丝毫不受它们的影响,依然故我地正常工作。这正是利用多态性希望达到的目标。我们对代码进行修改后,不会对程序中不应受到影响的部分造成影响。此外,我们认为多态性是一种至关重要的技术,它允许程序员“将发生改变的东西同没有发生改变的东西区分开”。
# 7.3 覆盖与重载
现在让我们用不同的眼光来看看本章的头一个例子。在下面这个程序中,方法play()的接口会在被覆盖的过程中发生变化。这意味着我们实际并没有“覆盖”方法,而是使其“重载”。编译器允许我们对方法进行重载处理,使其不报告出错。但这种行为可能并不是我们所希望的。下面是这个例子:
现在让我们用不同的眼光来看看本章的头一个例子。在下面这个程序中,方法`play()`的接口会在被覆盖的过程中发生变化。这意味着我们实际并没有“覆盖”方法,而是使其“重载”。编译器允许我们对方法进行重载处理,使其不报告出错。但这种行为可能并不是我们所希望的。下面是这个例子:
```
//: WindError.java
//: WindError.java
// Accidentally changing the interface
class NoteX {
......@@ -37,9 +37,9 @@ public class WindError {
} ///:~
```
这里还向大家引入了另一个易于混淆的概念。在InstrumentX中,play()方法采用了一个int(整数)数值,它的标识符是NoteX。也就是说,即使NoteX是一个类名,也可以把它作为一个标识符使用,编译器不会报告出错。但在WindX中,play()采用一个NoteX引用,它有一个标识符n。即便我们使用“play(NoteX NoteX)”,编译器也不会报告错误。这样一来,看起来就象是程序员有意覆盖play()的功能,但对方法的类型定义却稍微有些不确切。然而,编译器此时假定的是程序员有意进行“重载”,而非“覆盖”。请仔细体会这两个术语的区别。“重载”是指同一样东西在不同的地方具有多种含义;而“覆盖”是指它随时随地都只有一种含义,只是原先的含义完全被后来的含义取代了。请注意如果遵守标准的Java命名规范,参数标识符就应该是noteX,这样可把它与类名区分开。
这里还向大家引入了另一个易于混淆的概念。在`InstrumentX`中,`play()`方法采用了一个`int`(整数)数值,它的标识符是`NoteX`。也就是说,即使`NoteX`是一个类名,也可以把它作为一个标识符使用,编译器不会报告出错。但在`WindX`中,`play()`采用一个`NoteX`引用,它有一个标识符`n`。即便我们使用`play(NoteX NoteX)`,编译器也不会报告错误。这样一来,看起来就象是程序员有意覆盖`play()`的功能,但对方法的类型定义却稍微有些不确切。然而,编译器此时假定的是程序员有意进行“重载”,而非“覆盖”。请仔细体会这两个术语的区别。“重载”是指同一样东西在不同的地方具有多种含义;而“覆盖”是指它随时随地都只有一种含义,只是原先的含义完全被后来的含义取代了。请注意如果遵守标准的Java命名规范,参数标识符就应该是`noteX`,这样可把它与类名区分开。
tune中,“InstrumentX i”会发出play()消息,同时将某个NoteX成员作为参数使用(MIDDLE_C)。由于NoteX包含了int定义,重载的play()方法的int版本会得到调用。同时由于它尚未被“覆盖”,所以会使用基础类版本。
`tune`中,`InstrumentX i`会发出`play()`消息,同时将某个`NoteX`成员作为参数使用(`MIDDLE_C`)。由于`NoteX`包含了`int`定义,重载的`play()`方法的`int`版本会得到调用。同时由于它尚未被“覆盖”,所以会使用基础类版本。
输出是:
......
# 7.4 抽象类和方法
在我们所有乐器(Instrument)例子中,基础类Instrument内的方法都肯定是“伪”方法。若去调用这些方法,就会出现错误。那是由于Instrument的意图是为从它衍生出去的所有类都创建一个通用接口。
在我们所有乐器(`Instrument`)例子中,基础类`Instrument`内的方法都肯定是“伪”方法。若去调用这些方法,就会出现错误。那是由于`Instrument`的意图是为从它衍生出去的所有类都创建一个通用接口。
之所以要建立这个通用接口,唯一的原因就是它能为不同的子类型作出不同的表示。它为我们建立了一种基本形式,使我们能定义在所有衍生类里“通用”的一些东西。为阐述这个观念,另一个方法是把Instrument称为“抽象基础类”(简称“抽象类”)。若想通过该通用接口处理一系列类,就需要创建一个抽象类。对所有与基础类声明的签名相符的衍生类方法,都可以通过动态绑定机制进行调用(然而,正如上一节指出的那样,如果方法名与基础类相同,但参数不同,就会出现重载现象,那或许并非我们所愿意的)。
之所以要建立这个通用接口,唯一的原因就是它能为不同的子类型作出不同的表示。它为我们建立了一种基本形式,使我们能定义在所有衍生类里“通用”的一些东西。为阐述这个观念,另一个方法是把`Instrument`称为“抽象基础类”(简称“抽象类”)。若想通过该通用接口处理一系列类,就需要创建一个抽象类。对所有与基础类声明的签名相符的衍生类方法,都可以通过动态绑定机制进行调用(然而,正如上一节指出的那样,如果方法名与基础类相同,但参数不同,就会出现重载现象,那或许并非我们所愿意的)。
如果有一个象Instrument那样的抽象类,那个类的对象几乎肯定没有什么意义。换言之,Instrument的作用仅仅是表达接口,而不是表达一些具体的实现细节。所以创建一个Instrument对象是没有意义的,而且我们通常都应禁止用户那样做。为达到这个目的,可令Instrument内的所有方法都显示出错消息。但这样做会延迟信息到运行期,并要求在用户那一面进行彻底、可靠的测试。无论如何,最好的方法都是在编译期间捕捉到问题。
如果有一个象`Instrument`那样的抽象类,那个类的对象几乎肯定没有什么意义。换言之,`Instrument`的作用仅仅是表达接口,而不是表达一些具体的实现细节。所以创建一个`Instrument`对象是没有意义的,而且我们通常都应禁止用户那样做。为达到这个目的,可令`Instrument`内的所有方法都显示出错消息。但这样做会延迟信息到运行期,并要求在用户那一面进行彻底、可靠的测试。无论如何,最好的方法都是在编译期间捕捉到问题。
针对这个问题,Java专门提供了一种机制,名为“抽象方法”。它属于一种不完整的方法,只含有一个声明,没有方法主体。下面是抽象方法声明时采用的语法:
......@@ -13,15 +13,15 @@
abstract void X();
```
包含了抽象方法的一个类叫作“抽象类”。如果一个类里包含了一个或多个抽象方法,类就必须指定成abstract(抽象)。否则,编译器会向我们报告一条出错消息。
包含了抽象方法的一个类叫作“抽象类”。如果一个类里包含了一个或多个抽象方法,类就必须指定成`abstract`(抽象)。否则,编译器会向我们报告一条出错消息。
若一个抽象类是不完整的,那么一旦有人试图生成那个类的一个对象,编译器又会采取什么行动呢?由于不能安全地为一个抽象类创建属于它的对象,所以会从编译器那里获得一条出错提示。通过这种方法,编译器可保证抽象类的“纯洁性”,我们不必担心会误用它。
如果从一个抽象类继承,而且想生成新类型的一个对象,就必须为基础类中的所有抽象方法提供方法定义。如果不这样做(完全可以选择不做),则衍生类也会是抽象的,而且编译器会强迫我们用abstract关键字标志那个类的“抽象”本质。
如果从一个抽象类继承,而且想生成新类型的一个对象,就必须为基础类中的所有抽象方法提供方法定义。如果不这样做(完全可以选择不做),则衍生类也会是抽象的,而且编译器会强迫我们用`abstract`关键字标志那个类的“抽象”本质。
即使不包括任何abstract方法,亦可将一个类声明成“抽象类”。如果一个类没必要拥有任何抽象方法,而且我们想禁止那个类的所有实例,这种能力就会显得非常有用。
即使不包括任何`abstract`方法,亦可将一个类声明成“抽象类”。如果一个类没必要拥有任何抽象方法,而且我们想禁止那个类的所有实例,这种能力就会显得非常有用。
Instrument类可很轻松地转换成一个抽象类。只有其中一部分方法会变成抽象方法,因为使一个类抽象以后,并不会强迫我们将它的所有方法都同时变成抽象。下面是它看起来的样子:
`Instrument`类可很轻松地转换成一个抽象类。只有其中一部分方法会变成抽象方法,因为使一个类抽象以后,并不会强迫我们将它的所有方法都同时变成抽象。下面是它看起来的样子:
![](7-3.gif)
......@@ -69,7 +69,7 @@ class Brass4 extends Wind4 {
public void play() {
System.out.println("Brass4.play()");
}
public void adjust() {
public void adjust() {
System.out.println("Brass4.adjust()");
}
}
......
# 7.5 接口
“interface”(接口)关键字使抽象的概念更深入了一层。我们可将其想象为一个“纯”抽象类。它允许创建者规定一个类的基本形式:方法名、参数列表以及返回类型,但不规定方法主体。接口也包含了基本数据类型的数据成员,但它们都默认为static和final。接口只提供一种形式,并不提供实现的细节。
`interface`(接口)关键字使抽象的概念更深入了一层。我们可将其想象为一个“纯”抽象类。它允许创建者规定一个类的基本形式:方法名、参数列表以及返回类型,但不规定方法主体。接口也包含了基本数据类型的数据成员,但它们都默认为`static``final`。接口只提供一种形式,并不提供实现的细节。
接口这样描述自己:“对于实现我的所有类,看起来都应该象我现在这个样子”。因此,采用了一个特定接口的所有代码都知道对于那个接口可能会调用什么方法。这便是接口的全部含义。所以我们常把接口用于建立类和类之间的一个“协议”。有些面向对象的程序设计语言采用了一个名为“protocol”(协议)的关键字,它做的便是与接口相同的事情。
接口这样描述自己:“对于实现我的所有类,看起来都应该象我现在这个样子”。因此,采用了一个特定接口的所有代码都知道对于那个接口可能会调用什么方法。这便是接口的全部含义。所以我们常把接口用于建立类和类之间的一个“协议”。有些面向对象的程序设计语言采用了一个名为`protocol`(协议)的关键字,它做的便是与接口相同的事情。
为创建一个接口,请使用interface关键字,而不要用class。与类相似,我们可在interface关键字的前面增加一个public关键字(但只有接口定义于同名的一个文件内);或者将其省略,营造一种“友好的”状态。
为创建一个接口,请使用`interface`关键字,而不要用`class`。与类相似,我们可在`interface`关键字的前面增加一个`public`关键字(但只有接口定义于同名的一个文件内);或者将其省略,营造一种“友好的”状态。
为了生成与一个特定的接口(或一组接口)相符的类,要使用implements(实现)关键字。我们要表达的意思是“接口看起来就象那个样子,这儿是它具体的工作细节”。除这些之外,我们其他的工作都与继承极为相似。下面是乐器例子的示意图:
为了生成与一个特定的接口(或一组接口)相符的类,要使用`implements`(实现)关键字。我们要表达的意思是“接口看起来就象那个样子,这儿是它具体的工作细节”。除这些之外,我们其他的工作都与继承极为相似。下面是乐器例子的示意图:
![](7-4.gif)
具体实现了一个接口以后,就获得了一个普通的类,可用标准方式对其进行扩展。
可决定将一个接口中的方法声明明确定义为“public”。但即便不明确定义,它们也会默认为public。所以在实现一个接口的时候,来自接口的方法必须定义成public。否则的话,它们会默认为“友好的”,而且会限制我们在继承过程中对一个方法的访问——Java编译器不允许我们那样做。
可决定将一个接口中的方法声明明确定义为`public`。但即便不明确定义,它们也会默认为`public`。所以在实现一个接口的时候,来自接口的方法必须定义成`public`。否则的话,它们会默认为“友好的”,而且会限制我们在继承过程中对一个方法的访问——Java编译器不允许我们那样做。
Instrument例子的修改版本中,大家可明确地看出这一点。注意接口中的每个方法都严格地是一个声明,它是编译器唯一允许的。除此以外,Instrument5中没有一个方法被声明为public,但它们都会自动获得public属性。如下所示:
`Instrument`例子的修改版本中,大家可明确地看出这一点。注意接口中的每个方法都严格地是一个声明,它是编译器唯一允许的。除此以外,`Instrument5`中没有一个方法被声明为`public`,但它们都会自动获得`public`属性。如下所示:
```
//: Music5.java
......@@ -59,7 +59,7 @@ class Brass5 extends Wind5 {
public void play() {
System.out.println("Brass5.play()");
}
public void adjust() {
public void adjust() {
System.out.println("Brass5.adjust()");
}
}
......@@ -96,15 +96,15 @@ public class Music5 {
} ///:~
```
代码剩余的部分按相同的方式工作。我们可以自由决定向上转换到一个名为Instrument5的“普通”类,一个名为Instrument5的“抽象”类,或者一个名为Instrument5的“接口”。所有行为都是相同的。事实上,我们在tune()方法中可以发现没有任何证据显示Instrument5到底是个“普通”类、“抽象”类还是一个“接口”。这是做是故意的:每种方法都使程序员能对对象的创建与使用进行不同的控制。
代码剩余的部分按相同的方式工作。我们可以自由决定向上转换到一个名为`Instrument5`的“普通”类,一个名为`Instrument5`的“抽象”类,或者一个名为`Instrument5`的“接口”。所有行为都是相同的。事实上,我们在`tune()方`法中可以发现没有任何证据显示`Instrument5`到底是个“普通”类、“抽象”类还是一个“接口”。这是做是故意的:每种方法都使程序员能对对象的创建与使用进行不同的控制。
7.5.1 Java的“多重继承”
接口只是比抽象类“更纯”的一种形式。它的用途并不止那些。由于接口根本没有具体的实现细节——也就是说,没有与存储空间与“接口”关联在一起——所以没有任何办法可以防止多个接口合并到一起。这一点是至关重要的,因为我们经常都需要表达这样一个意思:“x从属于a,也从属于b,也从属于c”。在C++中,将多个类合并到一起的行动称作“多重继承”,而且操作较为不便,因为每个类都可能有一套自己的实现细节。在Java中,我们可采取同样的行动,但只有其中一个类拥有具体的实现细节。所以在合并多个接口的时候,C++的问题不会在Java中重演。如下所示:
接口只是比抽象类“更纯”的一种形式。它的用途并不止那些。由于接口根本没有具体的实现细节——也就是说,没有与存储空间与“接口”关联在一起——所以没有任何办法可以防止多个接口合并到一起。这一点是至关重要的,因为我们经常都需要表达这样一个意思:“`x`从属于`a`,也从属于`b`,也从属于`c`”。在C++中,将多个类合并到一起的行动称作“多重继承”,而且操作较为不便,因为每个类都可能有一套自己的实现细节。在Java中,我们可采取同样的行动,但只有其中一个类拥有具体的实现细节。所以在合并多个接口的时候,C++的问题不会在Java中重演。如下所示:
![](7-5.gif)
在一个衍生类中,我们并不一定要拥有一个抽象或具体(没有抽象方法)的基础类。如果确实想从一个非接口继承,那么只能从一个继承。剩余的所有基本元素都必须是“接口”。我们将所有接口名置于implements关键字的后面,并用逗号分隔它们。可根据需要使用多个接口,而且每个接口都会成为一个独立的类型,可对其进行向上转换。下面这个例子展示了一个“具体”类同几个接口合并的情况,它最终生成了一个新类:
在一个衍生类中,我们并不一定要拥有一个抽象或具体(没有抽象方法)的基础类。如果确实想从一个非接口继承,那么只能从一个继承。剩余的所有基本元素都必须是“接口”。我们将所有接口名置于`implements`关键字的后面,并用逗号分隔它们。可根据需要使用多个接口,而且每个接口都会成为一个独立的类型,可对其进行向上转换。下面这个例子展示了一个“具体”类同几个接口合并的情况,它最终生成了一个新类:
```
//: Adventure.java
......@@ -127,7 +127,7 @@ class ActionCharacter {
public void fight() {}
}
class Hero extends ActionCharacter
class Hero extends ActionCharacter
implements CanFight, CanSwim, CanFly {
public void swim() {}
public void fly() {}
......@@ -148,11 +148,11 @@ public class Adventure {
} ///:~
```
从中可以看到,Hero将具体类ActionCharacter同接口CanFight,CanSwim以及CanFly合并起来。按这种形式合并一个具体类与接口的时候,具体类必须首先出现,然后才是接口(否则编译器会报错)。
从中可以看到,`Hero`将具体类`ActionCharacter`同接口`CanFight``CanSwim`以及`CanFly`合并起来。按这种形式合并一个具体类与接口的时候,具体类必须首先出现,然后才是接口(否则编译器会报错)。
请注意fight()的签名在CanFight接口与ActionCharacter类中是相同的,而且没有在Hero中为fight()提供一个具体的定义。接口的规则是:我们可以从它继承(稍后就会看到),但这样得到的将是另一个接口。如果想创建新类型的一个对象,它就必须是已提供所有定义的一个类。尽管Hero没有为fight()明确地提供一个定义,但定义是随同ActionCharacter来的,所以这个定义会自动提供,我们可以创建Hero的对象。
请注意`fight()`的签名在`CanFight`接口与`ActionCharacter`类中是相同的,而且没有在`Hero`中为`fight()`提供一个具体的定义。接口的规则是:我们可以从它继承(稍后就会看到),但这样得到的将是另一个接口。如果想创建新类型的一个对象,它就必须是已提供所有定义的一个类。尽管`Hero`没有为`fight()`明确地提供一个定义,但定义是随同`ActionCharacter`来的,所以这个定义会自动提供,我们可以创建`Hero`的对象。
在类Adventure中,我们可看到共有四个方法,它们将不同的接口和具体类作为自己的参数使用。创建一个Hero对象后,它可以传递给这些方法中的任何一个。这意味着它们会依次向上转换到每一个接口。由于接口是用Java设计的,所以这样做不会有任何问题,而且程序员不必对此加以任何特别的关注。
在类`Adventure`中,我们可看到共有四个方法,它们将不同的接口和具体类作为自己的参数使用。创建一个`Hero`对象后,它可以传递给这些方法中的任何一个。这意味着它们会依次向上转换到每一个接口。由于接口是用Java设计的,所以这样做不会有任何问题,而且程序员不必对此加以任何特别的关注。
注意上述例子已向我们揭示了接口最关键的作用,也是使用接口最重要的一个原因:能向上转换至多个基础类。使用接口的第二个原因与使用抽象基础类的原因是一样的:防止客户程序员制作这个类的一个对象,以及规定它仅仅是一个接口。这样便带来了一个问题:到底应该使用一个接口还是一个抽象类呢?若使用接口,我们可以同时获得抽象类以及接口的好处。所以假如想创建的基础类没有任何方法定义或者成员变量,那么无论如何都愿意使用接口,而不要选择抽象类。事实上,如果事先知道某种东西会成为基础类,那么第一个选择就是把它变成一个接口。只有在必须使用方法定义或者成员变量的时候,才应考虑采用抽象类。
......@@ -181,7 +181,7 @@ class DragonZilla implements DangerousMonster {
public void destroy() {}
}
interface Vampire
interface Vampire
extends DangerousMonster, Lethal {
void drinkBlood();
}
......@@ -200,12 +200,12 @@ class HorrorShow {
} ///:~
```
DangerousMonster是对Monster的一个简单的扩展,最终生成了一个新接口。这是在DragonZilla里实现的。
Vampire的语法仅在继承接口时才可使用。通常,我们只能对单独一个类应用extends(扩展)关键字。但由于接口可能由多个其他接口构成,所以在构建一个新接口时,extends可能引用多个基础接口。正如大家看到的那样,接口的名字只是简单地使用逗号分隔。
`DangerousMonster`是对`Monster`的一个简单的扩展,最终生成了一个新接口。这是在`DragonZilla`里实现的。
`Vampire`的语法仅在继承接口时才可使用。通常,我们只能对单独一个类应用`extends`(扩展)关键字。但由于接口可能由多个其他接口构成,所以在构建一个新接口时,`extends`可能引用多个基础接口。正如大家看到的那样,接口的名字只是简单地使用逗号分隔。
7.5.3 常数分组
由于置入一个接口的所有字段都自动具有static和final属性,所以接口是对常数值进行分组的一个好工具,它具有与C或C++的enum非常相似的效果。如下例所示:
由于置入一个接口的所有字段都自动具有`static``final`属性,所以接口是对常数值进行分组的一个好工具,它具有与C或C++的`enum`非常相似的效果。如下例所示:
```
//: Months.java
......@@ -214,18 +214,18 @@ package c07;
public interface Months {
int
JANUARY = 1, FEBRUARY = 2, MARCH = 3,
APRIL = 4, MAY = 5, JUNE = 6, JULY = 7,
JANUARY = 1, FEBRUARY = 2, MARCH = 3,
APRIL = 4, MAY = 5, JUNE = 6, JULY = 7,
AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10,
NOVEMBER = 11, DECEMBER = 12;
} ///:~
```
注意根据Java命名规则,拥有固定标识符的static final基本数据类型(亦即编译期常数)都全部采用大写字母(用下划线分隔单个标识符里的多个单词)。
注意根据Java命名规则,拥有固定标识符的`static final`基本数据类型(亦即编译期常数)都全部采用大写字母(用下划线分隔单个标识符里的多个单词)。
接口中的字段会自动具备public属性,所以没必要专门指定。
接口中的字段会自动具备`public`属性,所以没必要专门指定。
现在,通过导入 `c07.*``c07.Months` ,我们可以从包的外部使用常数——就象对其他任何包进行的操作那样。此外,也可以用类似Months.JANUARY的表达式对值进行引用。当然,我们获得的只是一个int,所以不象C++的enum那样拥有额外的类型安全性。但与将数字强行编码(硬编码)到自己的程序中相比,这种(常用的)技术无疑已经是一个巨大的进步。我们通常把“硬编码”数字的行为称为“魔术数字”,它产生的代码是非常难以维护的。
现在,通过导入 `c07.*``c07.Months` ,我们可以从包的外部使用常数——就象对其他任何包进行的操作那样。此外,也可以用类似`Months.JANUARY`的表达式对值进行引用。当然,我们获得的只是一个`int`,所以不象C++的`enum`那样拥有额外的类型安全性。但与将数字强行编码(硬编码)到自己的程序中相比,这种(常用的)技术无疑已经是一个巨大的进步。我们通常把“硬编码”数字的行为称为“魔术数字”,它产生的代码是非常难以维护的。
如确实不想放弃额外的类型安全性,可构建象下面这样的一个类(注释①):
```
......@@ -238,7 +238,7 @@ public final class Month2 {
private Month2(String nm) { name = nm; }
public String toString() { return name; }
public final static Month2
JAN = new Month2("January"),
JAN = new Month2("January"),
FEB = new Month2("February"),
MAR = new Month2("March"),
APR = new Month2("April"),
......@@ -267,16 +267,16 @@ public final class Month2 {
①:是Rich Hoffarth的一封E-mail触发了我这样编写程序的灵感。
这个类叫作Month2,因为标准Java库里已经有一个Month。它是一个final类,并含有一个private构造器,所以没有人能从它继承,或制作它的一个实例。唯一的实例就是那些final static对象,它们是在类本身内部创建的,包括:JAN,FEB,MAR等等。这些对象也在month数组中使用,后者让我们能够按数字挑选月份,而不是按名字(注意数组中提供了一个多余的JAN,使偏移量增加了1,也使December确实成为12月)。在main()中,我们可注意到类型的安全性:m是一个Month2对象,所以只能将其分配给Month2。在前面的Months.java例子中,只提供了int值,所以本来想用来代表一个月份的int变量可能实际获得一个整数值,那样做可能不十分安全。
这儿介绍的方法也允许我们交换使用==或者equals(),就象main()尾部展示的那样。
这个类叫作`Month2`,因为标准Java库里已经有一个`Month`。它是一个`final`类,并含有一个`private`构造器,所以没有人能从它继承,或制作它的一个实例。唯一的实例就是那些`final static`对象,它们是在类本身内部创建的,包括:`JAN``FEB``MAR`等等。这些对象也在`month`数组中使用,后者让我们能够按数字挑选月份,而不是按名字(注意数组中提供了一个多余的`JAN`,使偏移量增加了1,也使`December`确实成为12月)。在`main()`中,我们可注意到类型的安全性:`m`是一个`Month2`对象,所以只能将其分配给`Month2`。在前面的`Months.java`例子中,只提供了`int`值,所以本来想用来代表一个月份的`int`变量可能实际获得一个整数值,那样做可能不十分安全。
这儿介绍的方法也允许我们交换使用`==`或者`equals()`,就象`main()`尾部展示的那样。
7.5.4 初始化接口中的字段
接口中定义的字段会自动具有static和final属性。它们不能是“空白final”,但可初始化成非常数表达式。例如:
接口中定义的字段会自动具有`static``final`属性。它们不能是“空白`final`”,但可初始化成非常数表达式。例如:
```
//: RandVals.java
// Initializing interface fields with
// Initializing interface fields with
// non-constant initializers
import java.util.*;
......@@ -288,7 +288,7 @@ public interface RandVals {
} ///:~
```
由于字段是static的,所以它们会在首次装载类之后、以及首次访问任何字段之前获得初始化。下面是一个简单的测试:
由于字段是`static`的,所以它们会在首次装载类之后、以及首次访问任何字段之前获得初始化。下面是一个简单的测试:
```
//: TestRandVals.java
......@@ -303,4 +303,4 @@ public class TestRandVals {
} ///:~
```
当然,字段并不是接口的一部分,而是保存于那个接口的static存储区域中。
当然,字段并不是接口的一部分,而是保存于那个接口的`static`存储区域中。
此差异已折叠。
......@@ -6,9 +6,9 @@
构造器调用的顺序已在第4章进行了简要说明,但那是在继承和多态性问题引入之前说的话。
用于基础类的构造器肯定在一个衍生类的构造器中调用,而且逐渐向上链接,使每个基础类使用的构造器都能得到调用。之所以要这样做,是由于构造器负有一项特殊任务:检查对象是否得到了正确的构建。一个衍生类只能访问它自己的成员,不能访问基础类的成员(这些成员通常都具有private属性)。只有基础类的构造器在初始化自己的元素时才知道正确的方法以及拥有适当的权限。所以,必须令所有构造器都得到调用,否则整个对象的构建就可能不正确。那正是编译器为什么要强迫对衍生类的每个部分进行构造器调用的原因。在衍生类的构造器主体中,若我们没有明确指定对一个基础类构造器的调用,它就会“默默”地调用默认构造器。如果不存在默认构造器,编译器就会报告一个错误(若某个类没有构造器,编译器会自动组织一个默认构造器)。
用于基础类的构造器肯定在一个衍生类的构造器中调用,而且逐渐向上链接,使每个基础类使用的构造器都能得到调用。之所以要这样做,是由于构造器负有一项特殊任务:检查对象是否得到了正确的构建。一个衍生类只能访问它自己的成员,不能访问基础类的成员(这些成员通常都具有`private`属性)。只有基础类的构造器在初始化自己的元素时才知道正确的方法以及拥有适当的权限。所以,必须令所有构造器都得到调用,否则整个对象的构建就可能不正确。那正是编译器为什么要强迫对衍生类的每个部分进行构造器调用的原因。在衍生类的构造器主体中,若我们没有明确指定对一个基础类构造器的调用,它就会“默默”地调用默认构造器。如果不存在默认构造器,编译器就会报告一个错误(若某个类没有构造器,编译器会自动组织一个默认构造器)。
下面让我们看看一个例子,它展示了按构建顺序进行合成、继承以及多态性的效果:
下面让我们看看一个例子,它展示了按构建顺序进行组合、继承以及多态性的效果:
```
//: Sandwich.java
......@@ -53,7 +53,7 @@ class Sandwich extends PortableLunch {
} ///:~
```
这个例子在其他类的外部创建了一个复杂的类,而且每个类都有一个构造器对自己进行了宣布。其中最重要的类是Sandwich,它反映出了三个级别的继承(若将从Object的默认继承算在内,就是四级)以及三个成员对象。在main()里创建了一个Sandwich对象后,输出结果如下:
这个例子在其他类的外部创建了一个复杂的类,而且每个类都有一个构造器对自己进行了宣布。其中最重要的类是`Sandwich`,它反映出了三个级别的继承(若将从`Object`的默认继承算在内,就是四级)以及三个成员对象。在`main()`里创建了一个`Sandwich`对象后,输出结果如下:
```
Meal()
......@@ -73,11 +73,11 @@ Sandwich()
(3) 调用衍生构造器的主体。
构造器调用的顺序是非常重要的。进行继承时,我们知道关于基础类的一切,并且能访问基础类的任何public和protected成员。这意味着当我们在衍生类的时候,必须能假定基础类的所有成员都是有效的。采用一种标准方法,构建行动已经进行,所以对象所有部分的成员均已得到构建。但在构造器内部,必须保证使用的所有成员都已构建。为达到这个要求,唯一的办法就是首先调用基础类构造器。然后在进入衍生类构造器以后,我们在基础类能够访问的所有成员都已得到初始化。此外,所有成员对象(亦即通过合成方法置于类内的对象)在类内进行定义的时候(比如上例中的b,c和l),由于我们应尽可能地对它们进行初始化,所以也应保证构造器内部的所有成员均为有效。若坚持按这一规则行事,会有助于我们确定所有基础类成员以及当前对象的成员对象均已获得正确的初始化。但不幸的是,这种做法并不适用于所有情况,这将在下一节具体说明。
构造器调用的顺序是非常重要的。进行继承时,我们知道关于基础类的一切,并且能访问基础类的任何`public``protected`成员。这意味着当我们在衍生类的时候,必须能假定基础类的所有成员都是有效的。采用一种标准方法,构建行动已经进行,所以对象所有部分的成员均已得到构建。但在构造器内部,必须保证使用的所有成员都已构建。为达到这个要求,唯一的办法就是首先调用基础类构造器。然后在进入衍生类构造器以后,我们在基础类能够访问的所有成员都已得到初始化。此外,所有成员对象(亦即通过组合方法置于类内的对象)在类内进行定义的时候(比如上例中的`b``c``l`),由于我们应尽可能地对它们进行初始化,所以也应保证构造器内部的所有成员均为有效。若坚持按这一规则行事,会有助于我们确定所有基础类成员以及当前对象的成员对象均已获得正确的初始化。但不幸的是,这种做法并不适用于所有情况,这将在下一节具体说明。
7.7.2 继承和finalize()
7.7.2 继承和`finalize()`
通过“合成”方法创建新类时,永远不必担心对那个类的成员对象的收尾工作。每个成员都是一个独立的对象,所以会得到正常的垃圾收集以及收尾处理——无论它是不是不自己某个类一个成员。但在进行初始化的时候,必须覆盖衍生类中的finalize()方法——如果已经设计了某个特殊的清除进程,要求它必须作为垃圾收集的一部分进行。覆盖衍生类的finalize()时,务必记住调用finalize()的基础类版本。否则,基础类的初始化根本不会发生。下面这个例子便是明证:
通过“组合”方法创建新类时,永远不必担心对那个类的成员对象的收尾工作。每个成员都是一个独立的对象,所以会得到正常的垃圾收集以及收尾处理——无论它是不是不自己某个类一个成员。但在进行初始化的时候,必须覆盖衍生类中的`finalize()`方法——如果已经设计了某个特殊的清除进程,要求它必须作为垃圾收集的一部分进行。覆盖衍生类的`finalize()`时,务必记住调用`finalize()`的基础类版本。否则,基础类的初始化根本不会发生。下面这个例子便是明证:
```
//: Frog.java
......@@ -173,12 +173,12 @@ public class Frog extends Amphibian {
} ///:~
```
DoBasefinalization类只是简单地容纳了一个标志,向分级结构中的每个类指出是否应调用super.finalize()。这个标志的设置建立在命令行参数的基础上,所以能够在进行和不进行基础类收尾工作的前提下查看行为。
分级结构中的每个类也包含了Characteristic类的一个成员对象。大家可以看到,无论是否调用了基础类收尾模块,Characteristic成员对象都肯定会得到收尾(清除)处理。
`DoBasefinalization`类只是简单地容纳了一个标志,向分级结构中的每个类指出是否应调用`super.finalize()`。这个标志的设置建立在命令行参数的基础上,所以能够在进行和不进行基础类收尾工作的前提下查看行为。
分级结构中的每个类也包含了`Characteristic`类的一个成员对象。大家可以看到,无论是否调用了基础类收尾模块,`Characteristi`c成员对象都肯定会得到收尾(清除)处理。
每个被覆盖的finalize()至少要拥有对protected成员的访问权力,因为Object类中的finalize()方法具有protected属性,而编译器不允许我们在继承过程中消除访问权限(“友好的”比“受到保护的”具有更小的访问权限)。
每个被覆盖的`finalize()`至少要拥有对`protected`成员的访问权力,因为`Object`类中的`finalize()`方法具有`protected`属性,而编译器不允许我们在继承过程中消除访问权限(“友好的”比“受到保护的”具有更小的访问权限)。
Frog.main()中,DoBaseFinalization标志会得到配置,而且会创建单独一个Frog对象。请记住垃圾收集(特别是收尾工作)可能不会针对任何特定的对象发生,所以为了强制采取这一行动,System.runFinalizersOnExit(true)添加了额外的开销,以保证收尾工作的正常进行。若没有基础类初始化,则输出结果是:
`Frog.main()`中,`DoBaseFinalization`标志会得到配置,而且会创建单独一个`Frog`对象。请记住垃圾收集(特别是收尾工作)可能不会针对任何特定的对象发生,所以为了强制采取这一行动,`System.runFinalizersOnExit(true)`添加了额外的开销,以保证收尾工作的正常进行。若没有基础类初始化,则输出结果是:
```
not finalizing bases
......@@ -196,7 +196,7 @@ finalizing Characteristic has heart
finalizing Characteristic can live in water
```
从中可以看出确实没有为基础类Frog调用收尾模块。但假如在命令行加入“finalize”参数,则会获得下述结果:
从中可以看出确实没有为基础类·调用收尾模块。但假如在命令行加入`finalize`参数,则会获得下述结果:
```
Creating Characteristic is alive
......@@ -261,7 +261,7 @@ public class PolyConstructors {
} ///:~
```
Glyph中,draw()方法是“抽象的”(abstract),所以它可以被其他方法覆盖。事实上,我们在RoundGlyph中不得不对其进行覆盖。但Glyph构造器会调用这个方法,而且调用会在RoundGlyph.draw()中止,这看起来似乎是有意的。但请看看输出结果:
`Glyph`中,`draw()`方法是“抽象的”(`abstract`),所以它可以被其他方法覆盖。事实上,我们在`RoundGlyph`中不得不对其进行覆盖。但`Glyph`构造器会调用这个方法,而且调用会在`RoundGlyph.draw()`中止,这看起来似乎是有意的。但请看看输出结果:
```
Glyph() before draw()
......@@ -270,19 +270,19 @@ Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
```
Glyph的构造器调用draw()时,radius的值甚至不是默认的初始值1,而是0。这可能是由于一个点号或者屏幕上根本什么都没有画而造成的。这样就不得不开始查找程序中的错误,试着找出程序不能工作的原因。
`Glyph`的构造器调用`draw()`时,`radius`的值甚至不是默认的初始值1,而是0。这可能是由于一个点号或者屏幕上根本什么都没有画而造成的。这样就不得不开始查找程序中的错误,试着找出程序不能工作的原因。
前一节讲述的初始化顺序并不十分完整,而那是解决问题的关键所在。初始化的实际过程是这样的:
(1) 在采取其他任何操作之前,为对象分配的存储空间初始化成二进制零。
(2) 就象前面叙述的那样,调用基础类构造器。此时,被覆盖的draw()方法会得到调用(的确是在RoundGlyph构造器调用之前),此时会发现radius的值为0,这是由于步骤(1)造成的。
(2) 就象前面叙述的那样,调用基础类构造器。此时,被覆盖的`draw()`方法会得到调用(的确是在`RoundGlyph`构造器调用之前),此时会发现`radius`的值为0,这是由于步骤(1)造成的。
(3) 按照原先声明的顺序调用成员初始化代码。
(4) 调用衍生类构造器的主体。
采取这些操作要求有一个前提,那就是所有东西都至少要初始化成零(或者某些特殊数据类型与“零”等价的值),而不是仅仅留作垃圾。其中包括通过“合成”技术嵌入一个类内部的对象引用。如果假若忘记初始化那个引用,就会在运行期间出现异常事件。其他所有东西都会变成零,这在观看结果时通常是一个严重的警告信号。
采取这些操作要求有一个前提,那就是所有东西都至少要初始化成零(或者某些特殊数据类型与“零”等价的值),而不是仅仅留作垃圾。其中包括通过“组合”技术嵌入一个类内部的对象引用。如果假若忘记初始化那个引用,就会在运行期间出现异常事件。其他所有东西都会变成零,这在观看结果时通常是一个严重的警告信号。
在另一方面,应对这个程序的结果提高警惕。从逻辑的角度说,我们似乎已进行了无懈可击的设计,所以它的错误行为令人非常不可思议。而且没有从编译器那里收到任何报错信息(C++在这种情况下会表现出更合理的行为)。象这样的错误会很轻易地被人忽略,而且要花很长的时间才能找出。
因此,设计构造器时一个特别有效的规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构造器内唯一能够安全调用的是在基础类中具有final属性的那些方法(也适用于private方法,它们自动具有final属性)。这些方法不能被覆盖,所以不会出现上述潜在的问题。
因此,设计构造器时一个特别有效的规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构造器内唯一能够安全调用的是在基础类中具有`final`属性的那些方法(也适用于`private`方法,它们自动具有`final`属性)。这些方法不能被覆盖,所以不会出现上述潜在的问题。
......@@ -3,7 +3,7 @@
学习了多态性的知识后,由于多态性是如此“聪明”的一种工具,所以看起来似乎所有东西都应该继承。但假如过度使用继承技术,也会使自己的设计变得不必要地复杂起来。事实上,当我们以一个现成类为基础建立一个新类时,如首先选择继承,会使情况变得异常复杂。
一个更好的思路是首先选择“合成”——如果不能十分确定自己应使用哪一个。合成不会强迫我们的程序设计进入继承的分级结构中。同时,合成显得更加灵活,因为可以动态选择一种类型(以及行为),而继承要求在编译期间准确地知道一种类型。下面这个例子对此进行了阐释:
一个更好的思路是首先选择“组合”——如果不能十分确定自己应使用哪一个。组合不会强迫我们的程序设计进入继承的分级结构中。同时,组合显得更加灵活,因为可以动态选择一种类型(以及行为),而继承要求在编译期间准确地知道一种类型。下面这个例子对此进行了阐释:
```
//: Transmogrify.java
......@@ -15,13 +15,13 @@ interface Actor {
}
class HappyActor implements Actor {
public void act() {
System.out.println("HappyActor");
public void act() {
System.out.println("HappyActor");
}
}
class SadActor implements Actor {
public void act() {
public void act() {
System.out.println("SadActor");
}
}
......@@ -42,9 +42,9 @@ public class Transmogrify {
} ///:~
```
在这里,一个Stage对象包含了指向一个Actor的引用,后者被初始化成一个HappyActor对象。这意味着go()会产生特定的行为。但由于引用在运行期间可以重新与一个不同的对象绑定或结合起来,所以SadActor对象的引用可在a中得到替换,然后由go()产生的行为发生改变。这样一来,我们在运行期间就获得了很大的灵活性。与此相反,我们不能在运行期间换用不同的形式来进行继承;它要求在编译期间完全决定下来。
在这里,一个`Stage`对象包含了指向一个`Actor`的引用,后者被初始化成一个`HappyActor`对象。这意味着`go()`会产生特定的行为。但由于引用在运行期间可以重新与一个不同的对象绑定或结合起来,所以`SadActor`对象的引用可在a中得到替换,然后由`go()`产生的行为发生改变。这样一来,我们在运行期间就获得了很大的灵活性。与此相反,我们不能在运行期间换用不同的形式来进行继承;它要求在编译期间完全决定下来。
一条常规的设计准则是:用继承表达行为间的差异,并用成员变量表达状态的变化。在上述例子中,两者都得到了应用:继承了两个不同的类,用于表达act()方法的差异;而Stage通过合成技术允许它自己的状态发生变化。在这种情况下,那种状态的改变同时也产生了行为的变化。
一条常规的设计准则是:用继承表达行为间的差异,并用成员变量表达状态的变化。在上述例子中,两者都得到了应用:继承了两个不同的类,用于表达`act()`方法的差异;而`Stage`通过组合技术允许它自己的状态发生变化。在这种情况下,那种状态的改变同时也产生了行为的变化。
7.8.1 纯继承与扩展
......@@ -77,7 +77,7 @@ public class Transmogrify {
为解决这个问题,必须有一种办法能够保证向下转换正确进行。只有这样,我们才不会冒然转换成一种错误的类型,然后发出一条对象不可能收到的消息。这样做是非常不安全的。
在某些语言中(如C++),为了进行保证“类型安全”的向下转换,必须采取特殊的操作。但在Java中,所有转换都会自动得到检查和核实!所以即使我们只是进行一次普通的括弧转换,进入运行期以后,仍然会毫无留情地对这个转换进行检查,保证它的确是我们希望的那种类型。如果不是,就会得到一个ClassCastException(类转换异常)。在运行期间对类型进行检查的行为叫作“运行期类型标识”(RTTI)。下面这个例子向大家演示了RTTI的行为:
在某些语言中(如C++),为了进行保证“类型安全”的向下转换,必须采取特殊的操作。但在Java中,所有转换都会自动得到检查和核实!所以即使我们只是进行一次普通的括弧转换,进入运行期以后,仍然会毫无留情地对这个转换进行检查,保证它的确是我们希望的那种类型。如果不是,就会得到一个`ClassCastException`(类转换异常)。在运行期间对类型进行检查的行为叫作“运行期类型标识”(RTTI)。下面这个例子向大家演示了RTTI的行为:
```
//: RTTI.java
......@@ -114,8 +114,8 @@ public class RTTI {
} ///:~
```
和在示意图中一样,MoreUseful(更有用的)对Useful(有用的)的接口进行了扩展。但由于它是继承来的,所以也能向上转换到一个Useful。我们可看到这会在对数组x(位于main()中)进行初始化的时候发生。由于数组中的两个对象都属于Useful类,所以可将f()和g()方法同时发给它们两个。而且假如试图调用u()(它只存在于MoreUseful),就会收到一条编译期出错提示。
和在示意图中一样,`MoreUseful`(更有用的)对`Useful`(有用的)的接口进行了扩展。但由于它是继承来的,所以也能向上转换到一个`Useful`。我们可看到这会在对数组`x`(位于`main()`中)进行初始化的时候发生。由于数组中的两个对象都属于`Useful`类,所以可将`f()``g()`方法同时发给它们两个。而且假如试图调用`u()`(它只存在于`MoreUseful`),就会收到一条编译期出错提示。
若想访问一个MoreUseful对象的扩展接口,可试着进行向下转换。如果它是正确的类型,这一行动就会成功。否则,就会得到一个ClassCastException。我们不必为这个异常编写任何特殊的代码,因为它指出的是一个可能在程序中任何地方发生的一个编程错误。
若想访问一个`MoreUseful`对象的扩展接口,可试着进行向下转换。如果它是正确的类型,这一行动就会成功。否则,就会得到一个`ClassCastException`。我们不必为这个异常编写任何特殊的代码,因为它指出的是一个可能在程序中任何地方发生的一个编程错误。
RTTI的意义远不仅仅反映在转换处理上。例如,在试图向下转换之前,可通过一种方法了解自己处理的是什么类型。整个第11章都在讲述Java运行期类型标识的方方面面。
......@@ -3,7 +3,6 @@
“多态性”意味着“不同的形式”。在面向对象的程序设计中,我们有相同的外观(基础类的通用接口)以及使用那个外观的不同形式:动态绑定或组织的、不同版本的方法。
通过这一章的学习,大家已知道假如不利用数据抽象以及继承技术,就不可能理解、甚至去创建多态性的一个例子。多态性是一种不可独立应用的特性(就象一个switch语句),只可与其他元素协同使用。我们应将其作为类总体关系的一部分来看待。人们经常混淆Java其他的、非面向对象的特性,比如方法重载等,这些特性有时也具有面向对象的某些特征。但不要被愚弄:如果以后没有绑定,就不成其为多态性。
通过这一章的学习,大家已知道假如不利用数据抽象以及继承技术,就不可能理解、甚至去创建多态性的一个例子。多态性是一种不可独立应用的特性(就象一个`switch`语句),只可与其他元素协同使用。我们应将其作为类总体关系的一部分来看待。人们经常混淆Java其他的、非面向对象的特性,比如方法重载等,这些特性有时也具有面向对象的某些特征。但不要被愚弄:如果以后没有绑定,就不成其为多态性。
为使用多态性乃至面向对象的技术,特别是在自己的程序中,必须将自己的编程视野扩展到不仅包括单独一个类的成员和消息,也要包括类与类之间的一致性以及它们的关系。尽管这要求学习时付出更多的精力,但却是非常值得的,因为只有这样才可真正有效地加快自己的编程速度、更好地组织代码、更容易做出包容面广的程序以及更易对自己的代码进行维护与扩展。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册