第6章 java类再生.docx
《第6章 java类再生.docx》由会员分享,可在线阅读,更多相关《第6章 java类再生.docx(38页珍藏版)》请在冰豆网上搜索。
第6章java类再生
本教程由yyc,spirit整理清风小木虫美化
-----------------------清风小木虫精彩无极限--------------------------
第6章类再生
“Java引人注目的一项特性是代码的重复使用或者再生。
但最具革命意义的是,除代码的复制和修改以外,我们还能做多得多的其他事情。
”
在象C那样的程序化语言里,代码的重复使用早已可行,但效果不是特别显著。
与Java的其他地方一样,这个方案解决的也是与类有关的问题。
我们通过创建新类来重复使用代码,但却用不着重新创建,可以直接使用别人已建好并调试好的现成类。
但这样做必须保证不会干扰原有的代码。
在这一章里,我们将介绍两个达到这一目标的方法。
第一个最简单:
在新类里简单地创建原有类的对象。
我们把这种方法叫作“合成”,因为新类由现有类的对象合并而成。
我们只是简单地重复利用代码的功能,而不是采用它的形式。
第二种方法则显得稍微有些技巧。
它创建一个新类,将其作为现有类的一个“类型”。
我们可以原样采取现有类的形式,并在其中加入新代码,同时不会对现有的类产生影响。
这种魔术般的行为叫作“继承”(Inheritance),涉及的大多数工作都是由编译器完成的。
对于面向对象的程序设计,“继承”是最重要的基础概念之一。
它对我们下一章要讲述的内容会产生一些额外的影响。
对于合成与继承这两种方法,大多数语法和行为都是类似的(因为它们都要根据现有的类型生成新类型)。
在本章,我们将深入学习这些代码再生或者重复使用的机制。
6.1合成的语法
就以前的学习情况来看,事实上已进行了多次“合成”操作。
为进行合成,我们只需在新类里简单地置入对象句柄即可。
举个例子来说,假定需要在一个对象里容纳几个String对象、两种基本数据类型以及属于另一个类的一个对象。
对于非基本类型的对象来说,只需将句柄置于新类即可;而对于基本数据类型来说,则需在自己的类中定义它们。
如下所示(若执行该程序时有麻烦,请参见第3章3.1.2小节“赋值”):
//:
SprinklerSystem.java
//Compositionforcodereuse
packagec06;
classWaterSource{
privateStrings;
WaterSource(){
System.out.println("WaterSource()");
s=newString("Constructed");
}
publicStringtoString(){returns;}
}
publicclassSprinklerSystem{
privateStringvalve1,valve2,valve3,valve4;
WaterSourcesource;
inti;
floatf;
voidprint(){
System.out.println("valve1="+valve1);
System.out.println("valve2="+valve2);
System.out.println("valve3="+valve3);
System.out.println("valve4="+valve4);
System.out.println("i="+i);
System.out.println("f="+f);
System.out.println("source="+source);
}
publicstaticvoidmain(String[]args){
SprinklerSystemx=newSprinklerSystem();
x.print();
}
}///:
~
WaterSource内定义的一个方法是比较特别的:
toString()。
大家不久就会知道,每种非基本类型的对象都有一个toString()方法。
若编译器本来希望一个String,但却获得某个这样的对象,就会调用这个方法。
所以在下面这个表达式中:
System.out.println("source="+source);
编译器会发现我们试图向一个WaterSource添加一个String对象("source=")。
这对它来说是不可接受的,因为我们只能将一个字串“添加”到另一个字串,所以它会说:
“我要调用toString(),把source转换成字串!
”经这样处理后,它就能编译两个字串,并将结果字串传递给一个System.out.println()。
每次随同自己创建的一个类允许这种行为的时候,都只需要写一个toString()方法。
如果不深究,可能会草率地认为编译器会为上述代码中的每个句柄都自动构造对象(由于Java的安全和谨慎的形象)。
例如,可能以为它会为WaterSource调用默认构建器,以便初始化source。
打印语句的输出事实上是:
valve1=null
valve2=null
valve3=null
valve4=null
i=0
f=0.0
source=null
在类内作为字段使用的基本数据会初始化成零,就象第2章指出的那样。
但对象句柄会初始化成null。
而且假若试图为它们中的任何一个调用方法,就会产生一次“违例”。
这种结果实际是相当好的(而且很有用),我们可在不丢弃一次违例的前提下,仍然把它们打印出来。
编译器并不只是为每个句柄创建一个默认对象,因为那样会在许多情况下招致不必要的开销。
如希望句柄得到初始化,可在下面这些地方进行:
(1)在对象定义的时候。
这意味着它们在构建器调用之前肯定能得到初始化。
(2)在那个类的构建器中。
(3)紧靠在要求实际使用那个对象之前。
这样做可减少不必要的开销——假如对象并不需要创建的话。
下面向大家展示了所有这三种方法:
//:
Bath.java
//Constructorinitializationwithcomposition
classSoap{
privateStrings;
Soap(){
System.out.println("Soap()");
s=newString("Constructed");
}
publicStringtoString(){returns;}
}
publicclassBath{
privateString
//Initializingatpointofdefinition:
s1=newString("Happy"),
s2="Happy",
s3,s4;
Soapcastille;
inti;
floattoy;
Bath(){
System.out.println("InsideBath()");
s3=newString("Joy");
i=47;
toy=3.14f;
castille=newSoap();
}
voidprint(){
//Delayedinitialization:
if(s4==null)
s4=newString("Joy");
System.out.println("s1="+s1);
System.out.println("s2="+s2);
System.out.println("s3="+s3);
System.out.println("s4="+s4);
System.out.println("i="+i);
System.out.println("toy="+toy);
System.out.println("castille="+castille);
}
publicstaticvoidmain(String[]args){
Bathb=newBath();
b.print();
}
}///:
~
请注意在Bath构建器中,在所有初始化开始之前执行了一个语句。
如果不在定义时进行初始化,仍然不能保证能在将一条消息发给一个对象句柄之前会执行任何初始化——除非出现不可避免的运行期违例。
下面是该程序的输出:
InsideBath()
Soap()
s1=Happy
s2=Happy
s3=Joy
s4=Joy
i=47
toy=3.14
castille=Constructed
调用print()时,它会填充s4,使所有字段在使用之前都获得正确的初始化。
6.2继承的语法
继承与Java(以及其他OOP语言)非常紧密地结合在一起。
我们早在第1章就为大家引入了继承的概念,并在那章之后到本章之前的各章里不时用到,因为一些特殊的场合要求必须使用继承。
除此以外,创建一个类时肯定会进行继承,因为若非如此,会从Java的标准根类Object中继承。
用于合成的语法是非常简单且直观的。
但为了进行继承,必须采用一种全然不同的形式。
需要继承的时候,我们会说:
“这个新类和那个旧类差不多。
”为了在代码里表面这一观念,需要给出类名。
但在类主体的起始花括号之前,需要放置一个关键字extends,在后面跟随“基础类”的名字。
若采取这种做法,就可自动获得基础类的所有数据成员以及方法。
下面是一个例子:
//:
Detergent.java
//Inheritancesyntax&properties
classCleanser{
privateStrings=newString("Cleanser");
publicvoidappend(Stringa){s+=a;}
publicvoiddilute(){append("dilute()");}
publicvoidapply(){append("apply()");}
publicvoidscrub(){append("scrub()");}
publicvoidprint(){System.out.println(s);}
publicstaticvoidmain(String[]args){
Cleanserx=newCleanser();
x.dilute();x.apply();x.scrub();
x.print();
}
}
publicclassDetergentextendsCleanser{
//Changeamethod:
publicvoidscrub(){
append("Detergent.scrub()");
super.scrub();//Callbase-classversion
}
//Addmethodstotheinterface:
publicvoidfoam(){append("foam()");}
//Testthenewclass:
publicstaticvoidmain(String[]args){
Detergentx=newDetergent();
x.dilute();
x.apply();
x.scrub();
x.foam();
x.print();
System.out.println("Testingbaseclass:
");
Cleanser.main(args);
}
}///:
~
这个例子向大家展示了大量特性。
首先,在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会自动插入对基础类构建器的调用。
下面这个例子向大家展示了对这种三级继承的应用:
//:
Cartoon.java
//Constructorcallsduringinheritance
classArt{
Art(){
System.out.println("Artconstructor");
}
}
classDrawingextendsArt{
Drawing(){
System.out.println("Drawingconstructor");
}
}
publicclassCartoonextendsDrawing{
Cartoon(){
System.out.println("Cartoonconstructor");
}
publicstaticvoidmain(String[]args){
Cartoonx=newCartoon();
}
}///:
~
该程序的输出显示了自动调用:
Artconstructor
Drawingconstructor
Cartoonconstructor
可以看出,构建是在基础类的“外部”进行的,所以基础类会在衍生类访问它之前得到正确的初始化。
即使没有为Cartoon()创建一个构建器,编译器也会为我们自动合成一个默认构建器,并发出对基础类构建器的调用。
1.含有自变量的构建器
上述例子有自己默认的构建器;也就是说,它们不含任何自变量。
编译器可以很容易地调用它们,因为不存在具体传递什么自变量的问题。
如果类没有默认的自变量,或者想调用含有一个自变量的某个基础类构建器,必须明确地编写对基础类的调用代码。
这是用super关键字以及适当的自变量列表实现的,如下所示:
//:
Chess.java
//Inheritance,constructorsandarguments
classGame{
Game(inti){
System.out.println("Gameconstructor");
}
}
classBoardGameextendsGame{
BoardGame(inti){
super(i);
System.out.println("BoardGameconstructor");
}
}
publicclassChessextendsBoardGame{
Chess(){
super(11);
System.out.println("Chessconstructor");
}
publicstaticvoidmain(String[]args){
Chessx=newChess();
}
}///:
~
如果不调用BoardGames()内的基础类构建器,编译器就会报告自己找不到Games()形式的一个构建器。
除此以外,在衍生类构建器中,对基础类构建器的调用是必须做的第一件事情(如操作失当,编译器会向我们指出)。
2.捕获基本构建器的违例
正如刚才指出的那样,编译器会强迫我们在衍生类构建器的主体中首先设置对基础类构建器的调用。
这意味着在它之前不能出现任何东西。
正如大家在第9章会看到的那样,这同时也会防止衍生类构建器捕获来自一个基础类的任何违例事件。
显然,这有时会为我们造成不便。
6.3合成与继承的结合
许多时候都要求将合成与继承两种技术结合起来使用。
下面这个例子展示了如何同时采用继承与合成技术,从而创建一个更复杂的类,同时进行必要的构建器初始化工作:
//:
PlaceSetting.java
//Combiningcomposition&inheritance
classPlate{
Plate(inti){
System.out.println("Plateconstructor");
}
}
classDinnerPlateextendsPlate{
DinnerPlate(inti){
super(i);
System.out.println(
"DinnerPlateconstructor");
}
}
classUtensil{
Utensil(inti){
System.out.println("Utensilconstructor");
}
}
classSpoonextendsUtensil{
Spoon(inti){
super(i);
System.out.println("Spoonconstructor");
}
}
classForkextendsUtensil{
Fork(inti){
super(i);
System.out.println("Forkconstructor");
}
}
classKnifeextendsUtensil{
Knife(inti){
super(i);
System.out.println("Knifeconstructor");
}
}
//Aculturalwayofdoingsomething:
classCustom{
Custom(inti){
System.out.println("Customconstructor");
}
}
publicclassPlaceSettingextendsCustom{
Spoonsp;
Forkfrk;
Knifekn;
DinnerPlatepl;
PlaceSetting(inti){
super(i+1);
sp=newSpoon(i+2);
frk=newFork(i+3);
kn=newKnife(i+4);
pl=newDinnerPlate(i+5);
System.out.println(
"PlaceSettingconstructor");
}
publicstaticvoidmain(String[]args){
PlaceSettingx=newPlaceSetting(9);
}
}///:
~
尽管编译器会强迫我们对基础类进行初始化,并要求我们在构建器最开头做这一工作,但它并不会监视我们是否正确初始化了成员对象。
所以对此必须特别加以留意。
6.3.1确保正确的清除
Java不具备象C++的“破坏器”那样的概念。
在C++中,一旦破坏(清除)一个对象,就会自动调用破坏器方法。
之所以将其省略,大概是由于在Java中只需简单地忘记对象,不需强行破坏它们。
垃圾收集器会在必要的时候自动回收内存。
垃圾收集器大多数时候都能很好地工作,但在某些情况下,我们的类可能在自己的存在时期采取一些行动,而这些行动要求必须进行明确的清除工作。
正如第4章已经指出的那样,我们并不知道垃圾收集器什么时候才会显身,或者说不知它何时会调用。
所以一旦希望为一个类清除什么东西,必须写一个特别的方法,明确、专门地来做这件事情。
同时,还要让客户程序员知道他们必须调用这个方