Java编程思想第4版[中文版](PDF格式)-第17部分
按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!
“接口”(Interface)规定了可对一个特定的对象发出哪些请求。然而,必须在某个地方存在着一些代码,
以便满足这些请求。这些代码与那些隐藏起来的数据便叫作“隐藏的实现”。站在程式化程序编写
(Procedural Programming )的角度,整个问题并不显得复杂。一种类型含有与每种可能的请求关联起来的
函数。一旦向对象发出一个特定的请求,就会调用那个函数。我们通常将这个过程总结为向对象“发送一条
消息”(提出一个请求)。对象的职责就是决定如何对这条消息作出反应(执行相应的代码)。
对于任何关系,重要一点是让牵连到的所有成员都遵守相同的规则。创建一个库时,相当于同客户程序员建
立了一种关系。对方也是程序员,但他们的目标是组合出一个特定的应用(程序),或者用您的库构建一个
更大的库。
若任何人都能使用一个类的所有成员,那么客户程序员可对那个类做任何事情,没有办法强制他们遵守任何
约束。即便非常不愿客户程序员直接操作类内包含的一些成员,但倘若未进行访问控制,就没有办法阻止这
一情况的发生——所有东西都会暴露无遗。
有两方面的原因促使我们控制对成员的访问。第一个原因是防止程序员接触他们不该接触的东西——通常是
内部数据类型的设计思想。若只是为了解决特定的问题,用户只需操作接口即可,毋需明白这些信息。我们
向用户提供的实际是一种服务,因为他们很容易就可看出哪些对自己非常重要,以及哪些可忽略不计。
进行访问控制的第二个原因是允许库设计人员修改内部结构,不用担心它会对客户程序员造成什么影响。例
如,我们最开始可能设计了一个形式简单的类,以便简化开发。以后又决定进行改写,使其更快地运行。若
接口与实现方法早已隔离开,并分别受到保护,就可放心做到这一点,只要求用户重新链接一下即可。
Java 采用三个显式(明确)关键字以及一个隐式(暗示)关键字来设置类边界:public,private,
protected 以及暗示性的friendly。若未明确指定其他关键字,则默认为后者。这些关键字的使用和含义都
是相当直观的,它们决定了谁能使用后续的定义内容。“public”(公共)意味着后续的定义任何人均可使
用。而在另一方面,“private”(私有)意味着除您自己、类型的创建者以及那个类型的内部函数成员,其
他任何人都不能访问后续的定义信息。private 在您与客户程序员之间竖起了一堵墙。若有人试图访问私有
29
…………………………………………………………Page 31……………………………………………………………
成员,就会得到一个编译期错误。“friendly ”(友好的)涉及“包装”或“封装”(Package)的概念——
即Java 用来构建库的方法。若某样东西是“友好的”,意味着它只能在这个包装的范围内使用(所以这一访
问级别有时也叫作“包装访问”)。“protected”(受保护的)与“private”相似,只是一个继承的类可
访问受保护的成员,但不能访问私有成员。继承的问题不久就要谈到。
1。4 方案的重复使用
创建并测试好一个类后,它应(从理想的角度)代表一个有用的代码单位。但并不象许多人希望的那样,这
种重复使用的能力并不容易实现;它要求较多的经验以及洞察力,这样才能设计出一个好的方案,才有可能
重复使用。
许多人认为代码或设计方案的重复使用是面向对象的程序设计提供的最伟大的一种杠杆。
为重复使用一个类,最简单的办法是仅直接使用那个类的对象。但同时也能将那个类的一个对象置入一个新
类。我们把这叫作“创建一个成员对象”。新类可由任意数量和类型的其他对象构成。无论如何,只要新类
达到了设计要求即可。这个概念叫作“组织”——在现有类的基础上组织一个新类。有时,我们也将组织称
作“包含”关系,比如“一辆车包含了一个变速箱”。
对象的组织具有极大的灵活性。新类的“成员对象”通常设为“私有”(Private),使用这个类的客户程序
员不能访问它们。这样一来,我们可在不干扰客户代码的前提下,从容地修改那些成员。也可以在“运行
期”更改成员,这进一步增大了灵活性。后面要讲到的“继承”并不具备这种灵活性,因为编译器必须对通
过继承创建的类加以限制。
由于继承的重要性,所以在面向对象的程序设计中,它经常被重点强调。作为新加入这一领域的程序员,或
许早已先入为主地认为“继承应当随处可见”。沿这种思路产生的设计将是非常笨拙的,会大大增加程序的
复杂程度。相反,新建类的时候,首先应考虑“组织”对象;这样做显得更加简单和灵活。利用对象的组
织,我们的设计可保持清爽。一旦需要用到继承,就会明显意识到这一点。
1。5 继承:重新使用接口
就其本身来说,对象的概念可为我们带来极大的便利。它在概念上允许我们将各式各样数据和功能封装到一
起。这样便可恰当表达“问题空间”的概念,不用刻意遵照基础机器的表达方式。在程序设计语言中,这些
概念则反映为具体的数据类型(使用class 关键字)。
我们费尽心思做出一种数据类型后,假如不得不又新建一种类型,令其实现大致相同的功能,那会是一件非
常令人灰心的事情。但若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就
显得理想多了。“继承”正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原始
类(正式名称叫作基础类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫作继承类或者子
类)也会反映出这种变化。在 Java 语言中,继承是通过 extends 关键字实现的
使用继承时,相当于创建了一个新类。这个新类不仅包含了现有类型的所有成员(尽管private 成员被隐藏
起来,且不能访问),但更重要的是,它复制了基础类的接口。也就是说,可向基础类的对象发送的所有消
息亦可原样发给衍生类的对象。根据可以发送的消息,我们能知道类的类型。这意味着衍生类具有与基础类
相同的类型!为真正理解面向对象程序设计的含义,首先必须认识到这种类型的等价关系。
由于基础类和衍生类具有相同的接口,所以那个接口必须进行特殊的设计。也就是说,对象接收到一条特定
的消息后,必须有一个“方法”能够执行。若只是简单地继承一个类,并不做其他任何事情,来自基础类接
口的方法就会直接照搬到衍生类。这意味着衍生类的对象不仅有相同的类型,也有同样的行为,这一后果通
常是我们不愿见到的。
有两种做法可将新得的衍生类与原来的基础类区分开。第一种做法十分简单:为衍生类添加新函数(功
能)。这些新函数并非基础类接口的一部分。进行这种处理时,一般都是意识到基础类不能满足我们的要
求,所以需要添加更多的函数。这是一种最简单、最基本的继承用法,大多数时候都可完美地解决我们的问
题。然而,事先还是要仔细调查自己的基础类是否真的需要这些额外的函数。
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()的代码里,最让人吃惊的是尽管我们没作出任