欢迎转载,转载请标明出处:http://blog.youkuaiyun.com/notbaron/article/details/51040239
“Java 引人注目的一项特性是代码的重复使用或者再生。但最具革命意义的是,除代码的复制和修改以外,我们还能做多得多的其他事情。”
在象C 那样的程序化语言里,代码的重复使用早已可行,但效果不是特别显著。与Java 的其他地方一样,这个方案解决的也是与类有关的问题。我们通过创建新类来重复使用代码,但却用不着重新创建,可以直接使用别人已建好并调试好的现成类。
但这样做必须保证不会干扰原有的代码。
在新类里简单地创建原有类的对象。我们把这种方法叫作“合成”,因为新类由现有类的对象合并而成。我们只是简单地重复利用代码的功能,而不是采用它的形式。
第二种方法创建一个新类,将其作为现有类的一个“类型”。我们可以原样采取现有类的形式,并在其中加入新代码,同时不会对现有的类产生影响。这种行为叫作“继承”
(Inheritance),涉及的大多数工作都是由编译器完成的。对于面向对象的程序设计,“继承”是最重要的基础概念之一。
对于合成与继承这两种方法,大多数语法和行为都是类似的(因为它们都要根据现有的类型生成新类型)。
将深入学习这些代码再生或者重复使用的机制。
1 合成的语法
为进行合成,只需在新类里简单地置入对象句柄即可。举个例子来说,假定需要在一个对象里容纳几个String对象、两种基本数据类型以及属于另一个类的一个对象。对于非基本类型的对象来说,只需将句柄置于新类即可;而对于基本数据类型来说,则需在自己的类中定义它们。
示例如下:
package com.toad6;
class WaterSource {
private Strings;
WaterSource() {
System.out.println("WaterSource()");
s =new String("Constructed");
}
public String toString() {returns; }
}
publicclass SprinklerSystem {
private Stringvalve1,valve2,valve3,valve4;
WaterSource source;
inti;
floatf;
void print() {
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);
}
publicstaticvoid main(String[] args) {
SprinklerSystem x =new SprinklerSystem();
x.print();
}
} ///:~
输出如下:
valve1= null
valve2= null
valve3= null
valve4= null
i= 0
f= 0.0
source= null
WaterSource内定义的一个方法是比较特别的:toString()。大家不久就会知道,每种非基本类型的对象都有一个toString()方法。若编译器本来希望一个String,但却获得某个这样的对象,就会调用这个方法。所以在下面这个表达式中:
System.out.println("source = " + source) ;
编译器会发现我们试图向一个WaterSource添加一个String 对象("source =")。这对它来说是不可接受的,因为我们只能将一个字串“添加”到另一个字串,所以它会说:“我要调用toString(),把source 转换成字串!”经这样处理后,它就能编译两个字串,并将结果字串传递给一个System.out.println()。每次随同自己创建的一个类允许这种行为的时候,都只需要写一个 toString()方法。
在类内作为字段使用的基本数据会初始化成零。但对象句柄会初始化成null。而且假若试图为它们中的任何一个调用方法,就会产生一次“违例”。这种结果实际是相当好的(而且很有用),我们可在不丢弃一次违例的前提下,仍然把它们打印出来。
编译器并不只是为每个句柄创建一个默认对象,因为那样会在许多情况下招致不必要的开销。如希望句柄得到初始化,可在下面这些地方进行:
(1) 在对象定义的时候。这意味着它们在构建器调用之前肯定能得到初始化。
(2) 在那个类的构建器中。
(3) 紧靠在要求实际使用那个对象之前。这样做可减少不必要的开销——假如对象并不需要创建的话。
示例如下:
package com.toad6;
class Soap {
private Strings;
Soap(){
System.out.println("Soap()");
s =new String("Constructed");
}
public String toString() {
returns;
}
}
publicclass Bath {
private Strings1 =new String("Happy"),s2 ="Happy",s3,s4;
Soapcastille;
inti;
floattoy;
Bath(){
System.out.println("InsideBath()");
s3 =new String("Joy");
i = 47;
toy = 3.14f;
castille =new Soap();
}
void print() {
// Delayedinitialization:
if (s4 == null)
s4 =new String("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);
}
publicstaticvoid main(String[] args) {
Bathb =new Bath();
b.print();
}
} // /:~
输出如下:
InsideBath()
Soap()
s1= Happy
s2= Happy
s3= Joy
s4= Joy
i= 47
toy= 3.14
castille= Constructed
在Bath 构建器中,在所有初始化开始之前执行了一个语句。如果不在定义时进行初始化,仍然不能保证能在将一条消息发给一个对象句柄之前会执行任何初始化——除非出现不可避免的运行期违例。
调用print()时,它会填充s4,使所有字段在使用之前都获得正确的初始化。
2 继承的语法
继承与Java(以及其他OOP 语言)非常紧密地结合在一起。
创建一个类时肯定会进行继承,因为若非如此,会从Java的标准根类 Object中继承。
用于合成的语法是非常简单且直观的。但为了进行继承,必须采用一种全然不同的形式。
在类主体的起始花括号之前,需要放置一个关键字extends,在后面跟随“基础类”的名字。若采取这种做法,就可自动获得基础类的所有数据成员以及方法。
示例如下:
package com.toad6;
class Cleanser {
private Strings =new String("Cleanser");
publicvoid append(Stringa) {
s +=a;
}
publicvoid dilute() {
append(" dilute()");
}
publicvoid apply() {
append(" apply()");
}
publicvoid scrub() {
append(" scrub()");
}
publicvoid print() {
System.out.println(s);
}
publicstaticvoid main(String[] args) {
Cleanserx =new Cleanser();
x.dilute();
x.apply();
x.scrub();
x.print();
}
}
publicclass Detergentextends Cleanser {
// Change a method:
publicvoid scrub() {
append("Detergent.scrub()");
super.scrub();// Call base-classversion
}
// Add methods to theinterface:
publicvoid foam() {
append(" foam()");
}
// Test the newclass:
publicstaticvoid main(String[] args) {
Detergentx =new Detergent();
x.dilute();
x.apply();
x.scrub();
x.foam();
x.print();
System.out.println("Testing baseclass:");
Cleanser.main(args);
}
} // /:~
输出如下:
Cleanserdilute() apply() Detergent.scrub() scrub() foam()
Testingbase class:
Cleanserdilute() apply() scrub()
无论 Cleanser 还是Detergent 都包含了一个main()方法。我们可为自己的每个类都创建一个main()。通常建议大家象这样进行编写代码,使自己的测试代码能够封装到类内。即便在程序中含有数量众多的类,但对于在命令行请求的public 类,只有main()才会得到调用。所以在这种情况下,当我们使用“java Detergent”的时候,调用的是Degergent.main()——即使Cleanser 并非一个public类。采用这种将main()置入每个类的做法,可方便地为每个类都进行单元测试。而且在完成测试以后,毋需将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())。
2.1 初始化基础类
基础类及衍生类,不再是以前的一个,所以在想象衍生类的结果对象时,可能会产生一些迷惑。
从外部看,似乎新类拥有与基础类相同的接口,而且可包含一些额外的方法和字段。但继承并非仅仅简单地复制基础类的接口了事。创建衍生类的一个对象时,它在其中包含了基础类的一个“子对象”。这个子对象就象我们根据基础类本身创建了它的一个对象。从外部看,基础类的子对象已封装到衍生类的对象里了。
基础类子对象应该正确地初始化,而且只有一种方法能保证这一点:在构建器中执行初始化,通过调用基础类构建器,后者有足够的能力和权限来执行对基础类的初始化。在衍生类的构建器中,Java 会自动插入对基础类构建器的调用。
示例如下:
package com.toad6;
class Art {
Art(){
System.out.println("Artconstructor");
}
}
class Drawingextends Art {
Drawing(){
System.out.println("Drawingconstructor");
}
}
publicclass Cartoonextends Drawing {
Cartoon(){
System.out.println("Cartoonconstructor");
}
publicstaticvoid main(String[] args) {
Cartoonx =new Cartoon();
}
} // /:~
输出:
Artconstructor
Drawingconstructor
Cartoon constructor
构建是在基础类的“外部”进行的,所以基础类会在衍生类访问它之前得到正确的初始化。 即使没有为 Cartoon()创建一个构建器,编译器也会为我们自动合成一个默认构建器,并发出对基础类构建器的调用。
2.1.1 含有自变量的构建器
上述例子有自己默认的构建器;也就是说,它们不含任何自变量。编译器可以很容易地调用它们,因为不存在具体传递什么自变量的问题。如果类没有默认的自变量,或者想调用含有一个自变量的某个基础类构建器,必须明确地编写对基础类的调用代码。这是用 super 关键字以及适当的自变量列表实现的.
示例如下:
package com.toad6;
class Game {
Game(inti) {
System.out.println("Gameconstructor");
}
}
class BoardGameextends Game {
BoardGame(inti) {
super(i);
System.out.println("BoardGameconstructor");
}
}
publicclass Chessextends BoardGame {
Chess() {
super(11);
System.out.println("Chessconstructor");
}
publicstaticvoid main(String[] args) {
Chess x =new Chess();
}
} ///:~
输出如下:
Gameconstructor
BoardGameconstructor
Chessconstructor
如果不调用 BoardGames()内的基础类构建器,编译器就会报告自己找不到Games()形式的一个构建器。除此以外,在衍生类构建器中,对基础类构建器的调用是必须做的第一件事情(如操作失当,编译器会向我们指出)。
大家可以尝试把 super语句注释。
2.1.2 捕获基本构建器的违例
编译器会强迫我们在衍生类构建器的主体中首先设置对基础类构建器的调用。这意味着在它之前不能出现任何东西。这同时也会防止衍生类构建器捕获来自一个基础类的任何违例事件。有时会为我们造成不便。
3 合成与继承的结合
许多时候都要求将合成与继承两种技术结合起来使用。下面这个例子展示了如何同时采用继承与合成技术,从而创建一个更复杂的类,同时进行必要的构建器初始化工作:
示例如下:
package com.toad6;
class Plate {
Plate(inti) {
System.out.println("Plateconstructor");
}
}
class DinnerPlateextends Plate {
DinnerPlate(inti) {
super(i);
System.out.println(
"DinnerPlateconstructor");
}
}
class Utensil {
Utensil(inti) {
System.out.println("Utensil constructor");
}
}
class Spoonextends Utensil {
Spoon(inti) {
super(i);
System.out.println("Spoonconstructor");
}
}
class Forkextends Utensil {
Fork(inti) {
super(i);
System.out.println("Forkconstructor");
}
}
class Knifeextends Utensil {
Knife(inti) {
super(i);
System.out.println("Knifeconstructor");
}
}
//A cultural way of doing something:
class Custom {
Custom(inti) {
System.out.println("Customconstructor");
}
}
publicclass PlaceSettingextends Custom {
Spoon sp;
Fork frk;
Knife kn;
DinnerPlate pl;
PlaceSetting(inti) {
super(i + 1);
sp =new Spoon(i + 2);
frk =new Fork(i + 3);
kn =new Knife(i + 4);
pl =new DinnerPlate(i + 5);
System.out.println(
"PlaceSettingconstructor");
}
publicstaticvoid main(String[] args) {
PlaceSetting x =new PlaceSetting(9);
}
} ///:~
输出如下:
Customconstructor
Utensilconstructor
Spoonconstructor
Utensilconstructor
Forkconstructor
Utensilconstructor
Knifeconstructor
Plateconstructor
DinnerPlateconstructor
PlaceSettingconstructor
编译器会强迫我们对基础类进行初始化,并要求我们在构建器最开头做这一工作,但它并不会监视我们是否正确初始化了成员对象。所以对此必须特别加以留意。
3.1 确保正确的清除
Java 不具备象C++的“破坏器”那样的概念。在 C++中,一旦破坏(清除)一个对象,就会自动调用破坏器方法。之所以将其省略,大概是由于在Java中只需简单地忘记对象,不需强行破坏它们。垃圾收集器会在必要的时候自动回收内存。
垃圾收集器大多数时候都能很好地工作,但在某些情况下,我们的类可能在自己的存在时期采取一些行动,而这些行动要求必须进行明确的清除工作。我们并不知道垃圾收集器什么时候才会显身,或者说不知它何时会调用。所以一旦希望为一个类清除什么东西,必须写一个特别的方法,明确、专门地来做这件事情。同时,还要让客户程序员知道他们必须调用这个方法。而在所有这一切的后面,(违例控制)要详细解释的那样,必须将这样的清除代码置于一个 finally从句中,从而防范任何可能出现的违例事件。
下面介绍的是一个计算机辅助设计系统的例子,它能在屏幕上描绘图形:
package com.toad6;
importjava.util.*;
class Shape {
Shape(inti) {
System.out.println("Shapeconstructor");
}
void cleanup() {
System.out.println("Shapecleanup");
}
}
class Circleextends Shape {
Circle(inti) {
super(i);
System.out.println("Drawing aCircle");
}
void cleanup() {
System.out.println("Erasing aCircle");
super.cleanup();
}
}
class Triangleextends Shape {
Triangle(inti) {
super(i);
System.out.println("Drawing aTriangle");
}
void cleanup() {
System.out.println("Erasing aTriangle");
super.cleanup();
}
}
class Lineextends Shape {
privateintstart,end;
Line(intstart,intend) {
super(start);
this.start = start;
this.end = end;
System.out.println("Drawing a Line:" + start +", " +end);
}
void cleanup() {
System.out.println("Erasing a Line:" + start +", " +end);
super.cleanup();
}
}
publicclass CADSystemextends Shape {
private Circlec;
private Trianglet;
private Line[]lines =new Line[10];
CADSystem(inti) {
super(i + 1);
for (intj = 0; j < 10;j++)
lines[j] =new Line(j,j *j);
c =new Circle(1);
t =new Triangle(1);
System.out.println("Combinedconstructor");
}
void cleanup() {
System.out.println("CADSystem.cleanup()");
t.cleanup();
c.cleanup();
for (inti = 0; i <lines.length;i++)
lines[i].cleanup();
super.cleanup();
}
publicstaticvoid main(String[] args) {
CADSystemx =new CADSystem(47);
try {
// Code and exceptionhandling...
}finally {
x.cleanup();
}
}
} // /:~
输出如下:
Shapeconstructor
Shapeconstructor
Drawinga Line: 0, 0
Shapeconstructor
Drawinga Line: 1, 1
Shapeconstructor
Drawinga Line: 2, 4
Shapeconstructor
Drawinga Line: 3, 9
Shapeconstructor
Drawinga Line: 4, 16
Shapeconstructor
Drawinga Line: 5, 25
Shapeconstructor
Drawinga Line: 6, 36
Shapeconstructor
Drawinga Line: 7, 49
Shapeconstructor
Drawinga Line: 8, 64
Shapeconstructor
Drawinga Line: 9, 81
Shapeconstructor
Drawinga Circle
Shapeconstructor
Drawinga Triangle
Combinedconstructor
CADSystem.cleanup()
Erasinga Triangle
Shapecleanup
Erasinga Circle
Shapecleanup
Erasinga Line: 0, 0
Shapecleanup
Erasinga Line: 1, 1
Shapecleanup
Erasinga Line: 2, 4
Shapecleanup
Erasinga Line: 3, 9
Shapecleanup
Erasinga Line: 4, 16
Shapecleanup
Erasinga Line: 5, 25
Shapecleanup
Erasinga Line: 6, 36
Shapecleanup
Erasinga Line: 7, 49
Shapecleanup
Erasinga Line: 8, 64
Shapecleanup
Erasinga Line: 9, 81
Shapecleanup
Shapecleanup
这个系统中的所有东西都属于某种 Shape(几何形状)。Shape本身是一种 Object(对象),因为它是从根类明确继承的。每个类都重新定义了Shape 的cleanup()方法,同时还要用super 调用那个方法的基础类版本。尽管对象存在期间调用的所有方法都可负责做一些要求清除的工作,但对于特定的Shape 类——Circle(圆)、Triangle(三角形)以及Line(直线),它们都拥有自己的构建器,能完成“作图”(draw)任务。
每个类都有它们自己的cleanup()方法,用于将非内存的东西恢复回对象存在之前的景象。 在main()中,可看到两个新关键字:try和finally。其中,try关键字指出后面跟随的块(由花括号定界)是一个“警戒区”。也就是说,它会受到特别的待遇。其中一种待遇就是:该警戒区后面跟随的finally从句的代码肯定会得以执行——不管try块到底存不存在(通过违例控制技术,try 块可有多种不寻常的应用)。在这里,finally从句的意思是“总是为 x 调用cleanup(),无论会发生什么事情”。这些关键字将在第9 章进行全面、完整的解释。
在自己的清除方法中,必须注意对基础类以及成员对象清除方法的调用顺序——假若一个子对象要以另一个为基础。通常,应采取与C++编译器对它的“破坏器”采取的同样的形式:首先完成与类有关的所有特殊工作(可能要求基础类元素仍然可见),然后调用基础类清除方法。
许多情况下,清除可能并不是个问题;只需让垃圾收集器尽它的职责即可。但一旦必须由自己明确清除,就必须特别谨慎,并要求周全的考虑。
3.1.1 垃圾收集的顺序
垃圾收集器可能永远不会得到调用。即使得到调用,它也可能以自己愿意的任何顺序回收对象。除此以外,Java 1.0实现的垃圾收集器机制通常不会调用finalize()方法。除内存的回收以外,其他任何东西都最好不要依赖垃圾收集器进行回收。若想明确地清除什么,请制作
自己的清除方法,而且不要依赖finalize()。然而正如以前指出的那样,可强迫Java1.1 调用所有收尾模块(Finalizer)。
3.2 名字的隐藏
只有C++程序员可能才会惊讶于名字的隐藏,因为它的工作原理与在C++里是完全不同的。如果Java 基础类有一个方法名被“过载”使用多次,在衍生类里对那个方法名的重新定义就不会隐藏任何基础类的版本。所以无论方法在这一级还是在一个基础类中定义,过载都会生效。