友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
Java编程思想第4版[中文版](PDF格式)-第16部分
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!
的消息后,必须有一个“方法”能够执行。若只是简单地继承一个类,并不做其他任何事情,来自基础类接
口的方法就会直接照搬到衍生类。这意味着衍生类的对象不仅有相同的类型,也有同样的行为,这一后果通
常是我们不愿见到的。
有两种做法可将新得的衍生类与原来的基础类区分开。第一种做法十分简单:为衍生类添加新函数(功
能)。这些新函数并非基础类接口的一部分。进行这种处理时,一般都是意识到基础类不能满足我们的要
求,所以需要添加更多的函数。这是一种最简单、最基本的继承用法,大多数时候都可完美地解决我们的问
题。然而,事先还是要仔细调查自己的基础类是否真的需要这些额外的函数。
1。5。1 改善基础类
尽管 extends 关键字暗示着我们要为接口“扩展”新功能,但实情并非肯定如此。为区分我们的新类,第二
个办法是改变基础类一个现有函数的行为。我们将其称作“改善”那个函数。
为改善一个函数,只需为衍生类的函数建立一个新定义即可。我们的目标是:“尽管使用的函数接口未变,
但它的新版本具有不同的表现”。
30
…………………………………………………………Page 32……………………………………………………………
1。5。2 等价与类似关系
针对继承可能会产生这样的一个争论:继承只能改善原基础类的函数吗?若答案是肯定的,则衍生类型就是
与基础类完全相同的类型,因为都拥有完全相同的接口。这样造成的结果就是:我们完全能够将衍生类的一
个对象换成基础类的一个对象!可将其想象成一种“纯替换”。在某种意义上,这是进行继承的一种理想方
式。此时,我们通常认为基础类和衍生类之间存在一种“等价”关系——因为我们可以理直气壮地说:“圆
就是一种几何形状”。为了对继承进行测试,一个办法就是看看自己是否能把它们套入这种“等价”关系
中,看看是否有意义。
但在许多时候,我们必须为衍生类型加入新的接口元素。所以不仅扩展了接口,也创建了一种新类型。这种
新类型仍可替换成基础类型,但这种替换并不是完美的,因为不可在基础类里访问新函数。我们将其称作
“类似”关系;新类型拥有旧类型的接口,但也包含了其他函数,所以不能说它们是完全等价的。举个例子
来说,让我们考虑一下制冷机的情况。假定我们的房间连好了用于制冷的各种控制器;也就是说,我们已拥
有必要的“接口”来控制制冷。现在假设机器出了故障,我们把它换成一台新型的冷、热两用空调,冬天和
夏天均可使用。冷、热空调“类似”制冷机,但能做更多的事情。由于我们的房间只安装了控制制冷的设
备,所以它们只限于同新机器的制冷部分打交道。新机器的接口已得到了扩展,但现有的系统并不知道除原
始接口以外的任何东西。
认识了等价与类似的区别后,再进行替换时就会有把握得多。尽管大多数时候“纯替换”已经足够,但您会
发现在某些情况下,仍然有明显的理由需要在衍生类的基础上增添新功能。通过前面对这两种情况的讨论,
相信大家已心中有数该如何做。
1。6 多形对象的互换使用
通常,继承最终会以创建一系列类收场,所有类都建立在统一的接口基础上。我们用一幅颠倒的树形图来阐
明这一点(注释⑤):
⑤:这儿采用了“统一记号法”,本书将主要采用这种方法。
对这样的一系列类,我们要进行的一项重要处理就是将衍生类的对象当作基础类的一个对象对待。这一点是
非常重要的,因为它意味着我们只需编写单一的代码,令其忽略类型的特定细节,只与基础类打交道。这样
一来,那些代码就可与类型信息分开。所以更易编写,也更易理解。此外,若通过继承增添了一种新类型,
如“三角形”,那么我们为“几何形状”新类型编写的代码会象在旧类型里一样良好地工作。所以说程序具
备了“扩展能力”,具有“扩展性”。
以上面的例子为基础,假设我们用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 )。若在堆栈或者静态存储空间里创建一个对象,编译
器会判断对象的持续时间有多长,到时会自动“破坏”或者“清除”它。程序员可用两种方法来破坏一个对
象:用程序化的方式决定何时破坏对象,或者利用由运行环境提供的一种“垃圾收集器”特性,自动寻找那
些不再使用的对象,并将其清除。当然,垃圾收集器显得方便得多,但要求所有应用程序都必须容忍垃圾收
集器的存在,并能默许随垃圾收集带来的额外开销。但这并不符合C++语言的设计宗旨,所以未能包括到 C++
里。但Java 确实提供了一个垃圾收集器(Smalltalk 也有这样的设计;尽管 Delphi 默认为没有垃圾收集
器,但可选择安装;而 C++亦可使用一些由其他公司开发的垃圾收集产品)。
本节剩下的部分将讨论操纵对象时要考虑的另一些因素。
1。7。1 集合与继承器
针对一个特定问题的解决,如果事先不知道需要多少个对象,或者它们的持续时间有多长,那么也不知道如
何保存那些对象。既然如此,怎样才能知道那些对象要求多少空间呢?事先上根本无法提前知道,除非进入
运行期。
在面向对象的设计中,大多数问题的解决办法似乎都有些轻率——只是简单地创建另一种类型的对象。用于
解决特定问题的新型对象容纳了指向其他对象的句柄。当然,也可以用数组来做同样的事情,那是大多数语
言都具有的一种功能。但不能只看到这一点。这种新对象通常叫作“集合”(亦叫作一个“容器”,但AWT
在不同的场合应用了这个术语,所以本书将一直沿用“集合”的称呼。在需要的时候,集合会自动扩充自
己,以便适应我们在其中置入的任何东西。所以我们事先不必知道要在一个集合里容下多少东西。只需创建
一个集合,以后的工作让它自己负责好了。
幸运的是,设计优良的 OOP 语言都配套提供了一系列集合。在 C++中,它们是以“标准模板库”(STL)的形
式提供的。Object Pascal 用自己的“可视组件库”(VCL)提供集合。Smalltalk 提供了一套非常完整的集
合。而 Java 也用自己的标准库提供了集合。在某些库中,一个常规集合便可满足人们的大多数要求;而在另
33
…………………………………………………………Page 35……………………………………………………………
一些库中(特别是 C++的库),则面向不同的需求提供了不同类型的集合。例如,可以用一个矢量统一对所
有元素的访问方式;一个链接列表则用于保证所有元素的插入统一。所以我们能根据自己的需要选择适当的
类型。其中包括集、队列、散列表、树、堆栈等等。
所有集合
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!