友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
Java编程思想第4版[中文版](PDF格式)-第30部分
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!
一般情况下,一个 C 程序项目会在 50K 到 100K 行代码之间的某个地方开始中断。这是由于 C 仅有一个“命名
136
…………………………………………………………Page 138……………………………………………………………
空间”,所以名字会开始互相抵触,从而造成额外的管理开销。而在Java 中,package 关键字、包命名方案
以及import关键字为我们提供对名字的完全控制,所以命名冲突的问题可以很轻易地得到避免。
有两方面的原因要求我们控制对成员的访问。第一个是防止用户接触那些他们不应碰的工具。对于数据类型
的内部机制,那些工具是必需的。但它们并不属于用户接口的一部分,用户不必用它来解决自己的特定问
题。所以将方法和字段变成“私有”(private)后,可极大方便用户。因为他们能轻易看出哪些对于自己来
说是最重要的,以及哪些是自己需要忽略的。这样便简化了用户对一个类的理解。
进行访问控制的第二个、也是最重要的一个原因是:允许库设计者改变类的内部工作机制,同时不必担心它
会对客户程序员产生什么影响。最开始的时候,可用一种方法构建一个类,后来发现需要重新构建代码,以
便达到更快的速度。如接口和实施细节早已进行了明确的分隔与保护,就可以轻松地达到自己的目的,不要
求用户改写他们的代码。
利用Java 中的访问指示符,可有效控制类的创建者。那个类的用户可确切知道哪些是自己能够使用的,哪些
则是可以忽略的。但更重要的一点是,它可确保没有任何用户能依赖一个类的基础实施机制的任何部分。作
为一个类的创建者,我们可自由修改基础的实施细节,这一改变不会对客户程序员产生任何影响,因为他们
不能访问类的那一部分。
有能力改变基础的实施细节后,除了能在以后改进自己的设置之外,也同时拥有了“犯错误”的自由。无论
当初计划与设计时有多么仔细,仍然有可能出现一些失误。由于知道自己能相当安全地犯下这种错误,所以
可以放心大胆地进行更多、更自由的试验。这对自己编程水平的提高是很有帮助的,使整个项目最终能更
快、更好地完成。
一个类的公共接口是所有用户都能看见的,所以在进行分析与设计的时候,这是应尽量保证其准确性的最重
要的一个部分。但也不必过于紧张,少许的误差仍然是允许的。若最初设计的接口存在少许问题,可考虑添
加更多的方法,只要保证不删除客户程序员已在他们的代码里使用的东西。
5。6 练习
(1) 用public、private、protected 以及“友好的”数据成员及方法成员创建一个类。创建属于这个类的一
个对象,并观察在试图访问所有类成员时会获得哪种类型的编译器错误提示。注意同一个目录内的类属于
“默认”包的一部分。
(2) 用protected 数据创建一个类。在相同的文件里创建第二个类,用一个方法操纵第一个类里的
protected 数据。
(3) 新建一个目录,并编辑自己的 CLASSPATH,以便包括那个新目录。将P。class 文件复制到自己的新目
录,然后改变文件名、P 类以及方法名(亦可考虑添加额外的输出,观察它的运行过程)。在一个不同的目
录里创建另一个程序,令其使用自己的新类。
(4) 在 c05 目录(假定在自己的CLASSPATH 里)创建下述文件:
//: PackagedClass。java
package c05;
class PackagedClass {
public PackagedClass() {
System。out。println(
〃Creating a packaged class〃);
}
} ///:~
然后在 c05 之外的另一个目录里创建下述文件:
//: Foreign。java
package c05。foreign;
import c05。*;
public class Foreign {
public static void main (String'' args) {
PackagedClass pc = new PackagedClass();
}
137
…………………………………………………………Page 139……………………………………………………………
} ///:~
解释编译器为什么会产生一个错误。将Foreign (外部)类作为c05 包的一部分改变了什么东西吗?
138
…………………………………………………………Page 140……………………………………………………………
第 6 章 类再生
“Java 引人注目的一项特性是代码的重复使用或者再生。但最具革命意义的是,除代码的复制和修改以外,
我们还能做多得多的其他事情。”
在象C 那样的程序化语言里,代码的重复使用早已可行,但效果不是特别显著。与Java 的其他地方一样,这
个方案解决的也是与类有关的问题。我们通过创建新类来重复使用代码,但却用不着重新创建,可以直接使
用别人已建好并调试好的现成类。
但这样做必须保证不会干扰原有的代码。在这一章里,我们将介绍两个达到这一目标的方法。第一个最简
单:在新类里简单地创建原有类的对象。我们把这种方法叫作“合成”,因为新类由现有类的对象合并而
成。我们只是简单地重复利用代码的功能,而不是采用它的形式。
第二种方法则显得稍微有些技巧。它创建一个新类,将其作为现有类的一个“类型”。我们可以原样采取现
有类的形式,并在其中加入新代码,同时不会对现有的类产生影响。这种魔术般的行为叫作“继承”
(Inheritance),涉及的大多数工作都是由编译器完成的。对于面向对象的程序设计,“继承”是最重要的
基础概念之一。它对我们下一章要讲述的内容会产生一些额外的影响。
对于合成与继承这两种方法,大多数语法和行为都是类似的(因为它们都要根据现有的类型生成新类型)。
在本章,我们将深入学习这些代码再生或者重复使用的机制。
6。1 合成的语法
就以前的学习情况来看,事实上已进行了多次“合成”操作。为进行合成,我们只需在新类里简单地置入对
象句柄即可。举个例子来说,假定需要在一个对象里容纳几个 String 对象、两种基本数据类型以及属于另一
个类的一个对象。对于非基本类型的对象来说,只需将句柄置于新类即可;而对于基本数据类型来说,则需
在自己的类中定义它们。如下所示(若执行该程序时有麻烦,请参见第3 章3。1。2 小节“赋值”):
//: SprinklerSystem。java
// position for code reuse
package c06;
class WaterSource {
private String s;
WaterSource() {
System。out。println(〃WaterSource()〃);
s = new String(〃Constructed〃);
}
public String toString() { return s; }
}
public class SprinklerSystem {
private String valve1; valve2; valve3; valve4;
WaterSource source;
int i;
float f;
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);
139
…………………………………………………………Page 141……………………………………………………………
}
public static void main(String'' args) {
SprinklerSystem x = new SprinklerSystem();
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
// Constructor initialization with position
class Soap {
private String s;
Soap() {
System。out。println(〃Soap()〃);
s = new String(〃Constructed〃);
}
public String toString() { return s ; }
}
public class Bath {
private String
140
…………………………………………………………Page 142……………………………………………………………
// Initializing at point of definition:
s1 = new String(〃Happy〃);
s2 = 〃Happy〃;
s3; s4;
Soap castille;
int i;
float toy;
Bath() {
System。out。println(〃Inside Bath()〃);
s3 = new String(〃Joy〃);
i = 47;
toy = 3。14f;
castille = new Soap();
}
void print() {
// Delayed initialization:
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);
}
public static void main(String'' args) {
Bath b = new Bath();
b。print();
}
} ///:~
请注意在Bath 构建器中,在所有初始化开始之前执行了一个语句。如果不在定义时进行初始化,仍然不能保
证能在将一条消息发给一个对象句柄之前会执行任何初始化——除非出现不可避免的运行期违例。
下面是该程序的输出:
Inside Bath()
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 中继承。
141
…………………………………………………………Page 143……………………………………………………………
用于合成的语法是非常简单且直观的。但为了进行继承,必须采用一种全然不同的形式。需要继承的时候,
我们会说:“这个新类和那个旧类差不多。”为了在代码里表面这一观念,需要给出类名。但在类主体的起
始花括号之前,需要放置一个关键字extends,在后面跟随“基础类”的名字。若采取这种做法,就可自动
获得基础类的所有数据成员以及方法。下面是一个例子:
//: Detergent。java
// Inheritance syntax & properties
class Cleanser {
private String s = new String(〃Cleanser〃);
public void append(String a) { s += a; }
public void dilute() { append(〃 dilute()〃); }
public void apply() { append(〃 apply()〃); }
public void scrub() { append(〃 scrub()〃); }
public void print() { System。out。println(s); }
public static void main(String'' args) {
Cleanser x = new Cleanser();
x。dilute(); x。apply(); x。scrub();
x。print();
}
}
public class Detergent extends Cleanser {
// Change a method:
public void scrub() {
append(〃 Detergent。scrub()〃);
super。scrub(); // Call base…class version
}
// Add methods to the interface:
public void foam() { append(〃 foam()〃); }
// Test the new class:
public static void main(String'' args) {
Detergent x = new Detergent();
x。dilute();
x。apply();
x。scrub();
x。foam();
x。print();
System。out。println(〃Testing base class:〃);
Cleanser。main(args);
}
} ///:~
这个例子向大家展示了大量特性。首先,在Cleanser append()方法里,字串同一个 s 连接起来。这是用
“+=”运算符实现的。同“+”一样,“+=”被Java 用于对字串进行“过载”处理。
其次,无论 Cleanser 还是Detergent 都包含了一个main()方法。我们可为自己的每个类都创建一个
main()。通常建议大家象这样进行编写代码,使自己的测试代码能够封装到类内。即便在程序中含有数量众
多的类,但对于在命令行请求的public 类,只有main()才会得到调用。所以在这种情况下,当我们使用
“java Detergent”的时候,调用的是Degergent。main()——即使Cleanser 并非一个public 类。采用这种
将main()置入每个类的做法,可方便地为每个类都进行单元测试。而且在完成测试以后,毋需将main()删
去;可把它保留下来,用于以后的测试。
在这里,大家可看到Deteregent。main()对 Cleanser。main()的调用是明确进行的。
142
…………………………………………………………Page 144……………………………………………………………
需要着重强调的是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
// Constructor calls during inheritance
class Art {
Art() {
Syste
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!