Java编程思想第4版[中文版](PDF格式)-第58部分
按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!
}
class Amphibian extends Animal {
Characteristic p =
new Characteristic(〃can live in water〃);
Amphibian() {
System。out。println(〃Amphibian()〃);
}
protected void finalize() {
200
…………………………………………………………Page 202……………………………………………………………
System。out。println(〃Amphibian finalize〃);
if(DoBaseFinalization。flag)
try {
super。finalize();
} catch(Throwable t) {}
}
}
public class Frog extends Amphibian {
Frog() {
System。out。println(〃Frog()〃);
}
protected void finalize() {
System。out。println(〃Frog finalize〃);
if(DoBaseFinalization。flag)
try {
super。finalize();
} catch(Throwable t) {}
}
public static void main(String'' args) {
if(args。length != 0 &&
args'0'。equals(〃finalize〃))
DoBaseFinalization。flag = true;
else
System。out。println(〃not finalizing bases〃);
new Frog(); // Instantly bees garbage
System。out。println(〃bye!〃);
// Must do this to guarantee that all
// finalizers will be called:
System。runFinalizersOnExit(true);
}
} ///:~
DoBasefinalization 类只是简单地容纳了一个标志,向分级结构中的每个类指出是否应调用
super。finalize()。这个标志的设置建立在命令行参数的基础上,所以能够在进行和不进行基础类收尾工作
的前提下查看行为。
分级结构中的每个类也包含了 Characteristic 类的一个成员对象。大家可以看到,无论是否调用了基础类收
尾模块,Characteristic 成员对象都肯定会得到收尾(清除)处理。
每个被覆盖的finalize()至少要拥有对 protected 成员的访问权力,因为 Object 类中的finalize()方法具
有protected 属性,而编译器不允许我们在继承过程中消除访问权限(“友好的”比“受到保护的”具有更
小的访问权限)。
在Frog。main()中,DoBaseFinalization 标志会得到配置,而且会创建单独一个Frog 对象。请记住垃圾收集
(特别是收尾工作)可能不会针对任何特定的对象发生,所以为了强制采取这一行动,
System。runFinalizersOnExit(true)添加了额外的开销,以保证收尾工作的正常进行。若没有基础类初始
化,则输出结果是:
not finalizing bases
Creating Characteristic is alive
LivingCreature()
Creating Characteristic has heart
Animal()
Creating Characteristic can live in water
201
…………………………………………………………Page 203……………………………………………………………
Amphibian()
Frog()
bye!
Frog finalize
finalizing Characteristic is alive
finalizing Characteristic has heart
finalizing Characteristic can live in water
从中可以看出确实没有为基础类Frog 调用收尾模块。但假如在命令行加入“finalize”自变量,则会获得下
述结果:
Creating Characteristic is alive
LivingCreature()
Creating Characteristic has heart
Animal()
Creating Characteristic can live in water
Amphibian()
Frog()
bye!
Frog finalize
Amphibian finalize
Animal finalize
LivingCreature finalize
finalizing Characteristic is alive
finalizing Characteristic has heart
finalizing Characteristic can live in water
尽管成员对象按照与它们创建时相同的顺序进行收尾,但从技术角度说,并没有指定对象收尾的顺序。但对
于基础类,我们可对收尾的顺序进行控制。采用的最佳顺序正是在这里采用的顺序,它与初始化顺序正好相
反。按照与 C++中用于“破坏器”相同的形式,我们应该首先执行衍生类的收尾,再是基础类的收尾。这是
由于衍生类的收尾可能调用基础类中相同的方法,要求基础类组件仍然处于活动状态。因此,必须提前将它
们清除(破坏)。
7。7。3 构建器内部的多形性方法的行为
构建器调用的分级结构(顺序)为我们带来了一个有趣的问题,或者说让我们进入了一种进退两难的局面。
若当前位于一个构建器的内部,同时调用准备构建的那个对象的一个动态绑定方法,那么会出现什么情况
呢?在原始的方法内部,我们完全可以想象会发生什么——动态绑定的调用会在运行期间进行解析,因为对
象不知道它到底从属于方法所在的那个类,还是从属于从它衍生出来的某些类。为保持一致性,大家也许会
认为这应该在构建器内部发生。
但实际情况并非完全如此。若调用构建器内部一个动态绑定的方法,会使用那个方法被覆盖的定义。然而,
产生的效果可能并不如我们所愿,而且可能造成一些难于发现的程序错误。
从概念上讲,构建器的职责是让对象实际进入存在状态。在任何构建器内部,整个对象可能只是得到部分组
织——我们只知道基础类对象已得到初始化,但却不知道哪些类已经继承。然而,一个动态绑定的方法调用
却会在分级结构里“向前”或者“向外”前进。它调用位于衍生类里的一个方法。如果在构建器内部做这件
事情,那么对于调用的方法,它要操纵的成员可能尚未得到正确的初始化——这显然不是我们所希望的。
通过观察下面这个例子,这个问题便会昭然若揭:
//: PolyConstructors。java
// Constructors and polymorphism
// don't produce what you might expect。
abstract class Glyph {
202
…………………………………………………………Page 204……………………………………………………………
abstract void draw();
Glyph() {
System。out。println(〃Glyph() before draw()〃);
draw();
System。out。println(〃Glyph() after draw()〃);
}
}
class RoundGlyph extends Glyph {
int radius = 1;
RoundGlyph(int r) {
radius = r;
System。out。println(
〃RoundGlyph。RoundGlyph(); radius = 〃
+ radius);
}
void draw() {
System。out。println(
〃RoundGlyph。draw(); radius = 〃 + radius);
}
}
public class PolyConstructors {
public static void main(String'' args) {
new RoundGlyph(5);
}
} ///:~
在Glyph 中,draw()方法是“抽象的”(abstract ),所以它可以被其他方法覆盖。事实上,我们在
RoundGlyph 中不得不对其进行覆盖。但Glyph 构建器会调用这个方法,而且调用会在RoundGlyph。draw()中
止,这看起来似乎是有意的。但请看看输出结果:
Glyph() before draw()
RoundGlyph。draw(); radius = 0
Glyph() after draw()
RoundGlyph。RoundGlyph(); radius = 5
当Glyph 的构建器调用draw()时,radius 的值甚至不是默认的初始值1,而是 0。这可能是由于一个点号或
者屏幕上根本什么都没有画而造成的。这样就不得不开始查找程序中的错误,试着找出程序不能工作的原
因。
前一节讲述的初始化顺序并不十分完整,而那是解决问题的关键所在。初始化的实际过程是这样的:
(1) 在采取其他任何操作之前,为对象分配的存储空间初始化成二进制零。
(2) 就象前面叙述的那样,调用基础类构建器。此时,被覆盖的draw()方法会得到调用(的确是在
RoundGlyph 构建器调用之前),此时会发现 radius 的值为 0,这是由于步骤(1)造成的。
(3) 按照原先声明的顺序调用成员初始化代码。
(4) 调用衍生类构建器的主体。
采取这些操作要求有一个前提,那就是所有东西都至少要初始化成零(或者某些特殊数据类型与“零”等价
的值),而不是仅仅留作垃圾。其中包括通过“合成”技术嵌入一个类内部的对象句柄。如果假若忘记初始
化那个句柄,就会在运行期间出现违例事件。其他所有东西都会变成零,这在观看结果时通常是一个严重的
警告信号。
在另一方面,应对这个程序的结果提高警惕。从逻辑的角度说,我们似乎已进行了无懈可击的设计,所以它
203
…………………………………………………………Page 205……………………………………………………………
的错误行为令人非常不可思议。而且没有从编译器那里收到任何报错信息(C++在这种情况下会表现出更合理
的行为)。象这样的错误会很轻易地被人忽略,而且要花很长的时间才能找出。
因此,设计构建器时一个特别有效的规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调
用任何方法。在构建器内唯一能够安全调用的是在基础类中具有final 属性的那些方法(也适用于private
方法,它们自动具有final 属性)。这些方法不能被覆盖,所以不会出现上述潜在的问题。
7。8 通过继承进行设计
学习了多形性的知识后,由于多形性是如此“聪明”的一种工具,所以看起来似乎所有东西都应该继承。但
假如过度使用继承技术,也会使自己的设计变得不必要地复杂起来。事实上,当我们以一个现成类为基础建
立一个新类时,如首先选择继承,会使情况变得异常复杂。
一个更好的思路是首先选择“合成”——如果不能十分确定自己应使用哪一个。合成不会强迫我们的程序设
计进入继承的分级结构中。同时,合成显得更加灵活,因为可以动态选择一种类型(以及行为),而继承要
求在编译期间准确地知道一种类型。下面这个例子对此进行了阐释:
//: Transmogrify。java
// Dynamically changing the behavior of
// an object via position。
interface Actor {
void act();
}
class HappyActor implements Actor {
public void act() {
System。out。println(〃HappyActor〃);
}
}
class SadActor implements Actor {
public void act() {
System。out。println(〃SadActor〃);
}
}
class Stage {
Actor a = new HappyActor();
void change() { a = new SadActor(); }
void go() { a。act(); }
}
public class Transmogrify {
public static void main(String'' args) {
Stage s = new Stage();
s。go(); // Prints 〃HappyActor〃
s。change();
s。go(); // Prints 〃SadActor〃
}
} ///:~
在这里,一个Stage 对象包含了指向一个Actor 的句柄,后者被初始化成一个 HappyActor 对象。这意味着
go()会产生特定的行为。但由于句柄在运行期间可以重新与一个不同的对象绑定或结合起来,所以SadActor
对象的句柄可在a 中得到替换,然后由go()产生的行为发生改变。这样一来,我们在运行期间就获得了很大
204
…………………………………………………………Page 206……………………………………………………………
的灵活性。与此相反,我们不能在运行期间换用不同的形式来进行继承;它要求在编译期间完全决定下来。
一条常规的设计准则是:用继承表达行为间的差异,并用成员变量表达状态的变化。在上述例子中,两者都
得到了应用:继承了两个不同的类,用于表达 act()方法的差异;而 Stage 通过合成技术允许它自己的状态
发生变化。在这种情况下,那种状态的改变同时也产生了行为的变化。
7。8。1 纯继承与扩展
学习继承时,为了创建继承分级结构,看来最明显的方法是采取一种“纯粹”的手段。也就是说,只有在基
础类或“接口”中已建立的方法才可在衍生类中被覆盖,如下面这张图所示: