按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!
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 纯继承与扩展
学习继承时,为了创建继承分级结构,看来最明显的方法是采取一种“纯粹”的手段。也就是说,只有在基
础类或“接口”中已建立的方法才可在衍生类中被覆盖,如下面这张图所示:
可将其描述成一种纯粹的“属于”关系,因为一个类的接口已规定了它到底“是什么”或者“属于什么”。
通过继承,可保证所有衍生类都只拥有基础类的接口。如果按上述示意图操作,衍生出来的类除了基础类的
接口之外,也不会再拥有其他什么。
可将其想象成一种“纯替换”,因为衍生类对象可为基础类完美地替换掉。使用它们的时候,我们根本没必
要知道与子类有关的任何额外信息。如下所示:
也就是说,基础类可接收我们发给衍生类的任何消息,因为两者拥有完全一致的接口。我们要做的全部事情
就是从衍生上溯造型,而且永远不需要回过头来检查对象的准确类型是什么。所有细节都已通过多形性获得
了完美的控制。
若按这种思路考虑问题,那么一个纯粹的“属于”关系似乎是唯一明智的设计方法,其他任何设计方法都会
导致混乱不清的思路,而且在定义上存在很大的困难。但这种想法又属于另一个极端。经过细致的研究,我
们发现扩展接口对于一些特定问题来说是特别有效的方案。可将其称为“类似于”关系,因为扩展后的衍生
类“类似于”基础类——它们有相同的基础接口——但它增加了一些特性,要求用额外的方法加以实现。如
下所示:
205
…………………………………………………………Page 207……………………………………………………………
尽管这是一种有用和明智的做法(由具体的环境决定),但它也有一个缺点:衍生类中对接口扩展的那一部
分不可在基础类中使用。所以一旦上溯造型,就不可再调用新方法:
若在此时不进行上溯造型,则不会出现此类问题。但在许多情况下,都需要重新核实对象的准确类型,使自
己能访问那个类型的扩展方法。在后面的小节里,我们具体讲述了这是如何实现的。
7。8。2 下溯造型与运行期类型标识
由于我们在上溯造型(在继承结构中向上移动)期间丢失了具体的类型信息,所以为了获取具体的类型信
息——亦即在分级结构中向下移动——我们必须使用 “下溯造型”技术。然而,我们知道一个上溯造型肯定
是安全的;基础类不可能再拥有一个比衍生类更大的接口。因此,我们通过基础类接口发送的每