《Java编程思想第4版[中文版](PDF格式)》第29章


一来,那些代码就可与类型信息分开。所以更易编写,也更易理解。此外,若通过继承增添了一种新类型, 
如“三角形”,那么我们为“几何形状”新类型编写的代码会象在旧类型里一样良好地工作。所以说程序具 
备了“扩展能力”,具有“扩展性”。
以上面的例子为基础,假设我们用Java 写了这样一个函数:
void doStuff(Shape s) {
s。erase();
// 。。。
s。draw();
31 
…………………………………………………………Page 33……………………………………………………………

这个函数可与任何“几何形状”(Shape)通信,所以完全独立于它要描绘(draw)和删除(erase)的任何 
特定类型的对象。如果我们在其他一些程序里使用 doStuff()函数:
Circle c = new Circle();
Triangle t = new Triangle();
Line l = new Line();
doStuff(c);
doStuff(t);
doStuff(l);
那么对 doStuff()的调用会自动良好地工作,无论对象的具体类型是什么。
这实际是一个非常有用的编程技巧。请考虑下面这行代码:
doStuff(c);
此时,一个 Circle (圆)句柄传递给一个本来期待Shape (形状)句柄的函数。由于圆是一种几何形状,所 
以doStuff()能正确地进行处理。也就是说,凡是 doStuff()能发给一个 Shape 的消息,Circle 也能接收。 
所以这样做是安全的,不会造成错误。
我们将这种把衍生类型当作它的基本类型处理的过程叫作“Upcasting”(上溯造型)。其中,“cast”(造 
型)是指根据一个现成的模型创建;而“Up”(向上)表明继承的方向是从“上面”来的——即基础类位于 
顶部,而衍生类在下方展开。所以,根据基础类进行造型就是一个从上面继承的过程,即“Upcasting”。
在面向对象的程序里,通常都要用到上溯造型技术。这是避免去调查准确类型的一个好办法。请看看 
doStuff()里的代码:
s。erase();
// 。。。
s。draw();
注意它并未这样表达:“如果你是一个Circle,就这样做;如果你是一个Square,就那样做;等等”。若那 
样编写代码,就需检查一个Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加 
了一种新的 Shape 类型后,都要相应地进行修改。在这儿,我们只需说:“你是一种几何形状,我知道你能 
将自己删掉,即erase();请自己采取那个行动,并自己去控制所有的细节吧。”
1。6。1 动态绑定
在 doStuff()的代码里,最让人吃惊的是尽管我们没作出任何特殊指示,采取的操作也是完全正确和恰当 
的。我们知道,为Circle 调用draw()时执行的代码与为一个 Square 或 Line 调用draw()时执行的代码是不 
同的。但在将draw()消息发给一个匿名 Shape 时,根据 Shape 句柄当时连接的实际类型,会相应地采取正确 
的操作。这当然令人惊讶,因为当 Java 编译器为doStuff()编译代码时,它并不知道自己要操作的准确类型 
是什么。尽管我们确实可以保证最终会为Shape 调用 erase(),为Shape 调用draw(),但并不能保证为特定 
的Circle,Square 或者Line 调用什么。然而最后采取的操作同样是正确的,这是怎么做到的呢?
将一条消息发给对象时,如果并不知道对方的具体类型是什么,但采取的行动同样是正确的,这种情况就叫 
作“多形性”(Polymorphism)。对面向对象的程序设计语言来说,它们用以实现多形性的方法叫作“动态 
绑定”。编译器和运行期系统会负责对所有细节的控制;我们只需知道会发生什么事情,而且更重要的是, 
如何利用它帮助自己设计程序。
有些语言要求我们用一个特殊的关键字来允许动态绑定。在C++中,这个关键字是 virtual。在Java 中,我 
们则完全不必记住添加一个关键字,因为函数的动态绑定是自动进行的。所以在将一条消息发给对象时,我 
们完全可以肯定对象会采取正确的行动,即使其中涉及上溯造型之类的处理。
1。6。2 抽象的基础类和接口
设计程序时,我们经常都希望基础类只为自己的衍生类提供一个接口。也就是说,我们不想其他任何人实际 
创建基础类的一个对象,只对上溯造型成它,以便使用它们的接口。为达到这个目的,需要把那个类变成 
32 
…………………………………………………………Page 34……………………………………………………………
“抽象”的——使用abstract 关键字。若有人试图创建抽象类的一个对象,编译器就会阻止他们。这种工具 
可有效强制实行一种特殊的设计。
亦可用abstract 关键字描述一个尚未实现的方法——作为一个“根”使用,指出:“这是适用于从这个类继 
承的所有类型的一个接口函数,但目前尚没有对它进行任何形式的实现。”抽象方法也许只能在一个抽象类 
里创建。继承了一个类后,那个方法就必须实现,否则继承的类也会变成“抽象”类。通过创建一个抽象方 
法,我们可以将一个方法置入接口中,不必再为那个方法提供可能毫无意义的主体代码。
interface (接口)关键字将抽象类的概念更延伸了一步,它完全禁止了所有的函数定义。“接口”是一种相 
当有效和常用的工具。另外如果自己愿意,亦可将多个接口都合并到一起(不能从多个普通class 或 
abstract class 中继承)。
1。7 对象的创建和存在时间
从技术角度说,OOP (面向对象程序设计)只是涉及抽象的数据类型、继承以及多形性,但另一些问题也可能 
显得非常重要。本节将就这些问题进行探讨。
最重要的问题之一是对象的创建及破坏方式。对象需要的数据位于哪儿,如何控制对象的“存在时间”呢? 
针对这个问题,解决的方案是各异其趣的。C++认为程序的执行效率是最重要的一个问题,所以它允许程序员 
作出选择。为获得最快的运行速度,存储以及存在时间可在编写程序时决定,只需将对象放置在堆栈(有时 
也叫作自动或定域变量)或者静态存储区域即可。这样便为存储空间的分配和释放提供了一个优先级。某些 
情况下,这种优先级的控制是非常有价值的。然而,我们同时也牺牲了灵活性,因为在编写程序时,必须知 
道对象的准确的数量、存在时间、以及类型。如果要解决的是一个较常规的问题,如计算机辅助设计、仓储 
管理或者空中交通控制,这一方法就显得太局限了。
第二个方法是在一个内存池中动态创建对象,该内存池亦叫“堆”或者“内存堆”。若采用这种方式,除非 
进入运行期,否则根本不知道到底需要多少个对象,也不知道它们的存在时间有多长,以及准确的类型是什 
么。这些参数都在程序正式运行时才决定的。若需一个新对象,只需在需要它的时候在内存堆里简单地创建 
它即可。由于存储空间的管理是运行期间动态进行的,所以在内存堆里分配存储空间的时间比在堆栈里创建 
的时间长得多(在堆栈里创建存储空间一般只需要一个简单的指令,将堆栈指针向下或向下移动即可)。由 
于动态创建方法使对象本来就倾向于复杂,所以查找存储空间以及释放它所需的额外开销不会为对象的创建 
造成明显的影响。除此以外,更大的灵活性对于常规编程问题的解决是至关重要的。
C++允许我们决定是在写程序时创建对象,还是在运行期间创建,这种控制方法更加灵活。大家或许认为既然 
它如此灵活,那么无论如何都应在内存堆里创建对象,而不是在堆栈中创建。但还要考虑另外一个问题,亦 
即对象的“存在时间”或者“生存时间”(Lifetime )。若在堆栈或者静态存储空间里创建一个对象,编译 
器会判断?
小说推荐
返回首页返回目录