第6章 类再生Word格式.docx
《第6章 类再生Word格式.docx》由会员分享,可在线阅读,更多相关《第6章 类再生Word格式.docx(20页珍藏版)》请在冰点文库上搜索。
classWaterSource{
privateStrings;
WaterSource(){
System.out.println("
WaterSource()"
);
s=newString("
Constructed"
}
publicStringtoString(){returns;
}
publicclassSprinklerSystem{
privateStringvalve1,valve2,valve3,valve4;
WaterSourcesource;
inti;
floatf;
voidprint(){
valve1="
+valve1);
valve2="
+valve2);
valve3="
+valve3);
valve4="
+valve4);
i="
+i);
f="
+f);
source="
+source);
publicstaticvoidmain(String[]args){
SprinklerSystemx=newSprinklerSystem();
x.print();
}///:
~
WaterSource内定义的一个方法是比较特别的:
toString()。
大家不久就会知道,每种非基本类型的对象都有一个toString()方法。
若编译器本来希望一个String,但却获得某个这样的对象,就会调用这个方法。
所以在下面这个表达式中:
System.out.println("
+source);
编译器会发现我们试图向一个WaterSource添加一个String对象("
source="
)。
这对它来说是不可接受的,因为我们只能将一个字串“添加”到另一个字串,所以它会说:
“我要调用toString(),把source转换成字串!
”经这样处理后,它就能编译两个字串,并将结果字串传递给一个System.out.println()。
每次随同自己创建的一个类允许这种行为的时候,都只需要写一个toString()方法。
如果不深究,可能会草率地认为编译器会为上述代码中的每个句柄都自动构造对象(由于Java的安全和谨慎的形象)。
例如,可能以为它会为WaterSource调用默认构建器,以便初始化source。
打印语句的输出事实上是:
219页下程序
valve1=null
valve2=null
valve3=null
valve4=null
i=0
f=0.0
source=null
在类内作为字段使用的基本数据会初始化成零,就象第2章指出的那样。
但对象句柄会初始化成null。
而且假若试图为它们中的任何一个调用方法,就会产生一次“违例”。
这种结果实际是相当好的(而且很有用),我们可在不丢弃一次违例的前提下,仍然把它们打印出来。
编译器并不只是为每个句柄创建一个默认对象,因为那样会在许多情况下招致不必要的开销。
如希望句柄得到初始化,可在下面这些地方进行:
(1)在对象定义的时候。
这意味着它们在构建器调用之前肯定能得到初始化。
(2)在那个类的构建器中。
(3)紧靠在要求实际使用那个对象之前。
这样做可减少不必要的开销——假如对象并不需要创建的话。
下面向大家展示了所有这三种方法:
220-221页程序
请注意在Bath构建器中,在所有初始化开始之前执行了一个语句。
如果不在定义时进行初始化,仍然不能保证能在将一条消息发给一个对象句柄之前会执行任何初始化——除非出现不可避免的运行期违例。
下面是该程序的输出:
221页中程序
调用print()时,它会填充s4,使所有字段在使用之前都获得正确的初始化。
6.2继承的语法
继承与Java(以及其他OOP语言)非常紧密地结合在一起。
我们早在第1章就为大家引入了继承的概念,并在那章之后到本章之前的各章里不时用到,因为一些特殊的场合要求必须使用继承。
除此以外,创建一个类时肯定会进行继承,因为若非如此,会从Java的标准根类Object中继承。
用于合成的语法是非常简单且直观的。
但为了进行继承,必须采用一种全然不同的形式。
需要继承的时候,我们会说:
“这个新类和那个旧类差不多。
”为了在代码里表面这一观念,需要给出类名。
但在类主体的起始花括号之前,需要放置一个关键字extends,在后面跟随“基础类”的名字。
若采取这种做法,就可自动获得基础类的所有数据成员以及方法。
下面是一个例子:
222页程序
这个例子向大家展示了大量特性。
首先,在Cleanserappend()方法里,字串同一个s连接起来。
这是用“+=”运算符实现的。
同“+”一样,“+=”被Java用于对字串进行“过载”处理。
其次,无论Cleanser还是Detergent都包含了一个main()方法。
我们可为自己的每个类都创建一个main()。
通常建议大家象这样进行编写代码,使自己的测试代码能够封装到类内。
即便在程序中含有数量众多的类,但对于在命令行请求的public类,只有main()才会得到调用。
所以在这种情况下,当我们使用“javaDetergent”的时候,调用的是Degergent.main()——即使Cleanser并非一个public类。
采用这种将main()置入每个类的做法,可方便地为每个类都进行单元测试。
而且在完成测试以后,毋需将main()删去;
可把它保留下来,用于以后的测试。
在这里,大家可看到Deteregent.main()对Cleanser.main()的调用是明确进行的。
需要着重强调的是Cleanser中的所有类都是public属性。
请记住,倘若省略所有访问指示符,则成员默认为“友好的”。
这样一来,就只允许对包成员进行访问。
在这个包内,任何人都可使用那些没有访问指示符的方法。
例如,Detergent将不会遇到任何麻烦。
然而,假设来自另外某个包的类准备继承Cleanser,它就只能访问那些public成员。
所以在计划继承的时候,一个比较好的规则是将所有字段都设为private,并将所有方法都设为public(protected成员也允许衍生出来的类访问它;
以后还会深入探讨这一问题)。
当然,在一些特殊的场合,我们仍然必须作出一些调整,但这并不是一个好的做法。
注意Cleanser在它的接口中含有一系列方法:
append(),dilute(),apply(),scrub()以及print()。
由于Detergent是从Cleanser衍生出来的(通过extends关键字),所以它会自动获得接口内的所有这些方法——即使我们在Detergent里并未看到对它们的明确定义。
这样一来,就可将继承想象成“对接口的重复利用”或者“接口的再生”(以后的实施细节可以自由设置,但那并非我们强调的重点)。
正如在scrub()里看到的那样,可以获得在基础类里定义的一个方法,并对其进行修改。
在这种情况下,我们通常想在新版本里调用来自基础类的方法。
但在scrub()里,不可只是简单地发出对scrub()的调用。
那样便造成了递归调用,我们不愿看到这一情况。
为解决这个问题,Java提供了一个super关键字,它引用当前类已从中继承的一个“超类”(Superclass)。
所以表达式super.scrub()调用的是方法scrub()的基础类版本。
进行继承时,我们并不限于只能使用基础类的方法。
亦可在衍生出来的类里加入自己的新方法。
这时采取的做法与在普通类里添加其他任何方法是完全一样的:
只需简单地定义它即可。
extends关键字提醒我们准备将新方法加入基础类的接口里,对其进行“扩展”。
foam()便是这种做法的一个产物。
在Detergent.main()里,我们可看到对于Detergent对象,可调用Cleanser以及Detergent内所有可用的方法(如foam())。
6.2.1初始化基础类
由于这儿涉及到两个类——基础类及衍生类,而不再是以前的一个,所以在想象衍生类的结果对象时,可能会产生一些迷惑。
从外部看,似乎新类拥有与基础类相同的接口,而且可包含一些额外的方法和字段。
但继承并非仅仅简单地复制基础类的接口了事。
创建衍生类的一个对象时,它在其中包含了基础类的一个“子对象”。
这个子对象就象我们根据基础类本身创建了它的一个对象。
从外部看,基础类的子对象已封装到衍生类的对象里了。
当然,基础类子对象应该正确地初始化,而且只有一种方法能保证这一点:
在构建器中执行初始化,通过调用基础类构建器,后者有足够的能力和权限来执行对基础类的初始化。
在衍生类的构建器中,Java会自动插入对基础类构建器的调用。
下面这个例子向大家展示了对这种三级继承的应用:
224-225页程序
该程序的输出显示了自动调用:
Artconstructor
Drawingconstructor
Cartoonconstructor
可以看出,构建是在基础类的“外部”进行的,所以基础类会在衍生类访问它之前得到正确的初始化。
即使没有为Cartoon()创建一个构建器,编译器也会为我们自动合成一个默认构建器,并发出对基础类构建器的调用。
1.含有自变量的构建器
上述例子有自己默认的构建器;
也就是说,它们不含任何自变量。
编译器可以很容易地调用它们,因为不存在具体传递什么自变量的问题。
如果类没有默认的自变量,或者想调用含有一个自变量的某个基础类构建器,必须明确地编写对基础类的调用代码。
这是用super关键字以及适当的自变量列表实现的,如下所示:
225-226页程序
如果不调用BoardGames()内的基础类构建器,编译器就会报告自己找不到Games()形式的一个构建器。
除此以外,在衍生类构建器中,对基础类构建器的调用是必须做的第一件事情(如操作失当,编译器会向我们指出)。
2.捕获基本构建器的违例
正如刚才指出的那样,编译器会强迫我们在衍生类构建器的主体中首先设置对基础类构建器的调用。
这意味着在它之前不能出现任何东西。
正如大家在第9章会看到的那样,这同时也会防止衍生类构建器捕获来自一个基础类的任何违例事件。
显然,这有时会为我们造成不便。
6.3合成与继承的结合
许多时候都要求将合成与继承两种技术结合起来使用。
下面这个例子展示了如何同时采用继承与合成技术,从而创建一个更复杂的类,同时进行必要的构建器初始化工作:
226-228页程序
尽管编译器会强迫我们对基础类进行初始化,并要求我们在构建器最开头做这一工作,但它并不会监视我们是否正确初始化了成员对象。
所以对此必须特别加以留意。
6.3.1确保正确的清除
Java不具备象C++的“破坏器”那样的概念。
在C++中,一旦破坏(清除)一个对象,就会自动调用破坏器方法。
之所以将其省略,大概是由于在Java中只需简单地忘记对象,不需强行破坏它们。
垃圾收集器会在必要的时候自动回收内存。
垃圾收集器大多数时候都能很好地工作,但在某些情况下,我们的类可能在自己的存在时期采取一些行动,而这些行动要求必须进行明确的清除工作。
正如第4章已经指出的那样,我们并不知道垃圾收集器什么时候才会显身,或者说不知它何时会调用。
所以一旦希望为一个类清除什么东西,必须写一个特别的方法,明确、专门地来做这件事情。
同时,还要让客户程序员知道他们必须调用这个方法。
而在所有这一切的后面,就如第9章(违例控制)要详细解释的那样,必须将这样的清除代码置于一个finally从句中,从而防范任何可能出现的违例事件。
下面介绍的是一个计算机辅助设计系统的例子,它能在屏幕上描绘图形:
229-230页程序
这个系统中的所有东西都属于某种Shape(几何形状)。
Shape本身是一种Object(对象),因为它是从根类明确继承的。
每个类都重新定义了Shape的cleanup()方法,同时还要用super调用那个方法的基础类版本。
尽管对象存在期间调用的所有方法都可负责做一些要求清除的工作,但对于特定的Shape类——Circle(圆)、Triangle(三角形)以及Line(直线),它们都拥有自己的构建器,能完成“作图”(draw)任务。
每个类都有它们自己的cleanup()方法,用于将非内存的东西恢复回对象存在之前的景象。
在main()中,可看到两个新关键字:
try和finally。
我们要到第9章才会向大家正式引荐它们。
其中,try关键字指出后面跟随的块(由花括号定界)是一个“警戒区”。
也就是说,它会受到特别的待遇。
其中一种待遇就是:
该警戒区后面跟随的finally从句的代码肯定会得以执行——不管try块到底存不存在(通过违例控制技术,try块可有多种不寻常的应用)。
在这里,finally从句的意思是“总是为x调用cleanup(),无论会发生什么事情”。
这些关键字将在第9章进行全面、完整的解释。
在自己的清除方法中,必须注意对基础类以及成员对象清除方法的调用顺序——假若一个子对象要以另一个为基础。
通常,应采取与C++编译器对它的“破坏器”采取的同样的形式:
首先完成与类有关的所有特殊工作(可能要求基础类元素仍然可见),然后调用基础类清除方法,就象这儿演示的那样。
许多情况下,清除可能并不是个问题;
只需让垃圾收集器尽它的职责即可。
但一旦必须由自己明确清除,就必须特别谨慎,并要求周全的考虑。
1.垃圾收集的顺序
不能指望自己能确切知道何时会开始垃圾收集。
垃圾收集器可能永远不会得到调用。
即使得到调用,它也可能以自己愿意的任何顺序回收对象。
除此以外,Java1.0实现的垃圾收集器机制通常不会调用finalize()方法。
除内存的回收以外,其他任何东西都最好不要依赖垃圾收集器进行回收。
若想明确地清除什么,请制作自己的清除方法,而且不要依赖finalize()。
然而正如以前指出的那样,可强迫Java1.1调用所有收尾模块(Finalizer)。
6.3.2名字的隐藏
只有C++程序员可能才会惊讶于名字的隐藏,因为它的工作原理与在C++里是完全不同的。
如果Java基础类有一个方法名被“过载”使用多次,在衍生类里对那个方法名的重新定义就不会隐藏任何基础类的版本。
所以无论方法在这一级还是在一个基础类中定义,过载都会生效:
232页程序
正如下一章会讲到的那样,很少会用与基础类里完全一致的签名和返回类型来覆盖同名的方法,否则会使人感到迷惑(这正是C++不允许那样做的原因,所以能够防止产生一些不必要的错误)。
6.4到底选择合成还是继承
无论合成还是继承,都允许我们将子对象置于自己的新类中。
大家或许会奇怪两者间的差异,以及到底该如何选择。
如果想利用新类内部一个现有类的特性,而不想使用它的接口,通常应选择合成。
也就是说,我们可嵌入一个对象,使自己能用它实现新类的特性。
但新类的用户会看到我们已定义的接口,而不是来自嵌入对象的接口。
考虑到这种效果,我们需在新类里嵌入现有类的private对象。
有些时候,我们想让类用户直接访问新类的合成。
也就是说,需要将成员对象的属性变为public。
成员对象会将自身隐藏起来,所以这是一种安全的做法。
而且在用户知道我们准备合成一系列组件时,接口就更容易理解。
car(汽车)对象便是一个很好的例子:
233-234页程序
由于汽车的装配是故障分析时需要考虑的一项因素(并非只是基础设计简单的一部分),所以有助于客户程序员理解如何使用类,而且类创建者的编程复杂程度也会大幅度降低。
如选择继承,就需要取得一个现成的类,并制作它的一个特殊版本。
通常,这意味着我们准备使用一个常规用途的类,并根据特定的需求对其进行定制。
只需稍加想象,就知道自己不能用一个车辆对象来合成一辆汽车——汽车并不“包含”车辆;
相反,它“属于”车辆的一种类别。
“属于”关系是用继承来表达的,而“包含”关系是用合成来表达的。
6.5protected
现在我们已理解了继承的概念,protected这个关键字最后终于有了意义。
在理想情况下,private成员随时都是“私有”的,任何人不得访问。
但在实际应用中,经常想把某些东西深深地藏起来,但同时允许访问衍生类的成员。
protected关键字可帮助我们做到这一点。
它的意思是“它本身是私有的,但可由从这个类继承的任何东西或者同一个包内的其他任何东西访问”。
也就是说,Java中的protected会成为进入“友好”状态。
我们采取的最好的做法是保持成员的private状态——无论如何都应保留对基础的实施细节进行修改的权利。
在这一前提下,可通过protected方法允许类的继承者进行受到控制的访问:
235页程序
可以看到,change()拥有对set()的访问权限,因为它的属性是protected(受到保护的)。
6.6累积开发
继承的一个好处是它支持“累积开发”,允许我们引入新的代码,同时不会为现有代码造成错误。
这样可将新错误隔离到新代码里。
通过从一个现成的、功能性的类继承,同时增添成员新的数据成员及方法(并重新定义现有方法),我们可保持现有代码原封不动(另外有人也许仍在使用它),不会为其引入自己的编程错误。
一旦出现错误,就知道它肯定是由于自己的新代码造成的。
这样一来,与修改现有代码的主体相比,改正错误所需的时间和精力就可以少很多。
类的隔离效果非常好,这是许多程序员事先没有预料到的。
甚至不需要方法的源代码来实现代码的再生。
最多只需要导入一个包(这对于继承和合并都是成立的)。
大家要记住这样一个重点:
程序开发是一个不断递增或者累积的过程,就象人们学习知识一样。
当然可根据要求进行尽可能多的分析,但在一个项目的设计之初,谁都不可能提前获知所有的答案。
如果能将自己的项目看作一个有机的、能不断进步的生物,从而不断地发展和改进它,就有望获得更大的成功以及更直接的反馈。
尽管继承是一种非常有用的技术,但在某些情况下,特别是在项目稳定下来以后,仍然需要从新的角度考察自己的类结构,将其收缩成一个更灵活的结构。
请记住,继承是对一种特殊关系的表达,意味着“这个新类属于那个旧类的一种类型”。
我们的程序不应纠缠于一些细树末节,而应着眼于创建和操作各种类型的对象,用它们表达出来自“问题空间”的一个模型。
6.7上溯造型
继承最值得注意的地方就是它没有为新类提供方法。
继承是对新类和基础类之间的关系的一种表达。
可这样总结该关系:
“新类属于现有类的一种类型”。
这种表达并不仅仅是对继承的一种形象化解释,继承是直接由语言提供支持的。
作为一个例子,大家可考虑一个名为Instrument的基础类,它用于表示乐器;
另一个衍生类叫作Wind。
由于继承意味着基础类的所有方法亦可在衍生出来的类中使用,所以我们发给基础类的任何消息亦可发给衍生类。
若Instrument类有一个play()方法,则Wind设备也会有这个方法。
这意味着我们能肯定地认为一个Wind对象也是Instrument的一种类型。
下面这个例子揭示出编译器如何提供对这一概念的支持:
236-237页程序
这个例子中最有趣的无疑是tune()方法,它能接受一个Instrument句柄。
但在Wind.main()中,tune()方法是通过为其赋予一个Wind句柄来调用的。
由于Java对类型检查特别严格,所以大家可能会感到很奇怪,为什么接收一种类型的方法也能接收另一种类型呢?
但是,我们一定要认识到一个Wind对象也是一个Instrument对象。
而且对于不在Wind中的一个Instrument(乐器),没有方法可以由tune()调用。
在tune()中,代码适用于Instrument以及从Instrument衍生出来的任何东西。
在这里,我们将从一个Wind句柄转换成一个Instrument句柄的行为叫作“上溯造型”。
6.7.1何谓“上溯造型”?
之所以叫作这个名字,除了有一定的历史原因外,也是由于在传统意义上,类继承图的画法是根位于最顶部,再逐渐向下扩展(当然,可根据自己的习惯用任何方法描绘这种图)。
因素,Wind.java的继承图就象下面这个样子:
237页图
由于造型的方向是从衍生类到基础类,箭头朝上,所以通常把它叫作“上溯造型”,即Upcasting。
上溯造型肯定是安全的,因为我们是从一个更特殊的类型到一个更常规的类型。
换言之,衍生类是基础类的一个超集。
它可以包含比基础类更多的方法,但它至少包含了基础类的方法。
进行上溯造型的时候,类接口可能出现的唯一一个问题是它可能丢失方法,而不是赢得这些方法。
这便是在没有任何明确的造型或者其他特殊标注的情况下,编译器为什么允许上溯造型的原因所在。
也可以执行下溯造型,但这时会面临第11章要详细讲述的一种困境。
1.再论合成与继承
在面向对象的程序设计中,创建和使用代码最可能采取的一种做法是:
将数据和方法统一封装到一个类里,并且使用那个类的对象。
有些时候,需通过“合成”技术用现成的类来构造新类。
而继承是最少见的一种做法。
因此,尽管继承在学习OOP的过程中得到了大量的强调,但并不意味着应该尽可能地到处使用它。
相反,使用它时要特别慎重。
只有在清楚知道继承在所有方法中最有效的前提下,才可考虑它。
为判断自己到底应该选用合成还是继承,一个最简单的办法就是考虑是否需要从新类上溯造型回基础类。
若必须上溯,就需要继承。
但如果不需要上溯造型,就应提醒自己防止继承的滥用。
在下一章里(多形性),会向大家介绍必须进行上溯造型的一种场合。
但只要记住经常问自己“我真的需要上