友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
Java编程思想第4版[中文版](PDF格式)-第106部分
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!
+ g。weight());
}
public void visit(VCardboard c) {
cSum += c。weight();
System。out。println(〃weight of Cardboard = 〃
+ c。weight());
}
void total() {
System。out。println(〃Total weight Aluminum:〃
+ alSum);
System。out。println(〃Total weight Paper:〃
+ pSum);
System。out。println(〃Total weight Glass:〃
+ gSum);
System。out。println(〃Total weight Cardboard:〃
+ cSum);
}
}
public class TrashVisitor {
public static void main(String'' args) {
Vector bin = new Vector();
// ParseTrash still works; without changes:
ParseTrash。fillBin(〃VTrash。dat〃; bin);
// You could even iterate through
// a list of visitors!
617
…………………………………………………………Page 619……………………………………………………………
PriceVisitor pv = new PriceVisitor();
WeightVisitor wv = new WeightVisitor();
Enumeration it = bin。elements();
while(it。hasMoreElements()) {
Visitable v = (Visitable)it。nextElement();
v。accept(pv);
v。accept(wv);
}
pv。total();
wv。total();
}
} ///:~
注意main()的形状已再次发生了变化。现在只有一个垃圾(Trash)筒。两个Visitor 对象被接收到序列中
的每个元素内,它们会完成自己份内的工作。Visitor 跟踪它们自己的内部数据,计算出总重和价格。
最好,将东西从序列中取出的时候,除了不可避免地向Trash 造型以外,再没有运行期的类型验证。若在
Java 里实现了参数化类型,甚至那个造型操作也可以避免。
对比之前介绍过的双重派遣方案,区分这两种方案的一个办法是:在双重派遣方案中,每个子类创建时只会
过载其中的一个过载方法,即 add()。而在这里,每个过载的visit()方法都必须在 Visitor 的每个子类中进
行过载。
1。 更多的结合?
这里还有其他许多代码,Trash 结构和 Visitor 结构之间存在着明显的“结合”(Coupling )关系。然而,
在它们所代表的类集内部,也存在着高度的凝聚力:都只做一件事情(Trash 描述垃圾或废品,而Visitor
描述对垃圾采取什么行动)。作为一套优秀的设计方案,这无疑是个良好的开端。当然就目前的情况来说,
只有在我们添加新的Visitor 类型时才能体会到它的好处。但在添加新类型的Trash 时,它却显得有些碍手
碍脚。
类与类之间低度的结合与类内高度的凝聚无疑是一个重要的设计目标。但只要稍不留神,就可能妨碍我们得
到一个本该更出色的设计。从表面看,有些类不可避免地相互间存在着一些“亲密”关系。这种关系通常是
成对发生的,可以叫作“对联”(Couplet)——比如集合和继承器(Enumeration)。前面的Trash
Visitor 对似乎也是这样的一种“对联”。
16。8 RTTI 真的有害吗
本章的各种设计方案都在努力避免使用RTTI,这或许会给大家留下“RTTI 有害”的印象(还记得可怜的
goto 吗,由于给人印象不佳,根本就没有放到Java 里来)。但实际情况并非绝对如此。正确地说,应该是
RTTI 使用不当才“有害”。我们之所以想避免 RTTI 的使用,是由于它的错误运用会造成扩展性受到损害。
而我们事前提出的目标就是能向系统自由加入新类型,同时保证对周围的代码造成尽可能小的影响。由于
RTTI 常被滥用(让它查找系统中的每一种类型),会造成代码的扩展能力大打折扣——添加一种新类型时,
必须找出使用了RTTI 的所有代码。即使仅遗漏了其中的一个,也不能从编译器那里得到任何帮助。
然而,RTTI 本身并不会自动产生非扩展性的代码。让我们再来看一看前面提到的垃圾回收例子。这一次准备
引入一种新工具,我把它叫作TypeMap。其中包含了一个Hashtable (散列表),其中容纳了多个Vector,
但接口非常简单:可以添加(add())一个新对象,可以获得(get())一个Vector,其中包含了属于某种特
定类型的所有对象。对于这个包含的散列表,它的关键在于对应的Vector 里的类型。这种设计方案的优点
(根据Larry O'Brien 的建议)是在遇到一种新类型的时候,TypeMap 会动态加入一种新类型。所以不管什
么时候,只要将一种新类型加入系统(即使在运行期间添加),它也会正确无误地得以接受。
我们的例子同样建立在 c16。Trash 这个“包”(Package)内的Trash 类型结构的基础上(而且那儿使用的
Trash。dat 文件可以照搬到这里来)。
//: DynaTrash。java
// Using a Hashtable of Vectors and RTTI
// to automatically sort trash into
// vectors。 This solution; desp ite the
618
…………………………………………………………Page 620……………………………………………………………
// use of RTTI; is extensible。
package c16。dynatrash;
import c16。trash。*;
import java。util。*;
// Generic TypeMap works in any situation:
class TypeMap {
private Hashtable t = new Hashtable();
public void add(Object o) {
Class type = o。getClass();
if(t。containsKey(type))
((Vector)t。get(type))。addElement(o);
else {
Vector v = new Vector();
v。addElement(o);
t。put(type;v);
}
}
public Vector get(Class type) {
return (Vector)t。get(type);
}
public Enumeration keys() { return t。keys(); }
// Returns handle to adapter class to allow
// callbacks from ParseTrash。fillBin():
public Fillable filler() {
// Anonymous inner class:
return new Fillable() {
public void addTrash(Trash t) { add(t); }
};
}
}
public class DynaTrash {
public static void main(String'' args) {
TypeMap bin = new TypeMap();
ParseTrash。fillBin(〃Trash。dat〃;bin。filler());
Enumeration keys = bin。keys();
while(keys。hasMoreElements())
Trash。sumValue(
bin。get((Class)keys。nextElement()));
}
} ///:~
尽管功能很强,但对TypeMap 的定义是非常简单的。它只是包含了一个散列表,同时add()负担了大部分的
工作。添加一个新类型时,那种类型的Class 对象的句柄会被提取出来。随后,利用这个句柄判断容纳了那
类对象的一个Vector 是否已存在于散列表中。如答案是肯定的,就提取出那个 Vector,并将对象加入其
中;反之,就将Class 对象及新Vector 作为一个“键-值”对加入。
利用keys() ,可以得到对所有Class 对象的一个“枚举”(Enumeration),而且可用get(),可通过Class
对象获取对应的Vector。
filler()方法非常有趣,因为它利用了 ParseTrash。fillBin()的设计——不仅能尝试填充一个Vector,也能
用它的 addTrash()方法试着填充实现了 Fillable (可填充)接口的任何东西。filter() 需要做的全部事情就
是将一个句柄返回给实现了Fillable 的一个接口,然后将这个句柄作为参数传递给 fillBin(),就象下面这
619
…………………………………………………………Page 621……………………………………………………………
样:
ParseTrash。fillBin(〃Trash。dat〃; bin。filler());
为产生这个句柄,我们采用了一个“匿名内部类”(已在第7 章讲述)。由于根本不需要用一个已命名的类
来实现Fillable ,只需要属于那个类的一个对象的句柄即可,所以这里使用匿名内部类是非常恰当的。
对这个设计,要注意的一个地方是尽管没有设计成对归类加以控制,但在 fillBin()每次进行归类的时候,
都会将一个 Trash 对象插入 bin。
通过前面那些例子的学习,DynaTrash 类的大多数部分都应当非常熟悉了。这一次,我们不再将新的 Trash
对象置入类型Vector 的一个bin 内。由于bin 的类型为TypeMap,所以将垃圾(Trash)丢进垃圾筒(Bin)
的时候,TypeMap 的内部归类机制会立即进行适当的分类。在TypeMap 里遍历并对每个独立的 Vector 进行操
作,这是一件相当简单的事情:
Enumeration keys = bin。keys();
while(keys。hasMoreElements())
Trash。sumValue(
bin。get((Class)keys。nextElement()));
就象大家看到的那样,新类型向系统的加入根本不会影响到这些代码,亦不会影响TypeMap 中的代码。这显
然是解决问题最圆满的方案。尽管它确实严重依赖 RTTI,但请注意散列表中的每个键-值对都只查找一种类
型。除此以外,在我们增加一种新类型的时候,不会陷入“忘记”向系统加入正确代码的尴尬境地,因为根
本就没有什么代码需要添加。
16。9 总结
从表面看,由于象 TrashVisitor。java 这样的设计包含了比早期设计数量更多的代码,所以会留下效率不高
的印象。试图用各种设计方案达到什么目的应该是我们考虑的重点。设计范式特别适合“将发生变化的东西
与保持不变的东西隔离开”。而“发生变化的东西”可以代表许多种变化。之所以发生变化,可能是由于程
序进入一个新环境,或者由于当前环境的一些东西发生了变化(例如“用户希望在屏幕上当前显示的图示中
添加一种新的几何形状”)。或者就象本章描述的那样,变化可能是对代码主体的不断改进。尽管废品分类
以前的例子强调了新型Trash 向系统的加入,但TrashVisitor。java 允许我们方便地添加新功能,同时不会
对Trash 结构造成干扰。TrashVisitor。java 里确实多出了许多代码,但在 Visitor 里添加新功能只需要极
小的代价。如果经常都要进行此类活动,那么多一些代码也是值得的。
变化序列的发现并非一件平常事;在程序的初始设计出台以前,那些分析家一般不可能预测到这种变化。除
非进入项目设计的后期,否则一些必要的信息是不会显露出来的:有时只有进入设计或最终实现阶段,才能
体会到对自己系统一个更深入或更不易察觉需要。添加新类型时(这是“回收”例子最主要的一个重点),
可能会意识到只有自己进入维护阶段,而且开始扩充系统时,才需要一个特定的继承结构。
通过设计范式的学习,大家可体会到最重要的一件事情就是本书一直宣扬的一个观点——多形性是OOP (面
向对象程序设计)的全部——已发生了彻底的改变。换句话说,很难“获得”多形性;而一旦获得,就需要
尝试将自己的所有设计都造型到一个特定的模子里去。
设计范式要表明的观点是“OOP 并不仅仅同多形性有关”。应当与 OOP 有关的是“将发生变化的东西同保持
不变的东西分隔开来”。多形性是达到这一目的的特别重要的手段。而且假如编程语言直接支持多形性,那
么它就显得尤其有用(由于直接支持,所以不必自己动手编写,从而节省大量的精力和时间)。但设计范式
向我们揭示的却是达到基本目标的另一些常规途径。而且一旦熟悉并掌握了它的用法,就会发现自己可以做
出更有创新性的设计。
由于《Design Patterns》这本书对程序员造成了如此重要的影响,所以他们纷纷开始寻找其他范式。随着的
时间的推移,这类范式必然会越来越多。JimCoplien (http://bell…labs。/~cope 主页作者)向我们
推荐了这样的一些站点,上面有许多很有价值的范式说明:
http://st…cs。uiuc。edu/users/patterns
http://c2。/cgi/wiki
http://c2。/ppr
http://bell…labs。/people/cope/Patterns/Process/index。html
http://bell…labs。/cgi…user/OrgPatterns/OrgPatterns
http://st…cs。uiuc。edu/cgi…bin/wikic/wikic
http://cs。wustl。edu/~schmidt/patterns。html
620
…………………………………………………………Page 622……………………………………………………………
http ://espinc。/patterns/overview。html
同时请留意每年都要召开一届权威性的设计范式会议,名为PLOP 。会议会出版许多学术论文,第三届已在
1997 年底召开过了,会议所有资料均由 Addison…Wesley 出版。
16。10 练习
(1) 将SingletonPattern。java 作为起点,创建一个类,用它管理自己固定数量的对象。
(2) 为TrashVisitor。java 添加一个名为Plastic (塑料)的类。
(3) 为DynaTrash。java 同样添加一个Plastic (塑料)类。
621
…………………………………………………………Page 623……………………………………………………………
第 17 章 项目
本章包含了一系列项目,它们都以本书介绍的内容为基础,并对早期的章节进行了一定程度的扩充。
与以前经历过的项目相比,这儿的大多数项目都明显要复杂得多,它们充分演示了新技术以及类库的运用。
17。1 文字处理
如果您有C 或C++的经验,那么最开始可能会对Java 控制文本的能力感到怀疑。事实上,我们最害怕的就是
速度特别慢,这可能妨碍我们创造能力的发挥。然而,Java 对应的工具(特别是String 类)具有很强的功
能,就象本节的例子展示的那样(而且性能也有一定程度的提升)。
正如大家即将看到的那样,建立这些例子的目的都是为了解决本书编制过程中遇到的一些问题。但是,它们
的能力并非仅止于此。通过简单的改造,即可让它们在其他场合大显身手。除此以外,它们还揭示出了本书
以前没有强调过的一项Java 特性。
17。1。1 提取代码列表
对于本书每一个完整的代码列表(不是代码段),大家无疑会注意到它们都用特殊的注释记号起始与结束
('//:'和'///:~')。之所以要包括这种标志信息,是为了能将代码从本书自动提取到兼容的源码文件中。
在我的前一本书里,我设计了一个系统,可将测试过的代码文件自动合并到书中。但对于这本书,我发现一
种更简便的做法是一旦通过了最初的测试,就把代码粘贴到书中。而且由于很难第一次就编译通过,所以我
在书的内部编辑代码。但如何提取并测试代码呢?这个程序就是关键。如果你打算解决一个文字处理的问
题,那么它也很有利用价值。该例也演示了String 类的许多特性。
我首先将整本书都以ASCII 文本格式保存成一个独立的文件。CodePackager 程序有两种运行模式(在
usageString 有相应的描述):如果使用…p 标志,程序就会检查一个包含了ASCII 文本(即本书的内容)的
一个输入文件。它会遍历这个文件,按照注释记号提取出代码,并用位于第一行的文件名来决定创建文件使
用什么名字。除此以外,在需要将文件置入一个特殊目录的时候,它还会检查package 语句(根据由
package 语句指定的路径选择)。
但这样还不够。程序还要对包(package)名进行跟踪,从而监视章内发生的变化。由于每一章使用的所有包
都以c02,c03,c04 等等起头,用于标记它们所属的是哪一章(除那些以 起头的以外,它们在对不同的
章进行跟踪的时候会被忽略)——只要每一章的第一个代码列表包含了一个package,所以CodePackager 程
序能知道每一章发生的变化,并将后续的文件放到新的子目录里。
每个文件提取出来时,都会置入一个SourceCodeFile 对象,随后再将那个对象置入一个集合(后面还会详尽
讲述这个过程)。这些 SourceCodeFile 对象可以简单地保存在文件中,那正是本项目的第二个用途。如果直
接调用 CodePackager,不添加…p 标志,它就会将一个“打包”文件作为输入。那个文件随后会被提取(释
放)进入单独的文件。所以…p 标志的意思就是提取出来的文件已被“打包”(packed)进入这个单一的文
件。
但为什么还要如此麻烦地使用打包文件呢?这是由于不同的计算机平台用不同的方式在文件里保存文本信
息。其中最大的问题是换行字符的表示方法;当然,还有可能存在另一些问题。然而,Java 有一种特殊类型
的IO 数据流——DataOutputStream——它可以保证“无论数据来自何种机器,只要使用一个
DataInputStream 收取这些数据,就可用本机正确的格式保存它们”。也就是说,Java 负责控制与不同平台
有关的所有细节,而这正是Java 最具魅力的一点。所以…p 标志能将所有东西都保存到单一的文件里,并采
用通用的格式。用户可从Web 下载这个文件以及 Java 程序,然后对这个文件运行CodePackager,同时不指
定…p 标志,文件便会释放到系统中正确的场所(亦可指定另一个子目录;否则就在当前目录创建子目录)。
为确保不会留下与特定平台有关的格式,凡是需要描述一个文件或路径的时候,我们就使用 File 对象。除此
以外,还有一项特别的安全措施:在每个子目录里都放入一个空文件;那个文件的名字指出在那个子目录里
应找到多少个文件。
下面是完整的代码,后面会对它进行详细的说明:
//: CodePackager。java
// 〃Packs〃 and 〃unpacks〃 the code in 〃Thinking
// in Java〃 for cross…platform distribution。
622
…………………………………………………………Page 624……………………………………………………………
/* mented so CodePackager sees it and starts
a new chapter directory; but so you don't
have to worry about the directory where this
program lives:
package c17;
*/
import java。util。*;
import java。io。*;
class Pr {
static void error(String e) {
System。err。println(〃ERROR: 〃 + e);
System。exit(1);
}
}
class IO {
static BufferedReader disOpen(File f) {
BufferedReader in = null;
try {
in = new BufferedReader(
new FileReader(f));
} catch(IOException e) {
Pr。error(〃could not open 〃 + f);
}
return in;
}
static BufferedReader disOp
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!