友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
Java编程思想第4版[中文版](PDF格式)-第60部分
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!
情况下,结果产生的Shape 的Vector 都会打印出来。下面列出它某一次运行的结果:
》java CADState
'class Circle color'3' xPos'…51' yPos'…99' dim'38'
; class Square color'3' xPos'2' yPos'61' dim'…46'
; class Line color'3' xPos'51' yPos'73' dim'64'
; class Circle color'3' xPos'…70' yPos'1' dim'16'
; class Square color'3' xPos'3' yPos'94' dim'…36'
; class Line color'3' xPos'…84' yPos'…21' dim'…35'
; class Circle color'3' xPos'…75' yPos'…43' dim'22'
; class Square color'3' xPos'81' yPos'30' dim'…45'
; class Line color'3' xPos'…29' yPos'92' dim'17'
; class Circle color'3' xPos'17' yPos'90' dim'…76'
'
》java CADState CADState。out
'class Circle color'1' xPos'…51' yPos'…99' dim'38'
; class Square color'0' xPos'2' yPos'61' dim'…46'
; class Line color'3' xPos'51' yPos'73' dim'64'
; class Circle color'1' xPos'…70' yPos'1' dim'16'
; class Square color'0' xPos'3' yPos'94' dim'…36'
; class Line color'3' xPos'…84' yPos'…21' dim'…35'
; class Circle color'1' xPos'…75' yPos'…43' dim'22'
; class Square color'0' xPos'81' yPos'30' dim'…45'
; class Line color'3' xPos'…29' yPos'92' dim'17'
; class Circle color'1' xPos'17' yPos'90' dim'…76'
'
从中可以看出,xPos,yPos 以及dim 的值都已成功保存和恢复出来。但在获取 static信息时却出现了问
题。所有“3”都已进入,但没有正常地出来。Circle 有一个 1值(定义为 RED),而Square 有一个 0 值
(记住,它们是在构建器里初始化的)。看上去似乎static 根本没有得到初始化!实情正是如此——尽管类
Class 是“可以序列化的”,但却不能按我们希望的工作。所以假如想序列化static值,必须亲自动手。
这正是 Line 中的 serializeStaticState()和 deserializeStaticState()两个 static 方法的用途。可以看
到,这两个方法都是作为存储和恢复进程的一部分明确调用的(注意写入序列化文件和从中读回的顺序不能
改变)。所以为了使CADState。java 正确运行起来,必须采用下述三种方法之一:
(1) 为几何形状添加一个serializeStaticState()和 deserializeStaticState()。
(2) 删除Vector shapeTypes 以及与之有关的所有代码
(3) 在几何形状内添加对新序列化和撤消序列化静态方法的调用
要注意的另一个问题是安全,因为序列化处理也会将private 数据保存下来。若有需要保密的字段,应将其
标记成 transient。但在这之后,必须设计一种安全的信息保存方法。这样一来,一旦需要恢复,就可以重
设那些private 变量。
331
…………………………………………………………Page 333……………………………………………………………
10。10 总结
Java IO 流库能满足我们的许多基本要求:可以通过控制台、文件、内存块甚至因特网(参见第 15章)进行
读写。可以创建新的输入和输出对象类型(通过从 InputStream和 OutputStream 继承)。向一个本来预期为
收到字串的方法传递一个对象时,由于Java 已限制了“自动类型转换”,所以会自动调用toString()方
法。而我们可以重新定义这个 toString(),扩展一个数据流能接纳的对象种类。
在 IO 数据流库的联机文档和设计过程中,仍有些问题没有解决。比如当我们打开一个文件以便输出时,完全
可以指定一旦有人试图覆盖该文件就“掷”出一个违例——有的编程系统允许我们自行指定想打开一个输出
文件,但唯一的前提是它尚不存在。但在 Java 中,似乎必须用一个File 对象来判断某个文件是否存在,因
为假如将其作为FileOutputStream 或者FileWriter 打开,那么肯定会被覆盖。若同时指定文件和目录路
径,File 类设计上的一个缺陷就会暴露出来,因为它会说“不要试图在单个类里做太多的事情”!
IO流库易使我们混淆一些概念。它确实能做许多事情,而且也可以移植。但假如假如事先没有吃透装饰器方
案的概念,那么所有的设计都多少带有一点盲目性质。所以不管学它还是教它,都要特别花一些功夫才行。
而且它并不完整:没有提供对输出格式化的支持,而其他几乎所有语言的 IO包都提供了这方面的支持(这一
点没有在Java 1。1 里得以纠正,它完全错失了改变库设计方案的机会,反而增添了更特殊的一些情况,使复
杂程度进一步提高)。Java 1。1 转到那些尚未替换的 IO 库,而不是增加新库。而且库的设计人员似乎没有
很好地指出哪些特性是不赞成的,哪些是首选的,造成库设计中经常都会出现一些令人恼火的反对消息。
然而,一旦掌握了装饰器方案,并开始在一些较为灵活的环境使用库,就会认识到这种设计的好处。到那个
时候,为此多付出的代码行应该不至于使你觉得太生气。
10。11 练习
(1) 打开一个文本文件,每次读取一行内容。将每行作为一个 String 读入,并将那个String 对象置入一个
Vector 里。按相反的顺序打印出 Vector 中的所有行。
(2) 修改练习 1,使读取那个文件的名字作为一个命令行参数提供。
(3) 修改练习2,又打开一个文本文件,以便将文字写入其中。将 Vector 中的行随同行号一起写入文件。
(4) 修改练习2,强迫Vector 中的所有行都变成大写形式,将结果发给 System。out。
(5) 修改练习2,在文件中查找指定的单词。打印出包含了欲找单词的所有文本行。
(6) 在Blips。java 中复制文件,将其重命名为BlipCheck。java。然后将类Blip2 重命名为BlipCheck (在进
程中将其标记为public)。删除文件中的//!记号,并执行程序。接下来,将BlipCheck 的默认构建器变成
注释信息。运行它,并解释为什么仍然能够工作。
(7) 在Blip3。java 中,将接在“You must do this:”字样后的两行变成注释,然后运行程序。解释得到的
结果为什么会与执行了那两行代码不同。
(8) 转换SortedWordCount。java 程序,以便使用Java 1。1 IO 流。
(9) 根据本章正文的说明修改程序 CADState。java 。
(10) 在第 7 章(中间部分)找到GreenhouseControls。java 示例,它应该由三个文件构成。在
GreenhouseControls。java 中,Restart()内部类有一个硬编码的事件集。请修改这个程序,使其能从一个文
本文件里动态读取事件以及它们的相关时间。
332
…………………………………………………………Page 334……………………………………………………………
第 11 章 运行期类型鉴定
运行期类型鉴定(RTTI )的概念初看非常简单——手上只有基础类型的一个句柄时,利用它判断一个对象的
正确类型。
然而,对RTTI 的需要暴露出了面向对象设计许多有趣(而且经常是令人困惑的)的问题,并把程序的构造问
题正式摆上了桌面。
本章将讨论如何利用Java 在运行期间查找对象和类信息。这主要采取两种形式:一种是“传统”RTTI ,它假
定我们已在编译和运行期拥有所有类型;另一种是 Java1。1 特有的“反射”机制,利用它可在运行期独立查
找类信息。首先讨论“传统”的RTTI,再讨论反射问题。
11。1 对 RTTI 的需要
请考虑下面这个熟悉的类结构例子,它利用了多形性。常规类型是Shape 类,而特别衍生出来的类型是
Circle,Square 和Triangle 。
这是一个典型的类结构示意图,基础类位于顶部,衍生类向下延展。面向对象编程的基本目标是用大量代码
控制基础类型(这里是 Shape)的句柄,所以假如决定添加一个新类(比如Rhomboid ,从Shape 衍生),从
而对程序进行扩展,那么不会影响到原来的代码。在这个例子中,Shape 接口中的动态绑定方法是draw(),
所以客户程序员要做的是通过一个普通Shape 句柄调用draw()。draw()在所有衍生类里都会被覆盖。而且由
于它是一个动态绑定方法,所以即使通过一个普通的Shape 句柄调用它,也有表现出正确的行为。这正是多
形性的作用。
所以,我们一般创建一个特定的对象(Circle,Square,或者 Triangle ),把它上溯造型到一个Shape (忽
略对象的特殊类型),以后便在程序的剩余部分使用匿名 Shape 句柄。
作为对多形性和上溯造型的一个简要回顾,可以象下面这样为上述例子编码(若执行这个程序时出现困难,
请参考第3 章3。1。2 小节“赋值”):
//: Shapes。java
package c11;
import java。util。*;
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() {
System。out。println(〃Circle。draw()〃);
}
}
class Square implements Shape {
333
…………………………………………………………Page 335……………………………………………………………
public void draw() {
System。out。println(〃Square。draw()〃);
}
}
class Triangle implements Shape {
public void draw() {
System。out。println(〃Triangle。draw()〃);
}
}
public class Shapes {
public static void main(String'' args) {
Vector s = new Vector();
s。addElement(new Circle());
s。addElement(new Square());
s。addElement(new Triangle());
Enumeration e = s。elements();
while(e。hasMoreElements())
((Shape)e。nextElement())。draw();
}
} ///:~
基础类可编码成一个 interface (接口)、一个abstract (抽象)类或者一个普通类。由于Shape 没有真正
的成员(亦即有定义的成员),而且并不在意我们创建了一个纯粹的Shape 对象,所以最适合和最灵活的表
达方式便是用一个接口。而且由于不必设置所有那些abstract 关键字,所以整个代码也显得更为清爽。
每个衍生类都覆盖了基础类draw 方法,所以具有不同的行为。在main()中创建了特定类型的Shape,然后将
其添加到一个Vector。这里正是上溯造型发生的地方,因为Vector 只容纳了对象。由于Java 中的所有东西
(除基本数据类型外)都是对象,所以Vector 也能容纳 Shape 对象。但在上溯造型至 Object 的过程中,任
何特殊的信息都会丢失,其中甚至包括对象是几何形状这一事实。对Vect or 来说,它们只是Object。
用nextElement()将一个元素从 Vector 提取出来的时候,情况变得稍微有些复杂。由于 Vector 只容纳
Object,所以 nextElement()会自然地产生一个 Object 句柄。但我们知道它实际是个 Shape 句柄,而且希望
将Shape 消息发给那个对象。所以需要用传统的〃(Shape)〃方式造型成一个Shape。这是RTTI 最基本的形
式,因为在 Java 中,所有造型都会在运行期间得到检查,以确保其正确性。那正是RTTI 的意义所在:在运
行期,对象的类型会得到鉴定。
在目前这种情况下,RTTI 造型只实现了一部分:Object 造型成 Shape,而不是造型成Circle,Square 或者
Triangle 。那是由于我们目前能够肯定的唯一事实就是Vector 里充斥着几何形状,而不知它们的具体类别。
在编译期间,我们肯定的依据是我们自己的规则;而在编译期间,却是通过造型来肯定这一点。
现在的局面会由多形性控制,而且会为Shape 调用适当的方法,以便判断句柄到底是提供Circle,Square,
还是提供给 Triangle 。而且在一般情况下,必须保证采用多形性方案。因为我们希望自己的代码尽可能少知
道一些与对象的具体类型有关的情况,只将注意力放在某一类对象(这里是Shape)的常规信息上。只有这
样,我们的代码才更易实现、理解以及修改。所以说多形性是面向对象程序设计的一个常规目标。
然而,若碰到一个特殊的程序设计问题,只有在知道常规句柄的确切类型后,才能最容易地解决这个问题,
这个时候又该怎么办呢?举个例子来说,我们有时候想让自己的用户将某一具体类型的几何形状(如三角
形)全都变成紫色,以便突出显示它们,并快速找出这一类型的所有形状。此时便要用到RTTI 技术,用它查
询某个 Shape 句柄引用的准确类型是什么。
11。1。1 Class 对象
为理解RTTI 在 Java 里如何工作,首先必须了解类型信息在运行期是如何表示的。这时要用到一个名为
“Class 对象”的特殊形式的对象,其中包含了与类有关的信息(有时也把它叫作“元类”)。事实上,我
们要用 Class 对象创建属于某个类的全部“常规”或“普通”对象。
对于作为程序一部分的每个类,它们都有一个 Class 对象。换言之,每次写一个新类时,同时也会创建一个
334
…………………………………………………………Page 336……………………………………………………………
Class 对象(更恰当地说,是保存在一个完全同名的。class 文件中)。在运行期,一旦我们想生成那个类的
一个对象,用于执行程序的 Java 虚拟机(JVM)首先就会检查那个类型的Class 对象是否已经载入。若尚未
载入,JVM 就会查找同名的。class 文件,并将其载入。所以Java 程序启动时并不是完全载入的,这一点与许
多传统语言都不同。
一旦那个类型的Class 对象进入内存,就用它创建那一类型的所有对象。
若这种说法多少让你产生了一点儿迷惑,或者并没有真正理解它,下面这个示范程序或许能提供进一步的帮
助:
//: SweetShop。java
// Examination of the way the class loader works
class Candy {
static {
System。out。println(〃Loading Candy〃);
}
}
class Gum {
static {
System。out。println(〃Loading Gum〃);
}
}
class Cookie {
static {
System。out。println(〃Loading Cookie〃);
}
}
public class SweetShop {
public static void main(String'' args) {
System。out。println(〃inside main〃);
new Candy();
System。out。println(〃After creating Candy〃);
try {
Class。forName(〃Gum〃);
} catch(ClassNotFoundException e) {
e。printStackTrace();
}
System。out。println(
〃After Class。forName(”Gum”)〃);
new Cookie();
System。out。println(〃After creating Cookie〃);
}
} ///:~
对每个类来说(Candy,Gum 和Cookie),它们都有一个 static从句,用于在类首次载入时执行。相应的信
息会打印出来,告诉我们载入是什么时候进行的。在main()中,对象的创建代码位于打印语句之间,以便侦
测载入时间。
特别有趣的一行是:
Class。forName(〃Gum〃);
该方法是Class (即全部Class 所从属的)的一个 static成员。而 Class 对象和其他任何对象都是类似的,
335
…………………………………………………………Page 337……………………………………………………………
所以能够获取和控制它的一个句柄(装载模块就是干这件事的)。为获得 Class 的一个句柄,一个办法是使
用forName()。它的作用是取得包含了目标类文本名字的一个String (注意拼写和大小写)。最后返回的是
一个Class 句柄。
该程序在某个JVM 中的输出如下:
inside main
Loading Candy
After creating Candy
Loading Gum
After Class。forName(〃Gum〃)
Loading Cookie
After creating Cookie
可以看到,每个Class 只有在它需要的时候才会载入,而 static 初始化工作是在类载入时执行的。
非常有趣的是,另一个 JVM 的输出变成了另一个样子:
Loading Candy
Loading Cookie
inside main
After creating Candy
Loading Gum
After Class。forName(〃Gum〃)
After creating Cookie
看来JVM 通过检查main()中的代码,已经预测到了对Candy 和Cookie 的需要,但却看不到Gum,因为它是通
过对forName()的一个调用创建的,而不是通过更典型的new 调用。尽管这个JVM 也达到了我们希望的效
果,因为确实会在我们需要之前载入那些类,但却不能肯定这儿展示的行为百分之百正确。
1。 类标记
在Java 1。1 中,可以采用第二种方式来产生Class 对象的句柄:使用“类标记”。对上述程序来说,看起来
就象下面这样:
Gum。class;
这样做不仅更加简单,而且更安全,因为它会在编译期间得到检查。由于它取消了对方法调用的需要,所以
执行的效率也会更高。
类标记不仅可以应用于普通类,也可以应用于接口、数组以及基本数据类型。除此以外,针对每种基本数据
类型的封装器类,它还存在一个名为TYPE 的标准字段。TYPE 字段的作用是为相关的基本数据类型产生 Class
对象的一个句柄,如下所示:
。。。 is equivalent to 。。。
boolean。class Boolean。TYPE
char。class Character。TYPE
byte。class Byte。TYPE
short。class Short。TYPE
int。class Integer。TYPE
long。class Long。TYPE
float。class Float。TYPE
double。class Double。TYPE
void。class Void。TYPE
336
…………………………………………………………Page 338……………………………………………………………
11。1。2 造型前的检查
迄今为止,我们已知的 RTTI 形式包括:
(1) 经典造型,如〃(Shape)〃,它用 RTTI 确保造型的正确性,并在遇到一个失败的造型后产生一个
ClassCastException 违例。
(2) 代表对象类型的Class 对象。可查询Class 对象,获取有用的运行期资料。
在C++中,经典的〃(Shape)〃造型并不执行RTTI 。它只是简单地告诉编译器将对象当作新类型处理。而Java
要执行类型检查,这通常叫作“类型安全”的下溯造型。之所以叫“下溯造型”,是由于类分层结构的历史
排列方式造成的。若将一个Circle (圆)造型到一个Shape (几何形状),就叫做上溯造型,因为圆只
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!