Free Essay

Public Policy

In:

Submitted By JIH75
Words 180792
Pages 724
《T hi nki ng I n J ava》中文版
作者:Br uce Eckel 主页:ht t p: //w w Br uceEckel . com w. 编译:Tr ans Bot 主页:ht t p: //m ber . net eas e. com r ans bot em /~t 致谢 --献给那些直到现在仍在孜孜不倦创造下一代计算机语言的人们! 指导您利用万维网的语言进行面向对象的程序设计 完整的正文、更新内容及程序代码可从 ht t p: //w w br uceeckel . com w. 下载 从 Java 的基本语法到它最高级的特性(网络编程、高级面向对象能力、多线程),《T hi nki ng I n Java》都 能对您有所裨益。Br uce Eckel 优美的行文以及短小、精悍的程序示例有助于您理解含义模糊的概念。 面向初学者和某种程度的专家 教授 Java 语言,而不是与平台有关的理论 覆盖 Java 1. 2 的大多数重要方面:Sw ng和新集合 i 系统讲述 Java 的高级理论:网络编程、多线程处理、虚拟机性能以及同非 Java 代码的连接 320 个有用的 Java 程序,15000 行以上代码 解释面向对象基本理论,从继承到设计方案 来自与众不同的获奖作者 Br uce Eckel 可通过万维网免费索取源码和持续更新的本书电子版 从 w w Br uceEckel . com w. 获得配套 C (含 15 小时以上的合成语音授课) D 读者如是说:“最好的 Java 参考书⋯⋯绝对让人震惊”;“购买 Java 参考书最明智的选择”;“我见过的 最棒的编程指南”。 Br uce Eckel 也是《T hi nki ng i n C ++》的作者,该书曾获 1995 年 Sof t w eD ar evel opm Jol t A ar d最佳书 ent w 籍大奖。作为一名有 20 经验的编程专家,曾教授过世界上许多地区的人进行对象编程。最开始涉及的领域是 C ++,现在也进军 Java。他是 C ++标准协会有表决权的成员之一,曾就面向对象程序设计这一主题写过其他 5 本书,发表过 150 多篇文章,并是多家计算机杂志的专栏作家,其中包括《W Techni ques 》的 Java 专栏。 eb 曾出席过 C ++和 Java 的“软件开发者会议”,并分获“应用物理”与“计算机工程”的学士和硕士学位。 读者的心声 比我看过的 Java 书好多了⋯⋯非常全面,举例都恰到好处,显得颇具“智慧”。和其他许多 Java 书 籍相比,我觉得它更成熟、连贯、更有说服力、更严谨。总之,写得非常好,肯定是一本学习 Java 的好书。(A ol y Vor obey,Techni onUni ver s i t y,Hai f a,以色列)。 nat 是我见过的最好的编程指南,对任何语言都不外如是。(Joaki m z i egl er ,FI X系统管理员) 感谢你写出如此优秀的一本 Java 参考书。(D . G n Pi l l ay,Regi s t r ar ,Ki ng Edw d VI I r avi ar Hos pi t al ,南非) 再次感谢您这本令人震惊的书。我以前真的有点儿不知所从的感觉(因为不是 C程序员),但你的书 浅显易懂,使我能很快掌握 Java——差不多就是阅读的速度吧。能从头掌握基本原理和概念的感觉 真好,再也不用通过不断的试验和出错来建立概念模型了。希望不久能有机会参加您的讲座。 (Randal l R. Haw ey,A om i on Techni ci an,El i Li l l y & C l ut at o) 我迄今为止看过的最好的计算机参考书。(Tom Hol l and ) 这是我读过的关于程序设计的最好的一本书⋯⋯第 16 章有关设计方案的内容是我这么久以来看过的

最有价值的。(Han Fi nci ,助教,计算机科学学院,耶路撒冷希伯来大学,以色列) 有史以来最好的一本 Java 参考书。(Ravi ndr a Pai ,O acl e 公司 SUN S 产品线) r O 这是关于 Java 的一本好书。非常不错,你干得太好了!书中涉及的深度真让人震惊。一旦正式出 版,我肯定会买下它。我从 96 年十月就开始学习 Java 了。通过比较几本书,你的书可以纳入“必 读”之列。这几个月来,我一直在搞一个完全用 Java 写的产品。你的书巩固了我一些薄弱的地方, 并大大延伸了我已知的东西。甚至在会见承包商的时候,我都引用了书中的一些解释,它对我们的开 发小组太有用了。通过询问组内成员我从书中学来的知识(比如数组和矢量的区别),可以判断他们 对 Java 的掌握有多深。(St eve W l ki ns on,M I 通信公司资深专家) i C 好书!我见过的最好的一本 Java 教材。(Jef f Si ncl ai r ,软件工程师,Kes t r al C put i ng公司) om 感谢你的《T hi nki ng i n Java》。终于有人能突破传统的计算机参考书模式,进入一个更全面、更深 入的境界。我读过许多书,只有你的和 Pat r i ck W ns t on 的书才在我心目中占据了一个位置。我已向 i 客户郑重推荐这本书。再次感谢。(Ri char d Br ooks ,Java 顾问,Sun专业服务公司,达拉斯市) 其他书讨论的都是 Java“是什么”(讲述语法和库),或者 Java“怎样用”(编程实例)。 《T hi nki ng i n Java 》显然与众不同,是我所知唯一一本解释 Java“为什么”的书:为什么象这样 设计,为什么象这样工作,为什么有时不能工作,为什么比 C ++好,为什么没有 C ++好,等等。尽管 这本书也很好讲述了“是什么”和“怎样用”的问题,但它的特色并在于此。这本书特别适合那些想 追根溯源的人。(Rober t S. St ephens on) 感谢您写出这么一本优秀的书,我对它越来越爱不释手。我的学生也喜欢它。(C huck I ver s on) 向你在《Thi nki ng i n Java》的工作致敬。这本书对因特网的未来进行了最恰当的揭示,我只是想对 你说声“谢谢”,它非常有价值。(Pat r i ck Bar r el l ,N w k O f i cer M co Q F M g公司) et or f am - A f 市面上大多数 Java 书作为新手指南都是不错的。但它们的立意大多雷同,举的例子也有过多的重 复。从未没见过象您这样的一本书,它和那些书完全是两码事。我认为它是迄今为止最好的一本参考 书。请快些出版它!⋯⋯另外,由于《T hi nki ng i n Java》都这么好了,我也赶快去买了一本 《Thi nki ng i n C ++》。(G ge Laf r am s e,Li ght W x 技术咨询公司) eor boi or 从前给你写过信,主要是表达对《T hi nki ng i n C ++》一书的惊叹(那本书在我的书架上占有突出的 位置)。今天,我很欣慰地看到你投向了 Java 领域,并有幸拜读了最新的《T hi nki ng i n Java 》电 子版。看过之后,我不得不说:“服了!”内容非常精彩,有很强的说服力,不象读那些干巴巴的参 考书。你讲到了 Java 开发最重要、也最易忽略的方面:基本原理。(Sean Br ady) 你举的例子都非常浅显,很容易理解。Java 的许多重要细节都照顾到了,而单薄的 Java 文档根本没 有涉及那些方面。另外,这本书没有浪费读者的时间。程序员已经知道了一些基本的事实,你在这个 基础上进行了很好的发挥。(Kai Enger t ,I nnovat i ve Sof t w e 公司,德国) ar 我是您的《T hi nki ng i n C ++》的忠实读者。通读了您的 Java 书的电子版以后,发现您在这两本书上 有同样高级别的写作水平。谢谢!(Pet er R. N al d euw ) 写得非常好的一本 Java 书⋯⋯我认为您的工作简直可以说“伟大”。我是芝加哥地区 Java 特别兴趣 组的头儿,已在最近的几次聚会上推荐了您的书和 W eb站点。以后每个月开 SI G会的时候,我都想把 《T hi nki ng i n Java 》作为基本的指导教材使用。一般来说,我们会每次讨论书中的一章内容。 (M k Er t es ) ar 衷心感谢你的书,它写得太好了。我已把它推荐给自己的用户和 Ph. D 学生。(Hugues . Ler oy//I r i s a- I nr i a Rennes Fr ance,Head of Sci ent i f i c C put i ngand I ndus t r i al Tr anf er t ) om 2

我到现在只读了《Thi nki ng i n Java》的 40 页内容,但已对它留下了深刻的印象。这无疑是见过的 最精彩的编程专业书⋯⋯而且我本身就是一个作家,所以这点儿看法还是有些权威吧。我已订购了 《T hi nki ng i n C ++》,已经等得迫不及待了——我是一名编程新手,最怕的就是散乱无章的学习线 索。所以必须在这里向您的出色工作表示敬意。以前看过的书似乎都有这方面的毛病,经常使我才提 起的兴致消弥于无形。但看了你的书以后,感觉好多了。(G enn Becker ,Educat i onal Theat r e l A s oci at i on) s 谢谢您这本出色的书。在终于认识了 Java 与 C ++之间纠缠不清的一些事实后,我真的要非常感谢这 本书。对您的书非常满意!(Fel i x Bi zaoui ,Tw n O I ndus t r i es ,Loui s a,Va) i aks 恭喜你写出这么好的一本书。我是在有了阅读《T hi nki ng i n C ++》的经历以后,才来看这本 《T hi nki ng i n Java 》的,它确实没让我失望。(Jaco van der M w ,软件专家,D aFus i on er e at Sys t em 有限公司,St el l enbos ch,南非) s 这是我看过的最好的 Java 书之一。(E. E. Pr i t char d,资深软件工程师,英国剑桥动画系统有限公 司) 你的东东让其他 Java 参考收黯然失色。看来其他作者都应该向你看齐了。(Br et t g Por t er ,资深 程序员,A t & Logi c r ) 我花了一、两个星期的时间来看你的书,并对以前我看过的一些 Java 书进行了比较。显然,只有你 的书才能让我真正“入门”。现在,我已向我的许多朋友推荐了这本书,他们都对其作出了很高的评 价。请接受我真诚的祝贺,并希望她早些正式出版。(Ram Kr i s hna Bhupat hi ,软件工程师,TC a SI 公司,圣琼斯) 这是一本充满智慧的书,与简单的参考书有着截然不同的风格。它现在已成了我进行 Java 创作一份 主要参考。你的目录做得相当不错,让人一目了然,很快就能找到自己需要的东西。更高兴的是,这 本书没有写成一本改头换面的 A 字典,也没有把我们这些程序员看作傻瓜。(G ant Sayer ,Java PI r C ponent s G oup Leader ,C om r eedat a Sys t em Pt y 有限公司,澳大利亚) s 啧啧,一本可读性强、论据充分的 Java 书。外面有太多用词贫乏的 Java 书(也有几本好的),只有 你的书是最好的。那些垃圾在你的书前面不值一提。(John Root ,W eb开发员,伦敦社会安全部) 我刚刚开始看《T hi nki ng i n Java》。我希望它能有更大的突破,因为《T hi nki ng i n C ++》写得实 在太好了。我是一名有经验的 C ++程序员,事先看那本书对学习 Java 很有帮助。但我在 Java 上的经 验不够,希望这本新书能让我满意。您真是一名“高产高质”作者。(Kevi n K. Lew s , i O ect Space 公司技术员) bj 我认为这是本好书。从这本书中,我学到了与 Java 有关的所有知识。谢谢你能让这本书通过互联网 免费发行。如果不那样做,我根本不可能象现在这样有巨大的进步。但最令人高兴的是,你的书并没 有成为一本官方 Java 手册,指出了 Java 一些不当的地方。你真是做了一件大好事。(Fr eder i k Fi x, Bel gi um ) 我现在经常查阅你的书。大约两年前,当我想开始学习 C ++的时候,是《C I ns i de& ut 》指导我游 ++ O 历C ++的世界。它使我在这方面的技能大增,并找到了一个较好的职位。现在出于工作上的原因需要 学习 Java,又是《T hi nki ng i n Java 》给我正确的指引。尽管现在可选择的书更多了,但我知道自 己别无选择。很奇妙,不是吗?现在看这本书的时候,我居然有一种重新认识自己的感觉。衷心感谢 你,我现在的理解又比以前深入多了。(A nand Kum S. ,软件工程师,C put er vi s i on 公司,印 ar om 度) 你的书给人一种“鹤立鸡群”的感觉。(Pet er Robi ns on,剑桥大学计算机实验室) 3

这是我看过的最好的一本 Java 参考书。现在想起来,能找到这样的一本书简直是幸运。谢谢! (C huck Pet er s on,因特网产品线主管,I VI S I nt er nat i onal 公司) 这本书太棒了!它已是我看过的第三本 Java 书了,真后悔没有早点儿发现它。前两本书都没坚持看 完,但我已决心看完这一本。不妨告诉你,当时我是想寻找关于内部类使用的一些资料,是我的朋友 告诉我网上能下载这本书。你干得真不错!(Jer r y N l i n,M S,Lucent Technol ogi es ) ow T 在我看过的 6 本 Java 书中,你的《T hi nki ng i n Java 》是最好和最有用的。(M chael Van W , i aas Ph. D,TM A s oci at es 公司总裁) R s 我很想对《T hi nki ng i n Java》说声谢谢。这是一本多么出色的书——并不单指它在网上免费发送! 作为一名学生,我认为你的书有不可估量的价值(我有《C I ns i de& ut 》的拷贝,那是关于 C ++ O ++的 另一本好书),因为它不仅教我怎样做,而且解释了为什么。这当然为我用 C ++或 Java 这样的语言 编程打下了坚实的基础。我有许多朋友都象我一样热爱编程,在向他们推荐了这本书后,反映都非常 好,他们的看法同我一样。再次感谢您。顺便提一句,我是一个印尼畜牲,整天都喜欢和 Java 泡在 一起!(Ray Fr eder i ck D aj adi nat a j ,Tr i s akt i 大学学生,I ndones i an Por k) 你把这本书放在网上引起了相当程度的轰动,我对你的做法表示真诚的感谢与支持!(Shane LeBout hi l l i er ,加拿大艾伯特大学计算机工程系学生) 告诉你吧,我是多么热烈地盼望读到你每个月的专栏!作为 O P 设计的新手,我要感谢你把即使最基 O 本的概念都讲得那么透彻和全面。我已下载了你的书,但我保证会在它正式出版后另行购买。感谢你 提供的所有帮助!(D C hm ,B. C Zi egl er & C ) an as er . o. 祝贺你完成了一件伟大的作品。我现在下载的是《Thi nki ng i n Java》的 PD 版。这本书还没有读 F 完,便迫不及待地跑到书店去找你的《T hi nki ng i n C ++》。我在计算机界干了 8 年,是一个顾问, 兼软件工程师、教师/培训专家,最近辞职自己开了一间公司。所以见过不少的书。但是,正是这些 书使我的女朋友称我为“书呆子”!并不是我概念掌握得不深入——只是由于现在的发展太快,使我 短期内不能适应新技术。但这两本书都给了我很大的启示,它与以前接触过或买过的计算机参考书都 大不相同。写作风格很棒,每个新概念都讲得很好,书中充满了“智慧”。(Si m G and, on ol s i m ez@ m t t . com ons s ar ,Si m Says C ul t i ng公司) on ons 必须认为你的《T hi nki ng i n Java》非常优秀!那正是我一直以来梦想的参考书。其中印象最深的是 有关使用 Java 1. 1 作软件设计时的一些优缺点分析。(D r kD i uehr ,Lexi kon Ver l ag,Ber t el smnn a A ,德国) G 谢谢您写出两本空前绝后的书(《T hi nki ng i n Java》和《T hi nki ng i n C ++》)。它们使我在面向 对象的程序设计上跨出了一大步。(D onal d Law on,D LEnt er pr i s es) s C 谢谢你花时间写出一本真正有用的 Java 参考书,你现在绝对能为自己的工作感到骄傲了。(D i ni c om Tur ner ,G C Suppor t ) EA 这是我见过的最好的一本 Java 书。(Jean- Yves M G N EN A T,C ef Sof t w e A chi t ect N Thi ar r A SYSTEM ,法国巴黎) 《T hi nki ng i n Java 》无论在覆盖的范围还是讲述方法上都有独到之处。看懂这本书非常容易,摘录 的代码段也很有说服力。(Ron C han,Ph. D,Exper t C ce 公司,Pi t t s bur gh PA) hoi 你的书太棒了。我看过许多编程书刊,只有你的书给人一种全新的视野。其他作者都该向你好好学习 才是。(N ngj i an W i ang,信息系统工程师,The Vangur ad G oup) r

4

《T hi nki ng i n Java 》是一本出色的、可读性极强的书,我已向我的学生推荐阅读。(D . Pual r G m or an,计算机科学系,O ago大学,D t unedi n 市,新西兰) 在我看过的书中,你的书最有品味,不象有的书那样粗制滥造。任何搞软件开发的人都不应错过。 (Jos e Sur i ol ,Scyl ax 公司) 感谢您免费提供这本书,它是我看过或翻过的最好的一本。(Jef f Lapchi ns ky,N Res ul t s et Technol ogi es 公司程序员) 这本书简明扼要,看起来不仅毫不费力,而且象是一种享受。(Kei t h Ri t chi e,Java 研发组,KL G oup公司) r 这真的是我看过的最好的一本 Java 书!(D el Eng) ani 我看过的最好的 Java 书!(Ri ch Hof f ar t h,Seni or A chi t ect ,W t G oup) r es r 感谢你这本出色的书籍,我好久都没有经历让人如此愉悦的阅读过程了。(Fr ed Tr i m e ct i um bl ,A 公司) 你的写作能准确把握轻重缓急,并能成功抓住细节。这本书让学习变成了一件有趣的事情,我感觉满 意,非常满意!谢谢你这本出色的学习教程。(Raj es h Rau,软件顾问) 《T hi nki ng i n Java 》让整个自由世界都感受到了震憾!(M ko O Sul l i van,I docs 公司总裁) i ' 关于《T hi nki ng i n C ++》: 荣获 1995 年由《软件开发》杂志评选的“最佳书籍”奖! “这本书可算一个完美的典型。把它放到自己的书架上绝对不会后悔。关于 I O数据流的那部分内容 包含了迄今为止我看过的最全面、最容易理解的文字。”(A St evens ,《道伯博士》杂志投稿编 l 辑) “Eckel 的书是唯一一本清楚解释了面向对象程序设计基础问题的书。这本书也是 C ++的一本出色教 材。”(A ew Bi ns t ock,《Uni x Revi ew ndr 》编辑)” “Br uce 用他对 C ++深刻的洞察力震惊了我们,《T hi nki ng i n C ++》无疑是各种伟大思想的出色组 合。如果想得到各种困难的 C ++问题的答案,请购买这本杰出的参考书”(G y Ent s m nger ,《对 ar i 象之道》的作者) “《T hi nki ng i n C ++》非常耐心和有技巧地讲述了关于 C ++的各种问题,包括如何使用内联、索 引、运算符过载以及动态对象。另外还包括一些高级主题,比如模板的正确使用、违例和多重继承 等。所有这些都精巧地编织在一起,成为 Eckel 独特的对象和程序设计思想。所有 C ++开发者的书架 上都应摆上这本书。如果你正在用 C ++搞正式开发,这本书绝对有借鉴价值。”(Ri char d Hal e Shaw ,《PC M agaz i ne》投稿编辑)。

5

写在前面的话
我的兄弟 T od d目前正在进行从硬件到编程领域的工作转变。我曾提醒他下一次大革命的重点将是遗传工程。 我们的微生物技术将能制造食品、燃油和塑料;它们都是清洁的,不会造成污染,而且能使人类进一步透视 物理世界的奥秘。我认为相比之下电脑的进步会显得微不足道。 但随后,我又意识到自己正在犯一些科幻作家常犯的错误:在技术中迷失了(这种事情在科幻小说里常有发 生)!如果是一名有经验的作家,就知道绝对不能就事论事,必须以人为中心。遗传对我们的生命有非常大 的影响,但不能十分确定它能抹淡计算机革命——或至少信息革命——的影响。信息涉及人相互间的沟通: 的确,汽车和轮子的发明都非常重要,但它们最终亦如此而已。真正重要的还是我们与世界的关系,而其中 最关键的就是通信。 这本书或许能说明一些问题。许多人认为我有点儿大胆或者稍微有些狂妄,居然把所有家当都摆到了 W eb 上。“这样做还有谁来买它呢?”他们问。假如我是一个十分守旧的人,那么绝对不这样干。但我确实不想 再沿原来的老路再写一本计算机参考书了。我不知道最终会发生什么事情,但的确认为这是我对一本书作出 的最明智的一个决定。 至少有一件事是可以肯定的,人们开始向我发送纠错反馈。这是一个令人震惊的体验,因为读者会看到书中 的每一个角落,并揪出那些藏匿得很深的技术及语法错误。这样一来,和其他以传统方式发行的书不同,我 就能及时改正已知的所有类别的错误,而不是让它们最终印成铅字,堂而皇之地出现在各位的面前。俗话 说,“当局者迷,旁观者清”。人们对书中的错误是非常敏感的,往往毫不客气地指出:“我想这样说是错 误的,我的看法是⋯⋯”。在我仔细研究后,往往发现自己确实有不当之处,而这是当初写作时根本没有意 识到的(检查多少遍也不行)。我意识到这是群体力量的一个可喜的反映,它使这本书显得的确与众不同。 但我随之又听到了另一个声音:“好吧,你在那儿放的电子版的确很有创意,但我想要的是从真正的出版社 那里印刷的一个版本!”事实上,我作出了许多努力,让它用普通打印机机就能得到很好的阅读效果,但仍 然不象真正印刷的书那样正规。许多人不想在屏幕上看完整本书,也不喜欢拿着一叠纸阅读。无论打印格式 有多么好,这些人喜欢是仍然是真正的“书”(激光打印机的墨盒也太贵了一点)。现在看来,计算机的革 命仍未使出版界完全走出传统的模式。但是,有一个学生向我推荐了未来出版的一种模式:书籍将首先在互 联网上出版,然后只有在绝对必要的前提下,才会印刷到纸张上。目前,为数众多的书籍销售都不十分理 想,许多出版社都在亏本。但如采用这种方式出版,就显得灵活得多,也更容易保证赢利。 这本书也从另一个角度也给了我深刻的启迪。我刚开始的时候以为 Java“只是另一种程序设计语言”。这个 想法在许多情况下都是成立的。但随着时间的推移,我对它的学习也愈加深入,开始意识到它的基本宗旨与 我见过的其他所有语言都有所区别。 程序设计与对复杂性的操控有很大的关系:对一个准备解决的问题,它的复杂程度取决用于解决它的机器的 复杂程度。正是由于这一复杂性的存在,我们的程序设计项目屡屡失败。对于我以前接触过的所有编程语 言,它们都没能跳过这一框框,由此决定了它们的主要设计目标就是克服程序开发与维护中的复杂性。当 然,许多语言在设计时就已考虑到了复杂性的问题。但从另一角度看,实际设计时肯定会有另一些问题浮现 出来,需把它们考虑到这个复杂性的问题里。不可避免地,其他那些问题最后会变成最让程序员头痛的。例 如,C ++必须同 C保持向后兼容(使 C程序员能尽快地适应新环境),同时又要保证编程的效率。C ++在这两 个方面都设计得很好,为其赢得了不少的声誉。但它们同时也暴露出了额外的复杂性,阻碍了某些项目的成 功实现(当然,你可以责备程序员和管理层,但假如一种语言能通过捕获你的错误而提供帮助,它为什么不 那样做呢?)。作为另一个例子,Vi s ual Bas i c(VB)同当初的 BA C有关的紧密的联系。而 BA C并没有 SI SI 打算设计成一种能全面解决问题的语言,所以堆加到 VB 身上的所有扩展都造成了令人头痛和难于管理和维护 的语法。另一方面,C ++、VB 和其他如 Sm l t al k 之类的语言均在复杂性的问题上下了一番功夫。由此得到 al 的结果便是,它们在解决特定类型的问题时是非常成功的。 在理解到 Java 最终的目标是减轻程序员的负担时,我才真正感受到了震憾,尽管它的潜台词好象是说:“除 了缩短时间和减小产生健壮代码的难度以外,我们不关心其他任何事情。”在目前这个初级阶段,达到那个 目标的后果便是代码不能特别快地运行(尽管有许多保证都说 Java 终究有一天会运行得多么快),但它确实 将开发时间缩短到令人惊讶的地步——几乎只有创建一个等效 C ++程序一半甚至更短的时间。这段节省下来 的时间可以产生更大的效益,但 Java 并不仅止于此。它甚至更上一层楼,将重要性越来越明显的一切复杂任 务都封装在内,比如网络程序和多线程处理等等。Java 的各种语言特性和库在任何时候都能使那些任务轻而 易举完成。而且最后,它解决了一些真正有些难度的复杂问题:跨平台程序、动态代码改换以及安全保护等 等。换在从前,其中任何每一个都能使你头大如斗。所以不管我们见到了什么性能问题,Java 的保证仍然是 非常有效的:它使程序员显著提高了程序设计的效率! 6

在我看来,编程效率提升后影响最大的就是 W 。网络程序设计以前非常困难,而 Java 使这个问题迎刃而解 eb (而且 Java 也在不断地进步,使解决这类问题变得越来越容易)。网络程序的设计要求我们相互间更有效率 地沟通,而且至少要比电话通信来得便宜(仅仅电子函件就为许多公司带来了好处)。随着我们网上通信越 来越频繁,令人震惊的事情会慢慢发生,而且它们令人吃惊的程度绝不亚于当初工业革命给人带来的震憾。 在各个方面:创建程序;按计划编制程序;构造用户界面,使程序能与用户沟通;在不同类型的机器上运行 程序;以及方便地编写程序,使其能通过因特网通信——Java 提高了人与人之间的“通信带宽”。而且我认 为通信革命的结果可能并不单单是数量庞大的比特到处传来传去那么简单。我们认为认清真正的革命发生在 哪里,因为人和人之间的交流变得更方便了——个体与个体之间,个体与组之间,组与组之间,甚至在星球 之间。有人预言下一次大革命的发生就是由于足够多的人和足够多的相互连接造成的,而这种革命是以整个 世界为基础发生的。Java 可能是、也可能不是促成那次革命的直接因素,但我在这里至少感觉自己在做一些 有意义的工作——尝试教会大家一种重要的语言!

7

引言
同人类任何语言一样,Java 为我们提供了一种表达思想的方式。如操作得当,同其他方式相比,随着问题变 得愈大和愈复杂,这种表达方式的方便性和灵活性会显露无遗。 不可将 Java 简单想象成一系列特性的集合;如孤立地看,有些特性是没有任何意义的。只有在考虑“设 计”、而非考虑简单的编码时,才可真正体会到 Java的强大。为了按这种方式理解 Java,首先必须掌握它 与编程的一些基本概念。本书讨论了编程问题、它们为何会成为问题以及 Java 用以解决它们的方法。所以, 我对每一章的解释都建立在如何用语言解决一种特定类型的问题基础上。按这种方式,我希望引导您一步一 步地进入 Java 的世界,使其最终成为您最自然的一种语言。 贯穿本书,我试图在您的大脑里建立一个模型——或者说一个“知识结构”。这样可加深对语言的理解。若 遇到难解之处,应学会把它填入这个模型的对应地方,然后自行演绎出答案。事实上,学习任何语言时,脑 海里有一个现成的知识结构往往会起到事半功倍的效果。

1. 前提
本书假定读者对编程多少有些熟悉。应已知道程序是一系列语句的集合,知道子程序/函数/宏是什么,知 道象“I f ”这样的控制语句,也知道象“w l e”这样的循环结构。注意这些东西在大量语言里都是类似的。 hi 假如您学过一种宏语言,或者用过 Per l 之类的工具,那么它们的基本概念并无什么区别。总之,只要能习惯 基本的编程概念,就可顺利阅读本书。当然,C ++程序员在阅读时能占到更多的便宜。但即使不熟悉 C /C ,一 样不要把自己排除在外(尽管以后的学习要付出更大的努力)。我会讲述面向对象编程的概念,以及 Java 的 基本控制机制,所以不用担心自己会打不好基础。况且,您需要学习的第一类知识就会涉及到基本的流程控 制语句。 尽管经常都会谈及 C和 C ++语言的一些特性,但并没有打算使它们成为内部参考,而是想帮助所有程序员都 能正确地看待那两种语言。毕竟,Java 是从它们那里衍生出来的。我将试着尽可能地简化这些引用和参考, 并合理地解释一名非 C ++程序员通常不太熟悉的内容。 /C

2. J ava 的学习
在我第一本书《Us i ng C ++》面市的几乎同一时间(O bor ne/M r aw Hi l l 于 1989 年出版),我开始教授那 s cG 种语言。程序设计语言的教授已成为我的专业。自 1989 年以来,我便在世界各地见过许多昏昏欲睡、满脸茫 然以及困惑不解的面容。开始在室内面向较少的一组人授课以后,我从作业中发现了一些特别的问题。即使 那些上课面带会心的微笑或者频频点头的学生,对许多问题也存在认识上的混淆。在过去几年间的“软件开 发会议”上,由我主持 C ++分组讨论会(现在变成了 Java 讨论会)。有的演讲人试图在很短的时间内向听众 灌输过多的主题。所以到最后,尽管听众的水平都还可以,而且提供的材料也很充足,但仍然损失了一部分 听众。这可能是由于问得太多了,但由于我是那些采取传统授课方式的人之一,所以很想使每个人都能跟上 讲课进度。 有段时间,我编制了大量教学简报。经过不断的试验和修订(或称“反复”,这是在 Java 程序设计中非常有 用的一项技术),最后成功地在一门课程中集成了从我的教学经验中总结出来的所有东西——我在很长一段 时间里都在使用。其中由一系列离散的、易于消化的小步骤组成,而且每个小课程结束后都有一些适当的练 习。我目前已在 Java 公开研讨会上公布了这一课程,大家可到 ht t p: //w w Br uceEckel . com了解详情(对研 w. 讨会的介绍也以 C - RO 的形式提供,具体信息可在同样的 W D M eb站点找到)。 从每一次研讨会收到的反馈都帮助我修改及重新制订学习材料的重心,直到我最后认为它成为一个完善的教 学载体为止。但本书并非仅仅是一本教科书——我尝试在其中装入尽可能多的信息,并按照主题进行了有序 的分类。无论如何,这本书的主要宗旨是为那些独立学习的人士服务,他们正准备深入一门新的程序设计语 言,而没有太大的可能参加此类专业研讨会。

3. 目标
就象我的前一本书《T hi nki ng i n C ++》一样,这本书面向语言的教授进行了良好的结构与组织。特别地,我 的目标是建立一套有序的机制,可帮助我在自己的研讨会上更好地进行语言教学。在我思考书中的一章时, 实际上是在想如何教好一堂课。我的目标是得到一系列规模适中的教学模块,可以在合理的时间内教完。随 后是一些精心挑选的练习,可以在课堂上当即完成。 在这本书中,我想达到的目标总结如下: 8

( 1) 每一次都将教学内容向前推进一小步,便于读者在继续后面的学习前消化前面的内容。 ( 2) 采用的示例尽可能简短。当然,这样做有时会妨碍我解决“现实世界”的问题。但我同时也发现对那些 新手来说,如果他们能理解每一个细节,那么一般会产生更大的学习兴趣。而假如他们一开始就被要解决的 问题的深度和广度所震惊,那么一般都不会收到很好的学习效果。另外在实际教学过程中,对能够摘录的代 码数量是有严重限制的。另一方面,这样做无疑会有些人会批评我采用了“不真实的例子”,但只要能起到 良好的效果,我宁愿接受这一指责。 ( 3) 要揭示的特性按照我精心挑选的顺序依次出场,而且尽可能符合读者的思想历程。当然,我不可能永远 都做到这一点;在那些情况下,会给出一段简要的声明,指出这个问题。 ( 4) 只把我认为有助于理解语言的东西介绍给读者,而不是把我知道的一切东西都抖出来,这并非藏私。我 认为信息的重要程度是存在一个合理的层次的。有些情况是 95%的程序员都永远不必了解的。如强行学习, 只会干扰他们的正常思维,从而加深语言在他们面前表现出来的难度。以 C语言为例,假如你能记住运算符 优先次序表(我从来记不住),那么就可以写出更“聪明”的代码。但再深入想一层,那也会使代码的读者 /维护者感到困扰。所以忘了那些次序吧,在拿不准的时候加上括号即可。 ( 5) 每一节都有明确的学习重点,所以教学时间(以及练习的间隔时间)非常短。这样做不仅能保持读者思 想的活跃,也能使问题更容易理解,对自己的学习产生更大的信心。 ( 6) 提供一个坚实的基础,使读者能充分理解问题,以便更容易转向一些更加困难的课程和书籍。

4. 联机文档
由 Sun 微系统公司提供的 Java 语言和库(可免费下载)配套提供了电子版的用户帮助手册,可用 W eb浏览器 阅读。此外,由其他厂商开发的几乎所有类似产品都有一套等价的文档系统。而目前出版的与 Java 有关的几 乎所有书籍都重复了这份文档。所以你要么已经拥有了它,要么需要下载。所以除非特别必要,否则本书不 会重复那份文档的内容。因为一般地说,用 W eb浏览器查找与类有关的资料比在书中查找方便得多(电子版 的东西更新也快)。只有在需要对文档进行补充,以便你能理解一个特定的例子时,本书才会提供有关类的 一些附加说明。

5. 章节
本书在设计时认真考虑了人们学习 Java 语言的方式。在我授课时,学生们的反映有效地帮助了我认识哪些部 分是比较困难的,需特别加以留意。我也曾经一次讲述了太多的问题,但得到的教训是:假如包括了大量新 特性,就需要对它们全部作出解释,而这特别容易加深学生们的混淆。因此,我进行了大量努力,使这本书 一次尽可能地少涉及一些问题。 所以,我在书中的目标是让每一章都讲述一种语言特性,或者只讲述少数几个相互关联的特性。这样一来, 读者在转向下一主题时,就能更容易地消化前面学到的知识。 下面列出对本书各章的一个简要说明,它们与我实际进行的课堂教学是对应的。 ( 1) 第 1 章:对象入门 这一章是对面向对象的程序设计(O P)的一个综述,其中包括对“什么是对象”之类的基本问题的回答,并 O 讲述了接口与实现、抽象与封装、消息与函数、继承与合成以及非常重要的多形性的概念。这一章会向大家 提出一些对象创建的基本问题,比如构建器、对象存在于何处、创建好后把它们置于什么地方以及魔术般的 垃圾收集器(能够清除不再需要的对象)。要介绍的另一些问题还包括通过违例实现的错误控制机制、反应 灵敏的用户界面的多线程处理以及连网和因特网等等。大家也会从中了解到是什么使得 Java 如此特别,它为 什么取得了这么大的成功,以及与面向对象的分析与设计有关的问题。 ( 2) 第 2 章:一切都是对象 本章将大家带到可以着手写自己的第一个 Java 程序的地方,所以必须对一些基本概念作出解释,其中包括对 象“句柄”的概念;怎样创建一个对象;对基本数据类型和数组的一个介绍;作用域以及垃圾收集器清除对 象的方式;如何将 Java 中的所有东西都归为一种新数据类型(类),以及如何创建自己的类;函数、自变量 以及返回值;名字的可见度以及使用来自其他库的组件;s t at i c关键字;注释和嵌入文档等等。 ( 3) 第 3 章:控制程序流程 本章开始介绍起源于 C和 C ++,由 Java 继承的所有运算符。除此以外,还要学习运算符一些不易使人注意的 问题,以及涉及造型、升迁以及优先次序的问题。随后要讲述的是基本的流程控制以及选择运算,这些是几 9

乎所有程序设计语言都具有的特性:用 i f - el s e 实现选择;用 f or 和 w l e 实现循环;用 br eak 和 cont i nue hi 以及 Java 的标签式 br eak 和 cont i une(它们被认为是 Java 中“不见的 gogo”)退出循环;以及用 s w t ch i 实现另一种形式的选择。尽管这些与 C和 C ++中见到的有一定的共通性,但多少存在一些区别。除此以外, 所有示例都是完整的 Java 示例,能使大家很快地熟悉 Java 的外观。 ( 4) 第 4 章:初始化和清除 本章开始介绍构建器,它的作用是担保初始化的正确实现。对构建器的定义要涉及函数过载的概念(因为可 能同时有几个构建器)。随后要讨论的是清除过程,它并非肯定如想象的那么简单。用完一个对象后,通常 可以不必管它,垃圾收集器会自动介入,释放由它占据的内存。这里详细探讨了垃圾收集器以及它的一些特 点。在这一章的最后,我们将更贴近地观察初始化过程:自动成员初始化、指定成员初始化、初始化的顺 序、s t at i c(静态)初始化以及数组初始化等等。 ( 5) 第 5 章:隐藏实现过程 本章要探讨将代码封装到一起的方式,以及在库的其他部分隐藏时,为什么仍有一部分处于暴露状态。首先 要讨论的是 package和 i m t 关键字,它们的作用是进行文件级的封装(打包)操作,并允许我们构建由类 por 构成的库(类库)。此时也会谈到目录路径和文件名的问题。本章剩下的部分将讨论 publ i c,pr i vat e以及 pr ot ect ed三个关键字、“友好”访问的概念以及各种场合下不同访问控制级的意义。 ( 6) 第 6 章:类再生 继承的概念是几乎所有 O P 语言中都占有重要的地位。它是对现有类加以利用,并为其添加新功能的一种有 O 效途径(同时可以修改它,这是第 7 章的主题)。通过继承来重复使用原有的代码时(再生),一般需要保 持“基础类”不变,只是将这儿或那儿的东西串联起来,以达到预期的效果。然而,继承并不是在现有类基 础上制造新类的唯一手段。通过“合成”,亦可将一个对象嵌入新类。在这一章中,大家将学习在 Java 中重 复使用代码的这两种方法,以及具体如何运用。 ( 7) 第 7 章:多形性 若由你自己来干,可能要花 9 个月的时间才能发现和理解多形性的问题,这一特性实际是 O P 一个重要的基 O 础。通过一些小的、简单的例子,读者可知道如何通过继承来创建一系列类型,并通过它们共有的基础类对 那个系列中的对象进行操作。通过 Java 的多形性概念,同一系列中的所有对象都具有了共通性。这意味着我 们编写的代码不必再依赖特定的类型信息。这使程序更易扩展,包容力也更强。由此,程序的构建和代码的 维护可以变得更方便,付出的代价也会更低。此外,Java 还通过“接口”提供了设置再生关系的第三种途 径。这儿所谓的“接口”是对对象物理“接口”一种纯粹的抽象。一旦理解了多形性的概念,接口的含义就 很容易解释了。本章也向大家介绍了 Java 1. 1 的“内部类”。 ( 8) 第 8 章:对象的容纳 对一个非常简单的程序来说,它可能只拥有一个固定数量的对象,而且对象的“生存时间”或者“存在时 间”是已知的。但是通常,我们的程序会在不定的时间创建新对象,只有在程序运行时才可了解到它们的详 情。此外,除非进入运行期,否则无法知道所需对象的数量,甚至无法得知它们确切的类型。为解决这个常 见的程序设计问题,我们需要拥有一种能力,可在任何时间、任何地点创建任何数量的对象。本章的宗旨便 是探讨在使用对象的同时用来容纳它们的一些 Java 工具:从简单的数组到复杂的集合(数据结构),如 Vect or 和 Has ht abl e 等。最后,我们还会深入讨论新型和改进过的 Java 1. 2 集合库。 ( 9) 第 9 章:违例差错控制 Java 最基本的设计宗旨之一便是组织错误的代码不会真的运行起来。编译器会尽可能捕获问题。但某些情况 下,除非进入运行期,否则问题是不会被发现的。这些问题要么属于编程错误,要么则是一些自然的出错状 况,它们只有在作为程序正常运行的一部分时才会成立。Java 为此提供了“违例控制”机制,用于控制程序 运行时产生的一切问题。这一章将解释 t r y、cat ch、t hr ow hr ow 以及 f i nal l y 等关键字在 Ja 、t s va中的工作 原理。并讲述什么时候应当“掷”出违例,以及在捕获到违例后该采取什么操作。此外,大家还会学习 Java 的一些标准违例,如何构建自己的违例,违例发生在构建器中怎么办,以及违例控制器如何定位等等。 ( 10) 第 10 章:Java I O系统 理论上,我们可将任何程序分割为三部分:输入、处理和输出。这意味着 I O (输入/输出)是所有程序最为 10

关键的部分。在这一章中,大家将学习 Java 为此提供的各种类,如何用它们读写文件、内存块以及控制台 等。“老”I O和 Java 1. 1 的“新”I O将得到着重强调。除此之外,本节还要探讨如何获取一个对象、对其 进行“流式”加工(使其能置入磁盘或通过网络传送)以及重新构建它等等。这些操作在 Java 的 1. 1 版中都 可以自动完成。另外,我们也要讨论 Java 1. 1 的压缩库,它将用在 Java 的归档文件格式中(JA R)。 ( 11) 第 11 章:运行期类型鉴定 若只有指向基础类的一个句柄,Java 的运行期类型标鉴定(RTTI )使我们能获知一个对象的准确类型是什 么。一般情况下,我们需要有意忽略一个对象的准确类型,让 Java 的动态绑定机制(多形性)为那一类型实 现正确的行为。但在某些场合下,对于只有一个基础句柄的对象,我们仍然特别有必要了解它的准确类型是 什么。拥有这个资料后,通常可以更有效地执行一次特殊情况下的操作。本章将解释 RTTI 的用途、如何使用 以及在适当的时候如何放弃它。此外,Java 1. 1 的“反射”特性也会在这里得到介绍。 ( 12) 第 12 章:传递和返回对象 由于我们在 Java 中同对象沟通的唯一途径是“句柄”,所以将对象传递到一个函数里以及从那个函数返回一 个对象的概念就显得非常有趣了。本章将解释在函数中进出时,什么才是为了管理对象需要了解的。同时也 会讲述 St r i ng(字串)类的概念,它用一种不同的方式解决了同样的问题。 ( 13) 第 13 章:创建窗口和程序片 Java 配套提供了“抽象 W ndow 工具包”(A T)。这实际是一系列类的集合,能以一种可移植的形式解决 i s W 视窗操纵问题。这些窗口化程序既可以程序片的形式出现,亦可作为独立的应用程序使用。本章将向大家介 绍 A T 以及网上程序片的创建过程。我们也会探讨 A T 的优缺点以及 Java 1. 1 在 G 方面的一些改进。同 W W UI 时,重要的“Java Beans ”技术也会在这里得到强调。Java Beans 是创建“快速应用开发”(RA )程序构 D 造工具的重要基础。我们最后介绍的是 Java 1. 2 的“Sw ng”库——它使 Java 的 UI 组件得到了显著的改 i 善。 ( 14) 第 14 章:多线程 Java 提供了一套内建的机制,可提供对多个并发子任务的支持,我们称其为“线程”。这线程均在单一的程 序内运行。除非机器安装了多个处理器,否则这就是多个子任务的唯一运行方式。尽管还有别的许多重要用 途,但在打算创建一个反应灵敏的用户界面时,多线程的运用显得尤为重要。举个例子来说,在采用了多线 程技术后,尽管当时还有别的任务在执行,但用户仍然可以毫无阻碍地按下一个按钮,或者键入一些文字。 本章将对 Java 的多线程处理机制进行探讨,并介绍相关的语法。 ( 15) 第 15 章 网络编程 开始编写网络应用时,就会发现所有 Java 特性和库仿佛早已串联到了一起。本章将探讨如何通过因特网通 信,以及 Java 用以辅助此类编程的一些类。此外,这里也展示了如何创建一个 Java 程序片,令其同一个 “通用网关接口”(C I )程序通信;揭示了如何用 C G ++编写 C I 程序;也讲述了与 Java 1. 1 的“Java 数据 G 库连接”(JD )和“远程方法调用”(RM )有关的问题。 BC I ( 16) 第 16 章 设计范式 本章将讨论非常重要、但同时也是非传统的“范式”程序设计概念。大家会学习设计进展过程的一个例子。 首先是最初的方案,然后经历各种程序逻辑,将方案不断改革为更恰当的设计。通过整个过程的学习,大家 可体会到使设计思想逐渐变得清晰起来的一种途径。 ( 17) 第 17 章 项目 本章包括了一系列项目,它们要么以本书前面讲述的内容为基础,要么对以前各章进行了一番扩展。这些项 目显然是书中最复杂的,它们有效演示了新技术和类库的应用。 有些主题似乎不太适合放到本书的核心位置,但我发现有必要在教学时讨论它们,这些主题都放入了本书的 附录。 ( 18) 附录 A :使用非 Java 代码 对一个完全能够移植的 Java 程序,它肯定存在一些严重的缺陷:速度太慢,而且不能访问与具体平台有关的 服务。若事先知道程序要在什么平台上使用,就可考虑将一些操作变成“固有方法”,从而显著加快执行速 11

度。这些“固有方法”实际是一些特殊的函数,以另一种程序设计语言写成(目前仅支持 C ++)。Java 还 /C 可通过另一些途径提供对非 Java 代码的支持,其中包括 C RBA O 。本附录将详细介绍这些特性,以便大家能创 建一些简单的例子,同非 Java 代码打交道。 ( 19) 附录 B:对比 C ++和 Java 对一个 C ++程序员,他应该已经掌握了面向对象程序设计的基本概念,而且 Java 语法对他来说无疑是非常眼 熟的。这一点是明显的,因为 Java 本身就是从 C ++衍生而来。但是,C ++和 Java 之间的确存在一些显著的差 异。这些差异意味着 Java 在 C ++基础上作出的重大改进。一旦理解了这些差异,就能理解为什么说 Java 是 一种杰出的语言。这一附录便是为这个目的设立的,它讲述了使 Java 与 C ++明显有别的一些重要特性。 ( 20) 附录 C :Java 编程规则 本附录提供了大量建议,帮助大家进行低级程序设计和代码编写。 ( 21) 附录 D :性能 通过这个附录的学习,大家可发现自己 Java 程序中存在的瓶颈,并可有效地改善执行速度。 ( 22) 附录 E:关于垃圾收集的一些话 这个附录讲述了用于实现垃圾收集的操作和方法。 ( 23) 附录 F:推荐读物 列出我感觉特别有用的一系列 Java 参考书。

6. 练习
为巩固对新知识的掌握,我发现简单的练习特别有用。所以读者在每一章结束时都能找到一系列练习。 大多数练习都很简单,在合理的时间内可以完成。如将本书作为教材,可考虑在课堂内完成。老师要注意观 察,确定所有学生都已消化了讲授的内容。有些练习要难些,他们是为那些有兴趣深入的读者准备的。大多 数练习都可在较短时间内做完,有效地检测和加深您的知识。有些题目比较具有挑战性,但都不会太麻烦。 事实上,练习中碰到的问题在实际应用中也会经常碰到。

7. 多媒体 CD- RO M
本书配套提供了一片多媒体 C - RO ,可单独购买及使用。它与其他计算机书籍的普通配套 C D M D不同,那些 C D 通常仅包含了书中用到的源码(本书的源码可从 w w Br uceEckel . com免费下载)。本 C - RO 是一个独立的 w. D M 产品,包含了一周“Hads - O nJava”培训课程的全部内容。这是一个由 Br uce Eckel 讲授的、长度在 15 小时 以上的课程,含 500 张以上的演示幻灯片。该课程建立在这本书的基础上,所以是非常理想的一个配套产 品。 C - RO 包含了本书的两个版本: D M ( 1) 本书一个可打印的版本,与下载版完全一致。 ( 2) 为方便读者在屏幕上阅读和索引,C - RO 提供了一个独特的超链接版本。这些超链接包括: D M ■230 个章、节和小标题链接 ■3600 个索引链接 C - RO 刻录了 600M 以上的数据。我相信它已对所谓“物超所值”进行了崭新的定义。 D M B C - RO 包含了本书打印版的所有东西,另外还有来自五天快速入门课程的全部材料。我相信它建立了一个新 D M 的书刊品质评定标准。 若想单独购买此 C - RO ,只能从 W D M eb站点 w w Br uceEckel . com处直接订购。 w.

8. 源代码
本书所有源码都作为保留版权的免费软件提供,可以独立软件包的形式获得,亦可从 ht t p: //w w Br uceEckel . com w. 下载。为保证大家获得的是最新版本,我用这个正式站点发行代码以及本书电 子版。亦可在其他站点找到电子书和源码的镜像版(有些站点已在 ht t p: //w w Br uceEckel . com w. 处列出)。 但无论如何,都应检查正式站点,确定镜像版确实是最新的版本。可在课堂和其他教育场所发布这些代码。 版权的主要目标是保证源码得到正确的引用,并防止在未经许可的情况下,在印刷材料中发布代码。通常, 12

只要源码获得了正确的引用,则在大多数媒体中使用本书的示例都没有什么问题。 在每个源码文件中,都能发现下述版本声明文字: //////////////////////////////////// ////////////// // C opyr i ght ( c) Br uce Eckel , 1998 // Sour ce code f i l e f r om t he book " Thi nki ng i n Java" // A l r i ght s r es er ved EXC l EPT as al l ow by t he ed // f ol l ow ng s t at em s : You can f r eel y us e t hi s f i l e i ent // f or your ow w k ( per s onal or com er ci al ) , n or m // i ncl udi ng m f i cat i ons and di s t r i but i on i n odi // execut abl e f or m onl y. Per m s s i on i s gr ant ed t o us e i // t hi s f i l e i n cl as s r oom s i t uat i ons , i ncl udi ng i t s // us e i n pr es ent at i on m er i al s , as l ong as t he book at // " Thi nki ng i n Java" i s ci t ed as t he s our ce. / / Except i n cl as s r oom s i t uat i ons , you cannot copy // and di s t r i but e t hi s code; i ns t ead, t he s ol e // di s t r i but i on poi nt i s ht t p: //w w Br uceEckel . com w. // ( and of f i ci al m r r or s i t es ) w e i t i s i her // f r eel y avai l abl e. You cannot r em ove t hi s // copyr i ght and not i ce. You cannot di s t r i but e // m f i ed ver s i ons of t he s our ce code i n t hi s odi // package. You cannot us e t hi s f i l e i n pr i nt ed // m a w t hout t he expr es s per m s s i on of t he edi i i // aut hor . Br uce Eckel m akes no r epr es ent at i on about // t he s ui t abi l i t y of t hi s s of t w e f o any pur pos e. ar r // I t i s pr ovi ded " as i s " w t hout expr es s or i m i ed i pl // w r ant y of any ki nd, i ncl udi ng any i m i ed ar pl // w r ant y of m chant abi l i t y, f i t nes s f or a ar er // par t i cul ar pur pos e or non- i nf r i ngem . The ent i r e ent // r i s k as t o t he qual i t y and per f or m ance of t he // s of t w e i s w t h you. Br uce Eckel and t he ar i // publ i s her s hal l not be l i abl e f or any dam ages // s uf f er ed by you or any t hi r d par t y as a r es ul t of // us i ng or di s t r i but i ng s of t w e. I n no event w l l ar i // Br uce Eckel or t he publ i s her be l i abl e f or any // l os t r evenue, pr of i t , or dat a, or f or di r ect , // i ndi r ect , s peci al , cons equent i al , i nci dent al , or // puni t i ve dam ages , how ever caus ed and r egar dl es s of // t he t heor y of l i abi l i t y, ar i s i ng out of t he us e of // or i nabi l i t y t o us e s of t w e, even i f Br uce Eckel ar // and t he publ i s her have been advi s ed of t he // pos s i bi l i t y of s uch dam ages . Shoul d t he s of t w e ar // pr ove def ect i ve, you as s um t he cos t of al l e // neces s ar y s er vi ci ng, r epai r , or cor r ect i on. I f you // t hi nk you' ve f ound an er r or , pl eas e em l al l ai / / m f i ed f i l es w t h cl ear l y com ent ed changes t o: odi i m // Br uce@ Eckel O ect s . com ( Pl eas e us e t he s am bj . e // addr es s f or non- code er r or s f ound i n t he book. ) ///////////////////////////////////////////////// 可在自己的开发项目中使用代码,并可在课堂上引用(包括学习材料)。但要确定版权声明在每个源文件中 得到了保留。

13

9. 编码样式
在本书正文中,标识符(函数、变量和类名)以粗体印刷。大多数关键字也采用粗体——除了一些频繁用到 的关键字(若全部采用粗体,会使页面拥挤难看,比如那些“类”)。 对于本书的示例,我采用了一种特定的编码样式。该样式得到了大多数 Java 开发环境的支持。该样式问世已 有几年的时间,最早起源于 Bj ar ne St r ous t r up先生在《The C Pr ogr am i ng Language》里采用的样式 ++ m (A s on- W l ey 1991 年出版,第 2 版)。由于代码样式目前是个敏感问题,极易招致数小时的激烈辩 ddi es 论,所以我在这儿只想指出自己并不打算通过这些示例建立一种样式标准。之所以采用这些样式,完全出于 我自己的考虑。由于 Java 是一种形式非常自由的编程语言,所以读者完全可以根据自己的感觉选用了适合的 编码样式。 本书的程序是由字处理程序包括在正文中的,它们直接取自编译好的文件。所以,本书印刷的代码文件应能 正常工作,不会造成编译器错误。会造成编译错误的代码已经用注释/ / ! 标出。所以很容易发现,也很容易用 自动方式进行测试。读者发现并向作者报告的错误首先会在发行的源码中改正,然后在本书的更新版中校订 (所有更新都会在 W eb站点 ht t p: //w w Br uceEckel . com w. 处出现)。

10. J ava 版本
尽管我用几家厂商的 Java 开发平台对本书的代码进行了测试,但在判断代码行为是否正确时,却通常以 Sun 公司的 Java 开发平台为准。 当您读到本书时,Sun应已发行了 Java 的三个重要版本:1. 0,1. 1 及 1. 2(Sun声称每 9 个月就会发布一个 主要更新版本)。就我看,1. 1 版对 Java 语言进行了显著改进,完全应标记成 2. 0 版(由于 1. 1 已作出了如 此大的修改,真不敢想象 2. 0 版会出现什么变化)。然而,它的 1. 2 版看起来最终将 Java 推入了一个全盛时 期,特别是其中考虑到了用户界面工具。 本书主要讨论了 1. 0 和 1. 1 版,1. 2 版有部分内容涉及。但在有些时候,新方法明显优于老方法。此时,我 会明显偏向于新方法,通常教给大家更好的方法,而完全忽略老方法。然而,有的新方法要以老方法为基 础,所以不可避免地要从老方法入手。这一特点尤以 A T 为甚,因为那儿不仅存在数量众多的老式 Java 1. 0 W 代码,有的平台仍然只支持 Java 1. 0。我会尽量指出哪些特性是哪个版本特有的。 大家会注意到我并未使用子版本号,比如 1. 1. 1。至本书完稿为止,Sun 公司发布的最后一个 1. 0 版是 1. 02;而 1. 1 的最后版本是 1. 1. 5(Java 1. 2 仍在做β 测试)。在这本书中,我只会提到 Java 1. 0,Java 1. 1 及 Java 1. 2,避免由于子版本编号过多造成的键入和印刷错误。

11. 课程和培训
我的公司提供了一个五日制的公共培训课程,以本书的内容为基础。每章的内容都代表着一堂课,并附有相 应的课后练习,以便巩固学到的知识。一些辅助用的幻灯片可在本书的配套光盘上找到,最大限度地方便各 位读者。欲了解更多的情况,请访问: ht t p: //w w Br uceEckel . com w. 或发函至: Br uce@ Eckel O ect s . com bj 我的公司也提供了咨询服务,指导客户完成整个开发过程——特别是您的单位首次接触 Java 开发的时候。

12. 错误
无论作者花多大精力来避免,错误总是从意想不到的地方冒出来。如果您认为自己发现了一个错误,请在源 文件(可在 ht t p: //w w Br uceEckel . com w. 处找到)里指出有可能是错误的地方,填好我们提供的表单。将您 推荐的纠错方法通过电子函件发给 Br uce@ Eckel O ect s . com bj 。经适当的核对与处理,W eb站点的电子版以及 本书的下一个印刷版本会作出相应的改正。具体格式如下: ( 1) 在主题行(Subj ect )写上“TI J C r ect i on”(去掉引号),以便您的函件进入对应的目录。 or ( 2) 在函件正文,采用下述形式: f i nd: 在这里写一个单行字串,以便我们搜索错误所在的地方 C m : 在这里可写多行批注正文,最好以“her e' s how I t hi nk i t s houd r ead”开头 om ent ### 其中,“### ”指出批注正文的结束。这样一来,我自己设计的一个纠错工具就能对原始正文来一次“搜 索”,而您建议的纠错方法会在随后的一个窗口中弹出。 14

若希望在本书的下一版添加什么内容,或对书中的练习题有什么意见,也欢迎您指出。我们感谢您的所有意 见。

13. 封面设计
《T hi nki ng i n Java》一书封面的创作灵感来源于 A er i can A t s & C af t s M m r r ovem (美洲艺术&手工艺品 ent 运动)。这一运动起始于世纪之交,1900 到 1920 年达到了顶峰。它起源于英格兰,具有一定的历史背景。 当时正是机器革命产生的风暴席卷整个大陆的时候,而且受到维多利亚地区强烈装饰风格的巨大影响。 A t s & r af t s 强调的是原始风格,回归自然的初衷是整个运动的核心。那时对手工制作推崇备至,手工艺人 r C 特别得到尊重。正因为如此,人们远远避开现代工具的使用。这场运动对整个艺术界造成了深远的影响,直 至今天仍受到人们的怀念。特别是我们面临又一次世纪之交,强烈的怀旧情绪难免涌上心来。计算机发展至 今,已走过了很长的一段路。我们更迫切地感到:软件设计中最重要的是设计者本身,而不是流水化的代码 编制。如设计者本身的素质和修养不高,那么最多只是“生产”代码的工具而已。 我以同样的眼光来看待 Java:作为一种将程序员从操作系统繁琐机制中解放出来的尝试,它的目的是使人们 成为真正的“软件艺术家”。 无论作者还是本书的封面设计者(自孩提时代就是我的朋友)都从这一场运动中获得了灵感。所以接下来的 事情就非常简单了,要么自己设计,要么直接采用来自那个时期的作品。 此外,封面向大家展示了一个收集箱,自然学者可能用它展示自己的昆虫标本。我们认为这些昆虫都是“对 象”,全部置于更大的“收集箱”对象里,再统一置入“封面”这个对象里。它向我们揭示了面向对象编程 技术最基本的“集合”概念。当然,作为一名程序员,大家对于“昆虫”或“虫”是非常敏感的(“虫”在 英语里是 Bug ,后指程序错误)。这里的“虫”已被抓获,在一只广口瓶中杀死,最后禁闭于一个小的展览 盒里——暗示 Java 有能力寻找、显示和消除程序里的“虫”(这是 Java 最具特色的特性之一)。

14. 致谢
首先,感谢 D e St r eet C oyl ohous i ng C m t y(道尔街住房社区)容忍我花两年的时间来写这本书(其实 om uni 他们一直都在容忍我的“胡做非为”)。非常感谢 Kevi n 和 Sonda D onovan,是他们把科罗拉多 C es t ed r But t e 市这个风景优美的地方租给我,使我整个夏天都能安心写作。感谢 C es t ed But t e 友好的居民;以及 r Rocky M ount ai n Bi ol ogi cal Labor at or y(岩石山生物实验室),他们的工作人员总是面带微笑。 这是我第一次找代理人出书,但却绝没有后悔。谢谢“摩尔文学代理公司”的 C audet t e M e小姐。是她 l oor 强大的信心与毅力使我最终梦想成真。 我的头两本书是与 O bor ne/M r aw Hi l l 出版社的编辑 Jef f Pepper 合作出版的。Jef f 又在正确的地方和正 s cG 确的时间出现在了 Pr ent i ce- Hal l 出版社,是他为了清除了所有可能遇到的障碍,也使我感受了一次愉快的 出书经历。谢谢你,Jef f ——你对我非常重要。 要特别感谢 G Ki yooka和他的 D gi gam 公司,我用的 W en i i eb服务器就是他们提供的;也要感谢 Scot t C l aw al ay,服务器是由他负责维护的。在我学习 W eb的过程中,一个服务器无疑是相当有价值的帮助。 谢谢 C Hor s t m ay ann(《C e Java》一书的副编辑,Pr ent i ce Hal l 于 1997 年出版)、D A cy Sm t h or ' r i (Sym ec 公司)和 Paul Tym ant a(《Java Pr i m Pl us 》一书的副编辑,The W t e G oup 于 1996 年出 er ai r 版),感谢他们帮助我澄清语言方面的一些概念。 感谢那些在“Java 软件开发会议”上我的 Java 小组发言的同志们,以及我教授过的那些学生,他们提出的 问题使我的教案愈发成熟起来。 特别感谢 Lar r y 和 Ti na O Br i en,是他们将这本书和我的教学内容制成一张教学 C - RO (关于这方面的问 ' D M 题,ht t p: //w w Br uceEckel . com w. 有更多的答案)。 有许多人送来了纠错报告,我真的很感激所有这些朋友,但特别要对下面这些人说声谢谢:Kevi n Raul er s on (发现了多处重大错误),Bob Res endes(发现的错误令人难以置信),John Pi nt o ,Joe D e,Joe ant Shar p,D d C bs (许多语法和表达不清的地方),D . Rober t St ephens on,Fr ankl i n C avi om r hen,Zev G i ner ,D d Kar r ,Leander A St r os chei n,St eve C ar k,C l es A Lee,A i nMhe ,D nni s P. r avi . l har . ust a r e Rot h,Roque O i vei r a,D l ougl as D unn,D an Ri s t i c,N l G ar neau,D d B. M kovs ky,St eve ej ei al avi al W l ki ns on,以及其他许多热心读者。 i 为了使这本书在欧洲发行,Pr of . I r . M c M r ens 进行了大量工作。 ar eur 有一些技术人员曾走进我的生活,他们后来都和我成了朋友。最不寻常的是他们全是素食主义者,平时喜欢 练习瑜珈功,以及另一些形式的精神训练。我在练习了以后,觉得对我保持精力的旺盛非常有好处。他们是 Kr ai g Br ocks chm dt ,G i enKi yooka和 A ea pr ovagl i o ndr ,是这些朋友帮我了解了 Java 和程序设计在意大利 15

的情况。 显然,在 D phi 上的一些经验使我更容易理解 Java,因为它们有许多概念和语言设计决定是相通的。我的 el D phi 朋友提供了许多帮助,使我能够洞察一些不易为人注意的编程环境。他们是 M co C u(另一个意 el ar ant 大利人——难道会说拉丁语的人在学习 Java 时有得天独厚的优势?)、N l Rubenki ng(他最喜欢瑜珈/素 ei 食/禅道,但也非常喜欢计算机)以及 Zack Ur l ocker (是我游历世界时碰面次数最多的一位同志)。 我的朋友 Ri char d Hal e Shaw (以及 Ki m )的一些意见和支持发挥了非常关键的作用。Ri char d和我花了数月 的时间将教学内容合并到一起,并探讨如何使学生感受到最完美的学习体验。也要感谢 KoA Vi kor en, nn Er i c Eaur ot ,D ebor ahSom er s ,Jul i e Shaw i col e Fr eem m ,N an,C ndy Bl ai r ,Ba b r a H nsc m,Re i na i r a a oe g Ri dl ey,A ex D l unne 以及 M 其他可敬的成员。 FI 书籍设计、封面设计以及封面照片是由我的朋友 D el W l l - Har r i s 制作的。他是一位著名的作家和设计家 ani i (ht t p: //w w W l l Har r i s . com w. i ),在初中的时候就已显露出了过人的数学天赋。但是,小样是由我制作的, 所以录入错误都是我的。我是用 M cr os of t W d 97 f or W ndow 来写这本书,并用它生成小样。正文字体 i or i s 采用的是 Bi t s t r eam C m na ar i ;标题采用 Bi t s t r eamC l i gr aph 421(w w b t st r ea . com al w. i m );每章开头的符 号采用的是来自 P22 的 Leonar do Ext r as(ht t p: //w w p22. com w. );封面字体采用 I TC Renni e M cki nt os h。 ar 感谢为我提供编译器程序的一些著名公司:Bor l and,M cr os of t ,Sym ec,Sybas e/Pow s of t /W com以 i ant er at 及 Sun。 特别感谢我的老师和我所有的学生(他们也是我的老师),其中最有趣的一位写作老师是 G i el l e Ri co abr (《W i t i ng t he N ur al W r at ay》一书的作者,Put nam于 1983 年出版)。 曾向我提供过支持的朋友包括(当然还不止):A ew Bi ns t ock,St eveSi nof s ky,JD Hi l debr andt ,Tom ndr Kef f er ,Br i an M hi nney,Br i nkl ey Bar r ,《M dni ght Engi neer i ng》杂志社的 Bi l l G es ,Lar r y cEl i at C t ant i ne和 LucyLockw ,G eg Per r y,D Put t er m ons ood r an an,C i st i W pha ,G Wng a a , hr est l ene a ,D veMyer D d I nt er s i m ,A ea Ros enf i el d l ai r e Saw s ,另一些意大利朋友(La a Fa l a ,C r r a o avi one ndr ,C yer ur l i o d, I l s a 和 C i s t i na G us t ozzi ),C i s 和 Laur a St r and l m s t s ,Br ad Jer bi c a i l yng C t a c, r i hr ,A qui ,Mr vi ni M ys,Haf l i nger s ,Pol l ocks ,Pet er Vi nci ,Robbi ns Fam l i es,M t er Fa i l i es(和M i l l a abr i oel m cM ns), M chael W l k,D i i ave St oner ,Laur i e A s,C ans t ons ,Lar r y Fogg i ke 和 Kar en Sequei r a,G y dam r ,M ar Ent s m nger 和 A l i s on Br ody,Kevi nD i l onovan和 Sonda Eas t l ack,C t er 和 Shannon A hes nder s en,Joe Lor di ,D ave 和 Br enda Bar t l et t ,D d Lee,Rent s chl er s,Sudeks ,D ck,Pa t y 和 Lee Eckel ,Lynn 和 avi i t Todd 以及他们的家人。最后,当然还有我的爸爸和妈妈。

16

T abl e of Cont ent s
《THINKING IN JAVA》 中 文 版 .............................................................................................................................................1 写在前面的话 ..............................................................................................................................................................................6 引 言 ................................................................................................................................................................................................8 1. 前提 ......................................................................................................................................................................................8 2. Java 的学习......................................................................................................................................................................... 8 3. 目标 ......................................................................................................................................................................................8 4. 联机文档.............................................................................................................................................................................9 5. 章节 ......................................................................................................................................................................................9 6. 练习 ....................................................................................................................................................................................12 7. 多媒体 CD-ROM .............................................................................................................................................................12 8. 源代码 ................................................................................................................................................................................12 9. 编码样式...........................................................................................................................................................................14 10. Java 版本.........................................................................................................................................................................14 11. 课程和培训.....................................................................................................................................................................14 12. 错误.................................................................................................................................................................................. 14 13. 封面设计 .........................................................................................................................................................................15 14. 致谢.................................................................................................................................................................................. 15 第 1 章 对 象 入 门 ....................................................................................................................................................................... 27 1.1 抽象的进步.....................................................................................................................................................................27 1.2 对象的接口.....................................................................................................................................................................28 1.3 实现方案的隐藏............................................................................................................................................................ 29 1.4 方案的重复使用............................................................................................................................................................ 30 1.5 继承:重新使用接口 ...................................................................................................................................................30 1.5.1 改善基础类 .............................................................................................................................................................. 30 1.5.2 等价与类似关系..................................................................................................................................................... 31 1.6 多形对象的互换使用 ...................................................................................................................................................31 1.6.1 动态绑定.................................................................................................................................................................. 32 1.6.2 抽象的基础类和接口............................................................................................................................................ 32 1.7 对象的创建和存在时间 ...............................................................................................................................................33 1.7.1 集合与继承器 ......................................................................................................................................................... 33 1.7.2 单根结构.................................................................................................................................................................. 34 1.7.3 集合库与方便使用集合........................................................................................................................................ 35 1.7.4 清除时的困境:由谁负责清除? ...................................................................................................................... 35 1.8 违例控制:解决错误 ...................................................................................................................................................36 1.9 多线程 ..............................................................................................................................................................................37 1.10 永久性 ...........................................................................................................................................................................37 1.11 Java 和因特网..............................................................................................................................................................37 1.11.1 什么是 Web? ...................................................................................................................................................... 37 1.11.2 客户端编程(注释⑧) ..................................................................................................................................... 38 1.11.3 服务器端编程 ....................................................................................................................................................... 41 1.11.4 一个独立的领域:应用程序............................................................................................................................. 41 1.12 分析和设计 ...................................................................................................................................................................42 1.12.1 不要迷失................................................................................................................................................................ 42 1.12.2 阶段 0:拟出一个计划....................................................................................................................................... 42 1.12.3 阶段 1:要制作什么?....................................................................................................................................... 43 1.12.4 阶段 2:如何构建? ........................................................................................................................................... 43 1.12.5 阶段 3:开始创建 ............................................................................................................................................... 44 1.12.6 阶段 4:校订 ........................................................................................................................................................ 44 1.12.7 计划的回报 ........................................................................................................................................................... 45 1.13 Java 还是 C++?........................................................................................................................................................45 17

第 2 章 一切都是对象 ..............................................................................................................................................................46 2.1 用句柄操纵对象............................................................................................................................................................ 46 2.2 所有对象都必须创建 ...................................................................................................................................................46 2.2.1 保存到什么地方..................................................................................................................................................... 46 2.2.2 特殊情况:主要类型............................................................................................................................................ 47 2.2.3 Java 的数组 ............................................................................................................................................................. 48 2.3 绝对不要清除对象 .......................................................................................................................................................48 2.3.1 作用域 ...................................................................................................................................................................... 48 2.3.2 对象的作用域 ......................................................................................................................................................... 49 2.4 新建数据类型:类 .......................................................................................................................................................49 2.4.1 字段和方法 .............................................................................................................................................................. 49 2.5 方法、自变量和返回值 ...............................................................................................................................................50 2.5.1 自变量列表 .............................................................................................................................................................. 51 2.6 构建 Java 程序 ...............................................................................................................................................................52 2.6.1 名字的可见性 ......................................................................................................................................................... 52 2.6.2 使用其他组件 ......................................................................................................................................................... 52 2.6.3 static 关键字............................................................................................................................................................ 52 2.7 我们的第一个 Java 程序.............................................................................................................................................53 2.8 注释和嵌入文档............................................................................................................................................................ 55 2.8.1 注释文档.................................................................................................................................................................. 56 2.8.2 具体语法.................................................................................................................................................................. 56 2.8.3 嵌入 HTML............................................................................................................................................................. 56 2.8.4 @see:引用其他类 ............................................................................................................................................... 57 2.8.5 类文档标记 .............................................................................................................................................................. 57 2.8.6 变量文档标记 ......................................................................................................................................................... 57 2.8.7 方法文档标记 ......................................................................................................................................................... 57 2.8.8 文档示例.................................................................................................................................................................. 58 2.9 编码样式 .........................................................................................................................................................................59 2.10 总结................................................................................................................................................................................59 2.11 练习................................................................................................................................................................................59 第 3 章 控制程序流程 ..............................................................................................................................................................60 3.1 使用 Java 运算符 ..........................................................................................................................................................60 3.1.1 优先级 ...................................................................................................................................................................... 60 3.1.2 赋值........................................................................................................................................................................... 60 3.1.3 算术运算符 .............................................................................................................................................................. 62 3.1.4 自动递增和递减..................................................................................................................................................... 64 3.1.5 关系运算符 .............................................................................................................................................................. 65 3.1.6 逻辑运算符 .............................................................................................................................................................. 66 3.1.7 按位运算符 .............................................................................................................................................................. 68 3.1.8 移位运算符 .............................................................................................................................................................. 68 3.1.9 三元 if-else 运算符 ................................................................................................................................................ 71 3.1.10 逗号运算符 ........................................................................................................................................................... 72 3.1.11 字串运算符 +......................................................................................................................................................... 72 3.1.12 运算符常规操作规则 .......................................................................................................................................... 72 3.1.13 造型运算符 ........................................................................................................................................................... 73 3.1.14 Java 没有“sizeof” ............................................................................................................................................ 74 3.1.15 复习计算顺序 ....................................................................................................................................................... 75 3.1.16 运算符总结 ........................................................................................................................................................... 75 3.2 执行控制 .........................................................................................................................................................................84 3.2.1 真和假 ...................................................................................................................................................................... 84 3.2.2 if-else........................................................................................................................................................................ 84 3.2.3 反复........................................................................................................................................................................... 85 18

3.2.4 do-while.................................................................................................................................................................... 85 3.2.5 for.............................................................................................................................................................................. 86 3.2.6 中断和继续 .............................................................................................................................................................. 87 3.2.7 开关........................................................................................................................................................................... 91 3.3 总结 .................................................................................................................................................................................. 94 3.4 练习.................................................................................................................................................................................. 94 第 4 章 初始化和清除 ..............................................................................................................................................................95 4.1 用构建器自动初始化 ...................................................................................................................................................95 4.2 方法过载 .........................................................................................................................................................................96 4.2.1 区分过载方法 ......................................................................................................................................................... 97 4.2.2 主类型的过载 ......................................................................................................................................................... 98 4.2.3 返回值过载 ............................................................................................................................................................ 101 4.2.4 默认构建器 ............................................................................................................................................................ 102 4.2.5 this 关键字 ............................................................................................................................................................. 102 4.3 清除:收尾和垃圾收集 ............................................................................................................................................ 105 4.3.1 finalize()用途何在................................................................................................................................................ 105 4.3.2 必须执行清除 ....................................................................................................................................................... 106 4.4 成员初始化.................................................................................................................................................................. 108 4.4.1 规定初始化 ............................................................................................................................................................ 109 4.4.2 构建器初始化 ....................................................................................................................................................... 111 4.5 数组初始化.................................................................................................................................................................. 116 4.5.1 多维数组................................................................................................................................................................ 119 4.6 总结............................................................................................................................................................................... 121 4.7 练习............................................................................................................................................................................... 121 第 5 章 隐藏实施过程 ...........................................................................................................................................................123 5.1 包:库单元.................................................................................................................................................................. 123 5.1.1 创建独一无二的包名.......................................................................................................................................... 124 5.1.2 自定义工具库 ....................................................................................................................................................... 126 5.1.3 利用导入改变行为 .............................................................................................................................................. 128 5.1.4 包的停用................................................................................................................................................................ 130 5.2 Java 访问指示符 ........................................................................................................................................................ 130 5.2.1 “友好的” ............................................................................................................................................................ 130 5.2.2 public:接口访问 ................................................................................................................................................ 131 5.2.3 private:不能接触!........................................................................................................................................... 132 5.2.4 protected:“友好的一种” .............................................................................................................................. 133 5.3 接口与实现.................................................................................................................................................................. 134 5.4 类访问 ........................................................................................................................................................................... 135 5.5 总结............................................................................................................................................................................... 136 5.6 练习............................................................................................................................................................................... 137 第 6 章 类再生 ........................................................................................................................................................................ 139 6.1 合成的语法.................................................................................................................................................................. 139 6.2 继承的语法 .................................................................................................................................................................. 141 6.2.1 初始化基础类 ....................................................................................................................................................... 143 6.3 合成与继承的结合 .................................................................................................................................................... 145 6.3.1 确保正确的清除................................................................................................................................................... 146 6.3.2 名字的隐藏 ............................................................................................................................................................ 148 6.4 到底选择合成还是继承 ............................................................................................................................................ 149 6.5 protected ....................................................................................................................................................................... 150 6.6 累积开发 ......................................................................................................................................................................151 6.7 上溯造型 ......................................................................................................................................................................151 6.7.1 何谓“上溯造型”?.......................................................................................................................................... 152 6.8 final 关键字.................................................................................................................................................................. 152 6.8.1 final 数据................................................................................................................................................................ 152 19

6.8.2 final 方法................................................................................................................................................................ 155 6.8.3 final 类.................................................................................................................................................................... 156 6.8.4 final 的注意事项 .................................................................................................................................................. 156 6.9 初始化和类装载......................................................................................................................................................... 157 6.9.1 继承初始化 ............................................................................................................................................................ 157 6.10 总结............................................................................................................................................................................. 158 6.11 练习............................................................................................................................................................................. 159 第 7 章 多形性 ........................................................................................................................................................................ 160 7.1 上溯造型 ......................................................................................................................................................................160 7.1.1 为什么要上溯造型 .............................................................................................................................................. 161 7.2 深入理解 ......................................................................................................................................................................162 7.2.1 方法调用的绑定................................................................................................................................................... 163 7.2.2 产生正确的行为................................................................................................................................................... 163 7.2.3 扩展性 .................................................................................................................................................................... 165 7.3 覆盖与过载.................................................................................................................................................................. 168 7.4 抽象类和方法 ............................................................................................................................................................. 169 7.5 接口............................................................................................................................................................................... 172 7.5.1 Java 的“多重继承”.......................................................................................................................................... 174 7.5.2 通过继承扩展接口 .............................................................................................................................................. 176 7.5.3 常数分组................................................................................................................................................................ 177 7.5.4 初始化接口中的字段.......................................................................................................................................... 178 7.6 内部类 ........................................................................................................................................................................... 179 7.6.1 内部类和上溯造型 .............................................................................................................................................. 180 7.6.2 方法和作用域中的内部类 ................................................................................................................................. 181 7.6.3 链接到外部类 ....................................................................................................................................................... 186 7.6.4 static 内部类.......................................................................................................................................................... 187 7.6.5 引用外部类对象................................................................................................................................................... 189 7.6.6 从内部类继承 ....................................................................................................................................................... 190 7.6.7 内部类可以覆盖吗?.......................................................................................................................................... 190 7.6.8 内部类标识符 ....................................................................................................................................................... 192 7.6.9 为什么要用内部类:控制框架......................................................................................................................... 192 7.7 构建器和多形性......................................................................................................................................................... 198 7.7.1 构建器的调用顺序 .............................................................................................................................................. 198 7.7.2 继承和 finalize() ................................................................................................................................................... 199 7.7.3 构建器内部的多形性方法的行为 .................................................................................................................... 202 7.8 通过继承进行设计 .................................................................................................................................................... 204 7.8.1 纯继承与扩展 ....................................................................................................................................................... 205 7.8.2 下溯造型与运行期类型标识............................................................................................................................. 206 7.9 总结............................................................................................................................................................................... 208 7.10 练习............................................................................................................................................................................. 208 第 8 章 对象的容纳 ................................................................................................................................................................ 209 8.1 数组............................................................................................................................................................................... 209 8.1.1 数组和第一类对象 .............................................................................................................................................. 209 8.1.2 数组的返回 ............................................................................................................................................................ 212 8.2 集合............................................................................................................................................................................... 213 8.2.1 缺点:类型未知................................................................................................................................................... 213 8.3 枚举器(反复器) .................................................................................................................................................... 217 8.4 集合的类型.................................................................................................................................................................. 220 8.4.1 Vector..................................................................................................................................................................... 220 8.4.2 BitSet...................................................................................................................................................................... 221 8.4.3 Stack ....................................................................................................................................................................... 222 8.4.4 Hashtable................................................................................................................................................................ 223 8.4.5 再论枚举器 ............................................................................................................................................................ 228 20

8.5 排序............................................................................................................................................................................... 229 8.6 通用集合库.................................................................................................................................................................. 232 8.7 新集合 ........................................................................................................................................................................... 233 8.7.1 使用 Collections................................................................................................................................................... 235 8.7.2 使用 Lists............................................................................................................................................................... 238 8.7.3 使用 Sets ................................................................................................................................................................ 242 8.7.4 使用 Maps.............................................................................................................................................................. 244 8.7.5 决定实施方案 ....................................................................................................................................................... 247 8.7.6 未支持的操作 ....................................................................................................................................................... 253 8.7.7 排序和搜索 ............................................................................................................................................................ 255 8.7.8 实用工具................................................................................................................................................................ 259 8.8 总结............................................................................................................................................................................... 261 8.9 练习............................................................................................................................................................................... 262 第 9 章 违例差错控制 ...........................................................................................................................................................263 9.1 基本违例 ......................................................................................................................................................................263 9.1.1 违例自变量 ............................................................................................................................................................ 264 9.2 违例的捕获.................................................................................................................................................................. 264 9.2.1 try 块 ....................................................................................................................................................................... 264 9.2.2 违例控制器 ............................................................................................................................................................ 265 9.2.3 违例规范................................................................................................................................................................ 265 9.2.4 捕获所有违例 ....................................................................................................................................................... 266 9.2.5 重新“掷”出违例 .............................................................................................................................................. 267 9.3 标准 Java 违例............................................................................................................................................................ 270 9.3.1 RuntimeException 的特殊情况 ......................................................................................................................... 270 9.4 创建自己的违例......................................................................................................................................................... 271 9.5 违例的限制.................................................................................................................................................................. 274 9.6 用 finally 清除............................................................................................................................................................. 276 9.6.1 用 finally 做什么 .................................................................................................................................................. 277 9.6.2 缺点:丢失的违例 .............................................................................................................................................. 279 9.7 构建器 ........................................................................................................................................................................... 280 9.8 违例匹配 ......................................................................................................................................................................283 9.8.1 违例准则................................................................................................................................................................ 284 9.9 总结............................................................................................................................................................................... 284 9.10 练习............................................................................................................................................................................. 284 第 10 章 J AVA IO 系 统 ......................................................................................................................................................... 285 10.1 输入和输出 ................................................................................................................................................................ 285 10.1.1 InputStream的类型 ........................................................................................................................................... 285 10.1.2 OutputStream的类型 ........................................................................................................................................ 286 10.2 增添属性和有用的接口 ......................................................................................................................................... 286 10.2.1 通过 FilterInputStream 从 InputStream里读入数据 ................................................................................... 287 10.2.2 通过 FilterOutputStream 向 OutputStream 里写入数据 ............................................................................. 287 10.3 本身的缺陷:RandomAccessFile......................................................................................................................... 288 10.4 File 类......................................................................................................................................................................... 288 10.4.1 目录列表器 ......................................................................................................................................................... 288 10.4.2 检查与创建目录.................................................................................................................................................292 10.5 IO 流的典型应用...................................................................................................................................................... 294 10.5.1 输入流 .................................................................................................................................................................. 296 10.5.2 输出流 .................................................................................................................................................................. 298 10.5.3 快捷文件处理 ..................................................................................................................................................... 298 10.5.4 从标准输入中读取数据 .................................................................................................................................... 300 10.5.5 管道数据流 ......................................................................................................................................................... 300 10.6 StreamTokenizer........................................................................................................................................................ 300 21

10.6.1 StringTokenizer .................................................................................................................................................. 303 10.7 Java 1.1 的 IO 流...................................................................................................................................................... 305 10.7.1 数据的发起与接收 ............................................................................................................................................ 305 10.7.2 修改数据流的行为 ............................................................................................................................................ 306 10.7.3 未改变的类 ......................................................................................................................................................... 306 10.7.4 一个例子.............................................................................................................................................................. 307 10.7.5 重导向标准 IO.................................................................................................................................................... 310 10.8 压缩............................................................................................................................................................................. 311 10.8.1 用 GZIP 进行简单压缩..................................................................................................................................... 311 10.8.2 用 Zip 进行多文件保存.................................................................................................................................... 312 10.8.3 Java 归档(jar)实用程序 ............................................................................................................................... 314 10.9 对象序列化 ................................................................................................................................................................ 315 10.9.1 寻找类 .................................................................................................................................................................. 318 10.9.2 序列化的控制 ..................................................................................................................................................... 319 10.9.3 利用“持久性”.................................................................................................................................................326 10.10 总结........................................................................................................................................................................... 332 10.11 练习........................................................................................................................................................................... 332 第 11 章 运 行 期 类 型 鉴 定..................................................................................................................................................... 333 11.1 对 RTTI 的需要 ......................................................................................................................................................... 333 11.1.1 Class 对象............................................................................................................................................................ 334 11.1.2 造型前的检查 ..................................................................................................................................................... 337 11.2 RTTI 语法................................................................................................................................................................... 342 11.3 反射:运行期类信息.............................................................................................................................................. 343 11.3.1 一个类方法提取器 ............................................................................................................................................ 344 11.4 总结............................................................................................................................................................................. 347 11.5 练习............................................................................................................................................................................. 348 第 12 章 传 递 和 返 回 对 象..................................................................................................................................................... 349 12.1 传递句柄.................................................................................................................................................................... 349 12.1.1 别名问题.............................................................................................................................................................. 349 12.2 制作本地副本...........................................................................................................................................................351 12.2.1 按值传递.............................................................................................................................................................. 351 12.2.2 克隆对象.............................................................................................................................................................. 352 12.2.3 使类具有克隆能力 ............................................................................................................................................ 353 12.2.4 成功的克隆 ......................................................................................................................................................... 353 12.2.5 Object.clone()的效果 ........................................................................................................................................ 355 12.2.6 克隆合成对象 ..................................................................................................................................................... 356 12.2.7 用 Vector 进行深层复制................................................................................................................................... 358 12.2.8 通过序列化进行深层复制............................................................................................................................... 359 12.2.9 使克隆具有更大的深度 .................................................................................................................................... 361 12.2.10 为什么有这个奇怪的设计 ............................................................................................................................. 362 12.3 克隆的控制 ................................................................................................................................................................ 363 12.3.1 副本构建器 ......................................................................................................................................................... 366 12.4 只读类 ........................................................................................................................................................................ 369 12.4.1 创建只读类 ......................................................................................................................................................... 370 12.4.2 “一成不变”的弊端 ........................................................................................................................................ 371 12.4.3 不变字串.............................................................................................................................................................. 373 12.4.4 String 和 StringBuffer 类.................................................................................................................................. 374 12.4.5 字串的特殊性 ..................................................................................................................................................... 376 12.5 总结............................................................................................................................................................................. 376 12.6 练习............................................................................................................................................................................. 376 第十三 章 创建窗口和程序片 .............................................................................................................................................. 378 13.1 为何要用 AWT ?...................................................................................................................................................... 378 22

13.2 基本程序片 ................................................................................................................................................................ 379 13.2.1 程序片的测试 ..................................................................................................................................................... 380 13.2.2 一个更图形化的例子 ........................................................................................................................................ 381 13.2.3 框架方法的演示.................................................................................................................................................381 13.3 制作按钮.................................................................................................................................................................... 382 13.4 捕获事件.................................................................................................................................................................... 382 13.5 文本字段.................................................................................................................................................................... 384 13.6 文本区域.................................................................................................................................................................... 385 13.7 标签............................................................................................................................................................................. 386 13.8 复选框 ........................................................................................................................................................................ 387 13.9 单选钮 ........................................................................................................................................................................ 388 13.10 下拉列表.................................................................................................................................................................. 389 13.11 列表框 ......................................................................................................................................................................390 13.11.1 handleEvent() .................................................................................................................................................... 391 13.12 布局的控制 ............................................................................................................................................................. 393 13.12.1 FlowLayout....................................................................................................................................................... 393 13.12.2 BorderLayout .................................................................................................................................................... 393 13.12.3 GridLayout........................................................................................................................................................ 394 13.12.4 CardLayout........................................................................................................................................................ 394 13.12.5 GridBagLayout.................................................................................................................................................396 13.13 action 的替代品...................................................................................................................................................... 396 13.14 程序片的局限......................................................................................................................................................... 400 13.14.1 程序片的优点................................................................................................................................................... 401 13.15 视窗化应用 ............................................................................................................................................................. 401 13.15.1 菜单.................................................................................................................................................................... 401 13.15.2 对话框 ................................................................................................................................................................ 404 13.16 新型 AWT ................................................................................................................................................................. 408 13.16.1 新的事件模型................................................................................................................................................... 409 13.16.2 事件和接收者类型 .......................................................................................................................................... 410 13.16.3 用 Java 1.1 AWT制作窗口和程序片 .......................................................................................................... 414 13.16.4 再研究一下以前的例子 ................................................................................................................................. 416 13.16.5 动态绑定事件................................................................................................................................................... 431 13.16.6 将事务逻辑与 UI 逻辑区分开 ...................................................................................................................... 433 13.16.7 推荐编码方法................................................................................................................................................... 435 13.17 Java 1.1 用户接口 API.......................................................................................................................................... 448 13.17.1 桌面颜色 ........................................................................................................................................................... 448 13.17.2 打印.................................................................................................................................................................... 448 13.17.3 剪贴板 ................................................................................................................................................................ 454 13.18 可视编程和 Beans.................................................................................................................................................456 13.18.1 什么是 Bean...................................................................................................................................................... 457 13.18.2 用 Introspector 提取 BeanInfo....................................................................................................................... 458 13.18.3 一个更复杂的 Bean ........................................................................................................................................ 463 13.18.4 Bean 的封装 ...................................................................................................................................................... 465 13.18.5 更复杂的 Bean 支持........................................................................................................................................ 466 13.18.6 Bean 更多的知识 ............................................................................................................................................. 466 13.19 Swing 入门(注释⑦)......................................................................................................................................... 467 13.19.1 Swing 有哪些优点 ........................................................................................................................................... 467 13.19.2 方便的转换 ....................................................................................................................................................... 467 13.19.3 显示框架 ........................................................................................................................................................... 468 13.19.4 工具提示 ........................................................................................................................................................... 469 13.19.5 边框.................................................................................................................................................................... 469 13.19.6 按钮.................................................................................................................................................................... 470 13.19.7 按钮组 ................................................................................................................................................................ 471 23

13.19.8 图标.................................................................................................................................................................... 472 13.19.9 菜单.................................................................................................................................................................... 474 13.19.10 弹出式菜单 ..................................................................................................................................................... 477 13.19.11 列表框和组合框............................................................................................................................................ 479 13.19.12 滑杆和进度指示条 ........................................................................................................................................ 479 13.19.13 树 ...................................................................................................................................................................... 480 13.19.14 表格.................................................................................................................................................................. 482 13.19.15 卡片式对话框................................................................................................................................................ 483 13.19.16 Swing 消息框 .................................................................................................................................................485 13.19.17 Swing 更多的知识......................................................................................................................................... 485 13.20 总结........................................................................................................................................................................... 485 13.21 练习........................................................................................................................................................................... 486 第 14 章 多线程 ......................................................................................................................................................................487 14.1 反应灵敏的用户界面.............................................................................................................................................. 487 14.1.1 从线程继承 ......................................................................................................................................................... 489 14.1.2 针对用户界面的多线程 .................................................................................................................................... 490 14.1.3 用主类合并线程.................................................................................................................................................493 14.1.4 制作多个线程 ..................................................................................................................................................... 495 14.1.5 Daemon 线程 ...................................................................................................................................................... 498 14.2 共享有限的资源....................................................................................................................................................... 499 14.2.1 资源访问的错误方法 ........................................................................................................................................ 499 14.2.2 Java 如何共享资源............................................................................................................................................ 503 14.2.3 回顾 Java Beans .................................................................................................................................................506 14.3 堵塞............................................................................................................................................................................. 510 14.3.1 为何会堵塞 ......................................................................................................................................................... 510 14.3.2 死锁 ...................................................................................................................................................................... 518 14.4 优先级 ........................................................................................................................................................................ 521 14.4.1 线程组 .................................................................................................................................................................. 525 14.5 回顾 runnable............................................................................................................................................................ 530 14.5.1 过多的线程 ......................................................................................................................................................... 532 14.6 总结............................................................................................................................................................................. 535 14.7 练习............................................................................................................................................................................. 535 第 15 章 网 络 编 程.................................................................................................................................................................. 537 15.1 机器的标识 ................................................................................................................................................................ 537 15.1.1 服务器和客户机.................................................................................................................................................538 15.1.2 端口:机器内独一无二的场所 ...................................................................................................................... 539 15.2 套接字 ........................................................................................................................................................................ 539 15.2.1 一个简单的服务器和客户机程序 .................................................................................................................. 539 15.3 服务多个客户 ...........................................................................................................................................................543 15.4 数据报 ........................................................................................................................................................................ 547 15.5 一个 Web 应用.......................................................................................................................................................... 551 15.5.1 服务器应用 ......................................................................................................................................................... 552 15.5.2 NameSender程序片 .......................................................................................................................................... 556 15.5.3 要注意的问题 ..................................................................................................................................................... 560 15.6 Java 与 CGI 的沟通 .................................................................................................................................................560 15.6.1 CGI 数据的编码 .................................................................................................................................................561 15.6.2 程序片 .................................................................................................................................................................. 562 15.6.3 用 C++写的 CGI 程序....................................................................................................................................... 566 15.6.4 POST 的概念 ...................................................................................................................................................... 573 15.7 用 JDBC 连接数据库 .............................................................................................................................................. 576 15.7.1 让示例运行起来 .................................................................................................................................................578 15.7.2 查找程序的 GUI 版本....................................................................................................................................... 580 24

15.7.3 JDBC API 为何如何复杂................................................................................................................................. 582 15.8 远程方法.................................................................................................................................................................... 582 15.8.1 远程接口概念 ..................................................................................................................................................... 582 15.8.2 远程接口的实施.................................................................................................................................................583 15.8.3 创建根与干 ......................................................................................................................................................... 585 15.8.4 使用远程对象 ..................................................................................................................................................... 585 15.8.5 RMI 的替选方案 ................................................................................................................................................ 586 15.9 总结............................................................................................................................................................................. 586 15.10 练习........................................................................................................................................................................... 586 第 16 章 设 计 范 式.................................................................................................................................................................. 588 16.1.1 单子 ...................................................................................................................................................................... 588 16.1.2 范式分类.............................................................................................................................................................. 589 16.2 观察器范式 ................................................................................................................................................................ 590 16.3 模拟垃圾回收站....................................................................................................................................................... 592 16.4 改进设计.................................................................................................................................................................... 595 16.4.1 “制作更多的对象” ........................................................................................................................................ 595 16.4.2 用于原型创建的一个范式............................................................................................................................... 597 16.5 抽象的应用 ................................................................................................................................................................ 604 16.6 多重派遣.................................................................................................................................................................... 607 16.6.1 实现双重派遣 ..................................................................................................................................................... 607 16.7 访问器范式 ................................................................................................................................................................ 612 16.8 RTTI 真的有害吗 ...................................................................................................................................................... 618 16.9 总结............................................................................................................................................................................. 620 16.10 练习........................................................................................................................................................................... 621 第 17 章 项 目 .......................................................................................................................................................................... 622 17.1 文字处理.................................................................................................................................................................... 622 17.1.1 提取代码列表 ..................................................................................................................................................... 622 17.1.2 检查大小写样式.................................................................................................................................................633 17.2 方法查找工具...........................................................................................................................................................639 17.3 复杂性理论 ................................................................................................................................................................ 643 17.4 总结............................................................................................................................................................................. 649 17.5 练习............................................................................................................................................................................. 649 附 录 A 使用非 JAVA 代 码 .................................................................................................................................................650 A.1 Java 固有接口 ............................................................................................................................................................ 650 A.1.1 调用固有方法 ...................................................................................................................................................... 650 A.1.2 访问 JNI 函数:JNIEnv 自变量....................................................................................................................... 652 A.1.3 传递和使用 Java 对象........................................................................................................................................ 653 A.1.4 JNI 和 Java 异常 .................................................................................................................................................. 654 A.1.5 JNI 和线程处理 ................................................................................................................................................... 655 A.1.6 使用现成代码 ...................................................................................................................................................... 655 A.2 微软的解决方案 ........................................................................................................................................................ 655 A.3 J/Direct......................................................................................................................................................................... 655 A.3.1 @dll.import 引导命令 ........................................................................................................................................ 656 A.3.2 com.ms.win32 包 .................................................................................................................................................657 A.3.3 汇集........................................................................................................................................................................ 658 A.3.4 编写回调函数 ...................................................................................................................................................... 659 A.3.5 其他 J/Direct 特性............................................................................................................................................... 659 A.4 本原接口(RNI)...................................................................................................................................................... 660 A.4.1 RNI 总结............................................................................................................................................................... 661 A.5 Java/COM 集成.......................................................................................................................................................... 661 A.5.1 COM 基础............................................................................................................................................................. 662 A.5.2 MS Java/COM 集成............................................................................................................................................ 663 25

A.5.3 用 Java 设计 COM 服务器 ................................................................................................................................ 663 A.5.4 用 Java 设计 COM 客户..................................................................................................................................... 665 A.5.5 ActiveX/Beans 集成............................................................................................................................................ 666 A.5.6 固有方法与程序片的注意事项........................................................................................................................ 666 A.6 CORBA ......................................................................................................................................................................... 666 A.6.1 CORBA 基础 ....................................................................................................................................................... 666 A.6.2 一个例子............................................................................................................................................................... 667 A.6.3 Java 程序片和 CORBA...................................................................................................................................... 671 A.6.4 比较 CORBA 与 RMI ........................................................................................................................................ 671 A.7 总结............................................................................................................................................................................... 671 附 录 B 对 比 C++和 J AVA.................................................................................................................................................... 672 附 录 C J AVA 编 程 规 则 ......................................................................................................................................................... 677 附 录 D 性 能 ............................................................................................................................................................................ 679 D.1 基本方法 ..................................................................................................................................................................... 679 D.2 寻找瓶颈 ..................................................................................................................................................................... 679 D.2.1 安插自己的测试代码 ......................................................................................................................................... 679 D.2.2 JDK 性能评测[2].................................................................................................................................................679 D.2.3 特殊工具............................................................................................................................................................... 680 D.2.4 性能评测的技巧.................................................................................................................................................. 680 D.3 提速方法 ..................................................................................................................................................................... 680 D.3.1 常规手段............................................................................................................................................................... 680 D.3.2 依赖语言的方法.................................................................................................................................................. 680 D.3.3 特殊情况............................................................................................................................................................... 681 D.4 参考资源 ..................................................................................................................................................................... 682 D.4.1 性能工具............................................................................................................................................................... 682 D.4. 2 Web 站点.............................................................................................................................................................. 682 D.4.3 文章........................................................................................................................................................................ 682 D.4.4 Java 专业书籍...................................................................................................................................................... 683 D.4.5 一般书籍............................................................................................................................................................... 683 附 录 E 关于垃圾收集的一些话 .......................................................................................................................................... 684 附 录 F 推 荐 读 物 .................................................................................................................................................................... 686

26

第 1 章 对象入门
“为什么面向对象的编程会在软件开发领域造成如此震憾的影响?” 面向对象编程(O P)具有多方面的吸引力。对管理人员,它实现了更快和更廉价的开发与维护过程。对分析 O 与设计人员,建模处理变得更加简单,能生成清晰、易于维护的设计方案。对程序员,对象模型显得如此高 雅和浅显。此外,面向对象工具以及库的巨大威力使编程成为一项更使人愉悦的任务。每个人都可从中获 益,至少表面如此。 如果说它有缺点,那就是掌握它需付出的代价。思考对象的时候,需要采用形象思维,而不是程序化的思 维。与程序化设计相比,对象的设计过程更具挑战性——特别是在尝试创建可重复使用(可再生)的对象 时。过去,那些初涉面向对象编程领域的人都必须进行一项令人痛苦的选择: ( 1) 选择一种诸如 Sm l t al k 的语言,“出师”前必须掌握一个巨型的库。 al ( 2) 选择几乎根本没有库的 C ++(注释①),然后深入学习这种语言,直至能自行编写对象库。 ①:幸运的是,这一情况已有明显改观。现在有第三方库以及标准的 C ++库供选用。 事实上,很难很好地设计出对象——从而很难设计好任何东西。因此,只有数量相当少的“专家”能设计出 最好的对象,然后让其他人享用。对于成功的 O P 语言,它们不仅集成了这种语言的语法以及一个编译程序 O (编译器),而且还有一个成功的开发环境,其中包含设计优良、易于使用的库。所以,大多数程序员的首 要任务就是用现有的对象解决自己的应用问题。本章的目标就是向大家揭示出面向对象编程的概念,并证明 它有多么简单。 本章将向大家解释 Java 的多项设计思想,并从概念上解释面向对象的程序设计。但要注意在阅读完本章后, 并不能立即编写出全功能的 Java 程序。所有详细的说明和示例会在本书的其他章节慢慢道来。

1. 1 抽象的进步
所有编程语言的最终目的都是提供一种“抽象”方法。一种较有争议的说法是:解决问题的复杂程度直接取 决于抽象的种类及质量。这儿的“种类”是指准备对什么进行“抽象”?汇编语言是对基础机器的少量抽 象。后来的许多“命令式”语言(如 FO RA ,BA C和 C RT N SI )是对汇编语言的一种抽象。与汇编语言相比,这 些语言已有了长足的进步,但它们的抽象原理依然要求我们着重考虑计算机的结构,而非考虑问题本身的结 构。在机器模型(位于“方案空间”)与实际解决的问题模型(位于“问题空间”)之间,程序员必须建立 起一种联系。这个过程要求人们付出较大的精力,而且由于它脱离了编程语言本身的范围,造成程序代码很 难编写,而且要花较大的代价进行维护。由此造成的副作用便是一门完善的“编程方法”学科。 为机器建模的另一个方法是为要解决的问题制作模型。对一些早期语言来说,如 LI SP 和 A PL,它们的做法是 “从不同的角度观察世界”——“所有问题都归纳为列表”或“所有问题都归纳为算法”。PRO G则将所有 LO 问题都归纳为决策链。对于这些语言,我们认为它们一部分是面向基于“强制”的编程,另一部分则是专为 处理图形符号设计的。每种方法都有自己特殊的用途,适合解决某一类的问题。但只要超出了它们力所能及 的范围,就会显得非常笨拙。 面向对象的程序设计在此基础上则跨出了一大步,程序员可利用一些工具表达问题空间内的元素。由于这种 表达非常普遍,所以不必受限于特定类型的问题。我们将问题空间中的元素以及它们在方案空间的表示物称 作“对象”(O ect )。当然,还有一些在问题空间没有对应体的其他对象。通过添加新的对象类型,程序 bj 可进行灵活的调整,以便与特定的问题配合。所以在阅读方案的描述代码时,会读到对问题进行表达的话 语。与我们以前见过的相比,这无疑是一种更加灵活、更加强大的语言抽象方法。总之,O P 允许我们根据 O 问题来描述问题,而不是根据方案。然而,仍有一个联系途径回到计算机。每个对象都类似一台小计算机; 它们有自己的状态,而且可要求它们进行特定的操作。与现实世界的“对象”或者“物体”相比,编程“对 象”与它们也存在共通的地方:它们都有自己的特征和行为。 A an Kay 总结了 Sm l t al k 的五大基本特征。这是第一种成功的面向对象程序设计语言,也是 Java 的基础 l al 语言。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的: ( 1) 所有东西都是对象。可将对象想象成一种新型变量;它保存着数据,但可要求它对自身进行操作。理论 上讲,可从要解决的问题身上提出所有概念性的组件,然后在程序中将其表达为一个对象。 ( 2) 程序是一大堆对象的组合;通过消息传递,各对象知道自己该做些什么。为了向对象发出请求,需向那 27

个对象“发送一条消息”。更具体地讲,可将消息想象为一个调用请求,它调用的是从属于目标对象的一个 子例程或函数。 ( 3) 每个对象都有自己的存储空间,可容纳其他对象。或者说,通过封装现有对象,可制作出新型对象。所 以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。 ( 4) 每个对象都有一种类型。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(C as s ) l 是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么消息发给它?”。 ( 5) 同一类所有对象都能接收相同的消息。这实际是别有含义的一种说法,大家不久便能理解。由于类型为 “圆”(C r cl e)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收形状消 i 息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括 “圆”。这一特性称为对象的“可替换性”,是 O P 最重要的概念之一。 O 一些语言设计者认为面向对象的程序设计本身并不足以方便解决所有形式的程序问题,提倡将不同的方法组 合成“多形程序设计语言”(注释②)。 ②:参见 Ti m hy Budd编著的《M t i par adi gmPr ogr am i ng i n Leda》,A s on- W ey 1995 年出版。 ot ul m ddi esl

1. 2 对象的接口
亚里士多德或许是认真研究“类型”概念的第一人,他曾谈及“鱼类和鸟类”的问题。在世界首例面向对象 语言 Si m a- 67 中,第一次用到了这样的一个概念: ul 所有对象——尽管各有特色——都属于某一系列对象的一部分,这些对象具有通用的特征和行为。在 Si m a 67 中,首次用到了 cl as s 这个关键字,它为程序引入了一个全新的类型(cl as 和 t ype 通常可互换使 ul 用;注释③)。 ③:有些人进行了进一步的区分,他们强调“类型”决定了接口,而“类”是那个接口的一种特殊实现方 式。 Si m a是一个很好的例子。正如这个名字所暗示的,它的作用是“模拟”(Si m at e)象“银行出纳员”这 ul ul 样的经典问题。在这个例子里,我们有一系列出纳员、客户、帐号以及交易等。每类成员(元素)都具有一 些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有 自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实 体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的 “类”,那个类具有自己的通用特征与行为。 因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据“类型”(Type),但几乎所 有面向对象的程序设计语言都采用了“cl as s ”关键字。当您看到“t ype”这个字的时候,请同时想到 “cl as s ”;反之亦然。 建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事 实上,当我们进行面向对象的程序设计时,面临的最大一项挑战性就是:如何在“问题空间”(问题实际存 在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对 一”对应或映射关系。 如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其做一些实际的事情,比如完 成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出 的请求是通过它的“接口”(I nt er f ace)定义的,对象的“类型”或“类”则规定了它的接口形式。“类 型”与“接口”的等价或对应关系是面向对象程序设计的基础。 下面让我们以电灯泡为例:

28

Li ght l t = new Li ght ( ) ; l t . on( ) ; 在这个例子中,类型/类的名称是 Li ght ,可向 Li ght 对象发出的请求包括包括打开(on)、关闭(of f )、 变得更明亮(br i ght en)或者变得更暗淡(di m )。通过简单地声明一个名字(l t ),我们为 Li ght 对象创建 了一个“句柄”。然后用 new关键字新建类型为 Li ght 的一个对象。再用等号将其赋给句柄。为了向对象发 送一条消息,我们列出句柄名(l t ),再用一个句点符号(. )把它同消息名称(on)连接起来。从中可以看 出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单和直观的。

1. 3 实现方案的隐藏
为方便后面的讨论,让我们先对这一领域的从业人员作一下分类。从根本上说,大致有两方面的人员涉足面 向对象的编程:“类创建者”(创建新数据类型的人)以及“客户程序员”(在自己的应用程序中采用现成 数据类型的人;注释④)。对客户程序员来讲,最主要的目标就是收集一个充斥着各种类的编程“工具 箱”,以便快速开发符合自己要求的应用。而对类创建者来说,他们的目标则是从头构建一个类,只向客户 程序员开放有必要开放的东西(接口),其他所有细节都隐藏起来。为什么要这样做?隐藏之后,客户程序 员就不能接触和改变那些细节,所以原创者不用担心自己的作品会受到非法修改,可确保它们不会对其他人 造成影响。 ④:感谢我的朋友 Scot t M eyer s ,是他帮我起了这个名字。 “接口”(I nt er f ace)规定了可对一个特定的对象发出哪些请求。然而,必须在某个地方存在着一些代码, 以便满足这些请求。这些代码与那些隐藏起来的数据便叫作“隐藏的实现”。站在程式化程序编写 (Pr ocedur al Pr ogr am i ng)的角度,整个问题并不显得复杂。一种类型含有与每种可能的请求关联起来的 m 函数。一旦向对象发出一个特定的请求,就会调用那个函数。我们通常将这个过程总结为向对象“发送一条 消息”(提出一个请求)。对象的职责就是决定如何对这条消息作出反应(执行相应的代码)。 对于任何关系,重要一点是让牵连到的所有成员都遵守相同的规则。创建一个库时,相当于同客户程序员建 立了一种关系。对方也是程序员,但他们的目标是组合出一个特定的应用(程序),或者用您的库构建一个 更大的库。 若任何人都能使用一个类的所有成员,那么客户程序员可对那个类做任何事情,没有办法强制他们遵守任何 约束。即便非常不愿客户程序员直接操作类内包含的一些成员,但倘若未进行访问控制,就没有办法阻止这 一情况的发生——所有东西都会暴露无遗。 有两方面的原因促使我们控制对成员的访问。第一个原因是防止程序员接触他们不该接触的东西——通常是 内部数据类型的设计思想。若只是为了解决特定的问题,用户只需操作接口即可,毋需明白这些信息。我们 向用户提供的实际是一种服务,因为他们很容易就可看出哪些对自己非常重要,以及哪些可忽略不计。 进行访问控制的第二个原因是允许库设计人员修改内部结构,不用担心它会对客户程序员造成什么影响。例 如,我们最开始可能设计了一个形式简单的类,以便简化开发。以后又决定进行改写,使其更快地运行。若 接口与实现方法早已隔离开,并分别受到保护,就可放心做到这一点,只要求用户重新链接一下即可。 Java 采用三个显式(明确)关键字以及一个隐式(暗示)关键字来设置类边界:publ i c,pr i vat e, pr ot ect ed以及暗示性的 f r i endl y。若未明确指定其他关键字,则默认为后者。这些关键字的使用和含义都 是相当直观的,它们决定了谁能使用后续的定义内容。“publ i c”(公共)意味着后续的定义任何人均可使 用。而在另一方面,“pr i vat e”(私有)意味着除您自己、类型的创建者以及那个类型的内部函数成员,其 他任何人都不能访问后续的定义信息。pr i vat e在您与客户程序员之间竖起了一堵墙。若有人试图访问私有 29

成员,就会得到一个编译期错误。“f r i endl y ”(友好的)涉及“包装”或“封装”(Package)的概念—— 即 Java 用来构建库的方法。若某样东西是“友好的”,意味着它只能在这个包装的范围内使用(所以这一访 问级别有时也叫作“包装访问”)。“pr ot ect ed”(受保护的)与“pr i vat e”相似,只是一个继承的类可 访问受保护的成员,但不能访问私有成员。继承的问题不久就要谈到。

1. 4 方案的重复使用
创建并测试好一个类后,它应(从理想的角度)代表一个有用的代码单位。但并不象许多人希望的那样,这 种重复使用的能力并不容易实现;它要求较多的经验以及洞察力,这样才能设计出一个好的方案,才有可能 重复使用。 许多人认为代码或设计方案的重复使用是面向对象的程序设计提供的最伟大的一种杠杆。 为重复使用一个类,最简单的办法是仅直接使用那个类的对象。但同时也能将那个类的一个对象置入一个新 类。我们把这叫作“创建一个成员对象”。新类可由任意数量和类型的其他对象构成。无论如何,只要新类 达到了设计要求即可。这个概念叫作“组织”——在现有类的基础上组织一个新类。有时,我们也将组织称 作“包含”关系,比如“一辆车包含了一个变速箱”。 对象的组织具有极大的灵活性。新类的“成员对象”通常设为“私有”(Pr i vat e),使用这个类的客户程序 员不能访问它们。这样一来,我们可在不干扰客户代码的前提下,从容地修改那些成员。也可以在“运行 期”更改成员,这进一步增大了灵活性。后面要讲到的“继承”并不具备这种灵活性,因为编译器必须对通 过继承创建的类加以限制。 由于继承的重要性,所以在面向对象的程序设计中,它经常被重点强调。作为新加入这一领域的程序员,或 许早已先入为主地认为“继承应当随处可见”。沿这种思路产生的设计将是非常笨拙的,会大大增加程序的 复杂程度。相反,新建类的时候,首先应考虑“组织”对象;这样做显得更加简单和灵活。利用对象的组 织,我们的设计可保持清爽。一旦需要用到继承,就会明显意识到这一点。

1. 5 继承:重新使用接口
就其本身来说,对象的概念可为我们带来极大的便利。它在概念上允许我们将各式各样数据和功能封装到一 起。这样便可恰当表达“问题空间”的概念,不用刻意遵照基础机器的表达方式。在程序设计语言中,这些 概念则反映为具体的数据类型(使用 cl as s 关键字)。 我们费尽心思做出一种数据类型后,假如不得不又新建一种类型,令其实现大致相同的功能,那会是一件非 常令人灰心的事情。但若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就 显得理想多了。“继承”正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原始 类(正式名称叫作基础类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫作继承类或者子 类)也会反映出这种变化。在 Java 语言中,继承是通过 ext ends 关键字实现的 使用继承时,相当于创建了一个新类。这个新类不仅包含了现有类型的所有成员(尽管 pr i vat e 成员被隐藏 起来,且不能访问),但更重要的是,它复制了基础类的接口。也就是说,可向基础类的对象发送的所有消 息亦可原样发给衍生类的对象。根据可以发送的消息,我们能知道类的类型。这意味着衍生类具有与基础类 相同的类型!为真正理解面向对象程序设计的含义,首先必须认识到这种类型的等价关系。 由于基础类和衍生类具有相同的接口,所以那个接口必须进行特殊的设计。也就是说,对象接收到一条特定 的消息后,必须有一个“方法”能够执行。若只是简单地继承一个类,并不做其他任何事情,来自基础类接 口的方法就会直接照搬到衍生类。这意味着衍生类的对象不仅有相同的类型,也有同样的行为,这一后果通 常是我们不愿见到的。 有两种做法可将新得的衍生类与原来的基础类区分开。第一种做法十分简单:为衍生类添加新函数(功 能)。这些新函数并非基础类接口的一部分。进行这种处理时,一般都是意识到基础类不能满足我们的要 求,所以需要添加更多的函数。这是一种最简单、最基本的继承用法,大多数时候都可完美地解决我们的问 题。然而,事先还是要仔细调查自己的基础类是否真的需要这些额外的函数。

1 . 5 . 1 改善基础类
尽管 ext ends 关键字暗示着我们要为接口“扩展”新功能,但实情并非肯定如此。为区分我们的新类,第二 个办法是改变基础类一个现有函数的行为。我们将其称作“改善”那个函数。 为改善一个函数,只需为衍生类的函数建立一个新定义即可。我们的目标是:“尽管使用的函数接口未变, 但它的新版本具有不同的表现”。 30

1 . 5 . 2 等价与类似关系
针对继承可能会产生这样的一个争论:继承只能改善原基础类的函数吗?若答案是肯定的,则衍生类型就是 与基础类完全相同的类型,因为都拥有完全相同的接口。这样造成的结果就是:我们完全能够将衍生类的一 个对象换成基础类的一个对象!可将其想象成一种“纯替换”。在某种意义上,这是进行继承的一种理想方 式。此时,我们通常认为基础类和衍生类之间存在一种“等价”关系——因为我们可以理直气壮地说:“圆 就是一种几何形状”。为了对继承进行测试,一个办法就是看看自己是否能把它们套入这种“等价”关系 中,看看是否有意义。 但在许多时候,我们必须为衍生类型加入新的接口元素。所以不仅扩展了接口,也创建了一种新类型。这种 新类型仍可替换成基础类型,但这种替换并不是完美的,因为不可在基础类里访问新函数。我们将其称作 “类似”关系;新类型拥有旧类型的接口,但也包含了其他函数,所以不能说它们是完全等价的。举个例子 来说,让我们考虑一下制冷机的情况。假定我们的房间连好了用于制冷的各种控制器;也就是说,我们已拥 有必要的“接口”来控制制冷。现在假设机器出了故障,我们把它换成一台新型的冷、热两用空调,冬天和 夏天均可使用。冷、热空调“类似”制冷机,但能做更多的事情。由于我们的房间只安装了控制制冷的设 备,所以它们只限于同新机器的制冷部分打交道。新机器的接口已得到了扩展,但现有的系统并不知道除原 始接口以外的任何东西。 认识了等价与类似的区别后,再进行替换时就会有把握得多。尽管大多数时候“纯替换”已经足够,但您会 发现在某些情况下,仍然有明显的理由需要在衍生类的基础上增添新功能。通过前面对这两种情况的讨论, 相信大家已心中有数该如何做。

1. 6 多形对象的互换使用
通常,继承最终会以创建一系列类收场,所有类都建立在统一的接口基础上。我们用一幅颠倒的树形图来阐 明这一点(注释⑤): ⑤:这儿采用了“统一记号法”,本书将主要采用这种方法。

对这样的一系列类,我们要进行的一项重要处理就是将衍生类的对象当作基础类的一个对象对待。这一点是 非常重要的,因为它意味着我们只需编写单一的代码,令其忽略类型的特定细节,只与基础类打交道。这样 一来,那些代码就可与类型信息分开。所以更易编写,也更易理解。此外,若通过继承增添了一种新类型, 如“三角形”,那么我们为“几何形状”新类型编写的代码会象在旧类型里一样良好地工作。所以说程序具 备了“扩展能力”,具有“扩展性”。 以上面的例子为基础,假设我们用 Java 写了这样一个函数: voi d doSt uf f ( Shape s ) { s . er as e( ) ; // . . . s . dr aw ) ; ( 31

} 这个函数可与任何“几何形状”(Shape)通信,所以完全独立于它要描绘(dr aw )和删除(er as e)的任何 特定类型的对象。如果我们在其他一些程序里使用 doSt uf f ( ) 函数: C r cl e c = new C r cl e( ) ; i i Tr i angl e t = new Tr i angl e( ) ; Li ne l = new Li ne( ) ; doSt uf f ( c) ; doSt uf f ( t ) ; doSt uf f ( l ) ; 那么对 doSt uf f ( ) 的调用会自动良好地工作,无论对象的具体类型是什么。 这实际是一个非常有用的编程技巧。请考虑下面这行代码: doSt uf f ( c) ; 此时,一个 C r cl e i (圆)句柄传递给一个本来期待 Shape(形状)句柄的函数。由于圆是一种几何形状,所 以 doSt uf f ( ) 能正确地进行处理。也就是说,凡是 doSt uf f ( ) 能发给一个 Shape的消息,C r cl e也能接收。 i 所以这样做是安全的,不会造成错误。 我们将这种把衍生类型当作它的基本类型处理的过程叫作“Upcas t i ng”(上溯造型)。其中,“cas t ”(造 型)是指根据一个现成的模型创建;而“Up”(向上)表明继承的方向是从“上面”来的——即基础类位于 顶部,而衍生类在下方展开。所以,根据基础类进行造型就是一个从上面继承的过程,即“Upcas t i ng”。 在面向对象的程序里,通常都要用到上溯造型技术。这是避免去调查准确类型的一个好办法。请看看 doSt uf f ( ) 里的代码: s . er as e( ) ; // . . . s . dr aw ) ; ( 注意它并未这样表达:“如果你是一个 C r cl e i ,就这样做;如果你是一个 Squar e,就那样做;等等”。若那 样编写代码,就需检查一个 Shape 所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加 了一种新的 Shape类型后,都要相应地进行修改。在这儿,我们只需说:“你是一种几何形状,我知道你能 将自己删掉,即 er as e( );请自己采取那个行动,并自己去控制所有的细节吧。”

1 . 6 . 1 动态绑定
在 doSt uf f ( ) 的代码里,最让人吃惊的是尽管我们没作出任何特殊指示,采取的操作也是完全正确和恰当 的。我们知道,为 C r cl e 调用 dr aw ) 时执行的代码与为一个 Squar e或 Li ne 调用 dr aw ) 时执行的代码是不 i ( ( 同的。但在将 dr aw ) 消息发给一个匿名 Shape时,根据 Shape句柄当时连接的实际类型,会相应地采取正确 ( 的操作。这当然令人惊讶,因为当 Java 编译器为 doSt uf f ( ) 编译代码时,它并不知道自己要操作的准确类型 是什么。尽管我们确实可以保证最终会为 Shape 调用 er as e( ) ,为 Shape 调用 dr aw ),但并不能保证为特定 ( 的 C r cl e,Squar e或者 Li ne 调用什么。然而最后采取的操作同样是正确的,这是怎么做到的呢? i 将一条消息发给对象时,如果并不知道对方的具体类型是什么,但采取的行动同样是正确的,这种情况就叫 作“多形性”(Pol ym phi s m or )。对面向对象的程序设计语言来说,它们用以实现多形性的方法叫作“动态 绑定”。编译器和运行期系统会负责对所有细节的控制;我们只需知道会发生什么事情,而且更重要的是, 如何利用它帮助自己设计程序。 有些语言要求我们用一个特殊的关键字来允许动态绑定。在 C ++中,这个关键字是 vi r t ual 。在 Java 中,我 们则完全不必记住添加一个关键字,因为函数的动态绑定是自动进行的。所以在将一条消息发给对象时,我 们完全可以肯定对象会采取正确的行动,即使其中涉及上溯造型之类的处理。

1 . 6 . 2 抽象的基础类和接口
设计程序时,我们经常都希望基础类只为自己的衍生类提供一个接口。也就是说,我们不想其他任何人实际 创建基础类的一个对象,只对上溯造型成它,以便使用它们的接口。为达到这个目的,需要把那个类变成 32

“抽象”的——使用 abs t r act 关键字。若有人试图创建抽象类的一个对象,编译器就会阻止他们。这种工具 可有效强制实行一种特殊的设计。 亦可用 abs t r act 关键字描述一个尚未实现的方法——作为一个“根”使用,指出:“这是适用于从这个类继 承的所有类型的一个接口函数,但目前尚没有对它进行任何形式的实现。”抽象方法也许只能在一个抽象类 里创建。继承了一个类后,那个方法就必须实现,否则继承的类也会变成“抽象”类。通过创建一个抽象方 法,我们可以将一个方法置入接口中,不必再为那个方法提供可能毫无意义的主体代码。 i nt er f ace(接口)关键字将抽象类的概念更延伸了一步,它完全禁止了所有的函数定义。“接口”是一种相 当有效和常用的工具。另外如果自己愿意,亦可将多个接口都合并到一起(不能从多个普通 cl as s 或 abs t r act cl as s 中继承)。

1. 7 对象的创建和存在时间
从技术角度说,O P(面向对象程序设计)只是涉及抽象的数据类型、继承以及多形性,但另一些问题也可能 O 显得非常重要。本节将就这些问题进行探讨。 最重要的问题之一是对象的创建及破坏方式。对象需要的数据位于哪儿,如何控制对象的“存在时间”呢? 针对这个问题,解决的方案是各异其趣的。C ++认为程序的执行效率是最重要的一个问题,所以它允许程序员 作出选择。为获得最快的运行速度,存储以及存在时间可在编写程序时决定,只需将对象放置在堆栈(有时 也叫作自动或定域变量)或者静态存储区域即可。这样便为存储空间的分配和释放提供了一个优先级。某些 情况下,这种优先级的控制是非常有价值的。然而,我们同时也牺牲了灵活性,因为在编写程序时,必须知 道对象的准确的数量、存在时间、以及类型。如果要解决的是一个较常规的问题,如计算机辅助设计、仓储 管理或者空中交通控制,这一方法就显得太局限了。 第二个方法是在一个内存池中动态创建对象,该内存池亦叫“堆”或者“内存堆”。若采用这种方式,除非 进入运行期,否则根本不知道到底需要多少个对象,也不知道它们的存在时间有多长,以及准确的类型是什 么。这些参数都在程序正式运行时才决定的。若需一个新对象,只需在需要它的时候在内存堆里简单地创建 它即可。由于存储空间的管理是运行期间动态进行的,所以在内存堆里分配存储空间的时间比在堆栈里创建 的时间长得多(在堆栈里创建存储空间一般只需要一个简单的指令,将堆栈指针向下或向下移动即可)。由 于动态创建方法使对象本来就倾向于复杂,所以查找存储空间以及释放它所需的额外开销不会为对象的创建 造成明显的影响。除此以外,更大的灵活性对于常规编程问题的解决是至关重要的。 C ++允许我们决定是在写程序时创建对象,还是在运行期间创建,这种控制方法更加灵活。大家或许认为既然 它如此灵活,那么无论如何都应在内存堆里创建对象,而不是在堆栈中创建。但还要考虑另外一个问题,亦 即对象的“存在时间”或者“生存时间”(Li f et i m e)。若在堆栈或者静态存储空间里创建一个对象,编译 器会判断对象的持续时间有多长,到时会自动“破坏”或者“清除”它。程序员可用两种方法来破坏一个对 象:用程序化的方式决定何时破坏对象,或者利用由运行环境提供的一种“垃圾收集器”特性,自动寻找那 些不再使用的对象,并将其清除。当然,垃圾收集器显得方便得多,但要求所有应用程序都必须容忍垃圾收 集器的存在,并能默许随垃圾收集带来的额外开销。但这并不符合 C ++语言的设计宗旨,所以未能包括到 C ++ 里。但 Java 确实提供了一个垃圾收集器(Sm l t al k 也有这样的设计;尽管 D phi 默认为没有垃圾收集 al el 器,但可选择安装;而 C ++亦可使用一些由其他公司开发的垃圾收集产品)。 本节剩下的部分将讨论操纵对象时要考虑的另一些因素。

1. 7 . 1 集合与继承器
针对一个特定问题的解决,如果事先不知道需要多少个对象,或者它们的持续时间有多长,那么也不知道如 何保存那些对象。既然如此,怎样才能知道那些对象要求多少空间呢?事先上根本无法提前知道,除非进入 运行期。 在面向对象的设计中,大多数问题的解决办法似乎都有些轻率——只是简单地创建另一种类型的对象。用于 解决特定问题的新型对象容纳了指向其他对象的句柄。当然,也可以用数组来做同样的事情,那是大多数语 言都具有的一种功能。但不能只看到这一点。这种新对象通常叫作“集合”(亦叫作一个“容器”,但 A T W 在不同的场合应用了这个术语,所以本书将一直沿用“集合”的称呼。在需要的时候,集合会自动扩充自 己,以便适应我们在其中置入的任何东西。所以我们事先不必知道要在一个集合里容下多少东西。只需创建 一个集合,以后的工作让它自己负责好了。 幸运的是,设计优良的 O P 语言都配套提供了一系列集合。在 C O ++中,它们是以“标准模板库”(ST L)的形 式提供的。O ect Pas cal 用自己的“可视组件库”(VC bj L)提供集合。Sm l t al k 提供了一套非常完整的集 al 合。而 Java 也用自己的标准库提供了集合。在某些库中,一个常规集合便可满足人们的大多数要求;而在另 33

一些库中(特别是 C ++的库),则面向不同的需求提供了不同类型的集合。例如,可以用一个矢量统一对所 有元素的访问方式;一个链接列表则用于保证所有元素的插入统一。所以我们能根据自己的需要选择适当的 类型。其中包括集、队列、散列表、树、堆栈等等。 所有集合都提供了相应的读写功能。将某样东西置入集合时,采用的方式是十分明显的。有一个叫作“推” (Pus h)、“添加”(A )或其他类似名字的函数用于做这件事情。但将数据从集合中取出的时候,方式却 dd 并不总是那么明显。如果是一个数组形式的实体,比如一个矢量(Vect or ),那么也许能用索引运算符或函 数。但在许多情况下,这样做往往会无功而返。此外,单选定函数的功能是非常有限的。如果想对集合中的 一系列元素进行操纵或比较,而不是仅仅面向一个,这时又该怎么办呢? 办法就是使用一个“继续器”(I t er at or ),它属于一种对象,负责选择集合内的元素,并把它们提供给继 承器的用户。作为一个类,它也提供了一级抽象。利用这一级抽象,可将集合细节与用于访问那个集合的代 码隔离开。通过继承器的作用,集合被抽象成一个简单的序列。继承器允许我们遍历那个序列,同时毋需关 心基础结构是什么——换言之,不管它是一个矢量、一个链接列表、一个堆栈,还是其他什么东西。这样一 来,我们就可以灵活地改变基础数据,不会对程序里的代码造成干扰。Java 最开始(在 1. 0 和 1. 1 版中)提 供的是一个标准继承器,名为 Enum at i on(枚举),为它的所有集合类提供服务。Java 1. 2 新增一个更复 er 杂的集合库,其中包含了一个名为 I t er at or 的继承器,可以做比老式的 Enum at i on更多的事情。 er 从设计角度出发,我们需要的是一个全功能的序列。通过对它的操纵,应该能解决自己的问题。如果一种类 型的序列即可满足我们的所有要求,那么完全没有必要再换用不同的类型。有两方面的原因促使我们需要对 集合作出选择。首先,集合提供了不同的接口类型以及外部行为。堆栈的接口与行为与队列的不同,而队列 的接口与行为又与一个集(Set )或列表的不同。利用这个特征,我们解决问题时便有更大的灵活性。 其次,不同的集合在进行特定操作时往往有不同的效率。最好的例子便是矢量(Vect or )和列表(Li s t )的 区别。它们都属于简单的序列,拥有完全一致的接口和外部行为。但在执行一些特定的任务时,需要的开销 却是完全不同的。对矢量内的元素进行的随机访问(存取)是一种常时操作;无论我们选择的选择是什么, 需要的时间量都是相同的。但在一个链接列表中,若想到处移动,并随机挑选一个元素,就需付出“惨重” 的代价。而且假设某个元素位于列表较远的地方,找到它所需的时间也会长许多。但在另一方面,如果想在 序列中部插入一个元素,用列表就比用矢量划算得多。这些以及其他操作都有不同的执行效率,具体取决于 序列的基础结构是什么。在设计阶段,我们可以先从一个列表开始。最后调整性能的时候,再根据情况把它 换成矢量。由于抽象是通过继承器进行的,所以能在两者方便地切换,对代码的影响则显得微不足道。 最后,记住集合只是一个用来放置对象的储藏所。如果那个储藏所能满足我们的所有需要,就完全没必要关 心它具体是如何实现的(这是大多数类型对象的一个基本概念)。如果在一个编程环境中工作,它由于其他 因素(比如在 W ndow 下运行,或者由垃圾收集器带来了开销)产生了内在的开销,那么矢量和链接列表之 i s 间在系统开销上的差异就或许不是一个大问题。我们可能只需要一种类型的序列。甚至可以想象有一个“完 美”的集合抽象,它能根据自己的使用方式自动改变基层的实现方式。

1 . 7 . 2 单根结构
在面向对象的程序设计中,由于 C ++的引入而显得尤为突出的一个问题是:所有类最终是否都应从单独一个 基础类继承。在 Java 中(与其他几乎所有 O P 语言一样),对这个问题的答案都是肯定的,而且这个终级基 O 础类的名字很简单,就是一个“O ect ”。这种“单根结构”具有许多方面的优点。 bj 单根结构中的所有对象都有一个通用接口,所以它们最终都属于相同的类型。另一种方案(就象 C ++那样) 是我们不能保证所有东西都属于相同的基本类型。从向后兼容的角度看,这一方案可与 C模型更好地配合, 而且可以认为它的限制更少一些。但假期我们想进行纯粹的面向对象编程,那么必须构建自己的结构,以期 获得与内建到其他 O P 语言里的同样的便利。需添加我们要用到的各种新类库,还要使用另一些不兼容的接 O 口。理所当然地,这也需要付出额外的精力使新接口与自己的设计方案配合(可能还需要多重继承)。为得 到C ++额外的“灵活性”,付出这样的代价值得吗?当然,如果真的需要——如果早已是 C专家,如果对 C 有难舍的情结——那么就真的很值得。但假如你是一名新手,首次接触这类设计,象 Java 那样的替换方案也 许会更省事一些。 单根结构中的所有对象(比如所有 Java 对象)都可以保证拥有一些特定的功能。在自己的系统中,我们知道 对每个对象都能进行一些基本操作。一个单根结构,加上所有对象都在内存堆中创建,可以极大简化参数的 传递(这在 C ++里是一个复杂的概念)。 利用单根结构,我们可以更方便地实现一个垃圾收集器。与此有关的必要支持可安装于基础类中,而垃圾收 集器可将适当的消息发给系统内的任何对象。如果没有这种单根结构,而且系统通过一个句柄来操纵对象, 那么实现垃圾收集器的途径会有很大的不同,而且会面临许多障碍。 由于运行期的类型信息肯定存在于所有对象中,所以永远不会遇到判断不出一个对象的类型的情况。这对系 34

统级的操作来说显得特别重要,比如违例控制;而且也能在程序设计时获得更大的灵活性。 但大家也可能产生疑问,既然你把好处说得这么天花乱坠,为什么 C ++没有采用单根结构呢?事实上,这是 早期在效率与控制上权衡的一种结果。单根结构会带来程序设计上的一些限制。而且更重要的是,它加大了 新程序与原有 C代码兼容的难度。尽管这些限制仅在特定的场合会真的造成问题,但为了获得最大的灵活程 度,C ++最终决定放弃采用单根结构这一做法。而 Java 不存在上述的问题,它是全新设计的一种语言,不必 与现有的语言保持所谓的“向后兼容”。所以很自然地,与其他大多数面向对象的程序设计语言一样,单根 结构在 Java 的设计方案中很快就落实下来。

1 . 7 . 3 集合库与方便使用集合
由于集合是我们经常都要用到的一种工具,所以一个集合库是十分必要的,它应该可以方便地重复使用。这 样一来,我们就可以方便地取用各种集合,将其插入自己的程序。Java 提供了这样的一个库,尽管它在 Java 1. 0 和 1. 1 中都显得非常有限(Java 1. 2 的集合库则无疑是一个杰作)。 1. 下溯造型与模板/通用性 为了使这些集合能够重复使用,或者“再生”,Java 提供了一种通用类型,以前曾把它叫作“O ect ”。单 bj 根结构意味着、所有东西归根结底都是一个对象”!所以容纳了 O ect 的一个集合实际可以容纳任何东西。 bj 这使我们对它的重复使用变得非常简便。 为使用这样的一个集合,只需添加指向它的对象句柄即可,以后可以通过句柄重新使用对象。但由于集合只 能容纳 O ect ,所以在我们向集合里添加对象句柄时,它会上溯造型成 O ect ,这样便丢失了它的身份或者 bj bj 标识信息。再次使用它的时候,会得到一个 O ect 句柄,而非指向我们早先置入的那个类型的句柄。所以怎 bj 样才能归还它的本来面貌,调用早先置入集合的那个对象的有用接口呢? 在这里,我们再次用到了造型(C t )。但这一次不是在分级结构中上溯造型成一种更“通用”的类型。而 as 是下溯造型成一种更“特殊”的类型。这种造型方法叫作“下溯造型”(D ncas t i ng ow )。举个例子来说,我 们知道在上溯造型的时候,C r cl e(圆)属于 Shape(几何形状)的一种类型,所以上溯造型是安全的。但 i 我们不知道一个 O ect 到底是 C r cl e 还是 Shape,所以很难保证下溯造型的安全进行,除非确切地知道自 bj i 己要操作的是什么。 但这也不是绝对危险的,因为假如下溯造型成错误的东西,会得到我们称为“违例”(Except i on)的一种运 行期错误。我们稍后即会对此进行解释。但在从一个集合提取对象句柄时,必须用某种方式准确地记住它们 是什么,以保证下溯造型的正确进行。 下溯造型和运行期检查都要求花额外的时间来运行程序,而且程序员必须付出额外的精力。既然如此,我们 能不能创建一个“智能”集合,令其知道自己容纳的类型呢?这样做可消除下溯造型的必要以及潜在的错 误。答案是肯定的,我们可以采用“参数化类型”,它们是编译器能自动定制的类,可与特定的类型配合。 例如,通过使用一个参数化集合,编译器可对那个集合进行定制,使其只接受 Shape,而且只提取 Shape。 参数化类型是 C ++一个重要的组成部分,这部分是 C ++没有单根结构的缘故。在 C ++中,用于实现参数化类型 的关键字是 t em at e(模板)。Java 目前尚未提供参数化类型,因为由于使用的是单根结构,所以使用它显 pl 得有些笨拙。但这并不能保证以后的版本不会实现,因为“gener i c”这个词已被 Java“保留到将来实现” (在 A da语言中,“gener i c”被用来实现它的模板)。Java 采取的这种关键字保留机制其实经常让人摸不 着头脑,很难断定以后会发生什么事情。

1 . 7 . 4 清除时的困境:由谁负责清除?
每个对象都要求资源才能“生存”,其中最令人注目的资源是内存。如果不再需要使用一个对象,就必须将 其清除,以便释放这些资源,以便其他对象使用。如果要解决的是非常简单的问题,如何清除对象这个问题 并不显得很突出:我们创建对象,在需要的时候调用它,然后将其清除或者“破坏”。但在另一方面,我们 平时遇到的问题往往要比这复杂得多。 举个例子来说,假设我们要设计一套系统,用它管理一个机场的空中交通(同样的模型也可能适于管理一个 仓库的货柜、或者一套影带出租系统、或者宠物店的宠物房。这初看似乎十分简单:构造一个集合用来容纳 飞机,然后创建一架新飞机,将其置入集合。对进入空中交通管制区的所有飞机都如此处理。至于清除,在 一架飞机离开这个区域的时候把它简单地删去即可。 但事情并没有这么简单,可能还需要另一套系统来记录与飞机有关的数据。当然,和控制器的主要功能不 同,这些数据的重要性可能一开始并不显露出来。例如,这条记录反映的可能是离开机场的所有小飞机的飞 行计划。所以我们得到了由小飞机组成的另一个集合。一旦创建了一个飞机对象,如果它是一架小飞机,那 35

么也必须把它置入这个集合。然后在系统空闲时期,需对这个集合中的对象进行一些后台处理。 问题现在显得更复杂了:如何才能知道什么时间删除对象呢?用完对象后,系统的其他某些部分可能仍然要 发挥作用。同样的问题也会在其他大量场合出现,而且在程序设计系统中(如 C ++),在用完一个对象之后 必须明确地将其删除,所以问题会变得异常复杂(注释⑥)。 ⑥:注意这一点只对内存堆里创建的对象成立(用 new命令创建的)。但在另一方面,对这儿描述的问题以 及其他所有常见的编程问题来说,都要求对象在内存堆里创建。 在 Java 中,垃圾收集器在设计时已考虑到了内存的释放问题(尽管这并不包括清除一个对象涉及到的其他方 面)。垃圾收集器“知道”一个对象在什么时候不再使用,然后会自动释放那个对象占据的内存空间。采用 这种方式,另外加上所有对象都从单个根类 O ect 继承的事实,而且由于我们只能在内存堆中以一种方式创 bj 建对象,所以 Java 的编程要比 C ++的编程简单得多。我们只需要作出少量的抉择,即可克服原先存在的大量 障碍。 1. 垃圾收集器对效率及灵活性的影响 既然这是如此好的一种手段,为什么在 C ++里没有得到充分的发挥呢?我们当然要为这种编程的方便性付出 一定的代价,代价就是运行期的开销。正如早先提到的那样,在 C ++中,我们可在堆栈中创建对象。在这种 情况下,对象会得以自动清除(但不具有在运行期间随心所欲创建对象的灵活性)。在堆栈中创建对象是为 对象分配存储空间最有效的一种方式,也是释放那些空间最有效的一种方式。在内存堆(Heap)中创建对象 可能要付出昂贵得多的代价。如果总是从同一个基础类继承,并使所有函数调用都具有“同质多形”特征, 那么也不可避免地需要付出一定的代价。但垃圾收集器是一种特殊的问题,因为我们永远不能确定它什么时 候启动或者要花多长的时间。这意味着在 Java 程序执行期间,存在着一种不连贯的因素。所以在某些特殊的 场合,我们必须避免用它——比如在一个程序的执行必须保持稳定、连贯的时候(通常把它们叫作“实时程 序”,尽管并不是所有实时编程问题都要这方面的要求——注释⑦)。 ⑦:根据本书一些技术性读者的反馈,有一个现成的实时 Java 系统(w w new oni cs . com w. m )确实能够保证垃 圾收集器的效能。 C ++语言的设计者曾经向 C程序员发出请求(而且做得非常成功),不要希望在可以使用 C的任何地方,向语 言里加入可能对 C ++的速度或使用造成影响的任何特性。这个目的达到了,但代价就是 C ++的编程不可避免地 复杂起来。Java 比 C ++简单,但付出的代价是效率以及一定程度的灵活性。但对大多数程序设计问题来说, Java 无疑都应是我们的首选。

1. 8 违例控制:解决错误
从最古老的程序设计语言开始,错误控制一直都是设计者们需要解决的一个大问题。由于很难设计出一套完 美的错误控制方案,许多语言干脆将问题简单地忽略掉,将其转嫁给库设计人员。对大多数错误控制方案来 说,最主要的一个问题是它们严重依赖程序员的警觉性,而不是依赖语言本身的强制标准。如果程序员不够 警惕——若比较匆忙,这几乎是肯定会发生的——程序所依赖的错误控制方案便会失效。 “违例控制”将错误控制方案内置到程序设计语言中,有时甚至内建到操作系统内。这里的“违例” (Except i on)属于一个特殊的对象,它会从产生错误的地方“扔”或“掷”出来。随后,这个违例会被设计 用于控制特定类型错误的“违例控制器”捕获。在情况变得不对劲的时候,可能有几个违例控制器并行捕获 对应的违例对象。由于采用的是独立的执行路径,所以不会干扰我们的常规执行代码。这样便使代码的编写 变得更加简单,因为不必经常性强制检查代码。除此以外,“掷”出的一个违例不同于从函数返回的错误 值,也不同于由函数设置的一个标志。那些错误值或标志的作用是指示一个错误状态,是可以忽略的。但违 例不能被忽略,所以肯定能在某个地方得到处置。最后,利用违例能够可靠地从一个糟糕的环境中恢复。此 时一般不需要退出,我们可以采取某些处理,恢复程序的正常执行。显然,这样编制出来的程序显得更加可 靠。 Java 的违例控制机制与大多数程序设计语言都有所不同。因为在 Java 中,违例控制模块是从一开始就封装 好的,所以必须使用它!如果没有自己写一些代码来正确地控制违例,就会得到一条编译期出错提示。这样 可保证程序的连贯性,使错误控制变得更加容易。 注意违例控制并不属于一种面向对象的特性,尽管在面向对象的程序设计语言中,违例通常是用一个对象表 示的。早在面向对象语言问世以前,违例控制就已经存在了。 36

1. 9 多线程
在计算机编程中,一个基本的概念就是同时对多个任务加以控制。许多程序设计问题都要求程序能够停下手 头的工作,改为处理其他一些问题,再返回主进程。可以通过多种途径达到这个目的。最开始的时候,那些 拥有机器低级知识的程序员编写一些“中断服务例程”,主进程的暂停是通过硬件级的中断实现的。尽管这 是一种有用的方法,但编出的程序很难移植,由此造成了另一类的代价高昂问题。 有些时候,中断对那些实时性很强的任务来说是很有必要的。但还存在其他许多问题,它们只要求将问题划 分进入独立运行的程序片断中,使整个程序能更迅速地响应用户的请求。在一个程序中,这些独立运行的片 断叫作“线程”(Thr ead ),利用它编程的概念就叫作“多线程处理”。多线程处理一个常见的例子就是用 户界面。利用线程,用户可按下一个按钮,然后程序会立即作出响应,而不是让用户等待程序完成了当前任 务以后才开始响应。 最开始,线程只是用于分配单个处理器的处理时间的一种工具。但假如操作系统本身支持多个处理器,那么 每个线程都可分配给一个不同的处理器,真正进入“并行运算”状态。从程序设计语言的角度看,多线程操 作最有价值的特性之一就是程序员不必关心到底使用了多少个处理器。程序在逻辑意义上被分割为数个线 程;假如机器本身安装了多个处理器,那么程序会运行得更快,毋需作出任何特殊的调校。 根据前面的论述,大家可能感觉线程处理非常简单。但必须注意一个问题:共享资源!如果有多个线程同时 运行,而且它们试图访问相同的资源,就会遇到一个问题。举个例子来说,两个进程不能将信息同时发送给 一台打印机。为解决这个问题,对那些可共享的资源来说(比如打印机),它们在使用期间必须进入锁定状 态。所以一个线程可将资源锁定,在完成了它的任务后,再解开(释放)这个锁,使其他线程可以接着使用 同样的资源。 Java 的多线程机制已内建到语言中,这使一个可能较复杂的问题变得简单起来。对多线程处理的支持是在对 象这一级支持的,所以一个执行线程可表达为一个对象。Java 也提供了有限的资源锁定方案。它能锁定任何 对象占用的内存(内存实际是多种共享资源的一种),所以同一时间只能有一个线程使用特定的内存空间。 为达到这个目的,需要使用 s ynchr oni z ed关键字。其他类型的资源必须由程序员明确锁定,这通常要求程序 员创建一个对象,用它代表一把锁,所有线程在访问那个资源时都必须检查这把锁。

1. 10 永久性
创建一个对象后,只要我们需要,它就会一直存在下去。但在程序结束运行时,对象的“生存期”也会宣告 结束。尽管这一现象表面上非常合理,但深入追究就会发现,假如在程序停止运行以后,对象也能继续存 在,并能保留它的全部信息,那么在某些情况下将是一件非常有价值的事情。下次启动程序时,对象仍然在 那里,里面保留的信息仍然是程序上一次运行时的那些信息。当然,可以将信息写入一个文件或者数据库, 从而达到相同的效果。但尽管可将所有东西都看作一个对象,如果能将对象声明成“永久性”,并令其为我 们照看其他所有细节,无疑也是一件相当方便的事情。 Java 1. 1 提供了对“有限永久性”的支持,这意味着我们可将对象简单地保存到磁盘上,以后任何时间都可 取回。之所以称它为“有限”的,是由于我们仍然需要明确发出调用,进行对象的保存和取回工作。这些工 作不能自动进行。在 Java 未来的版本中,对“永久性”的支持有望更加全面。

1. 11 J ava 和因特网
既然 Java 不过另一种类型的程序设计语言,大家可能会奇怪它为什么值得如此重视,为什么还有这么多的人 认为它是计算机程序设计的一个里程碑呢?如果您来自一个传统的程序设计背景,那么答案在刚开始的时候 并不是很明显。Java 除了可解决传统的程序设计问题以外,还能解决 W l d W de W or i eb(万维网)上的编程问 题。

1 . 1 1 . 1 什么是 W eb?
W eb这个词刚开始显得有些泛泛,似乎“冲浪”、“网上存在”以及“主页”等等都和它拉上了一些关系。 甚至还有一种“I nt er net 综合症”的说法,对许多人狂热的上网行为提出了质疑。我们在这里有必要作一些 深入的探讨,但在这之前,必须理解客户机/服务器系统的概念,这是充斥着许多令人迷惑的问题的又一个 计算领域。 1. 客户机/服务器计算 客户机/服务器系统的基本思想是我们能在一个统一的地方集中存放信息资源。一般将数据集中保存在某个 37

数据库中,根据其他人或者机器的请求将信息投递给对方。客户机/服务器概述的一个关键在于信息是“集 中存放”的。所以我们能方便地更改信息,然后将修改过的信息发放给信息的消费者。将各种元素集中到一 起,信息仓库、用于投递信息的软件以及信息及软件所在的那台机器,它们联合起来便叫作“服务器” (Ser ver )。而对那些驻留在远程机器上的软件,它们需要与服务器通信,取回信息,进行适当的处理,然 后在远程机器上显示出来,这些就叫作“客户”(C i ent )。 l 这样看来,客户机/服务器的基本概念并不复杂。这里要注意的一个主要问题是单个服务器需要同时向多个 客户提供服务。在这一机制中,通常少不了一套数据库管理系统,使设计人员能将数据布局封装到表格中, 以获得最优的使用。除此以外,系统经常允许客户将新信息插入一个服务器。这意味着必须确保客户的新数 据不会与其他客户的新数据冲突,或者说需要保证那些数据在加入数据库的时候不会丢失(用数据库的术语 来说,这叫作“事务处理”)。客户软件发生了改变之后,它们必须在客户机器上构建、调试以及安装。所 有这些会使问题变得比我们一般想象的复杂得多。另外,对多种类型的计算机和操作系统的支持也是一个大 问题。最后,性能的问题显得尤为重要:可能会有数百个客户同时向服务器发出请求。所以任何微小的延误 都是不能忽视的。为尽可能缓解潜伏的问题,程序员需要谨慎地分散任务的处理负担。一般可以考虑让客户 机负担部分处理任务,但有时亦可分派给服务器所在地的其他机器,那些机器亦叫作“中间件”(中间件也 用于改进对系统的维护)。 所以在具体实现的时候,其他人发布信息这样一个简单的概念可能变得异常复杂。有时甚至会使人产生完全 无从着手的感觉。客户机/服务器的概念在这时就可以大显身手了。事实上,大约有一半的程序设计活动都 可以采用客户机/服务器的结构。这种系统可负责从处理订单及信用卡交易,一直到发布各类数据的方方面 面的任务——股票市场、科学研究、政府运作等等。在过去,我们一般为单独的问题采取单独的解决方案; 每次都要设计一套新方案。这些方案无论创建还是使用都比较困难,用户每次都要学习和适应新界面。客户 机/服务器问题需要从根本上加以变革! 2. W eb是一个巨大的服务器 W eb实际就是一套规模巨大的客户机/服务器系统。但它的情况要复杂一些,因为所有服务器和客户都同时 存在于单个网络上面。但我们没必要了解更进一步的细节,因为唯一要关心的就是一次建立同一个服务器的 连接,并同它打交道(即使可能要在全世界的范围内搜索正确的服务器)。 最开始的时候,这是一个简单的单向操作过程。我们向一个服务器发出请求,它向我们回传一个文件,由于 本机的浏览器软件(亦即“客户”或“客户程序”)负责解释和格式化,并在我们面前的屏幕上正确地显示 出来。但人们不久就不满足于只从一个服务器传递网页。他们希望获得完全的客户机/服务器能力,使客户 (程序)也能反馈一些信息到服务器。比如希望对服务器上的数据库进行检索,向服务器添加新信息,或者 下一份订单等等(这也提供了比以前的系统更高的安全要求)。在 W eb的发展过程中,我们可以很清晰地看 出这些令人心喜的变化。 W eb浏览器的发展终于迈出了重要的一步:某个信息可在任何类型的计算机上显示出来,毋需任何改动。然 而,浏览器仍然显得很原始,在用户迅速增多的要求面前显得有些力不从心。它们的交互能力不够强,而且 对服务器和因特网都造成了一定程度的干扰。这是由于每次采取一些要求编程的操作时,必须将信息反馈回 服务器,在服务器那一端进行处理。所以完全可能需要等待数秒乃至数分钟的时间才会发现自己刚才拼错了 一个单词。由于浏览器只是一个纯粹的查看程序,所以连最简单的计算任务都不能进行(当然在另一方面, 它也显得非常安全,因为不能在本机上面执行任何程序,避开了程序错误或者病毒的骚扰)。 为解决这个问题,人们采取了许多不同的方法。最开始的时候,人们对图形标准进行了改进,使浏览器能显 示更好的动画和视频。为解决剩下的问题,唯一的办法就是在客户端(浏览器)内运行程序。这就叫作“客 户端编程”,它是对传统的“服务器端编程”的一个非常重要的拓展。

1 . 1 1 . 2 客户端编程(注释⑧)
W eb最初采用的“服务器-浏览器”方案可提供交互式内容,但这种交互能力完全由服务器提供,为服务器 和因特网带来了不小的负担。服务器一般为客户浏览器产生静态网页,由后者简单地解释并显示出来。基本 HTM 语言提供了简单的数据收集机制:文字输入框、复选框、单选钮、列表以及下拉列表等,另外还有一个 L 按钮,只能由程序规定重新设置表单中的数据,以便回传给服务器。用户提交的信息通过所有 W eb服务器均 能支持的“通用网关接口”(C I )回传到服务器。包含在提交数据中的文字指示 C I 该如何操作。最常见的 G G 行动是运行位于服务器的一个程序。那个程序一般保存在一个名为“cgi - bi n”的目录中(按下 W eb页内的一 个按钮时,请注意一下浏览器顶部的地址窗,经常都能发现“cgi - bi n”的字样)。大多数语言都可用来编制 这些程序,但其中最常见的是 Per l 。这是由于 Per l 是专为文字的处理及解释而设计的,所以能在任何服务 器上安装和使用,无论采用的处理器或操作系统是什么。 38

⑧:本节内容改编自某位作者的一篇文章。那篇文章最早出现在位于 w w m ns pr i ng. com的 M ns pr i ng w . ai ai 上。本节的采用已征得了对方的同意。 今天的许多 W eb站点都严格地建立在 C I 的基础上,事实上几乎所有事情都可用 C I 做到。唯一的问题就是 G G 响应时间。C I 程序的响应取决于需要传送多少数据,以及服务器和因特网两方面的负担有多重(而且 C I G G 程序的启动比较慢)。W eb的早期设计者并未预料到当初绰绰有余的带宽很快就变得不够用,这正是大量应 用充斥网上造成的结果。例如,此时任何形式的动态图形显示都几乎不能连贯地显示,因为此时必须创建一 个 G F 文件,再将图形的每种变化从服务器传递给客户。而且大家应该对输入表单上的数据校验有着深刻的 I 体会。原来的方法是我们按下网页上的提交按钮(Subm t );数据回传给服务器;服务器启动一个 C I 程 i G 序,检查用户输入是否有错;格式化一个 HTM 页,通知可能遇到的错误,并将这个页回传给我们;随后必须 L 回到原先那个表单页,再输入一遍。这种方法不仅速度非常慢,也显得非常繁琐。 解决的办法就是客户端的程序设计。运行 W eb浏览器的大多数机器都拥有足够强的能力,可进行其他大量工 作。与此同时,原始的静态 HTM 方法仍然可以采用,它会一直等到服务器送回下一个页。客户端编程意味着 L W eb浏览器可获得更充分的利用,并可有效改善 W eb服务器的交互(互动)能力。 对客户端编程的讨论与常规编程问题的讨论并没有太大的区别。采用的参数肯定是相同的,只是运行的平台 不同:W eb浏览器就象一个有限的操作系统。无论如何,我们仍然需要编程,仍然会在客户端编程中遇到大 量问题,同时也有很多解决的方案。在本节剩下的部分里,我们将对这些问题进行一番概括,并介绍在客户 端编程中采取的对策。 1. 插件 朝客户端编程迈进的时候,最重要的一个问题就是插件的设计。利用插件,程序员可以方便地为浏览器添加 新功能,用户只需下载一些代码,把它们“插入”浏览器的适当位置即可。这些代码的作用是告诉浏览器 “从现在开始,你可以进行这些新活动了”(仅需下载这些插入一次)。有些快速和功能强大的行为是通过 插件添加到浏览器的。但插件的编写并不是一件简单的任务。在我们构建一个特定的站点时,可能并不希望 涉及这方面的工作。对客户端程序设计来说,插件的价值在于它允许专业程序员设计出一种新的语言,并将 那种语言添加到浏览器,同时不必经过浏览器原创者的许可。由此可以看出,插件实际是浏览器的一个“后 门”,允许创建新的客户端程序设计语言(尽管并非所有语言都是作为插件实现的)。 2. 脚本编制语言 插件造成了脚本编制语言的爆炸性增长。通过这种脚本语言,可将用于自己客户端程序的源码直接插入 HTM L 页,而对那种语言进行解释的插件会在 HTM 页显示的时候自动激活。脚本语言一般都倾向于尽量简化,易于 L 理解。而且由于它们是从属于 HTM 页的一些简单正文,所以只需向服务器发出对那个页的一次请求,即可非 L 常快地载入。缺点是我们的代码全部暴露在人们面前。另一方面,由于通常不用脚本编制语言做过份复杂的 事情,所以这个问题暂且可以放在一边。 脚本语言真正面向的是特定类型问题的解决,其中主要涉及如何创建更丰富、更具有互动能力的图形用户界 面(G )。然而,脚本语言也许能解决客户端编程中 80%的问题。你碰到的问题可能完全就在那 80%里 UI 面。而且由于脚本编制语言的宗旨是尽可能地简化与快速,所以在考虑其他更复杂的方案之前(如 Java 及 A i veX),首先应想一下脚本语言是否可行。 ct 目前讨论得最多的脚本编制语言包括 JavaScr i pt (它与 Java 没有任何关系;之所以叫那个名字,完全是一 种市场策略)、VBScr i pt (同 Vi s ual Bas i c 很相似)以及 Tcl /Tk(来源于流行的跨平台 G 构造语言)。 UI 当然还有其他许多语言,也有许多正在开发中。 JavaScr i pt 也许是目常用的,它得到的支持也最全面。无论 N s capeN gat or ,M cr os of t I nt er net et avi i Expl or er ,还是 O a,目前都提供了对 JavaScr i pt 的支持。除此以外,市面上讲述 JavaScr i pt 的书籍也 per 要比讲述其他语言的书多得多。有些工具还能利用 JavaScr i pt 自动产生网页。当然,如果你已经有 Vi s ual Bas i c 或者 Tcl /Tk 的深厚功底,当然用它们要简单得多,起码可以避免学习新语言的烦恼(解决 W eb方面的 问题就已经够让人头痛了)。 3. Java 如果说一种脚本编制语言能解决 80%的客户端程序设计问题,那么剩下的 20%又该怎么办呢?它们属于一些 高难度的问题吗?目前最流行的方案就是 Java。它不仅是一种功能强大、高度安全、可以跨平台使用以及国 际通用的程序设计语言,也是一种具有旺盛生命力的语言。对 Java 的扩展是不断进行的,提供的语言特性和 39

库能够很好地解决传统语言不能解决的问题,比如多线程操作、数据库访问、连网程序设计以及分布式计算 等等。Java 通过“程序片”(A et )巧妙地解决了客户端编程的问题。 ppl 程序片(或“小应用程序”)是一种非常小的程序,只能在 W eb浏览器中运行。作为 W eb页的一部分,程序 片代码会自动下载回来(这和网页中的图片差不多)。激活程序片后,它会执行一个程序。程序片的一个优 点体现在:通过程序片,一旦用户需要客户软件,软件就可从服务器自动下载回来。它们能自动取得客户软 件的最新版本,不会出错,也没有重新安装的麻烦。由于 Java 的设计原理,程序员只需要创建程序的一个版 本,那个程序能在几乎所有计算机以及安装了 Java 解释器的浏览器中运行。由于 Java 是一种全功能的编程 语言,所以在向服务器发出一个请求之前,我们能先在客户端做完尽可能多的工作。例如,再也不必通过因 特网传送一个请求表单,再由服务器确定其中是否存在一个拼写或者其他参数错误。大多数数据校验工作均 可在客户端完成,没有必要坐在计算机前面焦急地等待服务器的响应。这样一来,不仅速度和响应的灵敏度 得到了极大的提高,对网络和服务器造成的负担也可以明显减轻,这对保障因特网的畅通是至关重要的。 与脚本程序相比,Java 程序片的另一个优点是它采用编译好的形式,所以客户端看不到源码。当然在另一方 面,反编译 Java 程序片也并不是件难事,而且代码的隐藏一般并不是个重要的问题。大家要注意另外两个重 要的问题。正如本书以前会讲到的那样,编译好的 Java 程序片可能包含了许多模块,所以要多次“命中” (访问)服务器以便下载(在 Java 1. 1 中,这个问题得到了有效的改善——利用 Java 压缩档,即 JA R文 件——它允许设计者将所有必要的模块都封装到一起,供用户统一下载)。在另一方面,脚本程序是作为 W eb页正文的一部分集成到 W eb页内的。这种程序一般都非常小,可有效减少对服务器的点击数。另一个因 素是学习方面的问题。不管你平时听别人怎么说,Java 都不是一种十分容易便可学会的语言。如果你以前是 一名 Vi s ual Bas i c程序员,那么转向 VBScr i pt 会是一种最快捷的方案。由于 VBScr i pt 可以解决大多数典型 的客户机/服务器问题,所以一旦上手,就很难下定决心再去学习 Java。如果对脚本编制语言比较熟,那么 在转向 Java 之前,建议先熟悉一下 JavaScr i pt 或者 VBScr i pt ,因为它们可能已经能够满足你的需要,不必 经历学习 Java 的艰苦过程。 4. A i veX ct 在某种程度上,Java 的一个有力竞争对手应该是微软的 A i veX,尽管它采用的是完全不同的一套实现机 ct 制。A i veX最早是一种纯 W ndow 的方案。经过一家独立的专业协会的努力,A i veX 现在已具备了跨平台 ct i s ct 使用的能力。实际上,A i ve 的意思是“假如你的程序同它的工作环境正常连接,它就能进入 W ct X eb页,并 在支持 A i veX 的浏览器中运行”(I E 固化了对 A i veX的支持,而 N s cape 需要一个插件)。所以, ct ct et A i veX并没有限制我们使用一种特定的语言。比如,假设我们已经是一名有经验的 W ndow 程序员,能熟 ct i s 练地使用象 C ++、Vi s ual Bas i c或者 Bor l andD phi 那样的语言,就能几乎不加任何学习地创建出 A i veX el ct 组件。事实上,A i veX是在我们的 W ct eb页中使用“历史遗留”代码的最佳途径。 5. 安全 自动下载和通过因特网运行程序听起来就象是一个病毒制造者的梦想。在客户端的编程中,A i veX带来了 ct 最让人头痛的安全问题。点击一个 W eb站点的时候,可能会随同 HTM 网页传回任何数量的东西:G F 文件、 L I 脚本代码、编译好的 Java 代码以及 A i veX 组件。有些是无害的;G F 文件不会对我们造成任何危害,而脚 ct I 本编制语言通常在自己可做的事情上有着很大的限制。Java 也设计成在一个安全“沙箱”里在它的程序片中 运行,这样可防止操作位于沙箱以外的磁盘或者内存区域。 A i veX是所有这些里面最让人担心的。用 A i veX编写程序就象编制 W ndow 应用程序——可以做自己想 ct ct i s 做的任何事情。下载回一个 A i veX组件后,它完全可能对我们磁盘上的文件造成破坏。当然,对那些下载 ct 回来并不限于在 W eb浏览器内部运行的程序,它们同样也可能破坏我们的系统。从 BBS 下载回来的病毒一直 是个大问题,但因特网的速度使得这个问题变得更加复杂。 目前解决的办法是“数字签名”,代码会得到权威机构的验证,显示出它的作者是谁。这一机制的基础是认 为病毒之所以会传播,是由于它的编制者匿名的缘故。所以假如去掉了匿名的因素,所有设计者都不得不为 它们的行为负责。这似乎是一个很好的主意,因为它使程序显得更加正规。但我对它能消除恶意因素持怀疑 态度,因为假如一个程序便含有 Bug ,那么同样会造成问题。 Java 通过“沙箱”来防止这些问题的发生。Java 解释器内嵌于我们本地的 W eb浏览器中,在程序片装载时会 检查所有有嫌疑的指令。特别地,程序片根本没有权力将文件写进磁盘,或者删除文件(这是病毒最喜欢做 的事情之一)。我们通常认为程序片是安全的。而且由于安全对于营建一套可靠的客户机/服务器系统至关 重要,所以会给病毒留下漏洞的所有错误都能很快得到修复(浏览器软件实际需要强行遵守这些安全规则; 而有些浏览器则允许我们选择不同的安全级别,防止对系统不同程度的访问)。 大家或许会怀疑这种限制是否会妨碍我们将文件写到本地磁盘。比如,我们有时需要构建一个本地数据库, 40

或将数据保存下来,以便日后离线使用。最早的版本似乎每个人都能在线做任何敏感的事情,但这很快就变 得非常不现实(尽管低价“互联网工具”有一天可能会满足大多数用户的需要)。解决的方案是“签了名的 程序片”,它用公共密钥加密算法验证程序片确实来自它所声称的地方。当然在通过验证后,签了名的一个 程序片仍然可以开始清除你的磁盘。但从理论上说,既然现在能够找到创建人“算帐”,他们一般不会干这 种蠢事。Java 1. 1 为数字签名提供了一个框架,在必要时,可让一个程序片“走”到沙箱的外面来。 数字签名遗漏了一个重要的问题,那就是人们在因特网上移动的速度。如下载回一个错误百出的程序,而它 很不幸地真的干了某些蠢事,需要多久的时间才能发觉这一点呢?这也许是几天,也可能几周之后。发现了 之后,又如何追踪当初肇事的程序呢(以及它当时的责任有多大)? 6. 因特网和内联网 W eb是解决客户机/服务器问题的一种常用方案,所以最好能用相同的技术解决此类问题的一些“子集”, 特别是公司内部的传统客户机/服务器问题。对于传统的客户机/服务器模式,我们面临的问题是拥有多种 不同类型的客户计算机,而且很难安装新的客户软件。但通过 W eb浏览器和客户端编程,这两类问题都可得 到很好的解决。若一个信息网络局限于一家特定的公司,那么在将 W eb技术应用于它之后,即可称其为“内 联网”(I nt r anet ),以示与国际性的“因特网”(I nt er net )有别。内联网提供了比因特网更大的安全级 别,因为可以物理性地控制对公司内部服务器的使用。说到培训,一般只要人们理解了浏览器的常规概念, 就可以非常轻松地掌握网页和程序片之间的差异,所以学习新型系统的开销会大幅度减少。 安全问题将我们引入客户端编程领域一个似乎是自动形成的分支。若程序是在因特网上运行,由于无从知晓 它会在什么平台上运行,所以编程时要特别留意,防范可能出现的编程错误。需作一些跨平台处理,以及适 当的安全防范,比如采用某种脚本语言或者 Java。 但假如在内联网中运行,面临的一些制约因素就会发生变化。全部机器均为 I nt el /W ndow 平台是件很平常 i s 的事情。在内联网中,需要对自己代码的质量负责。而且一旦发现错误,就可以马上改正。除此以外,可能 已经有了一些“历史遗留”的代码,并用较传统的客户机/服务器方式使用那些代码。但在进行升级时,每 次都要物理性地安装一道客户程序。浪费在升级安装上的时间是转移到浏览器的一项重要原因。使用了浏览 器后,升级就变得易如反掌,而且整个过程是透明和自动进行的。如果真的是牵涉到这样的一个内联网中, 最明智的方法是采用 A i veX,而非试图采用一种新的语言来改写程序代码。 ct 面临客户端编程问题令人困惑的一系列解决方案时,最好的方案是先做一次投资/回报分析。请总结出问题 的全部制约因素,以及什么才是最快的方案。由于客户端程序设计仍然要编程,所以无论如何都该针对自己 的特定情况采取最好的开发途径。这是准备面对程序开发中一些不可避免的问题时,我们可以作出的最佳姿 态。

1 . 1 1 . 3 服务器端编程
我们的整个讨论都忽略了服务器端编程的问题。如果向服务器发出一个请求,会发生什么事情?大多数时候 的请求都是很简单的一个“把这个文件发给我”。浏览器随后会按适当的形式解释这个文件:作为 HTM 页、 L 一幅图、一个 Java 程序片、一个脚本程序等等。向服务器发出的较复杂的请求通常涉及到对一个数据库进行 操作(事务处理)。其中最常见的就是发出一个数据库检索命令,得到结果后,服务器会把它格式化成 HTM L 页,并作为结果传回来(当然,假如客户通过 Java 或者某种脚本语言具有了更高的智能,那么原始数据就能 在客户端发送和格式化;这样做速度可以更快,也能减轻服务器的负担)。另外,有时需要在数据库中注册 自己的名字(比如加入一个组时),或者向服务器发出一份订单,这就涉及到对那个数据库的修改。这类服 务器请求必须通过服务器端的一些代码进行,我们称其为“服务器端的编程”。在传统意义上,服务器端编 程是用 Per l 和 C I 脚本进行的,但更复杂的系统已经出现。其中包括基于 Java 的 W G eb服务器,它允许我们 用 Java进行所有服务器端编程,写出的程序就叫作“小服务程序”(Ser vl et )。

1 . 1 1 . 4 一个独立的领域:应用程序
与 Java 有关的大多数争论都是与程序片有关的。Java 实际是一种常规用途的程序设计语言,可解决任何类 型的问题,至少理论上如此。而且正如前面指出的,可以用更有效的方式来解决大多数客户机/服务器问 题。如果将视线从程序片身上转开(同时放宽一些限制,比如禁止写盘等),就进入了常规用途的应用程序 的广阔领域。这种应用程序可独立运行,毋需浏览器,就象普通的执行程序那样。在这儿,Java 的特色并不 仅仅反应在它的移植能力,也反映在编程本身上。就象贯穿全书都会讲到的那样,Java 提供了许多有用的特 性,使我们能在较短的时间里创建出比用从前的程序设计语言更健壮的程序。 但要注意任何东西都不是十全十美的,我们为此也要付出一些代价。其中最明显的是执行速度放慢了(尽管 41

可对此进行多方面的调整)。和任何语言一样,Java 本身也存在一些限制,使得它不十分适合解决某些特殊 的编程问题。但不管怎样,Java 都是一种正在快速发展的语言。随着每个新版本的发布,它变得越来越可 爱,能充分解决的问题也变得越来越多。

1. 12 分析和设计
面向对象的范式是思考程序设计时一种新的、而且全然不同的方式,许多人最开始都会在如何构造一个项目 上皱起了眉头。事实上,我们可以作出一个“好”的设计,它能充分利用 O P 提供的所有优点。 O 有关 O P 分析与设计的书籍大多数都不尽如人意。其中的大多数书都充斥着莫名其妙的话语、笨拙的笔调以 O 及许多听起来似乎很重要的声明(注释⑨)。我认为这种书最好压缩到一章左右的空间,至多写成一本非常 薄的书。具有讽剌意味的是,那些特别专注于复杂事物管理的人往往在写一些浅显、明白的书上面大费周 章!如果不能说得简单和直接,一定没多少人喜欢看这方面的内容。毕竟,O P 的全部宗旨就是让软件开发 O 的过程变得更加容易。尽管这可能影响了那些喜欢解决复杂问题的人的生计,但为什么不从一开始就把事情 弄得简单些呢?因此,希望我能从开始就为大家打下一个良好的基础,尽可能用几个段落来说清楚分析与设 计的问题。 ⑨:最好的入门书仍然是 G ady Booch 的《O ect - O i ent ed D i gn w t hA i cat i ons ,第 2 版本》, r bj r es i ppl W el y & Sons 于 1996 年出版。这本书讲得很有深度,而且通俗易懂,尽管他的记号方法对大多数设计来说 i 都显得不必要地复杂。

1 . 1 2 . 1 不要迷失
在整个开发过程中,最重要的事情就是:不要将自己迷失!但事实上这种事情很容易发生。大多数方法都设 计用来解决最大范围内的问题。当然,也存在一些特别困难的项目,需要作者付出更为艰辛的努力,或者付 出更大的代价。但是,大多数项目都是比较“常规”的,所以一般都能作出成功的分析与设计,而且只需用 到推荐的一小部分方法。但无论多么有限,某些形式的处理总是有益的,这可使整个项目的开发更加容易, 总比直接了当开始编码好! 也就是说,假如你正在考察一种特殊的方法,其中包含了大量细节,并推荐了许多步骤和文档,那么仍然很 难正确判断自己该在何时停止。时刻提醒自己注意以下几个问题: ( 1) 对象是什么?(怎样将自己的项目分割成一系列单独的组件?) ( 2) 它们的接口是什么?(需要将什么消息发给每一个对象?) 在确定了对象和它们的接口后,便可着手编写一个程序。出于对多方面原因的考虑,可能还需要比这更多的 说明及文档,但要求掌握的资料绝对不能比这还少。 整个过程可划分为四个阶段,阶段 0 刚刚开始采用某些形式的结构。

1 . 1 2 . 2 阶段 0:拟出一个计划
第一步是决定在后面的过程中采取哪些步骤。这听起来似乎很简单(事实上,我们这儿说的一切都似乎很简 单),但很常见的一种情况是:有些人甚至没有进入阶段 1,便忙忙慌慌地开始编写代码。如果你的计划本 来就是“直接开始开始编码”,那样做当然也无可非议(若对自己要解决的问题已有很透彻的理解,便可考 虑那样做)。但最低程度也应同意自己该有个计划。 在这个阶段,可能要决定一些必要的附加处理结构。但非常不幸,有些程序员写程序时喜欢随心所欲,他们 认为“该完成的时候自然会完成”。这样做刚开始可能不会有什么问题,但我觉得假如能在整个过程中设置 几个标志,或者“路标”,将更有益于你集中注意力。这恐怕比单纯地为了“完成工作”而工作好得多。至 少,在达到了一个又一个的目标,经过了一个接一个的路标以后,可对自己的进度有清晰的把握,干劲也会 相应地提高,不会产生“路遥漫漫无期”的感觉。 座我刚开始学习故事结构起(我想有一天能写本小说出来),就一直坚持这种做法,感觉就象简单地让文字 “流”到纸上。在我写与计算机有关的东西时,发现结构要比小说简单得多,所以不需要考虑太多这方面的 问题。但我仍然制订了整个写作的结构,使自己对要写什么做到心中有数。因此,即使你的计划就是直接开 始写程序,仍然需要经历以下的阶段,同时向自己提出一些特定的问题。

42

1 . 1 2 . 3 阶段 1:要制作什么?
在上一代程序设计中(即“过程化或程序化设计”),这个阶段称为“建立需求分析和系统规格”。当然, 那些操作今天已经不再需要了,或者至少改换了形式。大量令人头痛的文档资料已成为历史。但当时的初衷 是好的。需求分析的意思是“建立一系列规则,根据它判断任务什么时候完成,以及客户怎样才能满意”。 系统规格则表示“这里是一些具体的说明,让你知道程序需要做什么(而不是怎样做)才能满足要求”。需 求分析实际就是你和客户之间的一份合约(即使客户就在本公司内部工作,或者是其他对象及系统)。系统 规格是对所面临问题的最高级别的一种揭示,我们依据它判断任务是否完成,以及需要花多长的时间。由于 这些都需要取得参与者的一致同意,所以我建议尽可能地简化它们——最好采用列表和基本图表的形式—— 以节省时间。可能还会面临另一些限制,需要把它们扩充成为更大的文档。 我们特别要注意将重点放在这一阶段的核心问题上,不要纠缠于细枝末节。这个核心问题就是:决定采用什 么系统。对这个问题,最有价值的工具就是一个名为“使用条件”的集合。对那些采用“假如⋯⋯,系统该 怎样做?”形式的问题,这便是最有说服力的回答。例如,“假如客户需要提取一张现金支票,但当时又没 有这么多的现金储备,那么自动取款机该怎样反应?”对这个问题,“使用条件”可以指示自动取款机在那 种“条件”下的正确操作。 应尽可能总结出自己系统的一套完整的“使用条件”或者“应用场合”。一旦完成这个工作,就相当于摸清 了想让系统完成的核心任务。由于将重点放在“使用条件”上,一个很好的效果就是它们总能让你放精力放 在最关键的东西上,并防止自己分心于对完成任务关系不大的其他事情上面。也就是说,只要掌握了一套完 整的“使用条件”,就可以对自己的系统作出清晰的描述,并转移到下一个阶段。在这一阶段,也有可能无 法完全掌握系统日后的各种应用场合,但这也没有关系。只要肯花时间,所有问题都会自然而然暴露出来。 不要过份在意系统规格的“完美”,否则也容易产生挫败感和焦燥情绪。 在这一阶段,最好用几个简单的段落对自己的系统作出描述,然后围绕它们再进行扩充,添加一些“名词” 和“动词”。“名词”自然成为对象,而“动词”自然成为要整合到对象接口中的“方法”。只要亲自试着 做一做,就会发现这是多么有用的一个工具;有些时候,它能帮助你完成绝大多数的工作。 尽管仍处在初级阶段,但这时的一些日程安排也可能会非常管用。我们现在对自己要构建的东西应该有了一 个较全面的认识,所以可能已经感觉到了它大概会花多长的时间来完成。此时要考虑多方面的因素:如果估 计出一个较长的日程,那么公司也许决定不再继续下去;或者一名主管已经估算出了这个项目要花多长的时 间,并会试着影响你的估计。但无论如何,最好从一开始就草拟出一份“诚实”的时间表,以后再进行一些 暂时难以作出的决策。目前有许多技术可帮助我们计算出准确的日程安排(就象那些预测股票市场起落的技 术),但通常最好的方法还是依赖自己的经验和直觉(不要忘记,直觉也要建立在经验上)。感觉一下大概 需要花多长的时间,然后将这个时间加倍,再加上 10%。你的感觉可能是正确的;“也许”能在那个时间里 完成。但“加倍”使那个时间更加充裕,“10%”的时间则用于进行最后的推敲和深化。但同时也要对此向 上级主管作出适当的解释,无论对方有什么抱怨和修改,只要明确地告诉他们:这样的一个日程安排,只是 我的一个估计!

1 . 1 2 . 4 阶段 2:如何构建?
在这一阶段,必须拿出一套设计方案,并解释其中包含的各类对象在外观上是什么样子,以及相互间是如何 沟通的。此时可考虑采用一种特殊的图表工具:“统一建模语言”(UM L)。请到 ht t p: //w w r at i onal . com w. 去下载一份 UM 规格书。作为第 1 阶段中的描述工具,UM 也是很有帮助的。此外,还可用它在第 2 阶段中 L L 处理一些图表(如流程图)。当然并非一定要使用 UM L,但它对你会很有帮助,特别是在希望描绘一张详尽 的图表,让许多人在一起研究的时候。除 UM 外,还可选择对对象以及它们的接口进行文字化描述(就象我 L 在《T hi nki ng i n C ++》里说的那样,但这种方法非常原始,发挥的作用亦较有限。 我曾有一次非常成功的咨询经历,那时涉及到一小组人的初始设计。他们以前还没有构建过 O P(面向对象 O 程序设计)项目,将对象画在白板上面。我们谈到各对象相互间该如何沟通(通信),并删除了其中的一部 分,以及替换了另一部分对象。这个小组(他们知道这个项目的目的是什么)实际上已经制订出了设计方 案;他们自己“拥有”了设计,而不是让设计自然而然地显露出来。我在那里做的事情就是对设计进行指 导,提出一些适当的问题,尝试作出一些假设,并从小组中得到反馈,以便修改那些假设。这个过程中最美 妙的事情就是整个小组并不是通过学习一些抽象的例子来进行面向对象的设计,而是通过实践一个真正的设 计来掌握 O P 的窍门,而那个设计正是他们当时手上的工作! O 作出了对对象以及它们的接口的说明后,就完成了第 2 阶段的工作。当然,这些工作可能并不完全。有些工 作可能要等到进入阶段 3 才能得知。但这已经足够了。我们真正需要关心的是最终找出所有的对象。能早些 发现当然好,但 O P 提供了足够完美的结构,以后再找出它们也不迟。 O 43

1 . 1 2 . 5 阶段 3:开始创建
读这本书的可能是程序员,现在进入的正是你可能最感兴趣的阶段。由于手头上有一个计划——无论它有多 么简要,而且在正式编码前掌握了正确的设计结构,所以会发现接下去的工作比一开始就埋头写程序要简单 得多。而这正是我们想达到的目的。让代码做到我们想做的事情,这是所有程序项目最终的目标。但切不要 急功冒进,否则只有得不偿失。根据我的经验,最后先拿出一套较为全面的方案,使其尽可能设想周全,能 满足尽可能多的要求。给我的感觉,编程更象一门艺术,不能只是作为技术活来看待。所有付出最终都会得 到回报。作为真正的程序员,这并非可有可无的一种素质。全面的思考、周密的准备、良好的构造不仅使程 序更易构建与调试,也使其更易理解和维护,而那正是一套软件赢利的必要条件。 构建好系统,并令其运行起来后,必须进行实际检验,以前做的那些需求分析和系统规格便可派上用场了。 全面地考察自己的程序,确定提出的所有要求均已满足。现在一切似乎都该结束了?是吗?

1 . 1 2 . 6 阶段 4:校订
事实上,整个开发周期还没有结束,现在进入的是传统意义上称为“维护”的一个阶段。“维护”是一个比 较暧昧的称呼,可用它表示从“保持它按设想的轨道运行”、“加入客户从前忘了声明的功能”或者更传统 的“除掉暴露出来的一切臭虫”等等意思。所以大家对“维护”这个词产生了许多误解,有的人认为:凡是 需要“维护”的东西,必定不是好的,或者是有缺陷的!因为这个词说明你实际构建的是一个非常“原始” 的程序,以后需要频繁地作出改动、添加新的代码或者防止它的落后、退化等。因此,我们需要用一个更合 理的词语来称呼以后需要继续的工作。 这个词便是“校订”。换言之,“你第一次做的东西并不完善,所以需为自己留下一个深入学习、认知的空 间,再回过头去作一些改变”。对于要解决的问题,随着对它的学习和了解愈加深入,可能需要作出大量改 动。进行这些工作的一个动力是随着不断的改革优化,终于能够从自己的努力中得到回报,无论这需要经历 一个较短还是较长的时期。 什么时候才叫“达到理想的状态”呢?这并不仅仅意味着程序必须按要求的那样工作,并能适应各种指定的 “使用条件”,它也意味着代码的内部结构应当尽善尽美。至少,我们应能感觉出整个结构都能良好地协调 运作。没有笨拙的语法,没有臃肿的对象,也没有一些华而不实的东西。除此以外,必须保证程序结构有很 强的生命力。由于多方面的原因,以后对程序的改动是必不可少。但必须确定改动能够方便和清楚地进行。 这里没有花巧可言。不仅需要理解自己构建的是什么,也要理解程序如何不断地进化。幸运的是,面向对象 的程序设计语言特别适合进行这类连续作出的修改——由对象建立起来的边界可有效保证结构的整体性,并 能防范对无关对象进行的无谓干扰、破坏。也可以对自己的程序作一些看似激烈的大变动,同时不会破坏程 序的整体性,不会波及到其他代码。事实上,对“校订”的支持是 O P 非常重要的一个特点。 O 通过校订,可创建出至少接近自己设想的东西。然后从整体上观察自己的作品,把它与自己的要求比较,看 看还短缺什么。然后就可以从容地回过头去,对程序中不恰当的部分进行重新设计和重新实现(注释⑩)。 在最终得到一套恰当的方案之前,可能需要解决一些不能回避的问题,或者至少解决问题的一个方面。而且 一般要多“校订”几次才行(“设计范式”在这里可起到很大的帮助作用。有关它的讨论,请参考本书第 16 章)。 构建一套系统时,“校订”几乎是不可避免的。我们需要不断地对比自己的需求,了解系统是否自己实际所 需要的。有时只有实际看到系统,才能意识到自己需要解决一个不同的问题。若认为这种形式的校订必然会 发生,那么最好尽快拿出自己的第一个版本,检查它是否自己希望的,使自己的思想不断趋向成熟。 反复的“校订”同“递增开发”有关密不可分的关系。递增开发意味着先从系统的核心入手,将其作为一个 框架实现,以后要在这个框架的基础上逐渐建立起系统剩余的部分。随后,将准备提供的各种功能(特性) 一个接一个地加入其中。这里最考验技巧的是架设起一个能方便扩充所有目标特性的一个框架(对这个问 题,大家可参考第 16 章的论述)。这样做的好处在于一旦令核心框架运作起来,要加入的每一项特性就象它 自身内的一个小项目,而非大项目的一部分。此外,开发或维护阶段合成的新特性可以更方便地加入。O P O 之所以提供了对递增开发的支持,是由于假如程序设计得好,每一次递增都可以成为完善的对象或者对象 组。 ⑩:这有点类似“快速造型”。此时应着眼于建立一个简单、明了的版本,使自己能对系统有个清楚的把 握。再把这个原型扔掉,并正式地构建一个。快速造型最麻烦的一种情况就是人们不将原型扔掉,而是直接 在它的基础上建造。如果再加上程序化设计中“结构”的缺乏,就会导致一个混乱的系统,致使维护成本增 加。

44

1 . 1 2 . 7 计划的回报
如果没有仔细拟定的设计图,当然不可能建起一所房子。如建立的是一所狗舍,尽管设计图可以不必那么详 尽,但仍然需要一些草图,以做到心中有数。软件开发则完全不同,它的“设计图”(计划)必须详尽而完 备。在很长的一段时间里,人们在他们的开发过程中并没有太多的结构,但那些大型项目很容易就会遭致失 败。通过不断的摸索,人们掌握了数量众多的结构和详细资料。但它们的使用却使人提心吊胆在意——似乎 需要把自己的大多数时间花在编写文档上,而没有多少时间来编程(经常如此)。我希望这里为大家讲述的 一切能提供一条折衷的道路。需要采取一种最适合自己需要(以及习惯)的方法。不管制订出的计划有多么 小,但与完全没有计划相比,一些形式的计划会极大改善你的项目。请记住:根据估计,没有计划的 50%以 上的项目都会失败!

1. 13 J ava 还是 C++?
Java 特别象 C ++;由此很自然地会得出一个结论:C ++似乎会被 Java 取代。但我对这个逻辑存有一些疑问。 无论如何,C ++仍有一些特性是 Java 没有的。而且尽管已有大量保证,声称 Java有一天会达到或超过 C ++的 速度。但这个突破迄今仍未实现(尽管 Java 的速度确实在稳步提高,但仍未达到 C ++的速度)。此外,许多 领域都存在为数众多的 C ++爱好者,所以我并不认为那种语言很快就会被另一种语言替代(爱好者的力量是 容忽视的。比如在我主持的一次“中/高级 Java 研讨会”上,A l en Hol ub声称两种最常用的语言是 Rexx l 和 C BO O L)。 我感觉 Java 强大之处反映在与 C ++稍有不同的领域。C ++是一种绝对不会试图迎合某个模子的语言。特别是 它的形式可以变化多端,以解决不同类型的问题。这主要反映在象 M cr os of t Vi s ual C i ++和 Bor l and C ++ Bui l der (我最喜欢这个)那样的工具身上。它们将库、组件模型以及代码生成工具等合成到一起,以开发视 窗化的末端用户应用(用于 M cr os of t W ndow 操作系统)。但在另一方面,W ndow 开发人员最常用的是 i i s i s 什么呢?是微软的 Vi s ual Bas i c(VB)。当然,我们在这儿暂且不提 VB 的语法极易使人迷惑的事实——即 使一个只有几页长度的程序,产生的代码也十分难于管理。从语言设计的角度看,尽管 VB 是那样成功和流 行,但仍然存在不少的缺点。最好能够同时拥有 VB 那样的强大功能和易用性,同时不要产生难于管理的代 码。而这正是 Java 最吸引人的地方:作为“下一代的 VB”。无论你听到这种主张后有什么感觉,请无论如 何都仔细想一想:人们对 Java 做了大量的工作,使它能方便程序员解决应用级问题(如连网和跨平台 UI 等),所以它在本质上允许人们创建非常大型和灵活的代码主体。同时,考虑到 Java 还拥有我迄今为止尚未 在其他任何一种语言里见到的最“健壮”的类型检查及错误控制系统,所以 Java 确实能大大提高我们的编程 效率。这一点是勿庸置疑的! 但对于自己某个特定的项目,真的可以不假思索地将 C ++换成 Java 吗?除了 W eb程序片,还有两个问题需要 考虑。首先,假如要使用大量现有的库(这样肯定可以提高不少的效率),或者已经有了一个坚实的 C或 C ++代码库,那么换成 Java 后,反映会阻碍开发进度,而不是加快它的速度。但若想从头开始构建自己的所 有代码,那么 Java 的简单易用就能有效地缩短开发时间。 最大的问题是速度。在原始的 Java 解释器中,解释过的 Java 会比 C慢上 20 到 50 倍。尽管经过长时间的发 展,这个速度有一定程度的提高,但和 C比起来仍然很悬殊。计算机最注重的就是速度;假如在一台计算机 上不能明显较快地干活,那么还不如用手做(有人建议在开发期间使用 Java,以缩短开发时间。然后用一个 工具和支撑库将代码转换成 C ++,这样可获得更快的执行速度)。 为使 Java 适用于大多数 W eb开发项目,关键在于速度上的改善。此时要用到人们称为“刚好及时”(Jus t I n Ti m e,或 JI T )的编译器,甚至考虑更低级的代码编译器(写作本书时,也有两款问世)。当然,低级代 码编译器会使编译好的程序不能跨平台执行,但同时也带来了速度上的提升。这个速度甚至接近 C和 C ++。 而且 Java 中的程序交叉编译应当比 C和 C ++中简单得多(理论上只需重编译即可,但实际仍较难实现;其他 语言也曾作出类似的保证)。 在本书附录,大家可找到与 Java/C ++比较.对 Java 现状的观察以及编码规则有关的内容。

45

第 2 章 一切都是对象
“尽管以 C ++为基础,但 Java 是一种更纯粹的面向对象程序设计语言”。 无论 C ++还是 Java 都属于杂合语言。但在 Java 中,设计者觉得这种杂合并不象在 C ++里那么重要。杂合语言 允许采用多种编程风格;之所以说 C ++是一种杂合语言,是因为它支持与 C语言的向后兼容能力。由于 C ++是 C的一个超集,所以包含的许多特性都是后者不具备的,这些特性使 C ++在某些地方显得过于复杂。 Java 语言首先便假定了我们只希望进行面向对象的程序设计。也就是说,正式用它设计之前,必须先将自己 的思想转入一个面向对象的世界(除非早已习惯了这个世界的思维方式)。只有做好这个准备工作,与其他 O P 语言相比,才能体会到 Java 的易学易用。在本章,我们将探讨 Java 程序的基本组件,并体会为什么说 O Java 乃至 Java 程序内的一切都是对象。

2. 1 用句柄操纵对象
每种编程语言都有自己的数据处理方式。有些时候,程序员必须时刻留意准备处理的是什么类型。您曾利用 一些特殊语法直接操作过对象,或处理过一些间接表示的对象吗(C或 C ++里的指针)? 所有这些在 Java 里都得到了简化,任何东西都可看作对象。因此,我们可采用一种统一的语法,任何地方均 可照搬不误。但要注意,尽管将一切都“看作”对象,但操纵的标识符实际是指向一个对象的“句柄” (Handl e)。在其他 Java 参考书里,还可看到有的人将其称作一个“引用”,甚至一个“指针”。可将这一 情形想象成用遥控板(句柄)操纵电视机(对象)。只要握住这个遥控板,就相当于掌握了与电视机连接的 通道。但一旦需要“换频道”或者“关小声音”,我们实际操纵的是遥控板(句柄),再由遥控板自己操纵 电视机(对象)。如果要在房间里四处走走,并想保持对电视机的控制,那么手上拿着的是遥控板,而非电 视机。 此外,即使没有电视机,遥控板亦可独立存在。也就是说,只是由于拥有一个句柄,并不表示必须有一个对 象同它连接。所以如果想容纳一个词或句子,可创建一个 St r i ng句柄: St r i ng s ; 但这里创建的只是句柄,并不是对象。若此时向 s 发送一条消息,就会获得一个错误(运行期)。这是由于 s 实际并未与任何东西连接(即“没有电视机”)。因此,一种更安全的做法是:创建一个句柄时,记住无 论如何都进行初始化: St r i ng s = " as df " ; 然而,这里采用的是一种特殊类型:字串可用加引号的文字初始化。通常,必须为对象使用一种更通用的初 始化类型。

2. 2 所有对象都必须创建
创建句柄时,我们希望它同一个新对象连接。通常用 new关键字达到这一目的。new的意思是:“把我变成 这些对象的一种新类型”。所以在上面的例子中,可以说: St r i ng s = new St r i ng( " as df " ) ; 它不仅指出“将我变成一个新字串”,也通过提供一个初始字串,指出了“如何生成这个新字串”。 当然,字串(St r i ng)并非唯一的类型。Java 配套提供了数量众多的现成类型。对我们来讲,最重要的就是 记住能自行创建类型。事实上,这应是 Java 程序设计的一项基本操作,是继续本书后余部分学习的基础。

2 . 2 . 1 保存到什么地方
程序运行时,我们最好对数据保存到什么地方做到心中有数。特别要注意的是内存的分配。有六个地方都可 以保存数据: ( 1) 寄存器。这是最快的保存区域,因为它位于和其他所有保存方式不同的地方:处理器内部。然而,寄存 器的数量十分有限,所以寄存器是根据需要由编译器分配。我们对此没有直接的控制权,也不可能在自己的 程序里找到寄存器存在的任何踪迹。 ( 2) 堆栈。驻留于常规 RA (随机访问存储器)区域,但可通过它的“堆栈指针”获得处理的直接支持。堆 M 栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。这是一种特别快、特别有效的数据保存 方式,仅次于寄存器。创建程序时,Java 编译器必须准确地知道堆栈内保存的所有数据的“长度”以及“存 在时间”。这是由于它必须生成相应的代码,以便向上和向下移动指针。这一限制无疑影响了程序的灵活 性,所以尽管有些 Java 数据要保存在堆栈里——特别是对象句柄,但 Java 对象并不放到其中。 46

( 3) 堆。一种常规用途的内存池(也在 RA 区域),其中保存了 Java 对象。和堆栈不同,“内存堆”或 M “堆”(Heap)最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要 在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用 new命 令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然 会付出一定的代价:在堆里分配存储空间时会花掉更长的时间! ( 4) 静态存储。这儿的“静态”(St at i c)是指“位于固定位置”(尽管也在 RA 里)。程序运行期间,静 M 态存储的数据将随时等候调用。可用 s t at i c 关键字指出一个对象的特定元素是静态的。但 Java 对象本身永 远都不会置入静态存储空间。 ( 5) 常数存储。常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。有的常数 需要严格地保护,所以可考虑将它们置入只读存储器(RO )。 M ( 6) 非 RA M存储。若数据完全独立于一个程序之外,则程序不运行时仍可存在,并在程序的控制范围之外。 其中两个最主要的例子便是“流式对象”和“固定对象”。对于流式对象,对象会变成字节流,通常会发给 另一台机器。而对于固定对象,对象保存在磁盘中。即使程序中止运行,它们仍可保持自己的状态不变。对 于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体中。一旦需要,甚至能将它们恢复 成普通的、基于 RA 的对象。Java 1. 1 提供了对 Li ght w ght per s i s t ence 的支持。未来的版本甚至可能提 M ei 供更完整的方案。

2 . 2 . 2 特殊情况:主要类型
有一系列类需特别对待;可将它们想象成“基本”、“主要”或者“主”(Pr i m t i ve)类型,进行程序设计 i 时要频繁用到它们。之所以要特别对待,是由于用 new创建对象(特别是小的、简单的变量)并不是非常有 效,因为 new将对象置于“堆”里。对于这些类型,Java 采纳了与 C和 C ++相同的方法。也就是说,不是用 new创建变量,而是创建一个并非句柄的“自动”变量。这个变量容纳了具体的值,并置于堆栈中,能够更 高效地存取。 Java 决定了每种主要类型的大小。就象在大多数语言里那样,这些大小并不随着机器结构的变化而变化。这 种大小的不可更改正是 Java 程序具有很强移植能力的原因之一。 主类型 大小 最小值 最大值 封装器类型 bool ea 1 位 - - Bool ean n char 16 位 Uni code 0 Uni code 2 的 16 次方- 1 C act er har byt e 8 位 - 128 +127 Byt e(注释①) s hor t 16 位 - 2 的 15 次方 +2 的 15 次方- 1 Shor t (注释①) i nt 32 位 - 2 的 31 次方 +2 的 31 次方- 1 I nt eger l ong 64 位 - 2 的 63 次方 +2 的 63 次方- 1 Long f l oat 32 位 I EEE754 I EEE754 Fl oat doubl e 64 位 I EEE754 I EEE754 Doubl e Voi d - - - Voi d (注释①) ①:到 Java 1. 1 才有,1. 0 版没有。 数值类型全都是有符号(正负号)的,所以不必费劲寻找没有符号的类型。 主数据类型也拥有自己的“封装器”(w apper )类。这意味着假如想让堆内一个非主要对象表示那个主类 r 型,就要使用对应的封装器。例如: char c = ' x' ; C act er C = new C act er ( ' c' ) ; har har 也可以直接使用: C act er C = new C act er ( ' x' ) ; har har 这样做的原因将在以后的章节里解释。 1. 高精度数字 Java 1. 1 增加了两个类,用于进行高精度的计算:Bi gI nt eger 和 Bi gD m 。尽管它们大致可以划分为 eci al “封装器”类型,但两者都没有对应的“主类型”。 47

这两个类都有自己特殊的“方法”,对应于我们针对主类型执行的操作。也就是说,能对 i nt 或 f l oat 做的 事情,对 Bi gI nt eger 和 Bi gD m 一样可以做。只是必须使用方法调用,不能使用运算符。此外,由于牵 eci al 涉更多,所以运算速度会慢一些。我们牺牲了速度,但换来了精度。 Bi gI nt eger 支持任意精度的整数。也就是说,我们可精确表示任意大小的整数值,同时在运算过程中不会丢 失任何信息。 Bi gD m 支持任意精度的定点数字。例如,可用它进行精确的币值计算。 eci al 至于调用这两个类时可选用的构建器和方法,请自行参考联机帮助文档。

2 . 2 . 3 J av a 的数组
几乎所有程序设计语言都支持数组。在 C和 C ++里使用数组是非常危险的,因为那些数组只是内存块。若程 序访问自己内存块以外的数组,或者在初始化之前使用内存(属于常规编程错误),会产生不可预测的后果 (注释②)。 ②:在 C ++里,应尽量不要使用数组,换用标准模板库(St a ndar d Tem at eLi br ar y)里更安全的容器。 pl Java 的一项主要设计目标就是安全性。所以在 C和 C ++里困扰程序员的许多问题都未在 Java 里重复。一个 Java 可以保证被初始化,而且不可在它的范围之外访问。由于系统自动进行范围检查,所以必然要付出一些 代价:针对每个数组,以及在运行期间对索引的校验,都会造成少量的内存开销。但由此换回的是更高的安 全性,以及更高的工作效率。为此付出少许代价是值得的。 创建对象数组时,实际创建的是一个句柄数组。而且每个句柄都会自动初始化成一个特殊值,并带有自己的 关键字:nul l (空)。一旦 Java 看到 nul l ,就知道该句柄并未指向一个对象。正式使用前,必须为每个句 柄都分配一个对象。若试图使用依然为 nul l 的一个句柄,就会在运行期报告问题。因此,典型的数组错误在 Java 里就得到了避免。 也可以创建主类型数组。同样地,编译器能够担保对它的初始化,因为会将那个数组的内存划分成零。 数组问题将在以后的章节里详细讨论。

2. 3 绝对不要清除对象
在大多数程序设计语言中,变量的“存在时间”(Li f et i m e)一直是程序员需要着重考虑的问题。变量应持 续多长的时间?如果想清除它,那么何时进行?在变量存在时间上纠缠不清会造成大量的程序错误。在下面 的小节里,将阐示 Java 如何帮助我们完成所有清除工作,从而极大了简化了这个问题。

2 . 3 . 1 作用域
大多数程序设计语言都提供了“作用域”(Scope)的概念。对于在作用域里定义的名字,作用域同时决定了 它的“可见性”以及“存在时间”。在 C ++和 Java 里,作用域是由花括号的位置决定的。参考下面这个 ,C 例子: { i nt x = 12; /* onl y x avai l abl e */ { i nt q = 96; / * bot h x & q avai l abl e * / } /* onl y x avai l abl e */ /* q “out of s cope” */ } 作为在作用域里定义的一个变量,它只有在那个作用域结束之前才可使用。 在上面的例子中,缩进排版使 Java 代码更易辨读。由于 Java 是一种形式自由的语言,所以额外的空格、制 表位以及回车都不会对结果程序造成影响。 注意尽管在 C和 C ++里是合法的,但在 Java 里不能象下面这样书写代码: 48

{ i nt x = 12; { i nt x = 96; /* i l l egal */ } } 编译器会认为变量 x 已被定义。所以 C和 C ++能将一个变量“隐藏”在一个更大的作用域里。但这种做法在 Java 里是不允许的,因为 Java 的设计者认为这样做使程序产生了混淆。

2 . 3 . 2 对象的作用域
Java 对象不具备与主类型一样的存在时间。用 new关键字创建一个 Java 对象的时候,它会超出作用域的范 围之外。所以假若使用下面这段代码: { St r i ng s = new St r i ng( " a s t r i ng" ) ; } /* 作用域的终点 * / 那么句柄 s 会在作用域的终点处消失。然而,s 指向的 St r i ng对象依然占据着内存空间。在上面这段代码 里,我们没有办法访问对象,因为指向它的唯一一个句柄已超出了作用域的边界。在后面的章节里,大家还 会继续学习如何在程序运行期间传递和复制对象句柄。 这样造成的结果便是:对于用 new创建的对象,只要我们愿意,它们就会一直保留下去。这个编程问题在 C 和C ++里特别突出。看来在 C ++里遇到的麻烦最大:由于不能从语言获得任何帮助,所以在需要对象的时候, 根本无法确定它们是否可用。而且更麻烦的是,在 C ++里,一旦工作完成,必须保证将对象清除。 这样便带来了一个有趣的问题。假如 Java 让对象依然故我,怎样才能防止它们大量充斥内存,并最终造成程 序的“凝固”呢。在 C ++里,这个问题最令程序员头痛。但 Java 以后,情况却发生了改观。Java 有一个特别 的“垃圾收集器”,它会查找用 new创建的所有对象,并辨别其中哪些不再被引用。随后,它会自动释放由 那些闲置对象占据的内存,以便能由新对象使用。这意味着我们根本不必操心内存的回收问题。只需简单地 创建对象,一旦不再需要它们,它们就会自动离去。这样做可防止在 C ++里很常见的一个编程问题:由于程 序员忘记释放内存造成的“内存溢出”。

2. 4 新建数据类型:类
如果说一切东西都是对象,那么用什么决定一个“类”(C as s )的外观与行为呢?换句话说,是什么建立起 l 了一个对象的“类型”(Type)呢?大家可能猜想有一个名为“t ype”的关键字。但从历史看来,大多数面 向对象的语言都用关键字“cl as s ”表达这样一个意思:“我准备告诉你对象一种新类型的外观”。cl as s 关 键字太常用了,以至于本书许多地方并没有用粗体字或双引号加以强调。在这个关键字的后面,应该跟随新 数据类型的名称。例如: cl as s A TypeN e { /*类主体置于这里} am 这样就引入了一种新类型,接下来便可用 new创建这种类型的一个新对象: A TypeN e a = new A am TypeN e( ) ; am 在A TypeN e 里,类主体只由一条注释构成(星号和斜杠以及其中的内容,本章后面还会详细讲述),所以 am 并不能对它做太多的事情。事实上,除非为其定义了某些方法,否则根本不能指示它做任何事情。

2 . 4 . 1 字段和方法
定义一个类时(我们在 Java 里的全部工作就是定义类、制作那些类的对象以及将消息发给那些对象),可在 自己的类里设置两种类型的元素:数据成员(有时也叫“字段”)以及成员函数(通常叫“方法”)。其 中,数据成员是一种对象(通过它的句柄与其通信),可以为任何类型。它也可以是主类型(并不是句柄) 之一。如果是指向对象的一个句柄,则必须初始化那个句柄,用一种名为“构建器”(第 4 章会对此详述) 的特殊函数将其与一个实际对象连接起来(就象早先看到的那样,使用 new关键字)。但若是一种主类型, 则可在类定义位置直接初始化(正如后面会看到的那样,句柄亦可在定义位置初始化)。 49

每个对象都为自己的数据成员保有存储空间;数据成员不会在对象之间共享。下面是定义了一些数据成员的 类示例: cl as s D aO y { at nl i nt i ; f l oat f ; bool ean b; } 这个类并没有做任何实质性的事情,但我们可创建一个对象: D aO y d = new D aO y( ) ; at nl at nl 可将值赋给数据成员,但首先必须知道如何引用一个对象的成员。为达到引用对象成员的目的,首先要写上 对象句柄的名字,再跟随一个点号(句点),再跟随对象内部成员的名字。即“对象句柄. 成员”。例如: d. i = 47; d. f = 1. 1f ; d. b = f al s e; 一个对象也可能包含了另一个对象,而另一个对象里则包含了我们想修改的数据。对于这个问题,只需保持 “连接句点”即可。例如: m ane. l ef t Tank. capaci t y = 100; yPl 除容纳数据之外,D aO y 类再也不能做更多的事情,因为它没有成员函数(方法)。为正确理解工作原 at nl 理,首先必须知道“自变量”和“返回值”的概念。我们马上就会详加解释。 1. 主成员的默认值 若某个主数据类型属于一个类成员,那么即使不明确(显式)进行初始化,也可以保证它们获得一个默认 值。 主类型 默认值 Bool ean f al s e C har ' \u0000' ( nul l ) byt e ( byt e) 0 s hor t ( s hor t ) 0 i nt 0 l ong 0L f l oat 0. 0f doubl e 0. 0d 一旦将变量作为类成员使用,就要特别注意由 Java 分配的默认值。这样做可保证主类型的成员变量肯定得到 了初始化(C ++不具备这一功能),可有效遏止多种相关的编程错误。 然而,这种保证却并不适用于“局部”变量——那些变量并非一个类的字段。所以,假若在一个函数定义中 写入下述代码: i nt x; 那么 x 会得到一些随机值(这与 C和 C ++是一样的),不会自动初始化成零。我们责任是在正式使用 x 前分 配一个适当的值。如果忘记,就会得到一条编译期错误,告诉我们变量可能尚未初始化。这种处理正是 Java 优于 C ++的表现之一。许多 C ++编译器会对变量未初始化发出警告,但在 Java 里却是错误。

2. 5 方法、自变量和返回值
迄今为止,我们一直用“函数”(Funct i on)这个词指代一个已命名的子例程。但在 Java 里,更常用的一个 词却是“方法”(M hod et ),代表“完成某事的途径”。尽管它们表达的实际是同一个意思,但从现在开 始,本书将一直使用“方法”,而不是“函数”。 Java 的“方法”决定了一个对象能够接收的消息。通过本节的学习,大家会知道方法的定义有多么简单! 方法的基本组成部分包括名字、自变量、返回类型以及主体。下面便是它最基本的形式: 50

返回类型 方法名( /* 自变量列表* / ) { /* 方法主体 */} 返回类型是指调用方法之后返回的数值类型。显然,方法名的作用是对具体的方法进行标识和引用。自变量 列表列出了想传递给方法的信息类型和名称。 Java 的方法只能作为类的一部分创建。只能针对某个对象调用一个方法(注释③),而且那个对象必须能够 执行那个方法调用。若试图为一个对象调用错误的方法,就会在编译期得到一条出错消息。为一个对象调用 方法时,需要先列出对象的名字,在后面跟上一个句点,再跟上方法名以及它的参数列表。亦即“对象名. 方 法名( 自变量 1,自变量 2,自变量 3. . . )。举个例子来说,假设我们有一个方法名叫 f ( ),它没有自变量,返 回的是类型为 i nt 的一个值。那么,假设有一个名为 a的对象,可为其调用方法 f ( ) ,则代码如下: i nt x = a. f ( ) ; 返回值的类型必须兼容 x 的类型。 象这样调用一个方法的行动通常叫作“向对象发送一条消息”。在上面的例子中,消息是 f ( ),而对象是 a。 面向对象的程序设计通常简单地归纳为“向对象发送消息”。 ③:正如马上就要学到的那样,“静态”方法可针对类调用,毋需一个对象。

2 . 5 . 1 自变量列表
自变量列表规定了我们传送给方法的是什么信息。正如大家或许已猜到的那样,这些信息——如同 Java 内其 他任何东西——采用的都是对象的形式。因此,我们必须在自变量列表里指定要传递的对象类型,以及每个 对象的名字。正如在 Java 其他地方处理对象时一样,我们实际传递的是“句柄”(注释④)。然而,句柄的 类型必须正确。倘若希望自变量是一个“字串”,那么传递的必须是一个字串。 ④:对于前面提及的“特殊”数据类型 bool ean,char ,byt e,s hor t ,i nt ,l ong,,f l oat 以及 d l e来 oub 说是一个例外。但在传递对象时,通常都是指传递指向对象的句柄。 下面让我们考虑将一个字串作为自变量使用的方法。下面列出的是定义代码,必须将它置于一个类定义里, 否则无法编译: i nt s t or age( St r i ng s ) { r et ur n s . l engt h( ) * 2; } 这个方法告诉我们需要多少字节才能容纳一个特定字串里的信息(字串里的每个字符都是 16 位,或者说 2 个 字节、长整数,以便提供对 Uni cod 字符的支持)。自变量的类型为 St r i ng,而且叫作 s 。一旦将 s 传递给 e 方法,就可将它当作其他对象一样处理(可向其发送消息)。在这里,我们调用的是 l engt h( ) 方法,它是 St r i ng的方法之一。该方法返回的是一个字串里的字符数。 通过上面的例子,也可以了解 r et ur n 关键字的运用。它主要做两件事情。首先,它意味着“离开方法,我已 完工了”。其次,假设方法生成了一个值,则那个值紧接在 r et ur n 语句的后面。在这种情况下,返回值是通 过计算表达式“s . l engt h( ) * 2”而产生的。 可按自己的愿望返回任意类型,但倘若不想返回任何东西,就可指示方法返回 voi d(空)。下面列出一些例 子。 bool ean f l ag( ) { r et ur n t r ue; } f l oat nat ur al LogBas e( ) { r et ur n 2. 718; } voi d not hi ng( ) { r et ur n; } voi d not hi ng2( ) { } 若返回类型为 voi d,则 r et ur n 关键字唯一的作用就是退出方法。所以一旦抵达方法末尾,该关键字便不需 要了。可在任何地方从一个方法返回。但假设已指定了一种非 voi d 的返回类型,那么无论从何地返回,编译 器都会确保我们返回的是正确的类型。 到此为止,大家或许已得到了这样的一个印象:一个程序只是一系列对象的集合,它们的方法将其他对象作 51

为自己的自变量使用,而且将消息发给那些对象。这种说法大体正确,但通过以后的学习,大家还会知道如 何在一个方法里作出决策,做一些更细致的基层工作。至于这一章,只需理解消息传送就足够了。

2. 6 构建 J ava 程序
正式构建自己的第一个 Java 程序前,还有几个问题需要注意。

2 . 6 . 1 名字的可见性
在所有程序设计语言里,一个不可避免的问题是对名字或名称的控制。假设您在程序的某个模块里使用了一 个名字,而另一名程序员在另一个模块里使用了相同的名字。此时,如何区分两个名字,并防止两个名字互 相冲突呢?这个问题在 C语言里特别突出。因为程序未提供很好的名字管理方法。C ++的类(即 Java 类的基 础)嵌套使用类里的函数,使其不至于同其他类里的嵌套函数名冲突。然而,C ++仍然允许使用全局数据以及 全局函数,所以仍然难以避免冲突。为解决这个问题,C ++用额外的关键字引入了“命名空间”的概念。 由于采用全新的机制,所以 Java 能完全避免这些问题。为了给一个库生成明确的名字,采用了与 I nt er net 域名类似的名字。事实上,Java 的设计者鼓励程序员反转使用自己的 I nt er net 域名,因为它们肯定是独一 无二的。由于我的域名是 Br uceEckel . com ,所以我的实用工具库就可命名为 com br uceeckel . ut i l i t y. f oi bl es。反转了域名后,可将点号想象成子目录。 . 在 Java 1. 0 和 Java 1. 1 中,域扩展名 com ,edu,or g,net 等都约定为大写形式。所以库的样子就变成: C M br uceeckel . ut i l i t y. f oi bl es。然而,在 Java 1. 2 的开发过程中,设计者发现这样做会造成一些问题。 O. 所以目前的整个软件包都以小写字母为标准。 Java 的这种特殊机制意味着所有文件都自动存在于自己的命名空间里。而且一个文件里的每个类都自动获得 一个独一无二的标识符(当然,一个文件里的类名必须是唯一的)。所以不必学习特殊的语言知识来解决这 个问题——语言本身已帮我们照顾到这一点。

2 . 6 . 2 使用其他组件
一旦要在自己的程序里使用一个预先定义好的类,编译器就必须知道如何找到它。当然,这个类可能就在发 出调用的那个相同的源码文件里。如果是那种情况,只需简单地使用这个类即可——即使它直到文件的后面 仍未得到定义。Java 消除了“向前引用”的问题,所以不要关心这些事情。 但假若那个类位于其他文件里呢?您或许认为编译器应该足够“联盟”,可以自行发现它。但实情并非如 此。假设我们想使用一个具有特定名称的类,但那个类的定义位于多个文件里。或者更糟,假设我们准备写 一个程序,但在创建它的时候,却向自己的库加入了一个新类,它与现有某个类的名字发生了冲突。 为解决这个问题,必须消除所有潜在的、纠缠不清的情况。为达到这个目的,要用 i m t 关键字准确告诉 por Java 编译器我们希望的类是什么。i m t 的作用是指示编译器导入一个“包”——或者说一个“类库”(在 por 其他语言里,可将“库”想象成一系列函数、数据以及类的集合。但请记住,Java 的所有代码都必须写入一 个类中)。 大多数时候,我们直接采用来自标准 Java 库的组件(部件)即可,它们是与编译器配套提供的。使用这些组 件时,没有必要关心冗长的保留域名;举个例子来说,只需象下面这样写一行代码即可: i m t j ava. ut i l . Vect or ; por 它的作用是告诉编译器我们想使用 Java 的 Vect or 类。然而,ut i l 包含了数量众多的类,我们有时希望使用 其中的几个,同时不想全部明确地声明它们。为达到这个目的,可使用“* ”通配符。如下所示: i m t j ava. ut i l . * ; por 需导入一系列类时,采用的通常是这个办法。应尽量避免一个一个地导入类。

2 . 6 . 3 s t at i c 关键字
通常,我们创建类时会指出那个类的对象的外观与行为。除非用 new创建那个类的一个对象,否则实际上并 未得到任何东西。只有执行了 new后,才会正式生成数据存储空间,并可使用相应的方法。 但在两种特殊的情形下,上述方法并不堪用。一种情形是只想用一个存储区域来保存一个特定的数据——无 论要创建多少个对象,甚至根本不创建对象。另一种情形是我们需要一个特殊的方法,它没有与这个类的任 何对象关联。也就是说,即使没有创建对象,也需要一个能调用的方法。为满足这两方面的要求,可使用 s t at i c(静态)关键字。一旦将什么东西设为 s t at i c,数据或方法就不会同那个类的任何对象实例联系到一 起。所以尽管从未创建那个类的一个对象,仍能调用一个 s t at i c方法,或访问一些 s t at i c数据。而在这之 52

前,对于非 s t at i c数据和方法,我们必须创建一个对象,并用那个对象访问数据或方法。这是由于非 s t at i c数据和方法必须知道它们操作的具体对象。当然,在正式使用前,由于 s t at i c方法不需要创建任何 对象,所以它们不可简单地调用其他那些成员,同时不引用一个已命名的对象,从而直接访问非 s t at i c成员 或方法(因为非 s t at i c成员和方法必须同一个特定的对象关联到一起)。 有些面向对象的语言使用了“类数据”和“类方法”这两个术语。它们意味着数据和方法只是为作为一个整 体的类而存在的,并不是为那个类的任何特定对象。有时,您会在其他一些 Java 书刊里发现这样的称呼。 为了将数据成员或方法设为 s t at i c,只需在定义前置和这个关键字即可。例如,下述代码能生成一个 s t at i c 数据成员,并对其初始化: cl as s St at i cTes t { St at i c i nt i = 47; } 现在,尽管我们制作了两个 St at i cTes t 对象,但它们仍然只占据 St at i cTes t . i 的一个存储空间。这两个对 象都共享同样的 i 。请考察下述代码: St at i cTes t s t 1 = new St at i cTes t ( ) ; St at i cTes t s t 2 = new St at i cTes t ( ) ; 此时,无论 s t 1. i 还是 s t 2. i 都有同样的值 47,因为它们引用的是同样的内存区域。 有两个办法可引用一个 s t at i c变量。正如上面展示的那样,可通过一个对象命名它,如 s t 2. i 。亦可直接用 它的类名引用,而这在非静态成员里是行不通的(最好用这个办法引用 s t at i c 变量,因为它强调了那个变量 的“静态”本质)。 St at i cTes t . i ++; 其中,++运算符会使变量增值。此时,无论 s t 1. i 还是 s t 2. i 的值都是 48。 类似的逻辑也适用于静态方法。既可象对其他任何方法那样通过一个对象引用静态方法,亦可用特殊的语法 格式“类名. 方法( )”加以引用。静态方法的定义是类似的: cl as s St at i cFun { s t at i c voi d i ncr ( ) { St at i cTes t . i ++; } } 从中可看出,St at i cFun 的方法 i ncr ( ) 使静态数据 i 增值。通过对象,可用典型的方法调用 i ncr ( ) : St at i cFun s f = new St at i cFun( ) ; s f . i ncr ( ) ; 或者,由于 i ncr ( )是一种静态方法,所以可通过它的类直接调用: St at i cFun. i ncr ( ) ; 尽管是“静态”的,但只要应用于一个数据成员,就会明确改变数据的创建方式(一个类一个成员,以及每 个对象一个非静态成员)。若应用于一个方法,就没有那么戏剧化了。对方法来说,s t at i c一项重要的用途 就是帮助我们在不必创建对象的前提下调用那个方法。正如以后会看到的那样,这一点是至关重要的——特 别是在定义程序运行入口方法 m n( ) 的时候。 ai 和其他任何方法一样,s t at i c方法也能创建自己类型的命名对象。所以经常把 s t at i c方法作为一个“领头 羊”使用,用它生成一系列自己类型的“实例”。

2. 7 我们的第一个 J ava 程序
最后,让我们正式编一个程序(注释⑤)。它能打印出与当前运行的系统有关的资料,并利用了来自 Java 标 准库的 Sys t em对象的多种方法。注意这里引入了一种额外的注释样式:“/ / ”。它表示到本行结束前的所有 内容都是注释: // Pr oper t y. j ava i m t j ava. ut i l . * ; por publ i c cl as s Pr oper t y { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Sys t em out . pr i nt l n( new D e( ) ) ; . at Pr oper t i es p = Sys t em get Pr oper t i es ( ) ; . 53

p. l i s t ( Sys t em out ) ; . Sys t em o . pr i nt l n( " - - - M or y Us age: " ) ; . ut em Runt i m r t = Runt i m get Runt i m ) ; e e. e( Sys t em out . pr i nt l n( " Tot al M or y = " . em + r t . t ot al M or y( ) em + " Fr ee M or y = " em + r t . f r eeM or y( ) ) ; em } } ⑤:在某些编程环境里,程序会在屏幕上一切而过,甚至没机会看到结果。可将下面这段代码置于 m n( ) 的 ai 末尾,用它暂停输出: try { Thr ead. cur r ent Thr ead( ) . s l eep( 5 * 1000) ; } cat ch( I nt er r upt edExcept i on e) { } } 它的作用是暂停输出 5 秒钟。这段代码涉及的一些概念要到本书后面才会讲到。所以目前不必深究,只知道 它是让程序暂停的一个技巧便可。

在每个程序文件的开头,都必须放置一个 i m t 语句,导入那个文件的代码里要用到的所有额外的类。注意 por 我们说它们是“额外”的,因为一个特殊的类库会自动导入每个 Java 文件:j ava. l ang。启动您的 W eb浏览 器,查看由 Sun提供的用户文档(如果尚未从 ht t p: //w w j ava. s un. com w. 下载,或用其他方式安装了 Ja 文 va 档,请立即下载)。在 packages . ht m 文件里,可找到 Java 配套提供的所有类库名称。请选择其中的 l j ava. l ang。在“C as s I ndex”下面,可找到属于那个库的全部类的列表。由于 j ava. l ang默认进入每个 l Java 代码文件,所以这些类在任何时候都可直接使用。在这个列表里,可发现 Sys t em Runt i m 和 e,我们在 Pr oper t y. j ava 里用到了它们。j ava. l ang里没有列出 D e 类,所以必须导入另一个类库才能使用它。如果 at 不清楚一个特定的类在哪个类库里,或者想检视所有的类,可在 Java 用户文档里选择“C as s Hi er ar chy” l (类分级结构)。在 W eb浏览器中,虽然要花不短的时间来建立这个结构,但可清楚找到与 Java 配套提供的 每一个类。随后,可用浏览器的“查找”(Fi nd)功能搜索关键字“D e”。经这样处理后,可发现我们的 at 搜索目标以 j ava. ut i l . D e的形式列出。我们终于知道它位于 ut i l 库里,所以必须导入 j ava. ut i l . *;否 at 则便不能使用 D e。 at 观察 packages . ht m 文档最开头的部分(我已将其设为自己的默认起始页),请选择 j ava. l ang,再选 l Sys t em 。这时可看到 Sys t em类有几个字段。若选择 out ,就可知道它是一个 s t at i c Pr i nt St r eam对象。由 于它是“静态”的,所以不需要我们创建任何东西。out 对象肯定是 3,所以只需直接用它即可。我们能对这 个 out 对象做的事情由它的类型决定:Pr i nt St r eam 。Pr i nt St r eam在说明文字中以一个超链接的形式列出, 这一点做得非常方便。所以假若单击那个链接,就可看到能够为 Pr i nt St r eam调用的所有方法。方法的数量 不少,本书后面会详细介绍。就目前来说,我们感兴趣的只有 pr i nt l n( ) 。它的意思是“把我给你的东西打 印到控制台,并用一个新行结束”。所以在任何 Java 程序中,一旦要把某些内容打印到控制台,就可条件反 射地写上 Sys t em out . pr i nt l n( " 内容" )。 . 类名与文件是一样的。若象现在这样创建一个独立的程序,文件中的一个类必须与文件同名(如果没这样 做,编译器会及时作出反应)。类里必须包含一个名为 m n( ) 的方法,形式如下: ai publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai 其中,关键字“publ i c”意味着方法可由外部世界调用(第 5 章会详细解释)。m n( ) 的自变量是包含了 ai St r i ng对象的一个数组。ar gs 不会在本程序中用到,但需要在这个地方列出,因为它们保存了在命令行调用 的自变量。 程序的第一行非常有趣: Sys t em out . pr i nt l n( new D e( ) ) ; . at 请观察它的自变量:创建 D e 对象唯一的目的就是将它的值发送给 pr i nt l n( ) 。一旦这个语句执行完毕, at D e 就不再需要。随之而来的“垃圾收集器”会发现这一情况,并在任何可能的时候将其回收。事实上,我 at 们没太大的必要关心“清除”的细节。 第二行调用了 Sys t em get Pr oper t i es ( ) 。若用 W . eb浏览器查看联机用户文档,就可知道 get Pr oper t i es ( ) 是 54

Sys t em 类的一个 s t at i c方法。由于它是“静态”的,所以不必创建任何对象便可调用该方法。无论是否存 在该类的一个对象,s t at i c 方法随时都可使用。调用 get Pr oper t i es ( ) 时,它会将系统属性作为 Pr oper t i es 类的一个对象生成(注意 Pr oper t i es 是“属性”的意思)。随后的的句柄保存在一个名为 p的 Pr oper t i es 句柄里。在第三行,大家可看到 Pr oper t i es 对象有一个名为 l i s t ( )的方法,它将自己的全部内容都发给一 个我们作为自变量传递的 Pr i nt St r eam对象。 m n( )的第四和第六行是典型的打印语句。注意为了打印多个 St r i ng值,用加号(+)分隔它们即可。然 ai 而,也要在这里注意一些奇怪的事情。在 St r i ng对象中使用时,加号并不代表真正的“相加”。处理字串 时,我们通常不必考虑“+”的任何特殊含义。但是,Java 的 St r i ng类要受一种名为“运算符过载”的机制 的制约。也就是说,只有在随同 St r i ng对象使用时,加号才会产生与其他任何地方不同的表现。对于字串, 它的意思是“连接这两个字串”。 但事情到此并未结束。请观察下述语句: Sys t em out . pr i nt l n( " Tot al M or y = " . em + r t . t ot al M or y( ) em + " Fr ee M or y = " em + r t . f r eeM or y( ) ) ; em 其中,t ot al M or y( ) 和 f r eeM or y( ) 返回的是数值,并非 St r i ng对象。如果将一个数值“加”到一个字串 em em 身上,会发生什么情况呢?同我们一样,编译器也会意识到这个问题,并魔术般地调用一个方法,将那个数 值(i nt ,f l oat 等等)转换成字串。经这样处理后,它们当然能利用加号“加”到一起。这种“自动类型转 换”亦划入“运算符过载”处理的范畴。 许多 Java 著作都在热烈地辩论“运算符过载”(C ++的一项特性)是否有用。目前就是反对它的一个好例 子!然而,这最多只能算编译器(程序)的问题,而且只是对 St r i ng对象而言。对于自己编写的任何源代 码,都不可能使运算符“过载”。 通过为 Runt i m 类调用 get Runt i m )方法,m n( )的第五行创建了一个 Runt i m e e( ai e对象。返回的则是指向一个 Runt i m e对象的句柄。而且,我们不必关心它是一个静态对象,还是由 new命令创建的一个对象。这是由于 我们不必为清除工作负责,可以大模大样地使用对象。正如显示的那样,Runt i m e可告诉我们与内存使用有 关的信息。

2. 8 注释和嵌入文档
Java 里有两种类型的注释。第一种是传统的、C语言风格的注释,是从 C ++继承而来的。这些注释用一个 “/ * ”起头,随后是注释内容,并可跨越多行,最后用一个“* /”结束。注意许多程序员在连续注释内容的 每一行都用一个“*”开头,所以经常能看到象下面这样的内容: / * 这是 * 一段注释, * 它跨越了多个行 */ 但请记住,进行编译时,/ * 和* / 之间的所有东西都会被忽略,所以上述注释与下面这段注释并没有什么不 同: / * 这是一段注释, 它跨越了多个行 * / 第二种类型的注释也起源于 C ++。这种注释叫作“单行注释”,以一个“/ /”起头,表示这一行的所有内容 都是注释。这种类型的注释更常用,因为它书写时更方便。没有必要在键盘上寻找“/ ”,再寻找“* ”(只 需按同样的键两次),而且不必在注释结尾时加一个结束标记。下面便是这类注释的一个例子: / / 这是一条单行注释

55

2 . 8 . 1 注释文档
对于 Java 语言,最体贴的一项设计就是它并没有打算让人们为了写程序而写程序——人们也需要考虑程序的 文档化问题。对于程序的文档化,最大的问题莫过于对文档的维护。若文档与代码分离,那么每次改变代码 后都要改变文档,这无疑会变成相当麻烦的一件事情。解决的方法看起来似乎很简单:将代码同文档“链 接”起来。为达到这个目的,最简单的方法是将所有内容都置于同一个文件。然而,为使一切都整齐划一, 还必须使用一种特殊的注释语法,以便标记出特殊的文档;另外还需要一个工具,用于提取这些注释,并按 有价值的形式将其展现出来。这些都是 Java 必须做到的。 用于提取注释的工具叫作 j avadoc。它采用了部分来自 Java 编译器的技术,查找我们置入程序的特殊注释标 记。它不仅提取由这些标记指示的信息,也将毗邻注释的类名或方法名提取出来。这样一来,我们就可用最 轻的工作量,生成十分专业的程序文档。 j avadoc输出的是一个 HTM 文件,可用自己的 W L eb浏览器查看。该工具允许我们创建和管理单个源文件,并 生动生成有用的文档。由于有了 j vadoc,所以我们能够用标准的方法创建文档。而且由于它非常方便,所以 我们能轻松获得所有 Java 库的文档。

2 . 8 . 2 具体语法
所有 j avadoc命令都只能出现于“/ * *”注释中。但和平常一样,注释结束于一个“* /”。主要通过两种方式 来使用 j avadoc:嵌入的 HTM ,或使用“文档标记”。其中,“文档标记”(D t ags)是一些以“@ L oc ”开头 的命令,置于注释行的起始处(但前导的“* ”会被忽略)。 有三种类型的注释文档,它们对应于位于注释后面的元素:类、变量或者方法。也就是说,一个类注释正好 位于一个类定义之前;变量注释正好位于变量定义之前;而一个方法定义正好位于一个方法定义的前面。如 下面这个简单的例子所示: /** publ /** publ /** publ } 一个类注释 * / i c cl as s docTes t { 一个变量注释 * / i c i nt i ; 一个方法注释 * / i c voi d f ( ) { }

注意 j avadoc只能为 publ i c(公共)和 pr ot ect ed(受保护)成员处理注释文档。“pr i vat e”(私有)和 “友好”(详见 5 章)成员的注释会被忽略,我们看不到任何输出(也可以用- pr i vat e标记包括 pr i vat e 成 员)。这样做是有道理的,因为只有 publ i c 和 pr ot ect ed成员才可在文件之外使用,这是客户程序员的希 望。然而,所有类注释都会包含到输出结果里。 上述代码的输出是一个 HTM 文件,它与其他 Java 文档具有相同的标准格式。因此,用户会非常熟悉这种格 L 式,可在您设计的类中方便地“漫游”。设计程序时,请务必考虑输入上述代码,用 j avadoc处理一下,观 看最终 HTM 文件的效果如何。 L

2 . 8 . 3 嵌入 HT M L j avadoc将 HTM 命令传递给最终生成的 HTM 文档。这便使我们能够充分利用 HTM 的巨大威力。当然,我们 L L L 的最终动机是格式化代码,不是为了哗众取宠。下面列出一个例子: /** * * Sys t em out . pr i nt l n( new D e( ) ) ; . at * */ 亦可象在其他 W eb文档里那样运用 HTM ,对普通文本进行格式化,使其更具条理、更加美观: L /** 56

* 您甚至可以插入一个列表: * * 项目一 * 项目二 * 项目三 * */ 注意在文档注释中,位于一行最开头的星号会被 j avadoc 丢弃。同时丢弃的还有前导空格。j avadoc 会对所 有内容进行格式化,使其与标准的文档外观相符。不要将或这样的标题当作嵌入 HTM 使用,因为 L j avadoc会插入自己的标题,我们给出的标题会与之冲撞。 所有类型的注释文档——类、变量和方法——都支持嵌入 HTM 。 L

2 . 8 . 4 @s ee :引用其他类
所有三种类型的注释文档都可包含@ ee 标记,它允许我们引用其他类里的文档。对于这个标记,j avadoc会 s 生成相应的 HTM ,将其直接链接到其他文档。格式如下: L @ see 类名 @ ee 完整类名 s @ ee 完整类名#方法名 s 每一格式都会在生成的文档里自动加入一个超链接的“See A s o”(参见)条目。注意 j avadoc不会检查我 l 们指定的超链接,不会验证它们是否有效。

2 . 8 . 5 类文档标记
随同嵌入 HTM 和@ ee 引用,类文档还可以包括用于版本信息以及作者姓名的标记。类文档亦可用于“接 L s 口”目的(本书后面会详细解释)。 1. @ s i on ver 格式如下: @ s i on 版本信息 ver 其中,“版本信息”代表任何适合作为版本说明的资料。若在 j avadoc命令行使用了“- ver s i on”标记,就 会从生成的 HTM 文档里提取出版本信息。 L 2. @ hor aut 格式如下: @ hor 作者信息 aut 其中,“作者信息”包括您的姓名、电子函件地址或者其他任何适宜的资料。若在 j avadoc命令行使用了“aut hor ”标记,就会专门从生成的 HTM 文档里提取出作者信息。 L 可为一系列作者使用多个这样的标记,但它们必须连续放置。全部作者信息会一起存入最终 HTM 代码的单独 L 一个段落里。

2 . 8 . 6 变量文档标记
变量文档只能包括嵌入的 HTM 以及@ ee 引用。 L s

2 . 8 . 7 方法文档标记
除嵌入 HTM 和@ ee 引用之外,方法还允许使用针对参数、返回值以及违例的文档标记。 L s 1. @ am par 格式如下: 57

@ am 参数名 说明 par 其中,“参数名”是指参数列表内的标识符,而“说明”代表一些可延续到后续行内的说明文字。一旦遇到 一个新文档标记,就认为前一个说明结束。可使用任意数量的说明,每个参数一个。 2. @ et ur n r 格式如下: @ et ur n 说明 r 其中,“说明”是指返回值的含义。它可延续到后面的行内。 3. @ except i on 有关“违例”(Except i on)的详细情况,我们会在第 9 章讲述。简言之,它们是一些特殊的对象,若某个方 法失败,就可将它们“扔出”对象。调用一个方法时,尽管只有一个违例对象出现,但一些特殊的方法也许 能产生任意数量的、不同类型的违例。所有这些违例都需要说明。所以,违例标记的格式如下: @ except i on 完整类名 说明 其中,“完整类名”明确指定了一个违例类的名字,它是在其他某个地方定义好的。而“说明”(同样可以 延续到下面的行)告诉我们为什么这种特殊类型的违例会在方法调用中出现。 4. @ depr ecat ed 这是 Java 1. 1 的新特性。该标记用于指出一些旧功能已由改进过的新功能取代。该标记的作用是建议用户不 必再使用一种特定的功能,因为未来改版时可能摒弃这一功能。若将一个方法标记为@ depr ecat ed ,则使用该 方法时会收到编译器的警告。

2 . 8 . 8 文档示例
下面还是我们的第一个 Java 程序,只不过已加入了完整的文档注释: //: Pr oper t y. j ava i m t j ava. ut i l . * ; por /** The f i r s t Thi nki ng i n Java exam e pr ogr am pl . * Li s t s s ys t em i nf or m i o on cur r ent m at n achi ne. * @ hor Br uce Eckel aut * @ hor ht t p: //w w Br uceEckel . com aut w. * @ s i on 1. 0 ver */ publ i c cl as s Pr oper t y { /** Sol e ent r y poi nt t o cl as s & appl i cat i on * @ am ar gs ar r ay of s t r i ng ar gum s par ent * @ et ur n N r et ur n val ue r o * @ except i on except i ons N except i ons t hr ow o n */ publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Sys t em out . pr i nt l n( new D e( ) ) ; . at Pr oper t i es p = Sys t em get Pr oper t i es ( ) ; . p. l i s t ( Sys t em out ) ; . Sys t em out . pr i nt l n( " - - - M or y Us age: " ) ; . em Runt i m r t = Runt i m get Runt i m ) ; e e. e( Sys t em out . pr i nt l n( " Tot al M or y = " . em + r t . t ot al M or y( ) em + " Fr ee M or y = " em + r t . f r eeM or y( ) ) ; em }

58

} ///: ~ 第一行: //: Pr oper t y. j ava 采用了我自己的方法:将一个“: ”作为特殊的记号,指出这是包含了源文件名字的一个注释行。最后一行也 用这样的一条注释结尾,它标志着源代码清单的结束。这样一来,可将代码从本书的正文中方便地提取出 来,并用一个编译器检查。这方面的细节在第 17 章讲述。

2. 9 编码样式
一个非正式的 Java 编程标准是大写一个类名的首字母。若类名由几个单词构成,那么把它们紧靠到一起(也 就是说,不要用下划线来分隔名字)。此外,每个嵌入单词的首字母都采用大写形式。例如: cl as s A l TheC or s O TheRai nbow { // . . . } l ol f 对于其他几乎所有内容:方法、字段(成员变量)以及对象句柄名称,可接受的样式与类样式差不多,只是 标识符的第一个字母采用小写。例如: cl as s A l TheC or s O TheRai nbow { l ol f i nt anI nt eger Repr es ent i ngC or s ; ol voi d changeTheHueO TheC or ( i nt new f ol Hue) { // . . . } // . . . } 当然,要注意用户也必须键入所有这些长名字,而且不能输错。

2. 10 总结
通过本章的学习,大家已接触了足够多的 Java 编程知识,已知道如何自行编写一个简单的程序。此外,对语 言的总体情况以及一些基本思想也有了一定程度的认识。然而,本章所有例子的模式都是单线形式的“这样 做,再那样做,然后再做另一些事情”。如果想让程序作出一项选择,又该如何设计呢?例如,“假如这样 做的结果是红色,就那样做;如果不是,就做另一些事情”。对于这种基本的编程方法,下一章会详细说明 在 Java 里是如何实现的。

2. 11 练习
( 1) 参照本章的第一个例子,创建一个“Hel l o,W l d”程序,在屏幕上简单地显示这句话。注意在自己的 or 类里只需一个方法(“m n”方法会在程序启动时执行)。记住要把它设为 s t at i c 形式,并置入自变量列 ai 表——即使根本不会用到这个列表。用 j avac 编译这个程序,再用 j ava 运行它。 ( 2) 写一个程序,打印出从命令行获取的三个自变量。 ( 3) 找出 Pr oper t y. j ava 第二个版本的代码,这是一个简单的注释文档示例。请对文件执行 j avadoc,并在 自己的 W eb浏览器里观看结果。 ( 4) 以练习( 1)的程序为基础,向其中加入注释文档。利用 j avadoc,将这个注释文档提取为一个 HTM 文 L 件,并用 W eb浏览器观看。

59

第 3 章 控制程序流程
“就象任何有感知的生物一样,程序必须能操纵自己的世界,在执行过程中作出判断与选择。” 在 Java 里,我们利用运算符操纵对象和数据,并用执行控制语句作出选择。Java 是建立在 C ++基础上的,所 以对 C和 C ++程序员来说,对 Java 这方面的大多数语句和运算符都应是非常熟悉的。当然,Java 也进行了自 己的一些改进与简化工作。

3. 1 使用 J ava 运算符
运算符以一个或多个自变量为基础,可生成一个新值。自变量采用与原始方法调用不同的一种形式,但效果 是相同的。根据以前写程序的经验,运算符的常规概念应该不难理解。 加号(+)、减号和负号(- )、乘号(* )、除号(/)以及等号(=)的用法与其他所有编程语言都是类似 的。 所有运算符都能根据自己的运算对象生成一个值。除此以外,一个运算符可改变运算对象的值,这叫作“副 作用”(Si de Ef f ect )。运算符最常见的用途就是修改自己的运算对象,从而产生副作用。但要注意生成的 值亦可由没有副作用的运算符生成。 几乎所有运算符都只能操作“主类型”(Pr i m t i ves )。唯一的例外是“=”、“==”和“! =”,它们能操作 i 所有对象(也是对象易令人混淆的一个地方)。除此以外,St r i ng类支持“+”和“+=”。

3 . 1 . 1 优先级
运算符的优先级决定了存在多个运算符时一个表达式各部分的计算顺序。Java 对计算顺序作出了特别的规 定。其中,最简单的规则就是乘法和除法在加法和减法之前完成。程序员经常都会忘记其他优先级规则,所 以应该用括号明确规定计算顺序。例如: A = X + Y - 2/2 + Z; 为上述表达式加上括号后,就有了一个不同的含义。 A = X + ( Y - 2) /( 2 + Z) ;

3 . 1 . 2 赋值
赋值是用等号运算符(=)进行的。它的意思是“取得右边的值,把它复制到左边”。右边的值可以是任何常 数、变量或者表达式,只要能产生一个值就行。但左边的值必须是一个明确的、已命名的变量。也就是说, 它必须有一个物理性的空间来保存右边的值。举个例子来说,可将一个常数赋给一个变量(A ),但不可 =4; 将任何东西赋给一个常数(比如不能 4=A )。 对主数据类型的赋值是非常直接的。由于主类型容纳了实际的值,而且并非指向一个对象的句柄,所以在为 其赋值的时候,可将来自一个地方的内容复制到另一个地方。例如,假设为主类型使用“A =B”,那么 B 处的 内容就复制到 A 。若接着又修改了 A ,那么 B 根本不会受这种修改的影响。作为一名程序员,这应成为自己的 常识。 但在为对象“赋值”的时候,情况却发生了变化。对一个对象进行操作时,我们真正操作的是它的句柄。所 以倘若“从一个对象到另一个对象”赋值,实际就是将句柄从一个地方复制到另一个地方。这意味着假若为 对象使用“C ”,那么 C和 D最终都会指向最初只有 D才指向的那个对象。下面这个例子将向大家阐示这一 =D 点。 这里有一些题外话。在后面,大家在代码示例里看到的第一个语句将是“package 03”使用的“package”语 句,它代表本书第 3 章。本书每一章的第一个代码清单都会包含象这样的一个“package”(封装、打包、包 裹)语句,它的作用是为那一章剩余的代码建立章节编号。在第 17 章,大家会看到第 3 章的所有代码清单 (除那些有不同封装名称的以外)都会自动置入一个名为 c03 的子目录里;第 4 章的代码置入 c04;以此类 推。所有这些都是通过第 17 章展示的 C odePackage. j ava程序实现的;“封装”的基本概念会在第 5 章进行 详尽的解释。就目前来说,大家只需记住象“package 03”这样的形式只是用于为某一章的代码清单建立相 应的子目录。 为运行程序,必须保证在 cl as s pat h 里包含了我们安装本书源码文件的根目录(那个目录里包含了 c02, c03c,c04 等等子目录)。 60

对于 Java 后续的版本(1. 1. 4 和更高版本),如果您的 m n( )用 package语句封装到一个文件里,那么必须 ai 在程序名前面指定完整的包裹名称,否则不能运行程序。在这种情况下,命令行是: j ava c03. A s i gnm s ent 运行位于一个“包裹”里的程序时,随时都要注意这方面的问题。 下面是例子: //: A s i gnm . j ava s ent // A s i gnm s ent w t h obj ect s i s a bi t t r i cky i package c03; cl as s N ber { um i nt i ; } publ i c cl as s A s i gnm s ent { publ i c s t at i c voi d m n( St r i ng[ ] ai N ber n1 = new N ber ( ) ; um um N ber n2 = new N ber ( ) ; um um n1. i = 9; n2. i = 47; Sys t em out . pr i nt l n( " 1: n1. i : " . " , n2. i : " + n2. i ) ; n1 = n2; Sys t em out . pr i nt l n( " 2: n1. i : " . " , n2. i : " + n2. i ) ; n1. i = 27; Sys t em out . pr i nt l n( " 3: n1. i : " . " , n2. i : " + n2. i ) ; } } ///: ~

ar gs ) {

+ n1. i +

+ n1. i +

+ n1. i +

N ber 类非常简单,它的两个实例(n1 和 n2)是在 m n( ) 里创建的。每个 N ber 中的 i 值都赋予了一个不 um ai um 同的值。随后,将 n2 赋给 n1,而且 n1 发生改变。在许多程序设计语言中,我们都希望 n1 和 n2 任何时候都 相互独立。但由于我们已赋予了一个句柄,所以下面才是真实的输出: 1: n1. i : 9, n2. i : 47 2: n1. i : 47, n2. i : 47 3: n1. i : 27, n2. i : 27 看来改变 n1 的同时也改变了 n2!这是由于无论 n1 还是 n2 都包含了相同的句柄,它指向相同的对象(最初 的句柄位于 n1 内部,指向容纳了值 9 的一个对象。在赋值过程中,那个句柄实际已经丢失;它的对象会由 “垃圾收集器”自动清除)。 这种特殊的现象通常也叫作“别名”,是 Java 操作对象的一种基本方式。但假若不愿意在这种情况下出现别 名,又该怎么操作呢?可放弃赋值,并写入下述代码: n1. i = n2. i ; 这样便可保留两个独立的对象,而不是将 n1 和 n2 绑定到相同的对象。但您很快就会意识到,这样做会使对 象内部的字段处理发生混乱,并与标准的面向对象设计准则相悖。由于这并非一个简单的话题,所以留待第 12 章详细论述,那一章是专门讨论别名的。其时,大家也会注意到对象的赋值会产生一些令人震惊的效果。 1. 方法调用中的别名处理 将一个对象传递到方法内部时,也会产生别名现象。 //: Pas s O ect . j ava bj // Pas s i ng obj ect s t o m hods can be a bi t t r i cky et 61

cl as s Let t er { char c; } publ i c cl as s Pas s O ect { bj s t at i c voi d f ( Let t er y) { y. c = ' z' ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Let t er x = new Let t er ( ) ; x. c = ' a' ; Sys t em out . pr i nt l n( " 1: x. c: " + x. c) ; . f ( x) ; Sys t em out . pr i nt l n( " 2: x. c: " + x. c) ; . } } ///: ~ 在许多程序设计语言中,f ( ) 方法表面上似乎要在方法的作用域内制作自己的自变量 Let t er y 的一个副本。 但同样地,实际传递的是一个句柄。所以下面这个程序行: y. c = ' z' ; 实际改变的是 f ( ) 之外的对象。输出结果如下: 1: x. c: a 2: x. c: z 别名和它的对策是非常复杂的一个问题。尽管必须等至第 12 章才可获得所有答案,但从现在开始就应加以重 视,以便提早发现它的缺点。

3 . 1 . 3 算术运算符
Java 的基本算术运算符与其他大多数程序设计语言是相同的。其中包括加号(+)、减号(- )、除号 (/ )、乘号(* )以及模数(% ,从整数除法中获得余数)。整数除法会直接砍掉小数,而不是进位。 Java 也用一种简写形式进行运算,并同时进行赋值操作。这是由等号前的一个运算符标记的,而且对于语言 中的所有运算符都是固定的。例如,为了将 4 加到变量 x,并将结果赋给 x,可用:x+=4。 下面这个例子展示了算术运算符的各种用法: //: M hO . j ava at ps // D ons t r at es t he m hem i cal oper at or s em at at i m t j ava. ut i l . * ; por publ i c cl as s M hO { at ps // C eat e a s hor t hand t o s ave t ypi ng: r s t at i c voi d pr t ( St r i ng s ) { Sys t em out . pr i nt l n( s ) ; . } // s hor t hand t o pr i nt a s t r i ng and an i nt : s t at i c voi d pI nt ( St r i ng s , i nt i ) { pr t ( s + " = " + i ) ; } // s hor t hand t o pr i nt a s t r i ng and a f l oat : s t at i c voi d pFl t ( St r i ng s , f l oat f ) { pr t ( s + " = " + f ) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai // C eat e a r andom num r ber gener at or , 62

// s eeds w t h cur r ent t i m by def aul t : i e Random r and = new Random ) ; ( i nt i , j , k; // ' % l i m t s m m val ue t o 99: ' i axi um j = r and. next I nt ( ) % 100; k = r and. next I nt ( ) % 100; pI nt ( " j " , j ) ; pI nt ( " k" , k) ; i = j + k; pI nt ( " j + k" , i ) ; i = j - k; pI nt ( " j - k" , i ) ; i = k / j ; pI nt ( " k / j " , i ) ; i = k * j ; pI nt ( " k * j " , i ) ; i = k % j ; pI nt ( " k % j " , i ) ; j % k; pI nt ( " j % k" , j ) ; = = / / Fl oat i ng poi nt num ber t es t s : f l oat u, v, w // appl i es t o doubl es , t oo ; v = r and. next Fl oat ( ) ; w = r and. next Fl oat ( ) ; pFl t ( " v" , v) ; pFl t ( " w , w ; " ) u = v + w pFl t ( " v + w , u) ; ; " u = v - w pFl t ( " v - w , u) ; ; " u = v * w pFl t ( " v * w , u) ; ; " u = v / w pFl t ( " v / w , u) ; ; " // t he f ol l ow ng al s o w ks f or i or // char , byt e, s hor t , i nt , l ong, // and doubl e: u += v; pFl t ( " u += v" , u) ; u - = v; pFl t ( " u - = v" , u) ; u *= v; pFl t ( " u *= v" , u) ; u /= v; pFl t ( " u /= v" , u) ; } } ///: ~ 我们注意到的第一件事情就是用于打印(显示)的一些快捷方法:pr t ( ) 方法打印一个 St r i ng;pI nt ( )先打 印一个 St r i ng,再打印一个 i nt ;而 pFl t ( ) 先打印一个 St r i ng ,再打印一个 f l oat 。当然,它们最终都要用 Sys t em out . pr i nt l n( )结尾。 . 为生成数字,程序首先会创建一个 Random (随机)对象。由于自变量是在创建过程中传递的,所以 Java 将 当前时间作为一个“种子值”,由随机数生成器利用。通过 Random对象,程序可生成许多不同类型的随机数 字。做法很简单,只需调用不同的方法即可:next I nt ( ) ,next Long( ) ,next Fl oat ( )或者 next D oubl e( ) 。 若随同随机数生成器的结果使用,模数运算符(% )可将结果限制到运算对象减 1 的上限(本例是 99)之 下。 1. 一元加、减运算符 一元减号(- )和一元加号(+)与二元加号和减号都是相同的运算符。根据表达式的书写形式,编译器会自 动判断使用哪一种。例如下述语句: x = - a; 它的含义是显然的。编译器能正确识别下述语句: x = a * - b; 但读者会被搞糊涂,所以最好更明确地写成: x = a * (- b ; ) 一元减号得到的运算对象的负值。一元加号的含义与一元减号相反,虽然它实际并不做任何事情。

63

3 . 1 . 4 自动递增和递减
和 C类似,Java 提供了丰富的快捷运算方式。这些快捷运算可使代码更清爽,更易录入,也更易读者辨读。 两种很不错的快捷运算方式是递增和递减运算符(常称作“自动递增”和“自动递减”运算符)。其中,递 减运算符是“- - ”,意为“减少一个单位”;递增运算符是“++”,意为“增加一个单位”。举个例子来 说,假设 A是一个 i nt (整数)值,则表达式++A就等价于(A = A + 1)。递增和递减运算符结果生成的是 变量的值。 对每种类型的运算符,都有两个版本可供选用;通常将其称为“前缀版”和“后缀版”。“前递增”表示++ 运算符位于变量或表达式的前面;而“后递增”表示++运算符位于变量或表达式的后面。类似地,“前递 减”意味着- - 运算符位于变量或表达式的前面;而“后递减”意味着- - 运算符位于变量或表达式的后面。对 于前递增和前递减(如++A或- - A ),会先执行运算,再生成值。而对于后递增和后递减(如 A ++或 A - ), 会先生成值,再执行运算。下面是一个例子: //: A oI nc. j ava ut // D ons t r at es t he ++ and - - oper at or s em publ i c cl as s A oI nc { ut publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai i nt i = 1; pr t ( " i : " + i ) ; pr t ( " ++i : " + ++i ) ; // Pr e- i ncr em ent pr t ( " i ++ : " + i ++) ; // Pos t - i ncr em ent pr t ( " i : " + i ) ; pr t ( " - - i : " + - - i ) ; // Pr e- decr em ent pr t ( " i - - : " + i - - ) ; // Pos t - decr em ent pr t ( " i : " + i ) ; } s t at i c voi d pr t ( St r i ng s ) { Sys t em out . pr i nt l n( s ) ; . } } ///: ~ 该程序的输出如下: i : ++i i ++ i : - -i i -i : 1 : : 3 : : 1

2 2 2 2

从中可以看到,对于前缀形式,我们在执行完运算后才得到值。但对于后缀形式,则是在运算执行之前就得 到值。它们是唯一具有“副作用”的运算符(除那些涉及赋值的以外)。也就是说,它们会改变运算对象, 而不仅仅是使用自己的值。 递增运算符正是对“C ++”这个名字的一种解释,暗示着“超载 C的一步”。在早期的一次 Java 演讲中, Bi l l Joy(始创人之一)声称“Java=C - ”(C加加减减),意味着 Java 已去除了 C ++++一些没来由折磨人 的地方,形成一种更精简的语言。正如大家会在这本书中学到的那样,Java 的许多地方都得到了简化,所以 Java 的学习比 C ++更容易。

64

3 . 1 . 5 关系运算符
关系运算符生成的是一个“布尔”(Bool ean)结果。它们评价的是运算对象值之间的关系。若关系是真实 的,关系表达式会生成 t r ue(真);若关系不真实,则生成 f al s e(假)。关系运算符包括小于()、小于或等于(=)、等于(==)以及不等于(! =)。等于和不等于适用于所有内 建的数据类型,但其他比较不适用于 bool ean 类型。 1. 检查对象是否相等 关系运算符==和! =也适用于所有对象,但它们的含义通常会使初涉 Java 领域的人找不到北。下面是一个例 子: //: Equi val ence. j ava publ i c cl as s Equi val ence { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai I nt eger n1 = new I nt eger ( 47) ; I nt eger n2 = new I nt eger ( 47) ; Sys t em out . pr i nt l n( n1 == n2) ; . Sys t em out . pr i nt l n( n1 ! = n2) ; . } } ///: ~ 其中,表达式 Sys t em out . pr i nt l n( n1 == n2) 可打印出内部的布尔比较结果。一般人都会认为输出结果肯定 . 先是 t r ue,再是 f al s e,因为两个 I nt eger 对象都是相同的。但尽管对象的内容相同,句柄却是不同的,而 ==和! =比较的正好就是对象句柄。所以输出结果实际上先是 f al s e,再是 t r ue。这自然会使第一次接触的人 感到惊奇。 若想对比两个对象的实际内容是否相同,又该如何操作呢?此时,必须使用所有对象都适用的特殊方法 equal s ( ) 。但这个方法不适用于“主类型”,那些类型直接使用==和! =即可。下面举例说明如何使用: //: Equal s M hod. j ava et publ i c cl as s Equal s M hod { et publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai I nt eger n1 = new I nt eger ( 47) ; I nt eger n2 = new I nt eger ( 47) ; Sys t em out . pr i nt l n( n1. equal s ( n2) ) ; . } } ///: ~ 正如我们预计的那样,此时得到的结果是 t r ue。但事情并未到此结束!假设您创建了自己的类,就象下面这 样: //: Equal s M hod2. j ava et cl as s Val ue { i nt i ; } publ i c cl as s Equal s M hod2 { et publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Val ue v1 = new Val ue( ) ; Val ue v2 = new Val ue( ) ; v1. i = v2. i = 100; 65

Sys t em out . pr i nt l n( v1. equal s ( v2) ) ; . } } ///: ~ 此时的结果又变回了 f al s e!这是由于 equal s ( ) 的默认行为是比较句柄。所以除非在自己的新类中改变了 equal s ( ) ,否则不可能表现出我们希望的行为。不幸的是,要到第 7 章才会学习如何改变行为。但要注意 equal s ( ) 的这种行为方式同时或许能够避免一些“灾难”性的事件。 大多数 Java 类库都实现了 equal s ( ),所以它实际比较的是对象的内容,而非它们的句柄。

3 . 1 . 6 逻辑运算符
逻辑运算符 A D & N (& )、O R(||)以及 N T(! )能生成一个布尔值(t r ue 或 f al s e)——以自变量的逻辑关 O 系为基础。下面这个例子向大家展示了如何使用关系和逻辑运算符。 //: Bool . j ava // Rel at i onal and l ogi cal oper at or s i m t j ava. ut i l . * ; por publ i c cl as s Bool { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Random r and = new Random ) ; ( i nt i = r and. next I nt ( ) % 100; i nt j = r and. next I nt ( ) % 100; pr t ( " i = " + i ) ; pr t ( " j = " + j ) ; pr t ( " i > j i s " + ( i > j ) ) ; pr t ( " i < j i s " + ( i < j ) ) ; pr t ( " i >= j i s " + ( i >= j ) ) ; pr t ( " i 5" , i >> 5) ; pBi nI nt ( " ( ~i ) >> 5" , ( ~i ) >> 5) ; pBi nI nt ( " i >>> 5" , i >>> 5) ; pBi nI nt ( " ( ~i ) >>> 5" , ( ~i ) >>> 5) ; l ong l = r and. next Long( ) ; l ong m = r and. next Long( ) ; pBi nLong( " - 1L" , - 1L) ; pBi nLong( " +1L" , +1L) ; 69

l ong l l = 9223372036854775807L; pBi nLong( " m xpos " , l l ) ; a l ong l l n = - 9223372036854775808L; pBi nLong( " m axneg" , l l n) ; pBi nLong( " l " , l ) ; pBi nLong( " ~l " , ~l ) ; pBi nLong( " - l " , - l ) ; pBi nLong( " m , m ; " ) pBi nLong( " l & m , l & m ; " ) pBi nLong( " l | m , l | m ; " ) pBi nLong( " l ^ m , l ^ m ; " ) pBi nLong( " l > 5" , l >> 5) ; pBi nLong( " ( ~l ) >> 5" , ( ~l ) >> 5) ; pBi nLong( " l >>> 5" , l >>> 5) ; pBi nLong( " ( ~l ) >>> 5" , ( ~l ) >>> 5) ; } s t at i c voi d pBi nI nt ( St r i ng s , i nt i ) { Sys t em out . pr i nt l n( . s + " , i nt : " + i + " , bi nar y: " ) ; Sys t em out . pr i nt ( " . " ); f or ( i nt j = 31; j >=0; j - - ) i f ( ( ( 1 =0; i - - ) i f ( ( ( 1L 5, i nt : 1846303, bi nar y: 00000000000111000010110000011111 ( ~i ) >> 5, i nt : - 1846304, bi nar y: 11111111111000111101001111100000 i >>> 5, i nt : 1846303, bi nar y: 00000000000111000010110000011111 ( ~i ) >>> 5, i nt : 132371424, bi nar y: 00000111111000111101001111100000 数字的二进制形式表现为“有符号 2 的补值”。

3 . 1 . 9 三元 i f - el s e 运算符
这种运算符比较罕见,因为它有三个运算对象。但它确实属于运算符的一种,因为它最终也会生成一个值。 这与本章后一节要讲述的普通 i f - el s e 语句是不同的。表达式采取下述形式: 布尔表达式 ? 值 0: 值 1 若“布尔表达式”的结果为 t r ue,就计算“值 0”,而且它的结果成为最终由运算符产生的值。但若“布尔 表达式”的结果为 f al s e,计算的就是“值 1”,而且它的结果成为最终由运算符产生的值。 当然,也可以换用普通的 i f - el s e 语句(在后面介绍),但三元运算符更加简洁。尽管 C引以为傲的就是它 是一种简练的语言,而且三元运算符的引入多半就是为了体现这种高效率的编程,但假若您打算频繁用它, 还是要先多作一些思量——它很容易就会产生可读性极差的代码。 可将条件运算符用于自己的“副作用”,或用于它生成的值。但通常都应将其用于值,因为那样做可将运算 符与 i f - el s e 明确区别开。下面便是一个例子: s t at i c i nt t er nar y( i nt i ) { r et ur n i < 10 ? i * 100 : i * 10; } 可以看出,假设用普通的 i f - el s e 结构写上述代码,代码量会比上面多出许多。如下所示: s t at i c i nt al t er nat i ve( i nt i ) { i f ( i < 10) 71

r et ur n i * 100; r et ur n i * 10; } 但第二种形式更易理解,而且不要求更多的录入。所以在挑选三元运算符时,请务必权衡一下利弊。

3 . 1 . 1 0 逗号运算符
在 C和 C ++里,逗号不仅作为函数自变量列表的分隔符使用,也作为进行后续计算的一个运算符使用。在 Java 里需要用到逗号的唯一场所就是 f or 循环,本章稍后会对此详加解释。

3. 1 . 1 1 字串运算符+
这个运算符在 Java 里有一项特殊用途:连接不同的字串。这一点已在前面的例子中展示过了。尽管与+的传 统意义不符,但用+来做这件事情仍然是非常自然的。在 C ++里,这一功能看起来非常不错,所以引入了一项 “运算符过载”机制,以便 C ++程序员为几乎所有运算符增加特殊的含义。但非常不幸,与 C ++的另外一些限 制结合,运算符过载成为一种非常复杂的特性,程序员在设计自己的类时必须对此有周到的考虑。与 C ++相 比,尽管运算符过载在 Java 里更易实现,但迄今为止仍然认为这一特性过于复杂。所以 Java 程序员不能象 C ++程序员那样设计自己的过载运算符。 我们注意到运用“St r i ng +”时一些有趣的现象。若表达式以一个 St r i ng起头,那么后续所有运算对象都必 须是字串。如下所示: i nt x = 0, y = 1, z = 2; St r i ng s St r i ng = " x, y, z " ; Sys t em out . pr i nt l n( s St r i ng + x + y + z) ; . 在这里,Java 编译程序会将 x,y 和 z 转换成它们的字串形式,而不是先把它们加到一起。然而,如果使用 下述语句: Sys t em out . pr i nt l n( x + s St r i ng) ; . 那么早期版本的 Java 就会提示出错(以后的版本能将 x 转换成一个字串)。因此,如果想通过“加号”连接 字串(使用 Java 的早期版本),请务必保证第一个元素是字串(或加上引号的一系列字符,编译能将其识别 成一个字串)。

3 . 1 . 1 2 运算符常规操作规则
使用运算符的一个缺点是括号的运用经常容易搞错。即使对一个表达式如何计算有丝毫不确定的因素,都容 易混淆括号的用法。这个问题在 Java 里仍然存在。 在 C和 C ++中,一个特别常见的错误如下: w l e( x = y) { hi //. . . } 程序的意图是测试是否“相等”(==),而不是进行赋值操作。在 C和 C ++中,若 y 是一个非零值,那么这 种赋值的结果肯定是 t r ue。这样使可能得到一个无限循环。在 Java 里,这个表达式的结果并不是布尔值, 而编译器期望的是一个布尔值,而且不会从一个 i nt 数值中转换得来。所以在编译时,系统就会提示出现错 误,有效地阻止我们进一步运行程序。所以这个缺点在 Java 里永远不会造成更严重的后果。唯一不会得到编 译错误的时候是 x 和 y 都为布尔值。在这种情况下,x = y 属于合法表达式。而在上述情况下,则可能是一 个错误。 在 C和 C ++里,类似的一个问题是使用按位 A D和 O N R,而不是逻辑 A D和 O N R。按位 A D和 O N R使用两个字符 之一(& 或|),而逻辑 A D和 O 使用两个相同的字符(& 或|| N R & )。就象“=”和“==”一样,键入一个字符

72

当然要比键入两个简单。在 Java 里,编译器同样可防止这一点,因为它不允许我们强行使用一种并不属于的 类型。

3 . 1 . 1 3 造型运算符
“造型”(C t )的作用是“与一个模型匹配”。在适当的时候,Java 会将一种数据类型自动转换成另一 as 种。例如,假设我们为浮点变量分配一个整数值,计算机会将 i nt 自动转换成 f l oat 。通过造型,我们可明 确设置这种类型的转换,或者在一般没有可能进行的时候强迫它进行。 为进行一次造型,要将括号中希望的数据类型(包括所有修改符)置于其他任何值的左侧。下面是一个例 子: voi d i nt i l ong l ong } cas t s ( ) { = 200; l = ( l ong) i ; l 2 = ( l ong) 200;

正如您看到的那样,既可对一个数值进行造型处理,亦可对一个变量进行造型处理。但在这儿展示的两种情 况下,造型均是多余的,因为编译器在必要的时候会自动进行 i nt 值到 l ong 值的转换。当然,仍然可以设置 一个造型,提醒自己留意,也使程序更清楚。在其他情况下,造型只有在代码编译时才显出重要性。 在 C和 C ++中,造型有时会让人头痛。在 Java 里,造型则是一种比较安全的操作。但是,若进行一种名为 “缩小转换”(N r ow ng C ar i onver s i on)的操作(也就是说,脚本是能容纳更多信息的数据类型,将其转换 成容量较小的类型),此时就可能面临信息丢失的危险。此时,编译器会强迫我们进行造型,就好象说: “这可能是一件危险的事情——如果您想让我不顾一切地做,那么对不起,请明确造型。”而对于“放大转 换”(W deni ng conver s i on),则不必进行明确造型,因为新类型肯定能容纳原来类型的信息,不会造成任 i 何信息的丢失。 Java 允许我们将任何主类型“造型”为其他任何一种主类型,但布尔值(bol l ean)要除外,后者根本不允 许进行任何造型处理。“类”不允许进行造型。为了将一种类转换成另一种,必须采用特殊的方法(字串是 一种特殊的情况,本书后面会讲到将对象造型到一个类型“家族”里;例如,“橡树”可造型为“树”;反 之亦然。但对于其他外来类型,如“岩石”,则不能造型为“树”)。 1. 字面值 最开始的时候,若在一个程序里插入“字面值”(Li t er al ),编译器通常能准确知道要生成什么样的类型。 但在有些时候,对于类型却是暧昧不清的。若发生这种情况,必须对编译器加以适当的“指导”。方法是用 与字面值关联的字符形式加入一些额外的信息。下面这段代码向大家展示了这些字符。 //: Li t er al s . j ava cl as s Li t er al s { char c = 0xf f f f ; // m char hex val ue ax byt e b = 0x7f ; // m byt e hex val ue ax s hor t s = 0x7f f f ; // m s hor t hex val ue ax i nt i 1 = 0x2f ; // Hexadeci m ( l ow cas e) al er i nt i 2 = 0X2F; // Hexadeci m ( upper cas e) al i nt i 3 = 0177; // O al ( l eadi ng zer o) ct // Hex and O al s o w k w t h l ong. ct or i l ong n1 = 200L; // l ong s uf f i x l ong n2 = 200l ; // l ong s uf f i x l ong n3 = 200; //! l ong l 6( 200) ; // not al l ow ed f l oat f 1 = 1; f l oat f 2 = 1F; // f l oat s uf f i x f l oat f 3 = 1f ; // f l oat s uf f i x 73

f l oat f 4 = 1e 45f ; // 10 t o t he pow er f l oat f 5 = 1e+9f ; // f l oat s uf f i x doubl e d1 = 1d; // doubl e s uf f i x d oubl e d2 = 1D // doubl e s uf f i x ; doubl e d3 = 47e47d; // 10 t o t he pow er } ///: ~ 十六进制(Bas e 16)——它适用于所有整数数据类型——用一个前置的 0x 或 0X指示。并在后面跟随采用大 写或小写形式的 0- 9 以及 a- f 。若试图将一个变量初始化成超出自身能力的一个值(无论这个值的数值形式 如何),编译器就会向我们报告一条出错消息。注意在上述代码中,最大的十六进制值只会在 char ,byt e 以 及 s hor t 身上出现。若超出这一限制,编译器会将值自动变成一个 i nt ,并告诉我们需要对这一次赋值进行 “缩小造型”。这样一来,我们就可清楚获知自己已超载了边界。 八进制(Bas e 8)是用数字中的一个前置 0 以及 0- 7 的数位指示的。在 C ++或者 Java 中,对二进制数字 ,C 没有相应的“字面”表示方法。 字面值后的尾随字符标志着它的类型。若为大写或小写的 L,代表 l ong;大写或小写的 F,代表 f l oat ;大写 或小写的 D ,则代表 doubl e。 指数总是采用一种我们认为很不直观的记号方法:1. 39e- 47f 。在科学与工程学领域,“e”代表自然对数的 基数,约等于 2. 718(Java 一种更精确的 doubl e 值采用 M h. E 的形式)。它在象“1. 39×e 的- 47 次方”这 at 样的指数表达式中使用,意味着“1. 39×2. 718 的- 47 次方”。然而,自 FO RA RT N语言发明后,人们自然而然 地觉得 e 代表“10 多少次幂”。这种做法显得颇为古怪,因为 FO RA RT N最初面向的是科学与工程设计领域。 理所当然,它的设计者应对这样的混淆概念持谨慎态度(注释①)。但不管怎样,这种特别的表达方法在 C ++以及现在的 Java 中顽固地保留下来了。所以倘若您习惯将 e 作为自然对数的基数使用,那么在 Java ,C 中看到象“1. 39e- 47f ”这样的表达式时,请转换您的思维,从程序设计的角度思考它;它真正的含义是 “1. 39×10 的- 47 次方”。 ①:John Ki r kham 这样写道:“我最早于 1962 年在一部 I BM 1620 机器上使用 FO RTRA I I 。那时——包括 N 60 年代以及 70 年代的早期,FO RA RT N一直都是使用大写字母。之所以会出现这一情况,可能是由于早期的输 入设备大多是老式电传打字机,使用 5 位 Baudot 码,那种码并不具备小写能力。乘幂表达式中的‘E’也肯 定是大写的,所以不会与自然对数的基数‘e’发生冲突,后者必然是小写的。‘E’这个字母的含义其实很 简单,就是‘Exponent i al ’的意思,即‘指数’或‘幂数’,代表计算系统的基数——一般都是 10。当 时,八进制也在程序员中广泛使用。尽管我自己未看到它的使用,但假若我在乘幂表达式中看到一个八进制 数字,就会把它认作 Bas e 8。我记得第一次看到用小写‘e’表示指数是在 70 年代末期。我当时也觉得它极 易产生混淆。所以说,这个问题完全是自己‘潜入’FO RA RT N里去的,并非一开始就有。如果你真的想使用 自然对数的基数,实际有现成的函数可供利用,但它们都是大写的。” 注意如果编译器能够正确地识别类型,就不必使用尾随字符。对于下述语句: l ong n3 = 200; 它并不存在含混不清的地方,所以 200 后面的一个 L 大可省去。然而,对于下述语句: f l oat f 4 = 1e- 47f ; //10 的幂数 编译器通常会将指数作为双精度数(doubl e)处理,所以假如没有这个尾随的 f ,就会收到一条出错提示, 告诉我们须用一个“造型”将 doubl e 转换成 f l oat 。 2. 转型 大家会发现假若对主数据类型执行任何算术或按位运算,只要它们“比 i nt 小”(即 char ,byt e 或者 s hor t ),那么在正式执行运算之前,那些值会自动转换成 i nt 。这样一来,最终生成的值就是 i nt 类型。所 以只要把一个值赋回较小的类型,就必须使用“造型”。此外,由于是将值赋回给较小的类型,所以可能出 现信息丢失的情况)。通常,表达式中最大的数据类型是决定了表达式最终结果大小的那个类型。若将一个 f l oat 值与一个 doubl e值相乘,结果就是 doubl e;如将一个 i nt 和一个 l ong 值相加,则结果为 l ong。

3 . 1 . 1 4 J av a 没有“s i z eof ”
在 C和 C ++中,s i zeof ( ) 运算符能满足我们的一项特殊需要:获知为数据项目分配的字符数量。在 C和 C ++ 中,s i z e( ) 最常见的一种应用就是“移植”。不同的数据在不同的机器上可能有不同的大小,所以在进行一 74

些对大小敏感的运算时,程序员必须对那些类型有多大做到心中有数。例如,一台计算机可用 32 位来保存整 数,而另一台只用 16 位保存。显然,在第一台机器中,程序可保存更大的值。正如您可能已经想到的那样, 移植是令 C和 C ++程序员颇为头痛的一个问题。 Java 不需要 s i zeof ( ) 运算符来满足这方面的需要,因为所有数据类型在所有机器的大小都是相同的。我们不 必考虑移植问题——Java 本身就是一种“与平台无关”的语言。

3 . 1 . 1 5 复习计算顺序
在我举办的一次培训班中,有人抱怨运算符的优先顺序太难记了。一名学生推荐用一句话来帮助记忆: “Ul cer A ct s Real l y Li ke C A l ot ”,即“溃疡患者特别喜欢(维生素)C ddi ”。 助记词 运算符类型 运算符 Ul cer (溃疡) Unar y:一元 + - + + - [ [ 其余的 ] ] A ct s(患者) A i t hm i c( s hi f t ) ;算术(和移位) * / % + - > ddi r et Real l y(特别) Rel at i onal :关系 > < >= B A Lot A s i gnm :赋值 =(以及复合赋值,如* =) s ent 当然,对于移位和按位运算符,上表并不是完美的助记方法;但对于其他运算来说,它确实很管用。

3 . 1 . 1 6 运算符总结
下面这个例子向大家展示了如何随同特定的运算符使用主数据类型。从根本上说,它是同一个例子反反复复 地执行,只是使用了不同的主数据类型。文件编译时不会报错,因为那些会导致错误的行已用/ / ! 变成了注释 内容。 //: A l O . j ava l ps // Tes t s al l t he oper at or s on al l t he // pr i m t i ve dat a t ypes t o s how w ch i hi // ones ar e accept ed by t he Java com l er . pi cl as s A l O { l ps // To accept t he r es ul t s of a bool ean t es t : voi d f ( bool ean b) { } voi d bool Tes t ( bool ean x, bool ean y) { // A i t hm i c oper at or s : r et //! x = x * y; //! x = x / y; //! x = x % y; //! x = x + y; //! x = x - y; //! x++; //! x- - ; //! x = +y; //! x = - y; // Rel at i onal and l ogi cal : //! f ( x > y) ; //! f ( x >= y) ; //! f ( x < y) ; //! f ( x 1; //! x = x >>> 1; // C pound as s i gnm : om ent //! x += y; //! x - = y; //! x *= y; //! x /= y; //! x % y; = //! x = 1; //! x >>>= 1; x & y; = x ^= y; x |= y; / / C t i ng: as //! char c = ( char ) x; //! byt e B = ( byt e) x; //! s hor t s = ( s hor t ) x; //! i nt i = ( i nt ) x; //! l ong l = ( l ong) x; //! f l oat f = ( f l oat ) x; //! doubl e d = ( doubl e) x; } voi d char Tes t ( char x, char y) { // A i t hm i c oper at or s : r et x = ( char ) ( x * y) ; x = ( char ) ( x / y) ; x = ( char ) ( x % y) ; x = ( char ) ( x + y) ; x = ( char ) ( x - y) ; x++; x- - ; x = ( char ) +y; x = ( char ) - y; // Rel at i onal and l ogi cal : f ( x > y) ; f ( x >= y) ; f ( x < y) ; f ( x 1) ; x = ( char ) ( x >>> 1) ; // C pound as s i gnm : om ent x += y; x - = y; x *= y; x /= y; x % y; = x = 1; x >>>= 1; x & y; = x ^= y; x |= y; // C t i ng: as //! bool ean b = ( bool ean) x; byt e B = ( byt e) x; s hor t s = ( s hor t ) x; i nt i = ( i nt ) x; l ong l = ( l ong) x; f l oat f = ( f l oat ) x; doubl e d = ( doubl e) x; } voi d byt eTes t ( byt e x, byt e y) { // A i t hm i c oper at or s : r et x = ( byt e) ( x* y) ; x = ( byt e) ( x / y) ; x = ( byt e) ( x % y) ; x = ( byt e) ( x + y) ; x = ( byt e) ( x - y) ; x++; x- - ; x = ( byt e) + y; x = ( byt e) - y; // Rel at i onal and l ogi cal : f ( x > y) ; f ( x >= y) ; f ( x < y) ; f ( x 1) ; x = ( byt e) ( x >>> 1) ; // C pound as s i gnm : om ent x += y; x - = y; x *= y; x /= y; x % y; = x = 1; x >>>= 1; x & y; = x ^= y; x |= y; // C t i ng: as //! bool ean b = ( bool ean) x; char c = ( char ) x; s hor t s = ( s hor t ) x; i nt i = ( i nt ) x; l ong l = ( l ong) x; f l oat f = (f l oat ) x; doubl e d = ( doubl e) x; } voi d s hor t Tes t ( s hor t x, s hor t y) { // A i t hm i c oper at or s : r et x = ( s hor t ) ( x * y) ; x = ( s hor t ) ( x / y) ; x = ( s hor t ) ( x % y) ; x = ( s hor t ) ( x + y) ; x = ( s hor t ) ( x - y) ; x++; x- - ; x = ( s hor t ) +y; x = ( s hor t )- y; // Rel at i onal and l ogi cal : f ( x > y) ; f ( x >= y) ; f ( x < y) ; f ( x 1) ; x = ( s hor t ) ( x >>> 1) ; // C pound as s i gnm : om ent x += y; x - = y; x *= y; x /= y; x % y; = x = 1; x >>>= 1; x & y; = x ^= y; x |= y; // C t i ng: as //! bool ean b = ( bool ean) x; char c = ( char ) x; byt e B = ( byt e) x; i nt i = ( i nt ) x; l ong l = ( l ong) x; f l oat f = ( f l oat ) x; doubl e d = ( doubl e) x; } voi d i nt Tes t ( i nt x, i nt y) { // A i t hm i c oper at or s : r et x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x- - ; x = +y; x = - y; // Rel at i onal and l ogi cal : f ( x > y) ; f ( x >= y) ; f ( x < y) ; f ( x 1; x = x >>> 1; // C pound as s i gnm : om ent x += y; x - = y; x *= y; x /= y; x % y; = x = 1; x >>>= 1; x & y; = x ^= y; x |= y; // C t i ng: as //! bool ean b = ( bool ean) x; char c = ( char ) x; byt e B = ( byt e) x; s hor t s = ( s hor t ) x; l ong l = ( l ong) x; f l oat f = ( f l oat ) x; doubl e d = ( doubl e) x; } voi d l ongTes t ( l ong x, l ong y) { // A i t hm i c oper at or s : r et x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x- - ; x = +y; x = - y; // Rel at i onal and l ogi cal : f ( x > y) ; f ( x >= y) ; f ( x < y) ; f ( x 1; x = x >>> 1; // C pound as s i gnm : om ent x += y; x - = y; x *= y; x /= y; x % y; = x = 1; x >>>= 1; x & y; = x ^= y; x |= y; // C t i ng: as //! bool ean b = ( bool ean) x; char c = ( char ) x; byt e B = ( byt e) x; s hor t s = ( s hor t ) x; i nt i = ( i nt ) x; f l oat f = ( f l oat ) x; doubl e d = ( doubl e) x; } voi d f l oat Tes t ( f l oat x, f l oat y) { // A i t hm i c oper at or s : r et x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x- - ; x = +y; x = - y; // Rel at i onal and l ogi cal : f ( x > y) ; f ( x >= y) ; f ( x < y) ; f ( x 1; //! x = x >>> 1; 81

// C pound as s i gnm : om ent x += y; x - = y; x *= y; x /= y; x % y; = //! x = 1; //! x >>>= 1; //! x & y; = //! x ^= y; //! x |= y; // C t i ng: as //! bool ean b = ( bool ean) x; char c = ( char ) x; byt e B = ( byt e) x; s hor t s = ( s hor t ) x; i nt i = ( i nt ) x; l ong l = ( l ong) x; doubl e d = ( doubl e) x; } voi d doubl eTes t ( do e x, doubl e y) { ubl // A i t hm i c oper at or s : r et x = x * y; x = x / y; x = x % y; x = x + y; x = x - y; x++; x- - ; x = +y; x = - y; // Rel at i onal and l ogi cal : f ( x > y) ; f ( x >= y) ; f ( x < y) ; f ( x 1; //! x = x >>> 1; // C pound as s i gnm : om ent x += y; 82

x - = y; x *= y; x /= y; x % y; = //! x = 1; //! x >>>= 1; //! x & y; = //! x ^= y; //! x |= y; // C t i ng: as //! bool ean b = ( bool ean) x; char c = ( char ) x; byt e B = ( byt e) x; s hor t s = ( s hor t ) x; i nt i = ( i nt ) x; l ong l = ( l ong) x; f l oat f = ( f l oat ) x; } } ///: ~ 注意布尔值(bool ean)的能力非常有限。我们只能为其赋予 t r ue 和 f al s e 值。而且可测试它为真还是为 假,但不可为它们再添加布尔值,或进行其他其他任何类型运算。 在 char ,byt e 和 s hor t 中,我们可看到算术运算符的“转型”效果。对这些类型的任何一个进行算术运算, 都会获得一个 i nt 结果。必须将其明确“造型”回原来的类型(缩小转换会造成信息的丢失),以便将值赋 回那个类型。但对于 i nt 值,却不必进行造型处理,因为所有数据都已经属于 i nt 类型。然而,不要放松警 惕,认为一切事情都是安全的。如果对两个足够大的 i nt 值执行乘法运算,结果值就会溢出。下面这个例子 向大家展示了这一点: //: O f l ow j ava ver . // Sur pr i s e! Java l et s you over f l ow . publ i c cl as s O f l ow { ver publ i c s t at i c voi d m n(St r i ng[ ] ar gs ) { ai i nt bi g = 0x7f f f f f f f ; // m i nt val ue ax pr t ( " bi g = " + bi g) ; i nt bi gger = bi g * 4; pr t ( " bi gger = " + bi gger ) ; } s t at i c voi d pr t ( St r i ng s ) { Sys t em out . pr i nt l n( s ) ; . } } ///: ~ 输出结果如下: bi g = 2147483647 bi gger = - 4 而且不会从编译器那里收到出错提示,运行时也不会出现异常反应。爪哇咖啡(Java)确实是很好的东西, 但却没有“那么”好! 对于 char ,byt e 或者 s hor t ,混合赋值并不需要造型。即使它们执行转型操作,也会获得与直接算术运算相 同的结果。而在另一方面,将造型略去可使代码显得更加简练。 83

大家可以看到,除 bool ean 以外,任何一种主类型都可通过造型变为其他主类型。同样地,当造型成一种较 小的类型时,必须留意“缩小转换”的后果。否则会在造型过程中不知不觉地丢失信息。

3. 2 执行控制
Java 使用了 C的全部控制语句,所以假期您以前用 C或 C ++编程,其中大多数都应是非常熟悉的。大多数程 序化的编程语言都提供了某种形式的控制语句,这在语言间通常是共通的。在 Java 里,涉及的关键字包括 i f - el s e、w l e、do- w l e、f or 以及一个名为 s w t ch的选择语句。然而,Java 并不支持非常有害的 got o hi hi i (它仍是解决某些特殊问题的权宜之计)。仍然可以进行象 got o 那样的跳转,但比典型的 got o 要局限多 了。

3 . 2 . 1 真和假
所有条件语句都利用条件表达式的真或假来决定执行流程。条件表达式的一个例子是 A ==B。它用条件运算符 “==”来判断 A值是否等于 B 值。该表达式返回 t r ue 或 f al s e。本章早些时候接触到的所有关系运算符都可 拿来构造一个条件语句。注意 Java 不允许我们将一个数字作为布尔值使用,即使它在 C和 C ++里是允许的 (真是非零,而假是零)。若想在一次布尔测试中使用一个非布尔值——比如在 i f ( a) 里,那么首先必须用 一个条件表达式将其转换成一个布尔值,例如 i f ( a! =0) 。

3 . 2 . 2 i f - el s e i f - el s e 语句或许是控制程序流程最基本的形式。其中的 el s e 是可选的,所以可按下述两种形式来使用 i f : i f (布尔表达式) 语句 或者 i f (布尔表达式) 语句 el s e 语句 条件必须产生一个布尔结果。“语句”要么是用分号结尾的一个简单语句,要么是一个复合语句——封闭在 括号内的一组简单语句。在本书任何地方,只要提及“语句”这个词,就有可能包括简单或复合语句。 作为 i f - el s e 的一个例子,下面这个 t es t ( ) 方法可告诉我们猜测的一个数字位于目标数字之上、之下还是相 等: s t at i c i nt t es t ( i nt t es t val ) { i nt r es ul t = 0; i f ( t es t val > t ar get ) r es ul t = - 1; el s e i f ( t es t val < t ar get ) r es ul t = +1; el s e r es ul t = 0; // m ch at r et ur n r es ul t ; } 最好将流程控制语句缩进排列,使读者能方便地看出起点与终点。 1. r et ur n r et ur n关键字有两方面的用途:指定一个方法返回什么值(假设它没有 voi d 返回值),并立即返回那个 值。可据此改写上面的 t es t ( )方法,使其利用这些特点: 84

s t at i c i nt t es t 2( i nt t es t val ) { i f ( t es t val > t ar get ) r et ur n - 1; i f ( t es t val < t ar get ) r et ur n +1; r et ur n 0; // m ch at } 不必加上 el s e,因为方法在遇到 r et ur n后便不再继续。

3 . 2 . 3 反复 w l e,do- w l e和 f or 控制着循环,有时将其划分为“反复语句”。除非用于控制反复的布尔表达式得到 hi hi “假”的结果,否则语句会重复执行下去。w l e 循环的格式如下: hi w l e(布尔表达式) hi 语句 在循环刚开始时,会计算一次“布尔表达式”的值。而对于后来每一次额外的循环,都会在开始前重新计算 一次。 下面这个简单的例子可产生随机数,直到符合特定的条件为止: //: W l eTes t . j ava hi // D ons t r at es t he w l e l oop em hi publ i c cl as s W l eTes t { hi publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai doubl e r = 0; w l e( r < 0. 99d) { hi r = M h. r a at ndom ) ; ( Sys t em out . pr i nt l n( r ) ; . } } } ///: ~ 它用到了 M h 库里的 s t at i c(静态)方法 r andom ) 。该方法的作用是产生 0 和 1 之间(包括 0,但不包括 at ( 1)的一个 doubl e值。w l e的条件表达式意思是说:“一直循环下去,直到数字等于或大于 0. 99”。由于 hi 它的随机性,每运行一次这个程序,都会获得大小不同的数字列表。

3 . 2 . 4 do- whi l e do- w l e 的格式如下: hi do 语句 w l e(布尔表达式) hi w l e 和 do- w l e 唯一的区别就是 do- w l e肯定会至少执行一次;也就是说,至少会将其中的语句“过一 hi hi hi 遍”——即便表达式第一次便计算为 f al s e。而在 w l e 循环结构中,若条件第一次就为 f al s e,那么其中的 hi 语句根本不会执行。在实际应用中,w l e 比 do- w l e 更常用一些。 hi hi

85

3 . 2 . 5 f or f or 循环在第一次反复之前要进行初始化。随后,它会进行条件测试,而且在每一次反复的时候,进行某种 形式的“步进”(St eppi ng)。f or 循环的形式如下: f or ( 初始表达式; 布尔表达式; 步进) 语句 无论初始表达式,布尔表达式,还是步进,都可以置空。每次反复前,都要测试一下布尔表达式。若获得的 结果是 f al s e,就会继续执行紧跟在 f or 语句后面的那行代码。在每次循环的末尾,会计算一次步进。 f or 循环通常用于执行“计数”任务: //: Li s t C act er s . j ava har // D ons t r at es " f or " l oop by l i s t i ng em // al l t he A I I char act er s . SC publ i c cl as s Li s t C act er s { har publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai f or ( char c = 0; c < 128; c++) i f ( c ! = 26 ) // A SI C ear s cr een N l Sys t em out . pr i nt l n( . " val ue: " + ( i nt ) c + " char act er : " + c) ; } } ///: ~ 注意变量 c 是在需要用到它的时候定义的——在 f or 循环的控制表达式内部,而非在由起始花括号标记的代 码块的最开头。c 的作用域是由 f or 控制的表达式。 以于象 C这样传统的程序化语言,要求所有变量都在一个块的开头定义。所以在编译器创建一个块的时候, 它可以为那些变量分配空间。而在 Java 和 C ++中,则可在整个块的范围内分散变量声明,在真正需要的地方 才加以定义。这样便可形成更自然的编码风格,也更易理解。 可在 f or 语句里定义多个变量,但它们必须具有同样的类型: f or ( i nt i = 0, j = 1; i < 10 & j ! = 11; & i ++, j ++) /* body of f or l oop * /; 其中,f or 语句内的 i nt 定义同时覆盖了 i 和 j 。只有 f or 循环才具备在控制表达式里定义变量的能力。对于 其他任何条件或循环语句,都不可采用这种方法。 1. 逗号运算符 早在第 1 章,我们已提到了逗号运算符——注意不是逗号分隔符;后者用于分隔函数的不同自变量。Java 里 唯一用到逗号运算符的地方就是 f or 循环的控制表达式。在控制表达式的初始化和步进控制部分,我们可使 用一系列由逗号分隔的语句。而且那些语句均会独立执行。前面的例子已运用了这种能力,下面则是另一个 例子: //: C m per at or . j ava om aO publ i c cl as s C m per at or { om aO publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai f or ( i nt i = 1, j = i + 10; i < 5; i ++, j = i * 2) { 86

Sys t em out . pr i nt l n( " i = " + i + " j = " + j ) ; . } } } ///: ~ 输出如下: i i i i = = = = 1 2 3 4 j j j j = = = = 11 4 6 8

大家可以看到,无论在初始化还是在步进部分,语句都是顺序执行的。此外,尽管初始化部分可设置任意数 量的定义,但都属于同一类型。

3 . 2 . 6 中断和继续
在任何循环语句的主体部分,亦可用 br eak 和 cont i nue 控制循环的流程。其中,br eak 用于强行退出循环, 不执行循环中剩余的语句。而 cont i nue 则停止执行当前的反复,然后退回循环起始和,开始新的反复。 下面这个程序向大家展示了 br eak 和 cont i nue 在 f or 和 w l e循环中的例子: hi //: Br eakA ont i nue. j ava ndC // D ons t r at es br eak and cont i nue keyw ds em or publ i c cl as s Br eakA ont i nue { ndC publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai f or ( i nt i = 0; i < 100; i ++) { i f ( i == 74) br eak; // O of f or l oop ut i f ( i % 9 ! = 0) cont i nue; // N ext i t er at i on Sys t em out . pr i nt l n( i ) ; . } i nt i = 0; // A " i nf i ni t e l oop" : n w l e( t r ue) { hi i ++; i nt j = i * 27; i f ( j == 1269) br eak; // O of l oop ut i f ( i % 10 ! = 0) cont i nue; // Top of l oop Sys t em out . pr i nt l n( i ) ; . } } } ///: ~ 在这个 f or 循环中,i 的值永远不会到达 100。因为一旦 i 到达 74,br eak 语句就会中断循环。通常,只有在 不知道中断条件何时满足时,才需象这样使用 br eak。只要 i 不能被 9 整除,cont i nue 语句会使程序流程返 回循环的最开头执行(所以使 i 值递增)。如果能够整除,则将值显示出来。 第二部分向大家揭示了一个“无限循环”的情况。然而,循环内部有一个 br eak 语句,可中止循环。除此以 外,大家还会看到 cont i nue 移回循环顶部,同时不完成剩余的内容(所以只有在 i 值能被 9 整除时才打印出 值)。输出结果如下: 0 9 18 87

27 36 45 54 63 72 10 20 30 40 之所以显示 0,是由于 0% 等于 0。 9 无限循环的第二种形式是 f or ( ; ; ) 。编译器将 w l e( t r ue)与 f or ( ; ; )看作同一回事。所以具体选用哪个取决 hi 于自己的编程习惯。 1. 臭名昭著的“got o” got o 关键字很早就在程序设计语言中出现。事实上,got o 是汇编语言的程序控制结构的始祖:“若条件 A , 则跳到这里;否则跳到那里”。若阅读由几乎所有编译器生成的汇编代码,就会发现程序控制里包含了许多 跳转。然而,got o 是在源码的级别跳转的,所以招致了不好的声誉。若程序总是从一个地方跳到另一个地 方,还有什么办法能识别代码的流程呢?随着 Eds ger D j ks t r a著名的“G o 有害”论的问世,got o 便从此 i ot 失宠。 事实上,真正的问题并不在于使用 got o,而在于 got o 的滥用。而且在一些少见的情况下,got o 是组织控制 流程的最佳手段。 尽管 got o 仍是 Java 的一个保留字,但并未在语言中得到正式使用;Java 没有 got o。然而,在 br eak 和 cont i nue 这两个关键字的身上,我们仍然能看出一些 got o 的影子。它并不属于一次跳转,而是中断循环语 句的一种方法。之所以把它们纳入 got o 问题中一起讨论,是由于它们使用了相同的机制:标签。 “标签”是后面跟一个冒号的标识符,就象下面这样: l abel 1: 对 Java 来说,唯一用到标签的地方是在循环语句之前。进一步说,它实际需要紧靠在循环语句的前方——在 标签和循环之间置入任何语句都是不明智的。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另 一个循环或者一个开关。这是由于 br eak 和 cont i nue 关键字通常只中断当前循环,但若随同标签使用,它们 就会中断到存在标签的地方。如下所示: l abel 1: 外部循环{ 内部循环{ //. . . br eak; //1 //. . . cont i nue; //2 //. . . cont i nue l abel 1; //3 //. . . br eak l abel 1; //4 } } 在条件 1 中,br eak 中断内部循环,并在外部循环结束。在条件 2 中,cont i nue 移回内部循环的起始处。但 在条件 3 中,cont i nue l abel 1 却同时中断内部循环以及外部循环,并移至 l abel 1 处。随后,它实际是继续 循环,但却从外部循环开始。在条件 4 中,br eak l abel 1 也会中断所有循环,并回到 l abel 1 处,但并不重 新进入循环。也就是说,它实际是完全中止了两个循环。 下面是 f or 循环的一个例子: 88

//: Label edFor . j ava // Java’s " l abel ed f or l oop" publ i c cl as s Label edFor { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai i nt i = 0; out er : // C t have s t at em s her e an' ent f or ( ; t r ue ; ) { // i nf i ni t e l oop i nner : // C t have s t at em s her e an' ent f or ( ; i < 10; i ++) { pr t ( " i = " + i ) ; i f ( i == 2) { pr t ( " cont i nue" ) ; cont i nue; } i f ( i == 3) { pr t ( " br eak" ) ; i ++; // O her w s e i never t i // get s i ncr em ed. ent br eak; } i f ( i == 7) { pr t ( " cont i nue out er " ) ; i ++; // O her w s e i never t i // get s i ncr em ed. ent cont i nue out er ; } i f ( i == 8) { pr t ( " br eak out er " ) ; br eak out er ; } f or ( i nt k = 0; k < 5; k++) { i f ( k == 3) { pr t ( " cont i nue i nner " ) ; cont i nue i nner ; } } } } // C t br eak or cont i nue an' // t o l abel s her e } s t at i c voi d pr t ( St r i ng s ) { Sys t em out . pr i nt l n( s ) ; . } } ///: ~ 这里用到了在其他例子中已经定义的 pr t ( ) 方法。 注意 br eak 会中断 f or 循环,而且在抵达 f or 循环的末尾之前,递增表达式不会执行。由于 br eak 跳过了递 增表达式,所以递增会在 i ==3 的情况下直接执行。在 i ==7 的情况下,cont i nue out er 语句也会到达循环顶 部,而且也会跳过递增,所以它也是直接递增的。 89

下面是输出结果: i = 0 cont i nue i nner i = 1 cont i nue i nner i = 2 cont i nue i = 3 br eak i = 4 cont i nue i nner i = 5 cont i nue i nner i = 6 cont i nue i nner i = 7 cont i nue out er i = 8 br eak out er 如果没有 br eak out er 语句,就没有办法在一个内部循环里找到出外部循环的路径。这是由于 br eak 本身只 能中断最内层的循环(对于 cont i nue 同样如此)。 当然,若想在中断循环的同时退出方法,简单地用一个 r et ur n 即可。 下面这个例子向大家展示了带标签的 br eak 以及 cont i nue 语句在 w l e 循环中的用法: hi //: Lab edW l e. j ava el hi // Java' s " l abel ed w l e" l oop hi publ i c cl as s Label edW l e { hi publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai i nt i = 0; out er : w l e( t r ue) { hi pr t ( " O er w l e l oop" ) ; ut hi w l e( t r ue) { hi i ++; pr t ( " i = " + i ) ; i f ( i == 1) { pr t ( " cont i nue" ) ; cont i nue; } i f ( i == 3) { pr t ( " cont i nue out er " ) ; cont i nue out er ; } i f ( i == 5) { pr t ( " br eak" ) ; br eak; } i f ( i == 7) { pr t ( " br eak out er " ) ; 90

br eak out er ; } } } } s t at i c voi d pr t ( St r i ng s ) { Sys t em out . pr i nt l n( s ) ; . } } ///: ~ 同样的规则亦适用于 w l e: hi ( 1) 简单的一个 cont i nue 会退回最内层循环的开头(顶部),并继续执行。 ( 2) 带有标签的 cont i nue 会到达标签的位置,并重新进入紧接在那个标签后面的循环。 ( 3) br eak 会中断当前循环,并移离当前标签的末尾。 ( 4) 带标签的 br eak 会中断当前循环,并移离由那个标签指示的循环的末尾。 这个方法的输出结果是一目了然的: O er w l e l oop ut hi i = 1 cont i nue i = 2 i = 3 cont i nue out er O er w l e l oop ut hi i = 4 i = 5 br eak O er w l e l oop ut hi i = 6 i = 7 br eak out er 大家要记住的重点是:在 Java 里唯一需要用到标签的地方就是拥有嵌套循环,而且想中断或继续多个嵌套级 别的时候。 在 D j ks t r a 的“G o 有害”论中,他最反对的就是标签,而非 got o。随着标签在一个程序里数量的增多, i ot 他发现产生错误的机会也越来越多。标签和 got o 使我们难于对程序作静态分析。这是由于它们在程序的执行 流程中引入了许多“怪圈”。但幸运的是,Java 标签不会造成这方面的问题,因为它们的活动场所已被限 死,不可通过特别的方式到处传递程序的控制权。由此也引出了一个有趣的问题:通过限制语句的能力,反 而能使一项语言特性更加有用。

3 . 2 . 7 开关
“开关”(Sw t ch)有时也被划分为一种“选择语句”。根据一个整数表达式的值,s w t ch语句可从一系列 i i 代码选出一段执行。它的格式如下: s w t ch(整数选择因子) i cas e 整数值 1 : 语句; cas e 整数值 2 : 语句; cas e 整数值 3 : 语句; cas e 整数值 4 : 语句; cas e 整数值 5 : 语句; //. . def aul t : 语句; { br eak; br eak; br eak; br eak; br eak;

91

} 其中,“整数选择因子”是一个特殊的表达式,能产生整数值。s w t ch 能将整数选择因子的结果与每个整数 i 值比较。若发现相符的,就执行对应的语句(简单或复合语句)。若没有发现相符的,就执行 def aul t 语 句。 在上面的定义中,大家会注意到每个 cas e 均以一个 br eak 结尾。这样可使执行流程跳转至 s w t ch主体的末 i 尾。这是构建 s w t ch 语句的一种传统方式,但 br eak 是可选的。若省略 br eak,会继续执行后面的 cas e 语 i 句的代码,直到遇到一个 br eak 为止。尽管通常不想出现这种情况,但对有经验的程序员来说,也许能够善 加利用。注意最后的 def aul t 语句没有 br eak,因为执行流程已到了 br eak 的跳转目的地。当然,如果考虑 到编程风格方面的原因,完全可以在 def aul t 语句的末尾放置一个 br eak,尽管它并没有任何实际的用处。 s w t ch语句是实现多路选择的一种易行方式(比如从一系列执行路径中挑选一个)。但它要求使用一个选择 i 因子,并且必须是 i nt 或 char 那样的整数值。例如,假若将一个字串或者浮点数作为选择因子使用,那么它 们在 s w t ch语句里是不会工作的。对于非整数类型,则必须使用一系列 i f 语句。 i 下面这个例子可随机生成字母,并判断它们是元音还是辅音字母: //: Vow s A ons onant s . j ava el ndC // D ons t r at es t he s w t ch s t at em em i ent publ i c cl as s Vow s A ons onant s { el ndC publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai f or ( i nt i = 0; i < 100; i ++) { char c = ( char ) ( M h. r andom ) * 26 + ' a' ) ; at ( Sys t em out . pr i nt ( c + " : " ) ; . s w t ch( c) { i cas e ' a' : cas e ' e' : cas e ' i ' : cas e ' o' : cas e ' u' : Sys t em out . pr i nt l n( " vow " ) ; . el br eak; cas e ' y' : cas e ' w : ' Sys t em out . pr i nt l n( . " Som i m a vow " ) ; et es el br eak; def aul t : Sys t em out . pr i nt l n( " cons onant " ) ; . } } } } ///: ~ 由于 M h. r andom ) 会产生 0 到 1 之间的一个值,所以只需将其乘以想获得的最大随机数(对于英语字母, at ( 这个数字是 26),再加上一个偏移量,得到最小的随机数。 尽管我们在这儿表面上要处理的是字符,但 s w t ch 语句实际使用的字符的整数值。在 cas e 语句中,用单引 i 号封闭起来的字符也会产生整数值,以便我们进行比较。 请注意 cas e 语句相互间是如何聚合在一起的,它们依次排列,为一部分特定的代码提供了多种匹配模式。也 应注意将 br eak 语句置于一个特定 cas e 的末尾,否则控制流程会简单地下移,并继续判断下一个条件是否相 符。 1. 具体的计算 92

应特别留意下面这个语句: char c = ( char ) ( M h. r andom ) * 26 + ' a' ) ; at ( M h. r andom ) 会产生一个 doubl e值,所以 26 会转换成 doubl e 类型,以便执行乘法运算。这个运算也会产 at ( 生一个 doubl e 值。这意味着为了执行加法,必须无将' a' 转换成一个 doubl e。利用一个“造型”,doubl e结 果会转换回 char 。 我们的第一个问题是,造型会对 char 作什么样的处理呢?换言之,假设一个值是 29. 7,我们把它造型成一 个 char ,那么结果值到底是 30 还是 29 呢?答案可从下面这个例子中得到: //: C t i ngN ber s . j ava as um // W hat happens w hen you cas t a f l oat or doubl e // t o an i nt egr al val ue? publ i c cl as s C t i ngN ber s { as um publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai doubl e above = 0. 7, bel ow = 0. 4; Sys t em out . pr i nt l n( " above: " + above) ; . Sys t em out . pr i nt l n( " bel ow " + bel ow ; . : ) Sys t em out . pr i nt l n( . " ( i nt ) above: " + ( i nt ) above) ; Sys t em out . pr i nt l n( . " ( i nt ) bel ow " + ( i nt ) bel ow ; : ) Sys t em out . pr i nt l n( . " ( char ) ( ' a' + above) : " + ( char ) ( ' a' + above) ) ; Sys t em out . pr i nt l n( . " ( char ) ( ' a' + bel ow : " + ) ( char ) ( ' a' + bel ow ) ; ) } } ///: ~ 输出结果如下: above: 0. 7 bel ow 0. 4 : ( i nt ) above: 0 ( i nt ) bel ow 0 : ( char ) ( ' a' + above) : a ( char ) ( ' a' + bel ow : a ) 所以答案就是:将一个 f l oat 或 doubl e 值造型成整数值后,总是将小数部分“砍掉”,不作任何进位处理。 第二个问题与 M h. r andom ) 有关。它会产生 0 和 1 之间的值,但是否包括值' 1' 呢?用正统的数学语言表 at ( 达,它到底是( 0, 1) ,[ 0, 1] ,( 0, 1] ,还是[ 0, 1) 呢(方括号表示“包括”,圆括号表示“不包括”)?同样 地,一个示范程序向我们揭示了答案: //: Random Bounds . j ava // D oes M h. r andom ) pr oduce 0. 0 and 1. 0? at ( publ i c cl as s Random Bounds { s t at i c voi d us age( ) { Sys t em er r . pr i nt l n( " Us age: \n\t " + . 93

" Random Bounds l ow \n\t " + er " Random Bounds upper " ) ; Sys t em exi t ( 1) ; . } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai i f ( ar gs . l engt h ! = 1) us age( ) ; i f ( ar gs [ 0] . equal s ( " l ow " ) ) { er w l e( M h. r andom ) ! = 0. 0) hi at ( ; // Keep t r yi ng Sys t em out . pr i nt l n( " Pr oduced 0. 0! " ) ; . } el s e i f ( ar gs [ 0] . equal s ( " upper " ) ) { w l e( M h. r andom ) ! = 1. 0) hi at ( ; // Keep t r yi ng Sys t em out . pr i nt l n( " Pr oduced 1. 0! " ) ; . } el s e us age( ) ; } } ///: ~ 为运行这个程序,只需在命令行键入下述命令即可: j ava Random Bounds l ow er 或 j ava Random Bounds upper 在这两种情况下,我们都必须人工中断程序,所以会发现 M h. r andom ) “似乎”永远都不会产生 0. 0 或 at ( 1. 0。但这只是一项实验而已。若想到 0 和 1 之间有 2 的 128 次方不同的双精度小数,所以如果全部产生这些 数字,花费的时间会远远超过一个人的生命。当然,最后的结果是在 M h. r andom ) 的输出中包括了 0. 0。或 at ( 者用数字语言表达,输出值范围是[ 0, 1) 。

3. 3 总结
本章总结了大多数程序设计语言都具有的基本特性:计算、运算符优先顺序、类型转换以及选择和循环等 等。现在,我们作好了相应的准备,可继续向面向对象的程序设计领域迈进。在下一章里,我们将讨论对象 的初始化与清除问题,再后面则讲述隐藏的基本实现方法。

3. 4 练习
( 1) 写一个程序,打印出 1 到 100 间的整数。 ( 2) 修改练习( 1) ,在值为 47 时用一个 br eak 退出程序。亦可换成 r et ur n试试。 ( 3) 创建一个 s w t ch 语句,为每一种 cas e 都显示一条消息。并将 s w t ch置入一个 f or 循环里,令其尝试每 i i 一种 cas e。在每个 cas e 后面都放置一个 br eak,并对其进行测试。然后,删除 br eak,看看会有什么情况出 现。

94

第 4 章 初始化和清除
“随着计算机的进步,‘不安全’的程序设计已成为造成编程代价高昂的罪魁祸首之一。” “初始化”和“清除”是这些安全问题的其中两个。许多 C程序的错误都是由于程序员忘记初始化一个变量 造成的。对于现成的库,若用户不知道如何初始化库的一个组件,就往往会出现这一类的错误。清除是另一 个特殊的问题,因为用完一个元素后,由于不再关心,所以很容易把它忘记。这样一来,那个元素占用的资 源会一直保留下去,极易产生资源(主要是内存)用尽的后果。 C ++为我们引入了“构建器”的概念。这是一种特殊的方法,在一个对象创建之后自动调用。Java 也沿用了 这个概念,但新增了自己的“垃圾收集器”,能在资源不再需要的时候自动释放它们。本章将讨论初始化和 清除的问题,以及 Java 如何提供它们的支持。

4. 1 用构建器自动初始化
对于方法的创建,可将其想象成为自己写的每个类都调用一次 i ni t i al i z e( )。这个名字提醒我们在使用对象 之前,应首先进行这样的调用。但不幸的是,这也意味着用户必须记住调用方法。在 Java 中,由于提供了名 为“构建器”的一种特殊方法,所以类的设计者可担保每个对象都会得到正确的初始化。若某个类有一个构 建器,那么在创建对象时,Java 会自动调用那个构建器——甚至在用户毫不知觉的情况下。所以说这是可以 担保的! 接着的一个问题是如何命名这个方法。存在两方面的问题。第一个是我们使用的任何名字都可能与打算为某 个类成员使用的名字冲突。第二是由于编译器的责任是调用构建器,所以它必须知道要调用是哪个方法。C ++ 采取的方案看来是最简单的,且更有逻辑性,所以也在 Java 里得到了应用:构建器的名字与类名相同。这样 一来,可保证象这样的一个方法会在初始化期间自动调用。 下面是带有构建器的一个简单的类(若执行这个程序有问题,请参考第 3 章的“赋值”小节)。 //: Si m eC t r uct or . j ava pl ons // D ons t r at i on of a s i m e cons t r uct or em pl package c04; cl as s Rock { Rock( ) { // Thi s i s t he cons t r uct or Sys t em out . pr i nt l n( " C eat i ng Rock" ) ; . r } } publ i c cl as s Si m eC t r uct or { pl ons publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai f or ( i nt i = 0; i < 10; i ++) new Rock( ) ; } } ///: ~ 现在,一旦创建一个对象: new Rock( ) ; 就会分配相应的存储空间,并调用构建器。这样可保证在我们经手之前,对象得到正确的初始化。 请注意所有方法首字母小写的编码规则并不适用于构建器。这是由于构建器的名字必须与类名完全相同! 和其他任何方法一样,构建器也能使用自变量,以便我们指定对象的具体创建方式。可非常方便地改动上述 例子,以便构建器使用自己的自变量。如下所示: cl as s Rock { Rock( i nt i ) { 95

Sys t em out . pr i nt l n( . " C eat i ng Rock num r ber " + i ) ; } } publ i c cl as s Si m eC t r uct or { pl ons publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai f or ( i nt i = 0; i < 10; i ++) new Rock( i ) ; } } 利用构建器的自变量,我们可为一个对象的初始化设定相应的参数。举个例子来说,假设类 Tr ee 有一个构建 器,它用一个整数自变量标记树的高度,那么就可以象下面这样创建一个 Tr ee 对象: t r ee t = new Tr ee( 12) ; // 12 英尺高的树 若 Tr ee( i nt ) 是我们唯一的构建器,那么编译器不会允许我们以其他任何方式创建一个 Tr ee 对象。 构建器有助于消除大量涉及类的问题,并使代码更易阅读。例如在前述的代码段中,我们并未看到对 i ni t i al i z e( )方法的明确调用——那些方法在概念上独立于定义内容。在 Java 中,定义和初始化属于统一的 概念——两者缺一不可。 构建器属于一种较特殊的方法类型,因为它没有返回值。这与 voi d 返回值存在着明显的区别。对于 voi d 返 回值,尽管方法本身不会自动返回什么,但仍然可以让它返回另一些东西。构建器则不同,它不仅什么也不 会自动返回,而且根本不能有任何选择。若存在一个返回值,而且假设我们可以自行选择返回内容,那么编 译器多少要知道如何对那个返回值作什么样的处理。

4. 2 方法过载
在任何程序设计语言中,一项重要的特性就是名字的运用。我们创建一个对象时,会分配到一个保存区域的 名字。方法名代表的是一种具体的行动。通过用名字描述自己的系统,可使自己的程序更易人们理解和修 改。它非常象写散文——目的是与读者沟通。 我们用名字引用或描述所有对象与方法。若名字选得好,可使自己及其他人更易理解自己的代码。 将人类语言中存在细致差别的概念“映射”到一种程序设计语言中时,会出现一些特殊的问题。在日常生活 中,我们用相同的词表达多种不同的含义——即词的“过载”。我们说“洗衬衫”、“洗车”以及“洗 狗”。但若强制象下面这样说,就显得很愚蠢:“衬衫洗 衬衫”、“车洗 车”以及“狗洗 狗”。这是由于 听众根本不需要对执行的行动作任何明确的区分。人类的大多数语言都具有很强的“冗余”性,所以即使漏 掉了几个词,仍然可以推断出含义。我们不需要独一无二的标识符——可从具体的语境中推论出含义。 大多数程序设计语言(特别是 C )要求我们为每个函数都设定一个独一无二的标识符。所以绝对不能用一个 名为 pr i nt ( )的函数来显示整数,再用另一个 pr i nt ( )显示浮点数——每个函数都要求具备唯一的名字。 在 Java 里,另一项因素强迫方法名出现过载情况:构建器。由于构建器的名字由类名决定,所以只能有一个 构建器名称。但假若我们想用多种方式创建一个对象呢?例如,假设我们想创建一个类,令其用标准方式进 行初始化,另外从文件里读取信息来初始化。此时,我们需要两个构建器,一个没有自变量(默认构建 器),另一个将字串作为自变量——用于初始化对象的那个文件的名字。由于都是构建器,所以它们必须有 相同的名字,亦即类名。所以为了让相同的方法名伴随不同的自变量类型使用,“方法过载”是非常关键的 一项措施。同时,尽管方法过载是构建器必需的,但它亦可应用于其他任何方法,且用法非常方便。 在下面这个例子里,我们向大家同时展示了过载构建器和过载的原始方法: //: O l oadi ng. j ava ver // D ons t r at i on of bot h cons t r uct or em // and or di nar y m hod over l oadi ng. et i m t j ava. ut i l . * ; por cl as s Tr ee { 96

i nt hei ght ; Tr ee( ) { pr t ( " Pl ant i ng a s eedl i ng" ) ; hei ght = 0; } Tr ee( i nt i ) { pr t ( " C eat i ng new Tr ee t hat i s " r + i + " f eet t al l " ) ; hei ght = i ; } voi d i nf o( ) { pr t ( " Tr ee i s " + hei ght + " f eet t al l " ) ; } voi d i nf o( St r i ng s ) { pr t ( s + " : Tr ee i s " + hei ght + " f eet t al l " ) ; } s t at i c voi d pr t ( St r i ng s ) { Sys t em out . pr i nt l n( s ) ; . } } publ i c cl as s O l oadi ng { ver publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai f or ( i nt i = 0; i < 5; i ++) { Tr ee t = new Tr ee( i ) ; t . i nf o( ) ; t . i nf o( " over l oaded m hod" ) ; et } // O l oaded cons t r uct or : ver new Tr ee( ) ; } } ///: ~ Tr ee 既可创建成一颗种子,不含任何自变量;亦可创建成生长在苗圃中的植物。为支持这种创建,共使用了 两个构建器,一个没有自变量(我们把没有自变量的构建器称作“默认构建器”,注释①),另一个采用现 成的高度。 ①:在 Sun 公司出版的一些 Java 资料中,用简陋但很说明问题的词语称呼这类构建器——“无参数构建器” (no- ar g cons t r uct or s)。但“默认构建器”这个称呼已使用了许多年,所以我选择了它。 我们也有可能希望通过多种途径调用 i nf o( ) 方法。例如,假设我们有一条额外的消息想显示出来,就使用 St r i ng自变量;而假设没有其他话可说,就不使用。由于为显然相同的概念赋予了两个独立的名字,所以看 起来可能有些古怪。幸运的是,方法过载允许我们为两者使用相同的名字。

4 . 2 . 1 区分过载方法
若方法有同样的名字,Java 怎样知道我们指的哪一个方法呢?这里有一个简单的规则:每个过载的方法都必 须采取独一无二的自变量类型列表。 若稍微思考几秒钟,就会想到这样一个问题:除根据自变量的类型,程序员如何区分两个同名方法的差异 呢? 即使自变量的顺序也足够我们区分两个方法(尽管我们通常不愿意采用这种方法,因为它会产生难以维护的 97

代码): //: O l oadi ngO der . j ava ver r // O l oadi ng bas ed on t he or der of ver // t he ar gum s . ent publ i c cl as s O l oadi ngO der { ver r s t at i c voi d pr i nt ( St r i ng s , i nt i ) { Sys t em out . pr i nt l n( . " St r i ng: " + s + " , i nt : " + i ) ; } s t at i c voi d pr i nt ( i nt i , St r i ng s ) { Sys t em out . pr i nt l n( . " i nt : " + i + " , St r i ng: " + s ) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai pr i nt ( " St r i ng f i r s t " , 11) ; pr i nt ( 99, " I nt f i r s t " ) ; } } ///: ~ 两个 pr i nt ( )方法有完全一致的自变量,但顺序不同,可据此区分它们。

4 . 2 . 2 主类型的过载
主(数据)类型能从一个“较小”的类型自动转变成一个“较大”的类型。涉及过载问题时,这会稍微造成 一些混乱。下面这个例子揭示了将主类型传递给过载的方法时发生的情况: //: Pr i m t i veO l oadi ng. j ava i ver // Pr om i on of pr i m t i ves and over l oadi ng ot i publ i c cl as s Pr i m t i veO l oadi ng { i ver // bool ean can' t be aut om i cal l y conver t ed at s t at i c voi d pr t ( St r i ng s ) { Sys t em out . pr i nt l n( s ) ; . } voi d voi d voi d voi d voi d voi d voi d voi d voi d voi d voi d voi d voi d f 1( char x) { pr t ( " f 1( char ) " ) ; } f 1( byt e x) { pr t ( " f 1( byt e) " ) ; } f 1( s hor t x) { pr t ( " f 1( s hor t ) " ) ; } f 1( i nt x) { pr t ( " f 1( i nt ) " ) ; } f 1( l ong x) { pr t ( " f 1( l ong) " ) ; } f 1( f l oat x) { pr t ( " f 1( f l oat ) " ) ; } f 1( doubl e x) { pr t ( " f 1( doubl e) " ) ; } f 2( byt e x) { pr t ( " f 2( byt e) " ) ; } f 2( s hor t x) { pr t ( " f 2( s hor t ) " ) ; } f 2( i nt x) { pr t ( " f 2( i nt ) " ) ; } f 2( l ong x) { pr t ( " f 2( l ong) " ) ; } f 2( f l oat x) { pr t ( " f 2( f l oat ) " ) ; } f 2( doubl e x) { pr t ( " f 2( doubl e) " ) ; } 98

voi d voi d voi d voi d voi d voi d voi d voi d voi d

f 3( s hor t x) { pr t ( " f 3( s hor t ) " ) ; } f 3( i nt x) { pr t ( " f 3( i nt ) " ) ; } f 3( l ong x) { pr t ( " f 3( l ong) " ) ; } f 3( f l oat x) { pr t ( " f 3( f l oat ) " ) ; } f 3( doubl e x) { pr t ( " f 3( doubl e) " ) ; } f 4( i nt x) { pr t ( " f 4( i nt ) " ) ; } f 4( l ong x) { pr t ( " f 4( l ong) " ) ; } f 4( f l oat x) { pr t ( " f 4( f l oat ) " ) ; } f 4( doubl e x) { pr t ( " f 4( doubl e) " ) ; }

voi d f 5( l ong x) { pr t ( " f 5( l ong) " ) ; } voi d f 5( f l oat x) { pr t ( " f 5( f l oat ) " ) ; } voi d f 5( doubl e x) { pr t ( " f 5( doubl e) " ) ; } voi d f 6( f l oat x) { pr t ( " f 6( f l oat ) " ) ; } voi d f 6( doubl e x) { pr t ( " f 6( doubl e) " ) ; } voi d f 7( doubl e x) { pr t ( " f 7( doubl e) " ) ; } voi d t es t C t Val ( ) { ons pr t ( " T es t i ng w t h 5" ) ; i f 1( 5) ; f 2( 5) ; f 3( 5) ; f 4( 5) ; f 5( 5) ; f 6( 5) ; f 7( 5) ; } voi d t es t C ( ) { har char x = ' x' ; pr t ( " char ar gum : " ) ; ent f 1( x) ; f 2( x) ; f 3( x) ; f 4( x) ; f 5( x) ; f 6( x) ; f 7( x) ; } voi d t es t Byt e( ) { byt e x = 0; pr t ( " byt e ar gum : " ) ; ent f 1( x) ; f 2( x) ; f 3( x) ; f 4( x) ; f 5( x) ; f 6( x) ; f 7( x) ; } voi d t es t Shor t ( ) { s hor t x = 0; pr t ( " s hor t ar gum : " ) ; ent f 1( x) ; f 2( x) ; f 3( x) ; f 4( x) ; f 5( x) ; f 6( x) ; f 7( x) ; } voi d t es t I nt ( ) { i nt x = 0; pr t ( " i nt ar gum : " ) ; ent f 1( x) ; f 2( x) ; f 3( x) ; f 4( x) ; f 5( x) ; f 6( x) ; f 7( x) ; } voi d t es t Long( ) { l ong x = 0; pr t ( " l ong ar gum : " ) ; ent f 1( x) ; f 2( x) ; f 3( x) ; f 4( x) ; f 5( x) ; f 6( x) ; f 7( x) ; } voi d t es t Fl oat ( ) { f l oat x = 0; 99

pr t ( " f l oat ar gum : " ) ; ent f 1( x) ; f 2( x) ; f 3( x) ; f 4( x) ; f 5( x) ; f 6( x) ; f 7( x) ; } voi d t es t D oubl e( ) { doubl e x = 0; pr t ( " doubl e ar gum : " ) ; ent f 1( x) ; f 2( x) ; f 3( x) ; f 4( x) ; f 5( x) ; f 6( x) ; f 7( x) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Pr i m t i veO l oadi ng p = i ver new Pr i m t i veO l oadi ng( ) ; i ver p. t es t C t Val ( ) ; ons p. t es t C ( ) ; har p. t es t Byt e( ) ; p. t es t Shor t ( ) ; p. t es t I nt ( ) ; p. t es t Long( ) ; p. t es t Fl oat ( ) ; p. t es t D oubl e( ) ; } } ///: ~ 若观察这个程序的输出,就会发现常数值 5 被当作一个 i nt 值处理。所以假若可以使用一个过载的方法,就 能获取它使用的 i nt 值。在其他所有情况下,若我们的数据类型“小于”方法中使用的自变量,就会对那种 数据类型进行“转型”处理。char 获得的效果稍有些不同,这是由于假期它没有发现一个准确的 char 匹 配,就会转型为 i nt 。 若我们的自变量“大于”过载方法期望的自变量,这时又会出现什么情况呢?对前述程序的一个修改揭示出 了答案: //: D ot i on. j ava em // D ot i on of pr i m t i ves and over l oadi ng em i publ i c cl as s D ot i on { em s t at i c voi d pr t ( St r i ng s ) { Sys t em out . pr i nt l n( s ) ; . } voi d voi d voi d voi d voi d voi d voi d f 1( char x) { pr t ( " f 1( char ) " ) ; } f 1( byt e x) { pr t ( " f 1( byt e) " ) ; } f 1( s hor t x) { pr t ( " f 1( s hor t ) " ) ; } f 1( i nt x) { pr t ( " f 1( i nt ) " ) ; } f 1( l ong x) { pr t ( " f 1( l ong) " ) ; } f 1( f l oat x) { pr t ( " f 1( f l oat ) " ) ; } f 1( doubl e x) { pr t ( " f 1( doubl e) " ) ; }

voi d f 2( char x) { pr t ( " f 2( char ) " ) ; } voi d f 2( byt e x) { pr t ( " f 2( byt e) " ) ; } voi d f 2( s hor t x) { pr t ( " f 2( s hor t ) " ) ; } voi d f 2( i nt x) { pr t ( " f 2( i nt ) " ) ; } voi d f 2( l ong x) { pr t ( " f 2( l ong) " ) ; } voi d f 2( f l oat x) { pr t ( " f 2( f l oat ) " ) ; }

100

voi d voi d voi d voi d voi d voi d voi d voi d voi d

f 3( char x) { pr t ( " f 3( char ) " ) ; } f 3( byt e x) { pr t ( " f 3( byt e) " ) ; } f 3( s hor t x) { pr t ( " f 3( s hor t ) " ) ; } f 3( i nt x) { pr t ( " f 3( i nt ) " ) ; } f 3( l ong x) { pr t ( " f 3( l ong) " ) ; } f 4( char x) { pr t ( " f 4( char ) " ) ; } f 4( byt e x) { pr t ( " f 4( byt e) " ) ; } f 4( s hor t x) { pr t ( " f 4( s hor t ) " ) ; } f 4( i nt x) { pr t ( " f 4( i nt ) " ) ; }

voi d f 5( char x) { pr t ( " f 5( char ) " ) ; } voi d f 5( byt e x) { pr t ( " f 5( byt e) " ) ; } voi d f 5( s hor t x) { pr t ( " f 5( s hor t ) " ) ; } voi d f 6( char x) { pr t ( " f 6( char ) " ) ; } voi d f 6( byt e x) { pr t ( " f 6( byt e) " ) ; } voi d f 7( char x) { pr t ( " f 7( char ) " ) ; } voi d t es t D oubl e( ) { doubl e x = 0; pr t ( " doubl e ar gum : " ) ; ent f 1( x) ; f 2( ( f l oat ) x) ; f 3( ( l ong) x) ; f 4( ( i nt ) x) ; f 5( ( s hor t ) x) ; f 6( ( byt e) x) ; f 7( ( char ) x) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai D ot i on p = new D ot i on( ) ; em em p. t es t D oubl e( ) ; } } ///: ~ 在这里,方法采用了容量更小、范围更窄的主类型值。若我们的自变量范围比它宽,就必须用括号中的类型 名将其转为适当的类型。如果不这样做,编译器会报告出错。 大家可注意到这是一种“缩小转换”。也就是说,在造型或转型过程中可能丢失一些信息。这正是编译器强 迫我们明确定义的原因——我们需明确表达想要转型的愿望。

4 . 2 . 3 返回值过载
我们很易对下面这些问题感到迷惑:为什么只有类名和方法自变量列出?为什么不根据返回值对方法加以区 分?比如对下面这两个方法来说,虽然它们有同样的名字和自变量,但其实是很容易区分的: voi d f ( ) { } i nt f ( ) { } 若编译器可根据上下文(语境)明确判断出含义,比如在 i nt x=f ( ) 中,那么这样做完全没有问题。然而, 我们也可能调用一个方法,同时忽略返回值;我们通常把这称为“为它的副作用去调用一个方法”,因为我 们关心的不是返回值,而是方法调用的其他效果。所以假如我们象下面这样调用方法: f (); Java 怎样判断 f ( )的具体调用方式呢?而且别人如何识别并理解代码呢?由于存在这一类的问题,所以不能 根据返回值类型来区分过载的方法。

101

4 . 2 . 4 默认构建器
正如早先指出的那样,默认构建器是没有自变量的。它们的作用是创建一个“空对象”。若创建一个没有构 建器的类,则编译程序会帮我们自动创建一个默认构建器。例如: //: D aul t C t r uct or . j ava ef ons cl as s Bi r d { i nt i ; } publ i c cl as s D aul t C t r uct or { ef ons publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Bi r d nc = new Bi r d( ) ; // def aul t ! } } ///: ~ 对于下面这一行: new Bi r d( ) ; 它的作用是新建一个对象,并调用默认构建器——即使尚未明确定义一个象这样的构建器。若没有它,就没 有方法可以调用,无法构建我们的对象。然而,如果已经定义了一个构建器(无论是否有自变量),编译程 序都不会帮我们自动合成一个: cl as s Bus h { Bus h( i nt i ) { } Bus h( doubl e d) { } } 现在,假若使用下述代码: new Bus h( ) ; 编译程序就会报告自己找不到一个相符的构建器。就好象我们没有设置任何构建器,编译程序会说:“你看 来似乎需要一个构建器,所以让我们给你制造一个吧。”但假如我们写了一个构建器,编译程序就会说: “啊,你已写了一个构建器,所以我知道你想干什么;如果你不放置一个默认的,是由于你打算省略它。”

4 . 2 . 5 t hi s 关键字
如果有两个同类型的对象,分别叫作 a和 b,那么您也许不知道如何为这两个对象同时调用一个 f ( )方法: cl as s Banana { voi d f ( i nt i ) { /* . . . */ } } Banana a = new Banana( ) , b = new Banana( ) ; a. f ( 1) ; b. f ( 2) ; 若只有一个名叫 f ( )的方法,它怎样才能知道自己是为 a还是为 b调用的呢? 为了能用简便的、面向对象的语法来书写代码——亦即“将消息发给对象”,编译器为我们完成了一些幕后 工作。其中的秘密就是第一个自变量传递给方法 f ( ),而且那个自变量是准备操作的那个对象的句柄。所以 前述的两个方法调用就变成了下面这样的形式: Banana. f ( a, 1) ; Banana. f ( b, 2) ; 这是内部的表达形式,我们并不能这样书写表达式,并试图让编译器接受它。但是,通过它可理解幕后到底 发生了什么事情。 102

假定我们在一个方法的内部,并希望获得当前对象的句柄。由于那个句柄是由编译器“秘密”传递的,所以 没有标识符可用。然而,针对这一目的有个专用的关键字:t hi s 。t hi s 关键字(注意只能在方法内部使用) 可为已调用了其方法的那个对象生成相应的句柄。可象对待其他任何对象句柄一样对待这个句柄。但要注 意,假若准备从自己某个类的另一个方法内部调用一个类方法,就不必使用 t hi s 。只需简单地调用那个方法 即可。当前的 t hi s 句柄会自动应用于其他方法。所以我们能使用下面这样的代码: cl as s A i cot { pr voi d pi ck( ) { /* . . . * / } voi d pi t ( ) { pi ck( ) ; /* . . . */ } } 在 pi t ( ) 内部,我们可以说 t hi s . pi ck( ),但事实上无此必要。编译器能帮我们自动完成。t hi s 关键字只能 用于那些特殊的类——需明确使用当前对象的句柄。例如,假若您希望将句柄返回给当前对象,那么它经常 在 r et ur n 语句中使用。 //: Leaf . j ava // Si m e us e of t he " t hi s " keyw d pl or publ i c cl as s Leaf { pr i vat e i nt i = 0; Leaf i ncr em ( ) { ent i ++; r et ur n t hi s ; } voi d pr i nt ( ) { Sys t em out . pr i nt l n( " i = " + i ) ; . } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Leaf x = new Leaf ( ) ; x. i ncr em ( ) . i ncr em ( ) . i ncr em ( ) . pr i nt ( ) ; ent ent ent } } ///: ~ 由于 i ncr em ( )通过 t hi s 关键字返回当前对象的句柄,所以可以方便地对同一个对象执行多项操作。 ent 1. 在构建器里调用构建器 若为一个类写了多个构建器,那么经常都需要在一个构建器里调用另一个构建器,以避免写重复的代码。可 用 t hi s 关键字做到这一点。 通常,当我们说 t hi s 的时候,都是指“这个对象”或者“当前对象”。而且它本身会产生当前对象的一个句 柄。在一个构建器中,若为其赋予一个自变量列表,那么 t hi s 关键字会具有不同的含义:它会对与那个自变 量列表相符的构建器进行明确的调用。这样一来,我们就可通过一条直接的途径来调用其他构建器。如下所 示: //: Fl o er . j ava w // C l i ng cons t r uct or s w t h " t hi s " al i publ i c cl as s Fl ow { er pr i vat e i nt pet al C ount = 0; pr i vat e St r i ng s = new St r i ng( " nul l " ) ; Fl ow ( i nt pet al s ) { er pet al C ount = pet al s ; Sys t em out . pr i nt l n( . 103

" C t r uct or w i nt ar g onl y, pet al C ons / ount = " + pet al C ount ) ; } Fl ow ( St r i ng s s ) { er Sys t em out . pr i nt l n( . " C t r uct or w St r i ng ar g onl y, s =" + s s ) ; ons / s = ss; } Fl ow ( St r i ng s , i nt pet al s ) { er t hi s ( pet al s ) ; //! t hi s ( s ) ; // C t cal l t w an' o! t hi s . s = s ; // A her us e of " t hi s " not Sys t em out . pr i nt l n( " St r i ng & i nt ar gs " ) ; . } Fl ow ( ) { er t hi s ( " hi " , 47) ; Sys t em out . pr i nt l n( . " def aul t cons t r uct or ( no ar gs ) " ) ; } voi d pr i nt ( ) { //! t hi s ( 11) ; // N i ns i de non- cons t r uct or ! ot Sys t em out . pr i nt l n( . " pet al C ount = " + pet al C ount + " s = " + s ) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Fl ow x = new Fl ow ( ) ; er er x. pr i nt ( ) ; } } ///: ~ 其中,构建器 Fl ow ( St r i ng s , i nt pet al s ) 向我们揭示出这样一个问题:尽管可用 t hi s 调用一个构建器, er 但不可调用两个。除此以外,构建器调用必须是我们做的第一件事情,否则会收到编译程序的报错信息。 这个例子也向大家展示了 t hi s 的另一项用途。由于自变量 s 的名字以及成员数据 s 的名字是相同的,所以会 出现混淆。为解决这个问题,可用 t hi s . s 来引用成员数据。经常都会在 Java 代码里看到这种形式的应用, 本书的大量地方也采用了这种做法。 在 pr i nt ( ) 中,我们发现编译器不让我们从除了一个构建器之外的其他任何方法内部调用一个构建器。 2. s t at i c 的含义 理解了 t hi s 关键字后,我们可更完整地理解 s t at i c(静态)方法的含义。它意味着一个特定的方法没有 t hi s 。我们不可从一个 s t at i c方法内部发出对非 s t at i c方法的调用(注释②),尽管反过来说是可以的。 而且在没有任何对象的前提下,我们可针对类本身发出对一个 s t at i c方法的调用。事实上,那正是 s t at i c 方法最基本的意义。它就好象我们创建一个全局函数的等价物(在 C语言中)。除了全局函数不允许在 Java 中使用以外,若将一个 s t at i c方法置入一个类的内部,它就可以访问其他 s t at i c 方法以及 s t at i c 字段。 ②:有可能发出这类调用的一种情况是我们将一个对象句柄传到 s t at i c 方法内部。随后,通过句柄(此时实 际是 t hi s ),我们可调用非 s t at i c方法,并访问非 s t at i c 字段。但一般地,如果真的想要这样做,只要制 作一个普通的、非 s t at i c 方法即可。 有些人抱怨 s t at i c方法并不是“面向对象”的,因为它们具有全局函数的某些特点;利用 s t at i c方法,我 们不必向对象发送一条消息,因为不存在 t hi s 。这可能是一个清楚的自变量,若您发现自己使用了大量静态 方法,就应重新思考自己的策略。然而,s t at i c 的概念是非常实用的,许多时候都需要用到它。所以至于它

104

们是否真的“面向对象”,应该留给理论家去讨论。事实上,即使 Sm l t al k 在自己的“类方法”里也有类 al 似于 s t at i c的东西。

4. 3 清除:收尾和垃圾收集
程序员都知道“初始化”的重要性,但通常忘记清除的重要性。毕竟,谁需要来清除一个 i nt 呢?但是对于 库来说,用完后简单地“释放”一个对象并非总是安全的。当然,Java 可用垃圾收集器回收由不再使用的对 象占据的内存。现在考虑一种非常特殊且不多见的情况。假定我们的对象分配了一个“特殊”内存区域,没 有使用 new 。垃圾收集器只知道释放那些由 new分配的内存,所以不知道如何释放对象的“特殊”内存。为 解决这个问题,Java 提供了一个名为 f i nal i ze( ) 的方法,可为我们的类定义它。在理想情况下,它的工作原 理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用 f i nal i ze( ) ,而且只有在下 一次垃圾收集过程中,才会真正回收对象的内存。所以如果使用 f i nal i ze( ) ,就可以在垃圾收集期间进行一 些重要的清除或清扫工作。 但也是一个潜在的编程陷阱,因为有些程序员(特别是在 C ++开发背景的)刚开始可能会错误认为它就是在 C ++中为“破坏器”(D t r uct or )使用的 f i nal i ze( ) ——破坏(清除)一个对象的时候,肯定会调用这个 es 函数。但在这里有必要区分一下 C ++和 Java 的区别,因为 C ++的对象肯定会被清除(排开编程错误的因 素),而 Java 对象并非肯定能作为垃圾被“收集”去。或者换句话说: 垃圾收集并不等于“破坏”! 若能时刻牢记这一点,踩到陷阱的可能性就会大大减少。它意味着在我们不再需要一个对象之前,有些行动 是必须采取的,而且必须由自己来采取这些行动。Java 并未提供“破坏器”或者类似的概念,所以必须创建 一个原始的方法,用它来进行这种清除。例如,假设在对象创建过程中,它会将自己描绘到屏幕上。如果不 从屏幕明确删除它的图像,那么它可能永远都不会被清除。若在 f i nal i ze( ) 里置入某种删除机制,那么假设 对象被当作垃圾收掉了,图像首先会将自身从屏幕上移去。但若未被收掉,图像就会保留下来。所以要记住 的第二个重点是: 我们的对象可能不会当作垃圾被收掉! 有时可能发现一个对象的存储空间永远都不会释放,因为自己的程序永远都接近于用光空间的临界点。若程 序执行结束,而且垃圾收集器一直都没有释放我们创建的任何对象的存储空间,则随着程序的退出,那些资 源会返回给操作系统。这是一件好事情,因为垃圾收集本身也要消耗一些开销。如永远都不用它,那么永远 也不用支出这部分开销。

4 . 3 . 1 f i nal i z e( ) 用途何在
此时,大家可能已相信了自己应该将 f i nal i ze( ) 作为一种常规用途的清除方法使用。它有什么好处呢? 要记住的第三个重点是: 垃圾收集只跟内存有关! 也就是说,垃圾收集器存在的唯一原因是为了回收程序不再使用的内存。所以对于与垃圾收集有关的任何活 动来说,其中最值得注意的是 f i nal i ze( ) 方法,它们也必须同内存以及它的回收有关。 但这是否意味着假如对象包含了其他对象,f i nal i ze( ) 就应该明确释放那些对象呢?答案是否定的——垃圾 收集器会负责释放所有对象占据的内存,无论这些对象是如何创建的。它将对 f i nal i ze( ) 的需求限制到特殊 的情况。在这种情况下,我们的对象可采用与创建对象时不同的方法分配一些存储空间。但大家或许会注意 到,Java 中的所有东西都是对象,所以这到底是怎么一回事呢? 之所以要使用 f i nal i ze( ) ,看起来似乎是由于有时需要采取与 Java的普通方法不同的一种方法,通过分配 内存来做一些具有 C风格的事情。这主要可以通过“固有方法”来进行,它是从 Java 里调用非 Java 方法的 一种方式(固有方法的问题在附录 A讨论)。C和 C ++是目前唯一获得固有方法支持的语言。但由于它们能调 用通过其他语言编写的子程序,所以能够有效地调用任何东西。在非 Java 代码内部,也许能调用 C的 m l oc( ) 系列函数,用它分配存储空间。而且除非调用了 f r ee( ) ,否则存储空间不会得到释放,从而造成内 al 存“漏洞”的出现。当然,f r ee( ) 是一个 C和 C ++函数,所以我们需要在 f i nal i ze( ) 内部的一个固有方法中 105

调用它。 读完上述文字后,大家或许已弄清楚了自己不必过多地使用 f i nal i ze( ) 。这个思想是正确的;它并不是进行 普通清除工作的理想场所。那么,普通的清除工作应在何处进行呢?

4 . 3 . 2 必须执行清除
为清除一个对象,那个对象的用户必须在希望进行清除的地点调用一个清除方法。这听起来似乎很容易做 到,但却与 C ++“破坏器”的概念稍有抵触。在 C ++中,所有对象都会破坏(清除)。或者换句话说,所有对 象都“应该”破坏。若将 C ++对象创建成一个本地对象,比如在堆栈中创建(在 Java 中是不可能的),那么 清除或破坏工作就会在“结束花括号”所代表的、创建这个对象的作用域的末尾进行。若对象是用 new创建 的(类似于 Java),那么当程序员调用 C ++的 del et e 命令时(Java 没有这个命令),就会调用相应的破坏 器。若程序员忘记了,那么永远不会调用破坏器,我们最终得到的将是一个内存“漏洞”,另外还包括对象 的其他部分永远不会得到清除。 相反,Java 不允许我们创建本地(局部)对象——无论如何都要使用 new 。但在 Java 中,没有“del et e”命 令来释放对象,因为垃圾收集器会帮助我们自动释放存储空间。所以如果站在比较简化的立场,我们可以说 正是由于存在垃圾收集机制,所以 Java 没有破坏器。然而,随着以后学习的深入,就会知道垃圾收集器的存 在并不能完全消除对破坏器的需要,或者说不能消除对破坏器代表的那种机制的需要(而且绝对不能直接调 用 f i nal i ze( ) ,所以应尽量避免用它)。若希望执行除释放存储空间之外的其他某种形式的清除工作,仍然 必须调用 Java 中的一个方法。它等价于 C ++的破坏器,只是没后者方便。 f i nal i ze( ) 最有用处的地方之一是观察垃圾收集的过程。下面这个例子向大家展示了垃圾收集所经历的过 程,并对前面的陈述进行了总结。 //: G bage. j ava ar // D ons t r at i on of t he gar bage em // col l ect or and f i nal i z at i on cl as s C r { hai s t at i c bool ean gcr un = f al s e; s t at i c bool ean f = f al s e; s t at i c i nt cr eat ed = 0; s t at i c i nt f i nal i zed = 0; i nt i ; C r () { hai i = ++cr eat ed; i f ( cr eat ed == 47) Sys t em out . pr i nt l n( " C eat ed 47" ) ; . r } pr ot ect ed voi d f i nal i z e( ) { i f ( ! gcr un) { gcr un = t r ue; Sys t em out . pr i nt l n( . " Begi nni ng t o f i nal i ze af t er " + cr eat ed + " C r s have been cr eat ed" ) ; hai } i f ( i == 47) { Sys t em out . pr i nt l n( . " Fi nal i zi ng C r #47, " + hai " Set t i ng f l ag t o s t op C r cr eat i on" ) ; hai f = t r ue; } f i nal i z ed++; i f ( f i nal i zed >= cr eat ed) Sys t em out . pr i nt l n( . 106

" A l " + f i nal i zed + " f i nal i z ed" ) ; l } } publ i c cl as s G bage { ar publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai i f ( ar gs . l engt h == 0) { Sys t em er r . pr i nt l n( " Us age: \n" + . " j ava G bage bef or e\n or : \n" + ar " j ava G bage af t er " ) ; ar r et ur n; } w l e( ! C r . f ) { hi hai new C r ( ) ; hai new St r i ng( " To t ake up s pace" ) ; } Sys t em out . pr i nt l n( . " A t er al l C r s have been cr eat ed: \n" + f hai " t ot al cr eat ed = " + C r . cr eat ed + hai " , t ot al f i nal i zed = " + C r . f i nal i zed) ; hai i f ( ar gs [ 0] . equal s ( " bef or e" ) ) { Sys t em out . pr i nt l n( " gc( ) : " ) ; . Sys t em gc( ) ; . Sys t em out . pr i nt l n( " r unFi nal i zat i on( ) : " ) ; . Sys t em r unFi nal i zat i on( ) ; . } Sys t em out . pr i nt l n( " bye! " ) ; . i f ( ar gs [ 0] . equal s ( " af t er " ) ) Sys t em r unFi nal i zer s O . nExi t ( t r ue) ; } } ///: ~ 上面这个程序创建了许多 C r 对象,而且在垃圾收集器开始运行后的某些时候,程序会停止创建 C r 。 hai hai 由于垃圾收集器可能在任何时间运行,所以我们不能准确知道它在何时启动。因此,程序用一个名为 gcr un 的标记来指出垃圾收集器是否已经开始运行。利用第二个标记 f ,C r 可告诉 m n( )它应停止对象的生 hai ai 成。这两个标记都是在 f i nal i ze( )内部设置的,它调用于垃圾收集期间。 另两个 s t at i c 变量——cr eat ed以及 f i nal i zed——分别用于跟踪已创建的对象数量以及垃圾收集器已进行 完收尾工作的对象数量。最后,每个 C r 都有它自己的(非 s t at i c)i nt i ,所以能跟踪了解它具体的编 hai 号是多少。编号为 47 的 C r 进行完收尾工作后,标记会设为 t r ue,最终结束 C r 对象的创建过程。 hai hai 所有这些都在 m n( ) 的内部进行——在下面这个循环里: ai w l e( ! C r . f ) { hi hai new C r ( ) ; hai new St r i ng( " To t ake up s pace" ) ; } 大家可能会疑惑这个循环什么时候会停下来,因为内部没有任何改变 C r . f 值的语句。然而,f i nal i ze( ) hai 进程会改变这个值,直至最终对编号 47 的对象进行收尾处理。 每次循环过程中创建的 St r i ng对象只是属于额外的垃圾,用于吸引垃圾收集器——一旦垃圾收集器对可用内 存的容量感到“紧张不安”,就会开始关注它。 运行这个程序的时候,提供了一个命令行自变量“bef or e”或者“af t er ”。其中,“bef or e”自变量会调用 Sys t em gc( )方法(强制执行垃圾收集器),同时还会调用 Sys t em r unFi nal i zat i on( ) 方法,以便进行收尾 . . 107

工作。这些方法都可在 Java 1. 0 中使用,但通过使用“af t er ”自变量而调用的 r unFi nal i zer s O nExi t ( ) 方 法却只有 Java 1. 1 及后续版本提供了对它的支持(注释③)。注意可在程序执行的任何时候调用这个方法, 而且收尾程序的执行与垃圾收集器是否运行是无关的。 ③:不幸的是,Java 1. 0 采用的垃圾收集器方案永远不能正确地调用 f i nal i ze( ) 。因此,f i nal i ze( ) 方法 (特别是那些用于关闭文件的)事实上经常都不会得到调用。现在有些文章声称所有收尾模块都会在程序退 出的时候得到调用——即使到程序中止的时候,垃圾收集器仍未针对那些对象采取行动。这并不是真实的情 况,所以我们根本不能指望 f i nal i ze( ) 能为所有对象而调用。特别地,f i nal i ze( ) 在 Java 1. 0 里几乎毫无 用处。 前面的程序向我们揭示出:在 Java 1. 1 中,收尾模块肯定会运行这一许诺已成为现实——但前提是我们明确 地强制它采取这一操作。若使用一个不是“bef or e”或“af t er ”的自变量(如“none”),那么两个收尾工 作都不会进行,而且我们会得到象下面这样的输出: C eat ed 47 r Begi nni ng t o f i nal i ze af t er 8694 C r s have been cr e ed hai at Fi nal i z i ng C r #47, Set t i ng f l ag t o s t op C r cr eat i on hai hai A t er al l C r s have been cr eat ed: f hai t ot al cr eat ed = 9834, t ot al f i nal i zed = 108 bye! 因此,到程序结束的时候,并非所有收尾模块都会得到调用(注释④)。为强制进行收尾工作,可先调用 Sys t em gc( ),再调用 Sys t em r unFi nal i zat i on( )。这样可清除到目前为止没有使用的所有对象。这样做一 . . 个稍显奇怪的地方是在调用 r unFi nal i zat i on( )之前调用 gc( ) ,这看起来似乎与 Sun公司的文档说明有些抵 触,它宣称首先运行收尾模块,再释放存储空间。然而,若在这里首先调用 r unFi nal i zat i on( ) ,再调用 gc( ) ,收尾模块根本不会执行。 ④:到你读到本书时,有些 Java 虚拟机(JVM )可能已开始表现出不同的行为。 针对所有对象,Java 1. 1 有时之所以会默认为跳过收尾工作,是由于它认为这样做的开销太大。不管用哪种 方法强制进行垃圾收集,都可能注意到比没有额外收尾工作时较长的时间延迟。

4. 4 成员初始化
Java 尽自己的全力保证所有变量都能在使用前得到正确的初始化。若被定义成相对于一个方法的“局部”变 量,这一保证就通过编译期的出错提示表现出来。因此,如果使用下述代码: voi d f ( ) { i nt i ; i ++; } 就会收到一条出错提示消息,告诉你 i 可能尚未初始化。当然,编译器也可为 i 赋予一个默认值,但它看起 来更象一个程序员的失误,此时默认值反而会“帮倒忙”。若强迫程序员提供一个初始值,就往往能够帮他 /她纠出程序里的“臭虫”。 然而,若将基本类型(主类型)设为一个类的数据成员,情况就会变得稍微有些不同。由于任何方法都可以 初始化或使用那个数据,所以在正式使用数据前,若还是强迫程序员将其初始化成一个适当的值,就可能不 是一种实际的做法。然而,若为其赋予一个垃圾值,同样是非常不安全的。因此,一个类的所有基本类型数 据成员都会保证获得一个初始值。可用下面这段小程序看到这些值: //: I ni t i al Val ues . j ava // Show def aul t i ni t i al val ues s cl as s M ur em eas ent { 108

bool ean t ; char c; byt e b; s hor t s ; i nt i ; l ong l ; f l oat f ; doubl e d; voi d pr i nt ( ) { Sys t em out . pr i nt l . " D a t ype at " bool ean " char " byt e " s hor t " i nt " l ong " f l oat " doubl e } }

n( I ni t al val ue\n" + " + t + " \n" + " + c + " \n" + " + b + " \n" + " + s + " \n" + " + i + " \n" + " + l + " \n" + " + f + " \n" + " + d) ;

publ i c cl as s I ni t i al Val ues { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai M s ur em ea ent d = new M ur em ( ) ; eas ent d. pr i nt ( ) ; /* I n t hi s cas e you coul d al s o s ay: new M ur em ( ) . pr i nt ( ) ; eas ent */ } } ///: ~ 输入结果如下: D a t ype at bool ean char byt e s hor t i nt l ong f l oat doubl e I ni t al val ue f al s e 0 0 0 0 0. 0 0. 0

其中,C 值为空(N ),没有数据打印出来。 har ULL 稍后大家就会看到:在一个类的内部定义一个对象句柄时,如果不将其初始化成新对象,那个句柄就会获得 一个空值。

4 . 4 . 1 规定初始化
如果想自己为变量赋予一个初始值,又会发生什么情况呢?为达到这个目的,一个最直接的做法是在类内部 定义变量的同时也为其赋值(注意在 C ++里不能这样做,尽管 C ++的新手们总“想”这样做)。在下面, M ur em 类内部的字段定义已发生了变化,提供了初始值: eas ent 109

cl as s M ur em eas ent { bool ean b = t r ue; char c = ' x' ; byt e B = 47; s hor t s = 0xf f ; i nt i = 999; l ong l = 1; f l oat f = 3. 14f ; doubl e d = 3. 14159; //. . . 亦可用相同的方法初始化非基本(主)类型的对象。若 D h 是一个类,那么可象下面这样插入一个变量并 ept 进行初始化: cl as s M ur em eas ent { D h o = new D h( ) ; ept ept bool ean b = t r ue; // . . . 若尚未为 o 指定一个初始值,同时不顾一切地提前试用它,就会得到一条运行期错误提示,告诉你产生了名 为“违例”(Except i on)的一个错误(在第 9 章详述)。 甚至可通过调用一个方法来提供初始值: cl as s C ni t { I i nt i = f ( ) ; //. . . } 当然,这个方法亦可使用自变量,但那些自变量不可是尚未初始化的其他类成员。因此,下面这样做是合法 的: cl as s C ni t { I i nt i = f ( ) ; i nt j = g( i ) ; //. . . } 但下面这样做是非法的: cl as s C ni t { I i nt j = g( i ) ; i nt i = f ( ) ; //. . . } 这正是编译器对“向前引用”感到不适应的一个地方,因为它与初始化的顺序有关,而不是与程序的编译方 式有关。 这种初始化方法非常简单和直观。它的一个限制是类型 M ur em 的每个对象都会获得相同的初始化值。 eas ent 有时,这正是我们希望的结果,但有时却需要盼望更大的灵活性。

110

4 . 4 . 2 构建器初始化
可考虑用构建器执行初始化进程。这样便可在编程时获得更大的灵活程度,因为我们可以在运行期调用方法 和采取行动,从而“现场”决定初始化值。但要注意这样一件事情:不可妨碍自动初始化的进行,它在构建 器进入之前就会发生。因此,假如使用下述代码: cl as s C ount er { i nt i ; C ount er ( ) { i = 7; } // . . . 那么 i 首先会初始化成零,然后变成 7。对于所有基本类型以及对象句柄,这种情况都是成立的,其中包括 在定义时已进行了明确初始化的那些一些。考虑到这个原因,编译器不会试着强迫我们在构建器任何特定的 场所对元素进行初始化,或者在它们使用之前——初始化早已得到了保证(注释⑤)。 ⑤:相反,C ++有自己的“构建器初始模块列表”,能在进入构建器主体之前进行初始化,而且它对于对象来 说是强制进行的。参见《T hi nki ng i n C ++》。 1. 初始化顺序 在一个类里,初始化的顺序是由变量在类内的定义顺序决定的。即使变量定义大量遍布于方法定义的中间, 那些变量仍会在调用任何方法之前得到初始化——甚至在构建器调用之前。例如: //: O der O I ni t i al i zat i on. j ava r f // D ons t r at es i ni t i al i zat i on or der . em // W hen t he cons t r uct or i s cal l ed, t o cr eat e a // Tag obj ect , you' l l s ee a m s age: es cl as s Tag { Tag( i nt m ker ) { ar Sys t em out . pr i nt l n( " Tag( " + m ker + " ) " ) ; . ar } } cl as s C d { ar Tag t 1 = new Tag( 1) ; // Bef or e cons t r uct o r C d( ) { ar // I ndi cat e w r e i n t he cons t r uct or : e' Sys t em out . pr i nt l n( " C d( ) " ) ; . ar t 3 = new Tag( 33) ; // Re- i ni t i al i ze t 3 } Tag t 2 = new Tag( 2) ; // A t er cons t r uct or f voi d f ( ) { Sys t em out . pr i nt l n( " f ( ) " ) ; . } Tag t 3 = new Tag( 3) ; // A end t } publ i c cl as s O der O I ni t i al i zat i on { r f publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C d t = new C d( ) ; ar ar t . f ( ) ; // Show t hat cons t r uct i on i s done s } 111

} ///: ~ 在 C d 中,T ag对象的定义故意到处散布,以证明它们全都会在构建器进入或者发生其他任何事情之前得到 ar 初始化。除此之外,t 3 在构建器内部得到了重新初始化。它的输入结果如下: Tag( 1) Tag( 2) Tag( 3) C d( ) ar T ag( 33) f () 因此,t 3 句柄会被初始化两次,一次在构建器调用前,一次在调用期间(第一个对象会被丢弃,所以它后来 可被当作垃圾收掉)。从表面看,这样做似乎效率低下,但它能保证正确的初始化——若定义了一个过载的 构建器,它没有初始化 t 3;同时在 t 3 的定义里并没有规定“默认”的初始化方式,那么会产生什么后果 呢? 2. 静态数据的初始化 若数据是静态的(s t at i c),那么同样的事情就会发生;如果它属于一个基本类型(主类型),而且未对其 初始化,就会自动获得自己的标准基本类型初始值;如果它是指向一个对象的句柄,那么除非新建一个对 象,并将句柄同它连接起来,否则就会得到一个空值(N )。 ULL 如果想在定义的同时进行初始化,采取的方法与非静态值表面看起来是相同的。但由于 s t at i c 值只有一个存 储区域,所以无论创建多少个对象,都必然会遇到何时对那个存储区域进行初始化的问题。下面这个例子可 将这个问题说更清楚一些: //: St at i cI ni t i al i zat i on. j ava // Speci f yi ng i ni t i al val ues i n a // cl as s def i ni t i on. cl as s Bow { l Bow ( i nt m ker ) { l ar Sys t em out . pr i nt l n( " Bow ( " + m ker + " ) " ) ; . l ar } voi d f ( i nt m ker ) { ar Sys t em out . pr i nt l n( " f ( " + m ker + " ) " ) ; . ar } } cl as s Tabl e { s t at i c Bow b1 = new Bow ( 1) ; l l Tabl e( ) { Sys t em out . pr i nt l n( " Tabl e( ) " ) ; . b2. f ( 1) ; } voi d f 2( i nt m ker ) { ar Sys t em out . pr i nt l n( " f 2( " + m ker + " ) " ) ; . ar } s t at i c Bow b2 = new Bow ( 2) ; l l } cl as s C upboar d { Bow b3 = new Bow ( 3) ; l l 112

s t at i c Bow b4 = new Bow ( 4) ; l l C upboar d( ) { Sys t em out . pr i nt l n( " C . upboa d( ) " ) ; r b4. f ( 2) ; } voi d f 3( i nt m ker ) { ar Sys t em out . pr i nt l n( " f 3( " + m ker + " ) " ) ; . ar } s t at i c Bow b5 = new Bow ( 5) ; l l } publ i c cl as s St at i cI ni t i al i zat i on { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Sys t em out . pr i nt l n( . " C eat i ng new C r upboar d( ) i n m n" ) ; ai new C upboar d( ) ; Sys t em out . pr i nt l n( . " C eat i ng new C r upboar d( ) i n m n" ) ; ai new C upboar d( ) ; t 2. f 2( 1) ; t 3. f 3( 1) ; } s t at i c Tabl e t 2 = new Tabl e( ) ; s t at i c C upboar d t 3 = new C upboar d( ) ; } ///: ~ Bow 允许我们检查一个类的创建过程,而 Tabl e 和 C l upboar d 能创建散布于类定义中的 Bow 的 s t at i c成 l 员。注意在 s t at i c定义之前,C upboar d 先创建了一个非 s t at i c 的 Bow b3。它的输出结果如下: l Bow ( 1) l Bow ( 2) l T abl e( ) f ( 1) Bow ( 4) l Bow ( 5) l Bow ( 3) l C upboar d( ) f ( 2) C eat i ng new C r upboar d( ) i n m n ai Bow ( 3) l C upboar d( ) f ( 2) C eat i ng new C r upboar d( ) i n m n ai Bow ( 3) l C upboar d( ) f ( 2) f 2( 1) f 3( 1) s t at i c初始化只有在必要的时候才会进行。如果不创建一个 Tabl e 对象,而且永远都不引用 Tabl e. b1 或 Tabl e. b2,那么 s t at i c Bow b1 和 b2 永远都不会创建。然而,只有在创建了第一个 Tabl e 对象之后(或者 l 发生了第一次 s t at i c 访问),它们才会创建。在那以后,s t at i c 对象不会重新初始化。 113

初始化的顺序是首先 s t at i c(如果它们尚未由前一次对象创建过程初始化),接着是非 s t at i c 对象。大家 可从输出结果中找到相应的证据。 在这里有必要总结一下对象的创建过程。请考虑一个名为 D og的类: ( 1) 类型为 D og的一个对象首次创建时,或者 D og类的 s t at i c方法/s t at i c 字段首次访问时,Java 解释器 必须找到 D cl as s (在事先设好的类路径里搜索)。 og. ( 2) 找到 D cl as s 后(它会创建一个 C as s 对象,这将在后面学到),它的所有 s t at i c初始化模块都会运 og. l 行。因此,s t at i c初始化仅发生一次——在 C as s 对象首次载入的时候。 l ( 3) 创建一个 new D og() 时,D og对象的构建进程首先会在内存堆(Heap)里为一个 D og对象分配足够多的存 储空间。 ( 4) 这种存储空间会清为零,将 D og中的所有基本类型设为它们的默认值(零用于数字,以及 bool ean和 char 的等价设定)。 ( 5) 进行字段定义时发生的所有初始化都会执行。 ( 6) 执行构建器。正如第 6 章将要讲到的那样,这实际可能要求进行相当多的操作,特别是在涉及继承的时 候。 3. 明确进行的静态初始化 Java 允许我们将其他 s t at i c初始化工作划分到类内一个特殊的“s t at i c 构建从句”(有时也叫作“静态 块”)里。它看起来象下面这个样子: cl as s Spoon { s t at i c i nt i ; s t at i c { i = 47; } // . . . 尽管看起来象个方法,但它实际只是一个 s t at i c 关键字,后面跟随一个方法主体。与其他 s t at i c初始化一 样,这段代码仅执行一次——首次生成那个类的一个对象时,或者首次访问属于那个类的一个 s t at i c 成员时 (即便从未生成过那个类的对象)。例如: //: Expl i ci t St at i c. j ava // Expl i ci t s t at i c i ni t i al i z at i on // w t h t he " s t at i c" cl aus e. i cl as s C { up C i nt m ker ) { up( ar Sys t em out . pr i nt l n( " C " + m ker + " ) " ) ; . up( ar } voi d f ( i nt m ker ) { ar Sys t em out . pr i nt l n( " f ( " + m ker + " ) " ) ; . ar } } cl as s C ups { s t at i c C c1; up s t at i c C c2; up s t at i c { c1 = new C 1) ; up( c2 = new C 2) ; up( } C () { ups Sys t em out . pr i nt l n( " C ( ) " ) ; . ups 114

} } publ i c cl as s Expl i ci t St at i c { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Sys t em out . pr i nt l n( " I ns i de m n( ) " ) ; . ai C . c1. f ( 99) ; // ( 1) ups } s t at i c C ups x = new C ( ) ; // ( 2) ups s t at i c C ups y = new C ( ) ; // ( 2) ups } ///: ~ 在标记为( 1)的行内访问 s t at i c 对象 c1 的时候,或在行( 1)标记为注释,同时( 2)行不标记成注释的时候,用 于 C 的 s t at i c初始化模块就会运行。若( 1)和( 2)都被标记成注释,则用于 C 的 s t at i c 初始化进程永 ups ups 远不会发生。 4. 非静态实例的初始化 针对每个对象的非静态变量的初始化,Java 1. 1 提供了一种类似的语法格式。下面是一个例子: //: M . j ava ugs // Java 1. 1 " I ns t ance I ni t i al i zat i on" cl as s M { ug M i nt m ker ) { ug( ar Sys t em out . pr i nt l n( " M " + m ker + " ) " ) ; . ug( ar } voi d f ( i nt m ker ) { ar Sys t em out . pr i nt l n( " f ( " + m ker + " ) " ) ; . ar } } publ i c cl as s M ugs { M c1; ug M c2; ug { c1 = new M 1) ; ug( c2 = new M 2) ; ug( Sys t em out . pr i nt l n( " c1 & c2 i ni t i al i zed" ) ; . } M () { ugs Sys t em out . pr i nt l n( " M ( ) " ) ; . ugs } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Sys t em out . pr i nt l n( " I ns i de m n( ) " ) ; . ai M ugs x = new M ( ) ; ugs } } ///: ~ 大家可看到实例初始化从句: { c1 = new M 1) ; ug( 115

c2 = new M 2) ; ug( Sys t em out . pr i nt l n( " c1 & c2 i ni t i al i zed" ) ; . } 它看起来与静态初始化从句极其相似,只是 s t at i c 关键字从里面消失了。为支持对“匿名内部类”的初始化 (参见第 7 章),必须采用这一语法格式。

4. 5 数组初始化
在 C中初始化数组极易出错,而且相当麻烦。C ++通过“集合初始化”使其更安全(注释⑥)。Java 则没有 象C ++那样的“集合”概念,因为 Java 中的所有东西都是对象。但它确实有自己的数组,通过数组初始化来 提供支持。 数组代表一系列对象或者基本数据类型,所有相同的类型都封装到一起——采用一个统一的标识符名称。数 组的定义和使用是通过方括号索引运算符进行的([ ] )。为定义一个数组,只需在类型名后简单地跟随一对 空方括号即可: i nt [ ] al ; 也可以将方括号置于标识符后面,获得完全一致的结果: i nt al [ ] ; 这种格式与 C和 C ++程序员习惯的格式是一致的。然而,最“通顺”的也许还是前一种语法,因为它指出类 型是“一个 i nt 数组”。本书将沿用那种格式。 编译器不允许我们告诉它一个数组有多大。这样便使我们回到了“句柄”的问题上。此时,我们拥有的一切 就是指向数组的一个句柄,而且尚未给数组分配任何空间。为了给数组创建相应的存储空间,必须编写一个 初始化表达式。对于数组,初始化工作可在代码的任何地方出现,但也可以使用一种特殊的初始化表达式, 它必须在数组创建的地方出现。这种特殊的初始化是一系列由花括号封闭起来的值。存储空间的分配(等价 于使用 new )将由编译器在这种情况下进行。例如: i nt [ ] a1 = { 1, 2, 3, 4, 5 } ; 那么为什么还要定义一个没有数组的数组句柄呢? i nt [ ] a2; 事实上在 Java 中,可将一个数组分配给另一个,所以能使用下述语句: a2 = a1; 我们真正准备做的是复制一个句柄,就象下面演示的那样: //: A r ays . j ava r // A r ays of pr i m t i ves . r i publ i c cl as s A r ays { r publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai i nt [ ] a1 = { 1, 2, 3, 4, 5 } ; i nt [ ] a2; a2 = a1; f or ( i nt i = 0; i < a2. l engt h; i ++) a2[ i ] ++; f or ( i nt i = 0; i < a1. l engt h; i ++) pr t ( " a1[ " + i + " ] = " + a1[ i ] ) ; } s t at i c voi d pr t ( St r i ng s ) { Sys t em out . pr i nt l n( s ) ; . } } ///: ~ 大家看到 a1 获得了一个初始值,而 a2 没有;a2 将在以后赋值——这种情况下是赋给另一个数组。 这里也出现了一些新东西:所有数组都有一个本质成员(无论它们是对象数组还是基本类型数组),可对其 进行查询——但不是改变,从而获知数组内包含了多少个元素。这个成员就是 l engt h。与 C和 C ++类似,由 116

于 Java 数组从元素 0 开始计数,所以能索引的最大元素编号是“l engt h- 1”。如超出边界,C和 C ++会“默 默”地接受,并允许我们胡乱使用自己的内存,这正是许多程序错误的根源。然而,Java 可保留我们这受这 一问题的损害,方法是一旦超过边界,就生成一个运行期错误(即一个“违例”,这是第 9 章的主题)。当 然,由于需要检查每个数组的访问,所以会消耗一定的时间和多余的代码量,而且没有办法把它关闭。这意 味着数组访问可能成为程序效率低下的重要原因——如果它们在关键的场合进行。但考虑到因特网访问的安 全,以及程序员的编程效率,Java 设计人员还是应该把它看作是值得的。 程序编写期间,如果不知道在自己的数组里需要多少元素,那么又该怎么办呢?此时,只需简单地用 new在 数组里创建元素。在这里,即使准备创建的是一个基本数据类型的数组,new也能正常地工作(new不会创建 非数组的基本类型): //: A r ayN . j ava r ew // C eat i ng ar r ays w t h new r i . i m t j ava. ut i l . * ; por publ i c cl as s A r ayN { r ew s t at i c Random r and = new Random ) ; ( s t at i c i nt pRand( i nt m od) { r et ur n M h. abs ( r and. next I nt ( ) ) % m + 1; at od } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai i nt [ ] a; a = new i nt [ pRand( 20) ] ; pr t ( " l engt h of a = " + a. l engt h) ; f or ( i nt i = 0; i < a. l engt h; i ++) pr t ( " a[ " + i + " ] = " + a[ i ] ) ; } s t at i c voi d pr t ( St r i ng s ) { Sys t em out . pr i nt l n( s ) ; . } } ///: ~ 由于数组的大小是随机决定的(使用早先定义的 pRand( ) 方法),所以非常明显,数组的创建实际是在运行 期间进行的。除此以外,从这个程序的输出中,大家可看到基本数据类型的数组元素会自动初始化成“空” 值(对于数值,空值就是零;对于 char ,它是 nul l ;而对于 bool ean,它却是 f al s e )。 当然,数组可能已在相同的语句中定义和初始化了,如下所示: i nt [ ] a = new i nt [ pRand( 20) ] ; 若操作的是一个非基本类型对象的数组,那么无论如何都要使用 new 。在这里,我们会再一次遇到句柄问 题,因为我们创建的是一个句柄数组。请大家观察封装器类型 I nt eger ,它是一个类,而非基本数据类型: //: A r ayC as s O . j ava r l bj // C eat i ng an ar r ay of non- pr i m t i ve obj ect s . r i i m t j ava. ut i l . * ; por publ i c cl as s A r ayC as s O { r l bj s t at i c Random r and = new Random ) ; ( s t at i c i nt pRand( i nt m od) { r et ur n M h. abs ( r and. next I nt ( ) ) % m + 1; at od } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai I nt eger [ ] a = new I nt eger [ pRand( 20) ] ; pr t ( " l engt h of a = " + a. l engt h) ; f or ( i nt i = 0; i < a. l engt h; i ++) { 117

a[ i ] = new I nt eger ( pRand( 500) ) ; pr t ( " a[ " + i + " ] = " + a[ i ] ) ; } } s t at i c voi d pr t ( St r i ng s ) { Sys t em out . pr i nt l n( s ) ; . } } ///: ~ 在这儿,甚至在 new调用后才开始创建数组: I nt eger [ ] a = new I nt eger [ pRand( 20) ] ; 它只是一个句柄数组,而且除非通过创建一个新的 I nt eger 对象,从而初始化了对象句柄,否则初始化进程 不会结束: a[ i ] = new I nt eger ( pRand( 500) ) ; 但若忘记创建对象,就会在运行期试图读取空数组位置时获得一个“违例”错误。 下面让我们看看打印语句中 St r i ng对象的构成情况。大家可看到指向 I nt eger 对象的句柄会自动转换,从而 产生一个 St r i ng,它代表着位于对象内部的值。 亦可用花括号封闭列表来初始化对象数组。可采用两种形式,第一种是 Java 1. 0 允许的唯一形式。第二种 (等价)形式自 Java 1. 1 才开始提供支持: //: A r ayI ni t . j ava r // A r ay i ni t i al i zat i on r publ i c cl as s A r ayI ni t { r publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai I nt eger [ ] a = { new I nt eger ( 1) , new I nt eger ( 2) , new I nt eger ( 3) , }; // Java 1. 1 onl y: I nt eger [ ] b = new I nt eger [ ] { new I nt eger ( 1) , new I nt eger ( 2) , new I nt eger ( 3) , }; } } ///: ~ 这种做法大多数时候都很有用,但限制也是最大的,因为数组的大小是在编译期间决定的。初始化列表的最 后一个逗号是可选的(这一特性使长列表的维护变得更加容易)。 数组初始化的第二种形式(Java 1. 1 开始支持)提供了一种更简便的语法,可创建和调用方法,获得与 C的 “变量参数列表”(C通常把它简称为“变参表”)一致的效果。这些效果包括未知的参数(自变量)数量 以及未知的类型(如果这样选择的话)。由于所有类最终都是从通用的根类 O ect 中继承的,所以能创建一 bj 个方法,令其获取一个 O ect 数组,并象下面这样调用它: bj //: Var A gs . j ava r // Us i ng t he Java 1. 1 ar r ay s ynt ax t o cr eat e // var i abl e ar gum ent l i s t s cl as s A { i nt i ; } 118

publ i c cl as s Var A gs { r s t at i c voi d f ( O ect [ ] x) { bj f or ( i nt i = 0; i < x. l engt h; i ++) Sys t em out . pr i nt l n( x[ i ] ) ; . } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai f ( new O ect [ ] { bj new I nt eger ( 47) , new Var A gs ( ) , r new Fl oat ( 3. 14) , new D oubl e( 11. 11) } ) ; f ( new O ect [ ] { " one" , " t w , " t hr ee" } ) ; bj o" f ( new O ect [ ] { new A ) , new A ) , new A ) } ) ; bj ( ( ( } } ///: ~ 此时,我们对这些未知的对象并不能采取太多的操作,而且这个程序利用自动 St r i ng转换对每个 O ect 做 bj 一些有用的事情。在第 11 章(运行期类型标识或 RTTI ),大家还会学习如何调查这类对象的准确类型,使 自己能对它们做一些有趣的事情。

4 . 5 . 1 多维数组
在 Java 里可以方便地创建多维数组: //: M t i D m r r ay. j ava ul i A // C eat i ng m t i di m i onal ar r ays . r ul ens i m t j ava. ut i l . * ; por publ i c cl as s M t i D m r r a { ul i A y s t at i c Random r and = new Random ) ; ( s t at i c i nt pRand( i nt m od) { r et ur n M h. abs ( r and. next I nt ( ) ) % m + 1; at od } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai i nt [ ] [ ] a1 = { { 1, 2, 3, } , { 4, 5, 6, } , }; f or ( i nt i = 0; i < a1. l engt h; i ++) f or ( i nt j = 0; j < a1[ i ] . l engt h; j ++) pr t ( " a1[ " + i + " ] [ " + j + " ] = " + a1[ i ] [ j ] ) ; // 3- D ar r ay w t h f i xed l engt h: i i nt [ ] [ ] [ ] a2 = new i nt [ 2] [ 2] [ 4] ; f or ( i nt i = 0; i < a2. l engt h; i ++) f or ( i nt j = 0; j < a2[ i ] . l engt h; j ++) f or ( i nt k = 0; k < a2[ i ] [ j ] . l engt h; k++) pr t ( " a2[ " + i + " ] [ " + j + "][" + k + " ] = " + a2[ i ] [ j ] [ k] ) ; // 3- D ar r ay w t h var i ed- l engt h vect or s : i i nt [ ] [ ] [ ] a3 = new i nt [ pRand( 7) ] [ ] [ ] ; f or ( i nt i = 0; i < a3. l engt h; i ++) { 119

a3[ i ] = new i nt [ pRand( 5) ] [ ] ; f or ( i nt j = 0; j < a3[ i ] . l engt h; j ++) a3[ i ] [ j ] = new i nt [ pRand( 5) ] ; } f or ( i nt i = 0; i < a3. l engt h; i ++) f or ( i nt j = 0; j < a3[ i ] . l engt h; j ++) f or ( i nt k = 0; k < a3[ i ] [ j ] . l engt h; k++) pr t ( " a3[ " + i + " ] [ " + j + "][" + k + " ] = " + a3[ i ] [ j ] [ k] ) ; // A r ay of non- pr i m t i ve obj ect s : r i I nt eger [ ] [ ] a4 = { { new I nt eger ( 1) , new I nt eger ( 2) } , { new I nt eger ( 3) , new I nt eger ( 4) } , { new I nt eger ( 5) , new I nt eger ( 6) } , }; f or ( i nt i = 0; i < a4. l engt h; i ++) f or ( i nt j = 0; j < a4[ i ] . l engt h; j ++) pr t ( " a4[ " + i + " ] [ " + j + " ] = " + a4[ i ] [ j ] ) ; I nt eger [ ] [ ] a5; a5 = new I nt eger [ 3] [ ] ; f or ( i nt i = 0; i < a5. l engt h; i ++) { a5[ i ] = new I nt eger [ 3] ; f or ( i nt j = 0; j < a5[ i ] . l engt h; j ++) a5[ i ] [ j ] = new I nt eger ( i *j ) ; } f or ( i nt i = 0; i < a5. l engt h; i ++) f or ( i nt j = 0; j < a5[ i ] . l engt h; j ++) pr t ( " a5[ " + i + " ] [ " + j + " ] = " + a5[ i ] [ j ] ) ; } s t at i c voi d pr t ( St r i ng s ) { Sys t em out . pr i nt l n( s ) ; . } } ///: ~ 用于打印的代码里使用了 l engt h,所以它不必依赖固定的数组大小。 第一个例子展示了基本数据类型的一个多维数组。我们可用花括号定出数组内每个矢量的边界: i nt [ ] [ ] a1 = { { 1, 2, 3, } , { 4, 5, 6, } , }; 每个方括号对都将我们移至数组的下一级。 第二个例子展示了用 new分配的一个三维数组。在这里,整个数组都是立即分配的: i nt [ ] [ ] [ ] a2 = new i nt [ 2] [ 2] [ 4] ; 但第三个例子却向大家揭示出构成矩阵的每个矢量都可以有任意的长度: i nt [ ] [ ] [ ] a3 = new i nt [ pRand( 7) ] [ ] [ ] ; 120

f or ( i nt i = 0; i < a3. l engt h; i ++) { a3[ i ] = new i nt [ pRand( 5) ] [ ] ; f or ( i nt j = 0; j < a3[ i ] . l engt h; j ++) a3[ i ] [ j ] = new i nt [ pRand( 5) ] ; } 对于第一个 new创建的数组,它的第一个元素的长度是随机的,其他元素的长度则没有定义。f or 循环内的 第二个 new则会填写元素,但保持第三个索引的未定状态——直到碰到第三个 new 。 根据输出结果,大家可以看到:假若没有明确指定初始化值,数组值就会自动初始化成零。 可用类似的表式处理非基本类型对象的数组。这从第四个例子可以看出,它向我们演示了用花括号收集多个 new表达式的能力: I nt eger [ ] [ ] a4 = { { new I nt eger ( 1) , new I nt eger ( 2) } , { new I nt eger ( 3) , new I nt eger ( 4) } , { new I nt eger ( 5) , new I nt eger ( 6) } , }; 第五个例子展示了如何逐渐构建非基本类型的对象数组: I nt eger [ ] [ ] a5; a5 = new I nt eger [ 3] [ ] ; f or ( i nt i = 0; i < a5. l engt h; i ++) { a5[ i ] = new I nt eger [ 3] ; f or ( i nt j = 0; j < a5[ i ] . l engt h; j ++) a5[ i ] [ j ] = new I nt eger ( i *j ) ; } i * j 只是在 I nt eger 里置了一个有趣的值。

4. 6 总结
作为初始化的一种具体操作形式,构建器应使大家明确感受到在语言中进行初始化的重要性。与 C ++的程序 设计一样,判断一个程序效率如何,关键是看是否由于变量的初始化不正确而造成了严重的编程错误(臭 虫)。这些形式的错误很难发现,而且类似的问题也适用于不正确的清除或收尾工作。由于构建器使我们能 保证正确的初始化和清除(若没有正确的构建器调用,编译器不允许对象创建),所以能获得完全的控制权 和安全性。 在C ++中,与“构建”相反的“破坏”(D t r uct i on)工作也是相当重要的,因为用 new创建的对象必须明 es 确地清除。在 Java 中,垃圾收集器会自动为所有对象释放内存,所以 Java 中等价的清除方法并不是经常都 需要用到的。如果不需要类似于构建器的行为,Java 的垃圾收集器可以极大简化编程工作,而且在内存的管 理过程中增加更大的安全性。有些垃圾收集器甚至能清除其他资源,比如图形和文件句柄等。然而,垃圾收 集器确实也增加了运行期的开销。但这种开销到底造成了多大的影响却是很难看出的,因为到目前为止, Java 解释器的总体运行速度仍然是比较慢的。随着这一情况的改观,我们应该能判断出垃圾收集器的开销是 否使 Java 不适合做一些特定的工作(其中一个问题是垃圾收集器不可预测的性质)。 由于所有对象都肯定能获得正确的构建,所以同这儿讲述的情况相比,构建器实际做的事情还要多得多。特 别地,当我们通过“创作”或“继承”生成新类的时候,对构建的保证仍然有效,而且需要一些附加的语法 来提供对它的支持。大家将在以后的章节里详细了解创作、继承以及它们对构建器造成的影响。

4. 7 练习
( 1) 用默认构建器创建一个类(没有自变量),用它打印一条消息。创建属于这个类的一个对象。 ( 2) 在练习 1 的基础上增加一个过载的构建器,令其采用一个 St r i ng自变量,并随同自己的消息打印出来。 ( 3) 以练习 2 创建的类为基础上,创建属于它的对象句柄的一个数组,但不要实际创建对象并分配到数组 121

里。运行程序时,注意是否打印出来自构建器调用的初始化消息。 ( 4) 创建同句柄数组联系起来的对象,最终完成练习 3。 ( 5) 用自变量“bef or e”,“af t er ”和“none”运行程序,试验 G bage. j ava ar 。重复这个操作,观察是否 从输出中看出了一些固定的模式。改变代码,使 Sys t em r unFi nal i zat i on( ) 在 Sys t em gc( ) 之前调用,再观 . . 察结果。

122

第 5 章 隐藏实施过程
“进行面向对象的设计时,一项基本的考虑是:如何将发生变化的东西与保持不变的东西分隔开。” 这一点对于库来说是特别重要的。那个库的用户(客户程序员)必须能依赖自己使用的那一部分,并知道一 旦新版本的库出台,自己不需要改写代码。而与此相反,库的创建者必须能自由地进行修改与改进,同时保 证客户程序员代码不会受到那些变动的影响。 为达到这个目的,需遵守一定的约定或规则。例如,库程序员在修改库内的一个类时,必须保证不删除已有 的方法,因为那样做会造成客户程序员代码出现断点。然而,相反的情况却是令人痛苦的。对于一个数据成 员,库的创建者怎样才能知道哪些数据成员已受到客户程序员的访问呢?若方法属于某个类唯一的一部分, 而且并不一定由客户程序员直接使用,那么这种痛苦的情况同样是真实的。如果库的创建者想删除一种旧有 的实施方案,并置入新代码,此时又该怎么办呢?对那些成员进行的任何改动都可能中断客户程序员的代 码。所以库创建者处在一个尴尬的境地,似乎根本动弹不得。 为解决这个问题,Java 推出了“访问指示符”的概念,允许库创建者声明哪些东西是客户程序员可以使用 的,哪些是不可使用的。这种访问控制的级别在“最大访问”和“最小访问”的范围之间,分别包括: publ i c,“友好的”(无关键字),pr ot ect ed以及 pr i vat e。根据前一段的描述,大家或许已总结出作为一 名库设计者,应将所有东西都尽可能保持为“pr i vat e”(私有),并只展示出那些想让客户程序员使用的方 法。这种思路是完全正确的,尽管它有点儿违背那些用其他语言(特别是 C )编程的人的直觉,那些人习惯 于在没有任何限制的情况下访问所有东西。到这一章结束时,大家应该可以深刻体会到 Java 访问控制的价 值。 然而,组件库以及控制谁能访问那个库的组件的概念现在仍不是完整的。仍存在这样一个问题:如何将组件 绑定到单独一个统一的库单元里。这是通过 Java 的 package(打包)关键字来实现的,而且访问指示符要受 到类在相同的包还是在不同的包里的影响。所以在本章的开头,大家首先要学习库组件如何置入包里。这样 才能理解访问指示符的完整含义。

5. 1 包:库单元
我们用 i m t 关键字导入一个完整的库时,就会获得“包”(Package)。例如: por i m t j ava. ut i l . * ; por 它的作用是导入完整的实用工具(Ut i l i t y)库,该库属于标准 Java 开发工具包的一部分。由于 Vect or 位于 j ava. ut i l 里,所以现在要么指定完整名称“j ava. ut i l . Vect or ”(可省略 i m t 语句),要么简单地指定 por 一个“Vect or ”(因为 i m t 是默认的)。 por 若想导入单独一个类,可在 i m t 语句里指定那个类的名字: por i m t j ava. ut i l . Vect or ; por 现在,我们可以自由地使用 Vect or 。然而,j ava. ut i l 中的其他任何类仍是不可使用的。 之所以要进行这样的导入,是为了提供一种特殊的机制,以便管理“命名空间”(N e Space)。我们所有 am 类成员的名字相互间都会隔离起来。位于类 A内的一个方法 f ( )不会与位于类 B 内的、拥有相同“签名” (自变量列表)的 f ( ) 发生冲突。但类名会不会冲突呢?假设创建一个 s t ack 类,将它安装到已有一个 s t ack 类(由其他人编写)的机器上,这时会出现什么情况呢?对于因特网中的 Java 应用,这种情况会在用户毫不 知晓的时候发生,因为类会在运行一个 Java 程序的时候自动下载。 正是由于存在名字潜在的冲突,所以特别有必要对 Java 中的命名空间进行完整的控制,而且需要创建一个完 全独一无二的名字,无论因特网存在什么样的限制。 迄今为止,本书的大多数例子都仅存在于单个文件中,而且设计成局部(本地)使用,没有同包名发生冲突 (在这种情况下,类名置于“默认包”内)。这是一种有效的做法,而且考虑到问题的简化,本书剩下的部 分也将尽可能地采用它。然而,若计划创建一个“对因特网友好”或者说“适合在因特网使用”的程序,必 须考虑如何防止类名的重复。 为 Java 创建一个源码文件的时候,它通常叫作一个“编辑单元”(有时也叫作“翻译单元”)。每个编译单 元都必须有一个以. j ava结尾的名字。而且在编译单元的内部,可以有一个公共(publ i c)类,它必须拥有 与文件相同的名字(包括大小写形式,但排除. j ava文件扩展名)。如果不这样做,编译器就会报告出错。 每个编译单元内都只能有一个 publ i c 类(同样地,否则编译器会报告出错)。那个编译单元剩下的类(如果 有的话)可在那个包外面的世界面前隐藏起来,因为它们并非“公共”的(非 publ i c),而且它们由用于主 123

publ i c类的“支撑”类组成。 编译一个. j ava文件时,我们会获得一个名字完全相同的输出文件;但对于. j ava文件中的每个类,它们都有 一个. cl as s 扩展名。因此,我们最终从少量的. j ava文件里有可能获得数量众多的. cl as s 文件。如以前用一 种汇编语言写过程序,那么可能已习惯编译器先分割出一种过渡形式(通常是一个. obj 文件),再用一个链 接器将其与其他东西封装到一起(生成一个可执行文件),或者与一个库封装到一起(生成一个库)。但那 并不是 Java 的工作方式。一个有效的程序就是一系列. cl as s 文件,它们可以封装和压缩到一个 JA R文件里 (使用 Java 1. 1 提供的 j ar 工具)。Java 解释器负责对这些文件的寻找、装载和解释(注释①)。 ①:Java 并没有强制一定要使用解释器。一些固有代码的 Java 编译器可生成单独的可执行文件。 “库”也由一系列类文件构成。每个文件都有一个 publ i c类(并没强迫使用一个 publ i c 类,但这种情况最 很典型的),所以每个文件都有一个组件。如果想将所有这些组件(它们在各自独立的. j ava和. cl as s 文件 里)都归纳到一起,那么 package 关键字就可以发挥作用)。 若在一个文件的开头使用下述代码: package m ypackage; 那么 package语句必须作为文件的第一个非注释语句出现。该语句的作用是指出这个编译单元属于名为 m ypackage 的一个库的一部分。或者换句话说,它表明这个编译单元内的 publ i c类名位于 m ypackage这个名 字的下面。如果其他人想使用这个名字,要么指出完整的名字,要么与 m ypackage 联合使用 i m t 关键字 por (使用前面给出的选项)。注意根据 Java 包(封装)的约定,名字内的所有字母都应小写,甚至那些中间单 词亦要如此。 例如,假定文件名是 M l as s . j ava yC 。它意味着在那个文件有一个、而且只能有一个 publ i c类。而且那个类 的名字必须是 M l as s (包括大小写形式): yC package m ypackage; publ i c cl as s M l as s { yC // . . . 现在,如果有人想使用 M l as s,或者想使用 m yC ypackage内的其他任何 publ i c 类,他们必须用 i m t 关键 por 字激活 m ypackage 内的名字,使它们能够使用。另一个办法则是指定完整的名称: m ypackage. M l as s m = new m yC ypackage. M l as s ( ) ; yC i m t 关键字则可将其变得简洁得多: por im t m por ypackage. * ; // . . . M l as s m = new M l as s ( ); yC yC 作为一名库设计者,一定要记住 package和 i m t 关键字允许我们做的事情就是分割单个全局命名空间,保 por 证我们不会遇到名字的冲突——无论有多少人使用因特网,也无论多少人用 Java 编写自己的类。

5 . 1 . 1 创建独一无二的包名
大家或许已注意到这样一个事实:由于一个包永远不会真的“封装”到单独一个文件里面,它可由多 个. cl as s 文件构成,所以局面可能稍微有些混乱。为避免这个问题,最合理的一种做法就是将某个特定包使 用的所有. cl as s 文件都置入单个目录里。也就是说,我们要利用操作系统的分级文件结构避免出现混乱局 面。这正是 Java 所采取的方法。 它同时也解决了另两个问题:创建独一无二的包名以及找出那些可能深藏于目录结构某处的类。正如我们在 第 2 章讲述的那样,为达到这个目的,需要将. cl as s 文件的位置路径编码到 package的名字里。但根据约 定,编译器强迫 package名的第一部分是类创建者的因特网域名。由于因特网域名肯定是独一无二的(由 I nt er N C保证——注释②,它控制着域名的分配),所以假如按这一约定行事,package 的名称就肯定不会 I 重复,所以永远不会遇到名称冲突的问题。换句话说,除非将自己的域名转让给其他人,而且对方也按照相 同的路径名编写 Java 代码,否则名字的冲突是永远不会出现的。当然,如果你没有自己的域名,那么必须创 124

造一个非常生僻的包名(例如自己的英文姓名),以便尽最大可能创建一个独一无二的包名。如决定发行自 己的 Java 代码,那么强烈推荐去申请自己的域名,它所需的费用是非常低廉的。 ②:f t p: //f t p. i nt er ni c. net 这个技巧的另一部分是将 package 名解析成自己机器上的一个目录。这样一来,Java 程序运行并需要装 载. cl as s 文件的时候(这是动态进行的,在程序需要创建属于那个类的一个对象,或者首次访问那个类的一 个 s t at i c 成员时),它就可以找到. cl as s 文件驻留的那个目录。 Java 解释器的工作程序如下:首先,它找到环境变量 C SSPA LA TH(将 Java 或者具有 Java 解释能力的工具— —如浏览器——安装到机器中时,通过操作系统进行设定)。C SSPA LA TH包含了一个或多个目录,它们作为 一种特殊的“根”使用,从这里展开对. cl as s 文件的搜索。从那个根开始,解释器会寻找包名,并将每个点 号(句点)替换成一个斜杠,从而生成从 C SSPA LA TH根开始的一个路径名(所以 package f oo. bar . baz 会变 成 f oo\bar \baz 或者 f oo/bar /baz;具体是正斜杠还是反斜杠由操作系统决定)。随后将它们连接到一起, 成为 C SSPA LA TH内的各个条目(入口)。以后搜索. cl as s 文件时,就可从这些地方开始查找与准备创建的类 名对应的名字。此外,它也会搜索一些标准目录——这些目录与 Java 解释器驻留的地方有关。 为进一步理解这个问题,下面以我自己的域名为例,它是 br uceeckel . com 。将其反转过来后, com br uceeckel 就为我的类创建了独一无二的全局名称(com . ,edu,or g,net 等扩展名以前在 Java 包中都 是大写的,但自 Java 1. 2 以来,这种情况已发生了变化。现在整个包名都是小写的)。由于决定创建一个名 为 ut i l 的库,我可以进一步地分割它,所以最后得到的包名如下: package com br uceeckel . ut i l ; . 现在,可将这个包名作为下述两个文件的“命名空间”使用: //: Vect or . j ava // C eat i ng a package r package com br uceeckel . ut i l ; . publ i c cl as s Vect or { publ i c Vect or ( ) { Sys t em out . pr i nt l n( . " com br uceeckel . ut i l . Vect or " ) ; . } } ///: ~ 创建自己的包时,要求 package语句必须是文件中的第一个“非注释”代码。第二个文件表面看起来是类似 的: //: Li s t . j ava // C eat i ng a package r package com br uceeckel . ut i l ; . publ i c cl as s Li s t { publ i c Li s t ( ) { Sys t em out . pr i nt l n( . " com br uceeckel . ut i l . Li s t " ) ; . } } ///: ~ 这两个文件都置于我自己系统的一个子目录中: C \D C : O \JavaT\com uceeckel \ut i l \br 若通过它往回走,就会发现包名 com br uceeckel . ut i l ,但路径的第一部分又是什么呢?这是由 C SSPA . LA TH 环境变量决定的。在我的机器上,它是: C SSPA LA TH=. ; D \JA A\LI B; C \D C : V : O \JavaT 125

可以看出,C SSPA LA TH里能包含大量备用的搜索路径。然而,使用 JA R文件时要注意一个问题:必须将 JA R 文件的名字置于类路径里,而不仅仅是它所在的路径。所以对一个名为 gr ape. j ar 的 JA R文件来说,我们的 类路径需要包括: C SSPA LA TH=. ; D \JA A\LI B; C \f l avor s\gr ape. j ar : V : 正确设置好类路径后,可将下面这个文件置于任何目录里(若在执行该程序时遇到麻烦,请参见第 3 章的 3. 1. 2 小节“赋值”): //: Li bTes t . j ava // Us es t he l i br ar y package c05; i m t com br uceeckel . ut i l . * ; por . publ i c cl as s Li bTes t { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Vect or v = new Vect or ( ) ; Li s t l = new Li s t ( ) ; } } ///: ~ 编译器遇到 i m t 语句后,它会搜索由 C SSPA por LA TH指定的目录,查找子目录 com uceeckel \ut i l ,然后查 \br 找名称适当的已编译文件(对于 Vect or 是 Vect or . cl as s,对于 Li s t 则是 Li s t . cl as s )。注意 Vect or 和 Li s t 内无论类还是需要的方法都必须设为 publ i c。 1. 自动编译 为导入的类首次创建一个对象时(或者访问一个类的 s t at i c 成员时),编译器会在适当的目录里寻找同名 的. cl as s 文件(所以如果创建类 X的一个对象,就应该是 X. cl as s )。若只发现 X. cl as s ,它就是必须使用 的那一个类。然而,如果它在相同的目录中还发现了一个 X. j ava ,编译器就会比较两个文件的日期标记。如 果 X. j ava比 X. cl as s 新,就会自动编译 X. j ava,生成一个最新的 X. cl as s。 对于一个特定的类,或在与它同名的. j ava文件中没有找到它,就会对那个类采取上述的处理。 2. 冲突 若通过*导入了两个库,而且它们包括相同的名字,这时会出现什么情况呢?例如,假定一个程序使用了下述 导入语句: i m t com br uceeckel . ut i l . *; por . i m t j ava. ut i l . * ; por 由于 j ava. ut i l . *也包含了一个 Vect or 类,所以这会造成潜在的冲突。然而,只要冲突并不真的发生,那么 就不会产生任何问题——这当然是最理想的情况,因为否则的话,就需要进行大量编程工作,防范那些可能 可能永远也不会发生的冲突。 如现在试着生成一个 Vect or ,就肯定会发生冲突。如下所示: Vect or v = new Vect or ( ) ; 它引用的到底是哪个 Vect or 类呢?编译器对这个问题没有答案,读者也不可能知道。所以编译器会报告一个 错误,强迫我们进行明确的说明。例如,假设我想使用标准的 Java Vect or ,那么必须象下面这样编程: j ava. ut i l . Vect or v = new j ava. ut i l . Vect or ( ) ; 由于它(与 C SSPA LA TH一起)完整指定了那个 Vect or 的位置,所以不再需要 i m t j ava. ut i l . * 语句,除 por 非还想使用来自 j ava. ut i l 的其他东西。

5 . 1 . 2 自定义工具库
掌握前述的知识后,接下来就可以开始创建自己的工具库,以便减少或者完全消除重复的代码。例如,可为 Sys t em out . pr i nt l n( )创建一个别名,减少重复键入的代码量。它可以是名为 t ool s 的一个包(p cka e)的 . a g 一部分: //: P. j ava 126

// The P. r i nt & P. r i nt l n s hor t hand package com br uceecke . t ool s ; . l publ i c cl as s P { publ i c s t at i c voi d r i nt ( O ect obj ) { bj Sys t em out . pr i nt ( obj ) ; . } publ i c s t at i c voi d r i nt ( St r i ng s ) { Sys t em out . pr i nt ( s ) ; . } publ i c s t at i c voi d r i nt ( char [ ] s ) { Sys t em out . pr i nt ( s ) ; . } publ i c s t at i c voi d r i nt ( char c) { Sys t em out . pr i nt ( c) ; . } publ i c s t at i c voi d r i nt ( i nt i ) { Sys t em out . pr i nt ( i ) ; . } publ i c s t at i c voi d r i nt ( l ong l ) { Sys t em out . pr i nt ( l ) ; . } publ i c s t at i c voi d r i nt ( f l oat f ) { Sys t em out . pr i nt ( f ) ; . } publ i c s t at i c voi d r i nt ( doubl e d) { Sys t em out . pr i nt ( d) ; . } publ i c s t at i c voi d r i nt ( bool ean b) { Sys t em out . pr i nt ( b) ; . } publ i c s t at i c voi d r i nt l n( ) { Sys t em out . pr i nt l n( ) ; . } publ i c s t at i c voi d r i nt l n( O ect obj ) { bj Sys t em out . pr i nt l n( obj ) ; . } p i c s t at i c voi d r i nt l n( St r i ng s ) { ubl Sys t em out . pr i nt l n( s ) ; . } publ i c s t at i c voi d r i nt l n( char [ ] s ) { Sys t em out . pr i nt l n( s ) ; . } publ i c s t at i c voi d r i nt l n( char c) { Sys t em out . pr i nt l n( c) ; . } publ i c s t at i c voi d r i nt l n( i nt i ) { Sys t em o . pr i nt l n( i ) ; . ut } publ i c s t at i c voi d r i nt l n( l ong l ) { Sys t em out . pr i nt l n( l ) ; . } 127

publ i c s t at i c voi d r i nt l n( f l oat f ) { Sys t em out . pr i nt l n( f ) ; . } publ i c s t at i c voi d r i nt l n( doubl e d) { Sys t em out . pr i nt l n( d) ; . } publ i c s t at i c voi d r i nt l n( bool ean b) { Sys t em out . pr i nt l n( b) ; . } } ///: ~ 所有不同的数据类型现在都可以在一个新行输出(P. r i nt l n( ) ),或者不在一个新行输出(P. r i nt ( ) )。 大家可能会猜想这个文件所在的目录必须从某个 C SSPA LA TH位置开始,然后继续 com uceeckel /t ool s 。编 /br 译完毕后,利用一个 i m t 语句,即可在自己系统的任何地方使用 P. cl as s 文件。如下所示: por //: Tool Tes t . j ava // Us es t he t ool s l i br ar y i m t com br uceeckel . t ool s . * ; por . publ i c cl as s Tool Tes t { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai P. r i nt l n( " A l abl e f r om now on! " ) ; vai } } ///: ~ 所以从现在开始,无论什么时候只要做出了一个有用的新工具,就可将其加入 t ool s 目录(或者自己的个人 ut i l 或 t ool s 目录)。 1. C SSPA H的陷阱 LA T P. j ava文件存在一个非常有趣的陷阱。特别是对于早期的 Java 实现方案来说,类路径的正确设定通常都是 很困难的一项工作。编写这本书的时候,我引入了 P. j ava文件,它最初看起来似乎工作很正常。但在某些情 况下,却开始出现中断。在很长的时间里,我都确信这是 Java 或其他什么在实现时一个错误。但最后,我终 于发现在一个地方引入了一个程序(即第 17 章要说明的 C odePackager . j ava ),它使用了一个不同的类 P。 由于它作为一个工具使用,所以有时候会进入类路径里;另一些时候则不会这样。但只要它进入类路径,那 么假若执行的程序需要寻找 com br uceeckel . t ool s 中的类,Java 首先发现的就是 C . odePackager . j ava中的 P。此时,编译器会报告一个特定的方法没有找到。这当然是非常令人头疼的,因为我们在前面的类 P 里明明 看到了这个方法,而且根本没有更多的诊断报告可为我们提供一条线索,让我们知道找到的是一个完全不同 的类(那甚至不是 publ i c 的)。 乍一看来,这似乎是编译器的一个错误,但假若考察 i m t 语句,就会发现它只是说:“在这里可能发现了 por P”。然而,我们假定的是编译器搜索自己类路径的任何地方,所以一旦它发现一个 P,就会使用它;若在搜 索过程中发现了“错误的”一个,它就会停止搜索。这与我们在前面表述的稍微有些区别,因为存在一些讨 厌的类,它们都位于包内。而这里有一个不在包内的 P,但仍可在常规的类路径搜索过程中找到。 如果您遇到象这样的情况,请务必保证对于类路径的每个地方,每个名字都仅存在一个类。

5 . 1 . 3 利用导入改变行为
Java 已取消的一种特性是 C的“条件编译”,它允许我们改变参数,获得不同的行为,同时不改变其他任何 代码。Java 之所以抛弃了这一特性,可能是由于该特性经常在 C里用于解决跨平台问题:代码的不同部分根 据具体的平台进行编译,否则不能在特定的平台上运行。由于 Java 的设计思想是成为一种自动跨平台的语 言,所以这种特性是没有必要的。 然而,条件编译还有另一些非常有价值的用途。一种很常见的用途就是调试代码。调试特性可在开发过程中 使用,但在发行的产品中却无此功能。A en Hol ub(w w hol ub. com l w. )提出了利用包(package)来模仿条件 编译的概念。根据这一概念,它创建了 C “断定机制”一个非常有用的 Java 版本。之所以叫作“断定机 128

制”,是由于我们可以说“它应该为真”或者“它应该为假”。如果语句不同意你的断定,就可以发现相关 的情况。这种工具在调试过程中是特别有用的。 可用下面这个类进行程序调试: //: A s er t . j ava s // A s er t i on t ool f or debuggi ng s package com br uceeckel . t ool s . debug; . publ i c cl as s A s er t { s pr i vat e s t at i c voi d per r ( St r i ng m g) { s Sys t em er r . pr i nt l n( m g) ; . s } publ i c f i nal s t at i c voi d i s _t r ue( bool ean exp) { i f ( ! exp) per r ( " A s er t i on f ai l ed" ) ; s } publ i c f i nal s t at i c voi d i s _f al s e( bool ean exp) { i f ( exp) per r ( " A s er t i on f ai l ed" ) ; s } publ i c f i nal s t at i c voi d i s _t r ue( bool ean exp, St r i ng m g) { s i f ( ! exp) per r ( " A s er t i on f ai l ed: " + m g) ; s s } publ i c f i nal s t at i c voi d i s _f al s e( bool ean exp, St r i ng m g) { s i f ( exp) per r ( " A s er t i on f ai l ed: " + m g) ; s s } } ///: ~ 这个类只是简单地封装了布尔测试。如果失败,就显示出出错消息。在第 9 章,大家还会学习一个更高级的 错误控制工具,名为“违例控制”。但在目前这种情况下,per r ( ) 方法已经可以很好地工作。 如果想使用这个类,可在自己的程序中加入下面这一行: i m t com br uceeckel . t ool s . debug. *; por . 如欲清除断定机制,以便自己能发行最终的代码,我们创建了第二个 A s er t 类,但却是在一个不同的包里: s //: A s er t . j ava s // Tur ni ng of f t he as s er t i on out put // s o you can s hi p t he pr ogr am . package com br uceeckel . t ool s ; . publ i c cl as s A s er t { s publ i c f i nal s t at i c voi d i s _t r ue( bool ean exp) { } publ i c f i nal s t at i c voi d i s _f al s e( bool ean exp) { } publ i c f i nal s t at i c voi d i s _t r ue( bool ean exp, St r i ng m g) { } s publ i c f i nal s t at i c voi d i s _f al s e( bool ean exp, St r i ng m g) { } s } ///: ~ 现在,假如将前一个 i m t 语句变成下面这个样子: por i m t com br uceeckel . t ool s . * ; por . 程序便不再显示出断言。下面是个例子:

129

//: Tes t A s er t . j ava s // D ons t r at i ng t he as s er t i on t ool em package c05; // C m om ent t he f ol l ow ng, and uncom ent t he i m // s ubs equent l i ne t o change as s er t i on behavi or : i m t com br uceeckel . t ool s . debug. *; por . // i m t com br uceeckel . t ool s . *; por . publ i c cl as s Tes t A s er t { s publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai A s er t . i s _t r ue( ( 2 + 2) == 5) ; s A s er t . i s _f al s e( ( 1 + 1) == 2) ; s A s er t . i s _t r ue( ( 2 + 2) == 5, " 2 + 2 == 5" ) ; s A s er t . i s _f al s e( ( 1 + 1) == 2, " 1 +1 ! = 2" ) ; s } } ///: ~ 通过改变导入的 package,我们可将自己的代码从调试版本变成最终的发行版本。这种技术可应用于任何种 类的条件代码。

5 . 1 . 4 包的停用
大家应注意这样一个问题:每次创建一个包后,都在为包取名时间接地指定了一个目录结构。这个包必须存 在(驻留)于由它的名字规定的目录内。而且这个目录必须能从 C SSPA LA TH开始搜索并发现。最开始的时 候,package关键字的运用可能会令人迷惑,因为除非坚持遵守根据目录路径指定包名的规则,否则就会在 运行期获得大量莫名其妙的消息,指出找不到一个特定的类——即使那个类明明就在相同的目录中。若得到 象这样的一条消息,请试着将 package 语句作为注释标记出去。如果这样做行得通,就可知道问题到底出在 哪儿。

5. 2 J ava 访问指示符
针对类内每个成员的每个定义,Java 访问指示符 poubl i c,pr ot ect ed以及 pr i vat e都置于它们的最前面— —无论它们是一个数据成员,还是一个方法。每个访问指示符都只控制着对那个特定定义的访问。这与 C ++ 存在着显著不同。在 C ++中,访问指示符控制着它后面的所有定义,直到又一个访问指示符加入为止。 通过千丝万缕的联系,程序为所有东西都指定了某种形式的访问。在后面的小节里,大家要学习与各类访问 有关的所有知识。首次从默认访问开始。

5 . 2 . 1 “友好的”
如果根本不指定访问指示符,就象本章之前的所有例子那样,这时会出现什么情况呢?默认的访问没有关键 字,但它通常称为“友好”(Fr i endl y )访问。这意味着当前包内的其他所有类都能访问“友好的”成员, 但对包外的所有类来说,这些成员却是“私有”(Pr i vat e)的,外界不得访问。由于一个编译单元(一个文 件)只能从属于单个包,所以单个编译单元内的所有类相互间都是自动“友好”的。因此,我们也说友好元 素拥有“包访问”权限。 友好访问允许我们将相关的类都组合到一个包里,使它们相互间方便地进行沟通。将类组合到一个包内以后 (这样便允许友好成员的相互访问,亦即让它们“交朋友”),我们便“拥有”了那个包内的代码。只有我 们已经拥有的代码才能友好地访问自己拥有的其他代码。我们可认为友好访问使类在一个包内的组合显得有 意义,或者说前者是后者的原因。在许多语言中,我们在文件内组织定义的方式往往显得有些牵强。但在 Java 中,却强制用一种颇有意义的形式进行组织。除此以外,我们有时可能想排除一些类,不想让它们访问 当前包内定义的类。 对于任何关系,一个非常重要的问题是“谁能访问我们的‘私有’或 pr i vat e 代码”。类控制着哪些代码能 够访问自己的成员。没有任何秘诀可以“闯入”。另一个包内推荐可以声明一个新类,然后说:“嗨,我是 Bob的朋友!”,并指望看到 Bob的“pr ot ect ed”(受到保护的)、友好的以及“pr i vat e”(私有)的成 130

员。为获得对一个访问权限,唯一的方法就是: ( 1) 使成员成为“publ i c”(公共的)。这样所有人从任何地方都可以访问它。 ( 2) 变成一个“友好”成员,方法是舍弃所有访问指示符,并将其类置于相同的包内。这样一来,其他类就 可以访问成员。 ( 3) 正如以后引入“继承”概念后大家会知道的那样,一个继承的类既可以访问一个 pr ot ect ed成员,也可 以访问一个 publ i c成员(但不可访问 pr i vat e成员)。只有在两个类位于相同的包内时,它才可以访问友好 成员。但现在不必关心这方面的问题。 ( 4) 提供“访问器/变化器”方法(亦称为“获取/设置”方法),以便读取和修改值。这是 O P 环境中最 O 正规的一种方法,也是 Java Beans 的基础——具体情况会在第 13 章介绍。

5 . 2 . 2 publ i c:接口访问
使用 publ i c关键字时,它意味着紧随在 publ i c 后面的成员声明适用于所有人,特别是适用于使用库的客户 程序员。假定我们定义了一个名为 des s er t 的包,其中包含下述单元(若执行该程序时遇到困难,请参考第 3 章 3. 1. 2 小节“赋值”): //: C ooki e. j ava // C eat es a l i br ar y r package c05. des s er t ; publ i c cl as s C ooki e { publ i c C ooki e( ) { Sys t em out . pr i nt l n( " C . ooki e cons t r uct or " ) ; } voi d f oo( ) { Sys t em out . pr i nt l n( " f oo" ) ; } . } ///: ~ 请记住,C ooki e. j ava必须驻留在名为 des s er t 的一个子目录内,而这个子目录又必须位于由 C SSPA LA TH指 定的 C 目录下面(C 代表本书的第 5 章)。不要错误地以为 Java 无论如何都会将当前目录作为搜索的起 05 05 点看待。如果不将一个“. ”作为 C SSPA LA TH的一部分使用,Java 就不会考虑当前目录。 现在,假若创建使用了 C ooki e的一个程序,如下所示: //: D nner . j ava i // Us es t he l i br ar y i m t c05. des s er t . * ; por publ i c cl as s D nner { i publ i c D nner ( ) { i Sys t em out . pr i nt l n( " D nner cons t r uct or " ) ; . i } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C ooki e x = new C ooki e( ) ; //! x. f oo( ) ; // C t acces s an' } } ///: ~ 就可以创建一个 C ooki e对象,因为它的构建器是 publ i c的,而且类也是 publ i c的(公共类的概念稍后还会 进行更详细的讲述)。然而,f oo( ) 成员不可在 D nner . j ava内访问,因为 f oo( ) 只有在 des s er t 包内才是 i “友好”的。 1. 默认包 大家可能会惊讶地发现下面这些代码得以顺利编译——尽管它看起来似乎已违背了规则:

131

//: C ake. j ava // A cces s es a cl as s i n a s epar at e // com l at i on uni t . pi cl as s C ake { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Pi e x = new Pi e( ) ; x. f ( ) ; } } ///: ~ 在位于相同目录的第二个文件里: //: Pi e. j ava // The ot her cl as s cl as s Pi e { voi d f ( ) { Sys t em out . pr i nt l n( " Pi e. f ( ) " ) ; } . } ///: ~ 最初可能会把它们看作完全不相干的文件,然而 C 能创建一个 Pi e对象,并能调用它的 f ( )方法!通常的 ake 想法会认为 Pi e和 f ( ) 是“友好的”,所以不适用于 C ake。它们确实是友好的——这部分结论非常正确。但 它们之所以仍能在 C ake. j ava中使用,是由于它们位于相同的目录中,而且没有明确的包名。Java 把象这样 的文件看作那个目录“默认包”的一部分,所以它们对于目录内的其他文件来说是“友好”的。

5 . 2 . 3 pr i v at e :不能接触! pr i vat e关键字意味着除非那个特定的类,而且从那个类的方法里,否则没有人能访问那个成员。同一个包 内的其他成员不能访问 pr i vat e成员,这使其显得似乎将类与我们自己都隔离起来。另一方面,也不能由几 个合作的人创建一个包。所以 pr i vat e 允许我们自由地改变那个成员,同时毋需关心它是否会影响同一个包 内的另一个类。默认的“友好”包访问通常已经是一种适当的隐藏方法;请记住,对于包的用户来说,是不 能访问一个“友好”成员的。这种效果往往能令人满意,因为默认访问是我们通常采用的方法。对于希望变 成 publ i c(公共)的成员,我们通常明确地指出,令其可由客户程序员自由调用。而且作为一个结果,最开 始的时候通常会认为自己不必频繁使用 pr i vat e关键字,因为完全可以在不用它的前提下发布自己的代码 (这与 C ++是个鲜明的对比)。然而,随着学习的深入,大家就会发现 pr i vat e 仍然有非常重要的用途,特 别是在涉及多线程处理的时候(详情见第 14 章)。 下面是应用了 pr i vat e 的一个例子: //: I ceC eam j ava r . // D ons t r at es " pr i vat e" keyw d em or cl as s Sundae { pr i vat e Sundae( ) { } s t at i c Sundae m akeA Sundae( ) { r et ur n new Sundae( ) ; } } publ i c cl as s I ceC eam { r publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai //! Sundae x = new Sundae( ) ; Sundae x = Sundae. m akeA Sundae( ) ; 132

} } ///: ~ 这个例子向我们证明了使用 pr i vat e的方便:有时可能想控制对象的创建方式,并防止有人直接访问一个特 定的构建器(或者所有构建器)。在上面的例子中,我们不可通过它的构建器创建一个 Sundae 对象;相反, 必须调用 m akeA Sundae( )方法来实现(注释③)。 ③:此时还会产生另一个影响:由于默认构建器是唯一获得定义的,而且它的属性是 pr i vat e,所以可防止 对这个类的继承(这是第 6 章要重点讲述的主题)。 若确定一个类只有一个“助手”方法,那么对于任何方法来说,都可以把它们设为 pr i vat e,从而保证自己 不会误在包内其他地方使用它,防止自己更改或删除方法。将一个方法的属性设为 pr i vat e后,可保证自己 一直保持这一选项(然而,若一个句柄被设为 pr i vat e,并不表明其他对象不能拥有指向同一个对象的 publ i c句柄。有关“别名”的问题将在第 12 章详述)。

5 . 2 . 4 pr ot ect ed:“友好的一种” pr ot ect ed(受到保护的)访问指示符要求大家提前有所认识。首先应注意这样一个事实:为继续学习本书一 直到继承那一章之前的内容,并不一定需要先理解本小节的内容。但为了保持内容的完整,这儿仍然要对此 进行简要说明,并提供相关的例子。 pr ot ect ed关键字为我们引入了一种名为“继承”的概念,它以现有的类为基础,并在其中加入新的成员, 同时不会对现有的类产生影响——我们将这种现有的类称为“基础类”或者“基本类”(Bas e C as s )。亦 l 可改变那个类现有成员的行为。对于从一个现有类的继承,我们说自己的新类“扩展”(ext ends)了那个现 有的类。如下所示: cl as s Foo ext ends Bar { 类定义剩余的部分看起来是完全相同的。 若新建一个包,并从另一个包内的某个类里继承,则唯一能够访问的成员就是原来那个包的 publ i c 成员。当 然,如果在相同的包里进行继承,那么继承获得的包能够访问所有“友好”的成员。有些时候,基础类的创 建者喜欢提供一个特殊的成员,并允许访问衍生类。这正是 pr ot ect ed的工作。若往回引用 5. 2. 2 小节 “publ i c:接口访问”的那个 C ooki e. j ava文件,则下面这个类就不能访问“友好”的成员: //: C hocol at eC p. j ava hi // C t acces s f r i endl y m ber an' em // i n anot her cl as s i m t c05. des s er t . * ; por publ i c cl as s C hocol at eC p ext ends C hi ooki e { publ i c C hocol at eC p( ) { hi Sys t em out . pr i nt l n( . "C hocol at eC p cons t r uct or " ) ; hi } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C hocol at eC p x = new C hi hocol at eC p( ) ; hi //! x. f oo( ) ; // C t acces s f oo an' } } ///: ~ 对于继承,值得注意的一件有趣的事情是倘若方法 f oo( ) 存在于类 C ooki e 中,那么它也会存在于从 C ooki e 继承的所有类中。但由于 f oo( ) 在外部的包里是“友好”的,所以我们不能使用它。当然,亦可将其变成 publ i c。但这样一来,由于所有人都能自由访问它,所以可能并非我们所希望的局面。若象下面这样修改类 C ooki e : publ i c cl as s C ooki e { 133

publ i c C ooki e( ) { Sys t em out . pr i nt l n( " C . ooki e cons t r uct or " ) ; } pr ot ect ed voi d f oo( ) { Sys t em out . pr i nt l n( " f oo" ) ; . } } 那么仍然能在包 des s er t 里“友好”地访问 f oo( ) ,但从 C ooki e 继承的其他东西亦可自由地访问它。然而, 它并非公共的(publ i c)。

5. 3 接口与实现
我们通常认为访问控制是“隐藏实施细节”的一种方式。将数据和方法封装到类内后,可生成一种数据类 型,它具有自己的特征与行为。但由于两方面重要的原因,访问为那个数据类型加上了自己的边界。第一个 原因是规定客户程序员哪些能够使用,哪些不能。我们可在结构里构建自己的内部机制,不用担心客户程序 员将其当作接口的一部分,从而自由地使用或者“滥用”。 这个原因直接导致了第二个原因:我们需要将接口同实施细节分离开。若结构在一系列程序中使用,但用户 除了将消息发给 publ i c接口之外,不能做其他任何事情,我们就可以改变不属于 publ i c 的所有东西(如 “友好的”、pr ot ect ed以及 pr i vat e),同时不要求用户对他们的代码作任何修改。 我们现在是在一个面向对象的编程环境中,其中的一个类(cl as s )实际是指“一类对象”,就象我们说“鱼 类”或“鸟类”那样。从属于这个类的所有对象都共享这些特征与行为。“类”是对属于这一类的所有对象 的外观及行为进行的一种描述。 在一些早期 O P 语言中,如 Si m a 67,关键字 cl as s 的作用是描述一种新的数据类型。同样的关键字在大 O ul 多数面向对象的编程语言里都得到了应用。它其实是整个语言的焦点:需要新建数据类型的场合比那些用于 容纳数据和方法的“容器”多得多。 在 Java 中,类是最基本的 O P 概念。它是本书未采用粗体印刷的关键字之一——由于数量太多,所以会造成 O 页面排版的严重混乱。 为清楚起见,可考虑用特殊的样式创建一个类:将 publ i c成员置于最开头,后面跟随 pr ot ect ed、友好以及 pr i vat e成员。这样做的好处是类的使用者可从上向下依次阅读,并首先看到对自己来说最重要的内容(即 publ i c成员,因为它们可从文件的外部访问),并在遇到非公共成员后停止阅读,后者已经属于内部实施细 节的一部分了。然而,利用由 j avadoc 提供支持的注释文档(已在第 2 章介绍),代码的可读性问题已在很 大程度上得到了解决。 publ i c cl as s X { publ i c voi d pub1( ) publ i c voi d pub2( ) publ i c voi d pub3( ) pr i vat e voi d pr i v1( pr i vat e voi d pr i v2( pr i vat e voi d pr i v3( pr i vat e i nt i ; // . . . }

{ { { ) ) )

/* . /* . /* . { /* { /* { /*

. . . . . .

. . . . . .

*/ } */ } */ } . */ } . */ } . */ }

由于接口和实施细节仍然混合在一起,所以只是部分容易阅读。也就是说,仍然能够看到源码——实施的细 节,因为它们需要保存在类里面。向一个类的消费者显示出接口实际是“类浏览器”的工作。这种工具能查 找所有可用的类,总结出可对它们采取的全部操作(比如可以使用哪些成员等),并用一种清爽悦目的形式 显示出来。到大家读到这本书的时候,所有优秀的 Java 开发工具都应推出了自己的浏览器。

134

5. 4 类访问
在 Java 中,亦可用访问指示符判断出一个库内的哪些类可由那个库的用户使用。若想一个类能由客户程序员 调用,可在类主体的起始花括号前面某处放置一个 publ i c关键字。它控制着客户程序员是否能够创建属于这 个类的一个对象。 为控制一个类的访问,指示符必须在关键字 cl as s 之前出现。所以我们能够使用: publ i c cl as s W dget { i 也就是说,假若我们的库名是 m i b,那么所有客户程序员都能访问 W dget ——通过下述语句: yl i i m t m i b. W dget ; por yl i 或者 i m t m i b. * ; por yl 然而,我们同时还要注意到一些额外的限制: ( 1) 每个编译单元(文件)都只能有一个 publ i c 类。每个编译单元有一个公共接口的概念是由那个公共类表 达出来的。根据自己的需要,它可拥有任意多个提供支撑的“友好”类。但若在一个编译单元里使用了多个 publ i c类,编译器就会向我们提示一条出错消息。 ( 2) publ i c类的名字必须与包含了编译单元的那个文件的名字完全相符,甚至包括它的大小写形式。所以对 于 W dget 来说,文件的名字必须是 W dget . j ava,而不应是 w dget . j ava或者 W D ET. j ava。同样地,如果 i i i I G 出现不符,就会报告一个编译期错误。 ( 3) 可能(但并常见)有一个编译单元根本没有任何公共类。此时,可按自己的意愿任意指定文件名。 如果已经获得了 m i b内部的一个类,准备用它完成由 W dget 或者 m i b内部的其他某些 publ i c 类执行的 yl i yl 任务,此时又会出现什么情况呢?我们不希望花费力气为客户程序员编制文档,并感觉以后某个时候也许会 进行大手笔的修改,并将自己的类一起删掉,换成另一个不同的类。为获得这种灵活处理的能力,需要保证 没有客户程序员能够依赖自己隐藏于 m i b内部的特定实施细节。为达到这个目的,只需将 publ i c 关键字从 yl 类中剔除即可,这样便把类变成了“友好的”(类仅能在包内使用)。 注意不可将类设成 pr i vat e(那样会使除类之外的其他东西都不能访问它),也不能设成 pr ot ect ed(注释 ④)。因此,我们现在对于类的访问只有两个选择:“友好的”或者 publ i c。若不愿其他任何人访问那个 类,可将所有构建器设为 pr i vat e。这样一来,在类的一个 s t at i c 成员内部,除自己之外的其他所有人都无 法创建属于那个类的一个对象(注释⑤)。如下例所示: //: Lunch. j ava // D ons t r at es cl as s acces s s peci f i er s . em // M ake a cl as s ef f ect i vel y pr i vat e // w t h pr i vat e cons t r uct or s : i cl as s Soup { pr i vat e Soup( ) { } // ( 1) A l ow cr eat i on vi a s t at i c m hod: l et publ i c s t at i c Soup m akeSoup( ) { r et ur n new Soup( ) ; } // ( 2) C eat e a s t at i c obj ect and r // r et ur n a r ef er ence upon r eques t . // ( The " Si ngl et on" pat t er n) : pr i vat e s t at i c Soup ps 1 = new Soup( ) ; publ i c s t at i c Soup acces s ( ) { r et ur n ps 1; } publ i c voi d f ( ) { } } cl as s Sandw ch { // Us es Lunch i voi d f ( ) { new Lunch( ) ; } 135

} // O y one publ i c cl as s al l ow per f i l e: nl ed publ i c cl as s Lunch { voi d t es t ( ) { // C t do t hi s ! Pr i vat e cons t r uct or : an' //! Soup pr i v1 = new Soup( ) ; Soup pr i v2 = Soup. m akeSoup( ) ; Sandw ch f 1 = new Sandw ch( ) ; i i Soup. acces s ( ) . f ( ) ; } } ///: ~ ④:实际上,Java 1. 1 内部类既可以是“受到保护的”,也可以是“私有的”,但那属于特别情况。第 7 章 会详细解释这个问题。 ⑤:亦可通过从那个类继承来实现。 迄今为止,我们创建过的大多数方法都是要么返回 voi d,要么返回一个基本数据类型。所以对下述定义来 说: publ i c s t at i c Soup acces s ( ) { r et ur n ps l ; } 它最开始多少会使人有些迷惑。位于方法名(acces s)前的单词指出方法到底返回什么。在这之前,我们看 到的都是 voi d,它意味着“什么也不返回”(voi d 在英语里是“虚无”的意思。但亦可返回指向一个对象的 句柄,此时出现的就是这个情况。该方法返回一个句柄,它指向类 Soup 的一个对象。 Soup 类向我们展示出如何通过将所有构建器都设为 pr i vat e,从而防止直接创建一个类。请记住,假若不明 确地至少创建一个构建器,就会自动创建默认构建器(没有自变量)。若自己编写默认构建器,它就不会自 动创建。把它变成 pr i vat e 后,就没人能为那个类创建一个对象。但别人怎样使用这个类呢?上面的例子为 我们揭示出了两个选择。第一个选择,我们可创建一个 s t at i c 方法,再通过它创建一个新的 Soup,然后返 回指向它的一个句柄。如果想在返回之前对 Soup 进行一些额外的操作,或者想了解准备创建多少个 Soup 对 象(可能是为了限制它们的个数),这种方案无疑是特别有用的。 第二个选择是采用“设计方案”(D i gn Pat t er n)技术,本书后面会对此进行详细介绍。通常方案叫作 es “独子”,因为它仅允许创建一个对象。类 Soup 的对象被创建成 Soup 的一个 s t at i c pr i vat e 成员,所以有 一个而且只能有一个。除非通过 publ i c 方法 acces s ( ) ,否则根本无法访问它。 正如早先指出的那样,如果不针对类的访问设置一个访问指示符,那么它会自动默认为“友好的”。这意味 着那个类的对象可由包内的其他类创建,但不能由包外创建。请记住,对于相同目录内的所有文件,如果没 有明确地进行 package 声明,那么它们都默认为那个目录的默认包的一部分。然而,假若那个类一个 s t at i c 成员的属性是 publ i c,那么客户程序员仍然能够访问那个 s t at i c 成员——即使它们不能创建属于那个类的 一个对象。

5. 5 总结
对于任何关系,最重要的一点都是规定好所有方面都必须遵守的界限或规则。创建一个库时,相当于建立了 同那个库的用户(即“客户程序员”)的一种关系——那些用户属于另外的程序员,可能用我们的库自行构 建一个应用程序,或者用我们的库构建一个更大的库。 如果不制订规则,客户程序员就可以随心所欲地操作一个类的所有成员,无论我们本来愿不愿意其中的一些 成员被直接操作。所有东西都在别人面前都暴露无遗。 本章讲述了如何构建类,从而制作出理想的库。首先,我们讲述如何将一组类封装到一个库里。其次,我们 讲述类如何控制对自己成员的访问。 一般情况下,一个 C程序项目会在 50K 到 100K 行代码之间的某个地方开始中断。这是由于 C仅有一个“命名 136

空间”,所以名字会开始互相抵触,从而造成额外的管理开销。而在 Java 中,package关键字、包命名方案 以及 i m t 关键字为我们提供对名字的完全控制,所以命名冲突的问题可以很轻易地得到避免。 por 有两方面的原因要求我们控制对成员的访问。第一个是防止用户接触那些他们不应碰的工具。对于数据类型 的内部机制,那些工具是必需的。但它们并不属于用户接口的一部分,用户不必用它来解决自己的特定问 题。所以将方法和字段变成“私有”(pr i vat e)后,可极大方便用户。因为他们能轻易看出哪些对于自己来 说是最重要的,以及哪些是自己需要忽略的。这样便简化了用户对一个类的理解。 进行访问控制的第二个、也是最重要的一个原因是:允许库设计者改变类的内部工作机制,同时不必担心它 会对客户程序员产生什么影响。最开始的时候,可用一种方法构建一个类,后来发现需要重新构建代码,以 便达到更快的速度。如接口和实施细节早已进行了明确的分隔与保护,就可以轻松地达到自己的目的,不要 求用户改写他们的代码。 利用 Java 中的访问指示符,可有效控制类的创建者。那个类的用户可确切知道哪些是自己能够使用的,哪些 则是可以忽略的。但更重要的一点是,它可确保没有任何用户能依赖一个类的基础实施机制的任何部分。作 为一个类的创建者,我们可自由修改基础的实施细节,这一改变不会对客户程序员产生任何影响,因为他们 不能访问类的那一部分。 有能力改变基础的实施细节后,除了能在以后改进自己的设置之外,也同时拥有了“犯错误”的自由。无论 当初计划与设计时有多么仔细,仍然有可能出现一些失误。由于知道自己能相当安全地犯下这种错误,所以 可以放心大胆地进行更多、更自由的试验。这对自己编程水平的提高是很有帮助的,使整个项目最终能更 快、更好地完成。 一个类的公共接口是所有用户都能看见的,所以在进行分析与设计的时候,这是应尽量保证其准确性的最重 要的一个部分。但也不必过于紧张,少许的误差仍然是允许的。若最初设计的接口存在少许问题,可考虑添 加更多的方法,只要保证不删除客户程序员已在他们的代码里使用的东西。

5. 6 练习
( 1) 用 publ i c、pr i vat e、pr ot ect ed以及“友好的”数据成员及方法成员创建一个类。创建属于这个类的一 个对象,并观察在试图访问所有类成员时会获得哪种类型的编译器错误提示。注意同一个目录内的类属于 “默认”包的一部分。 ( 2) 用 pr ot ect ed数据创建一个类。在相同的文件里创建第二个类,用一个方法操纵第一个类里的 pr ot ect ed数据。 ( 3) 新建一个目录,并编辑自己的 C SSPA LA TH,以便包括那个新目录。将 P. cl as s 文件复制到自己的新目 录,然后改变文件名、P 类以及方法名(亦可考虑添加额外的输出,观察它的运行过程)。在一个不同的目 录里创建另一个程序,令其使用自己的新类。 ( 4) 在 c05 目录(假定在自己的 C SSPA LA TH里)创建下述文件: //: Package l as s . j ava dC package c05; cl as s PackagedC as s { l publ i c PackagedC as s ( ) { l Sys t em out . pr i nt l n( . " C eat i ng a packaged cl as s " ) ; r } } ///: ~ 然后在 c05 之外的另一个目录里创建下述文件: //: For ei gn. j ava package c05. f or ei gn; i m t c05. *; por publ i c cl as s For ei gn { publ i c s t at i c voi d m n ( St r i ng[ ] ar gs ) { ai PackagedC as s pc = new PackagedC as s ( ) ; l l } 137

} ///: ~ 解释编译器为什么会产生一个错误。将 For ei gn(外部)类作为 c05 包的一部分改变了什么东西吗?

138

第 6 章 类再生
“Java 引人注目的一项特性是代码的重复使用或者再生。但最具革命意义的是,除代码的复制和修改以外, 我们还能做多得多的其他事情。” 在象 C那样的程序化语言里,代码的重复使用早已可行,但效果不是特别显著。与 Java 的其他地方一样,这 个方案解决的也是与类有关的问题。我们通过创建新类来重复使用代码,但却用不着重新创建,可以直接使 用别人已建好并调试好的现成类。 但这样做必须保证不会干扰原有的代码。在这一章里,我们将介绍两个达到这一目标的方法。第一个最简 单:在新类里简单地创建原有类的对象。我们把这种方法叫作“合成”,因为新类由现有类的对象合并而 成。我们只是简单地重复利用代码的功能,而不是采用它的形式。 第二种方法则显得稍微有些技巧。它创建一个新类,将其作为现有类的一个“类型”。我们可以原样采取现 有类的形式,并在其中加入新代码,同时不会对现有的类产生影响。这种魔术般的行为叫作“继承” (I nher i t ance),涉及的大多数工作都是由编译器完成的。对于面向对象的程序设计,“继承”是最重要的 基础概念之一。它对我们下一章要讲述的内容会产生一些额外的影响。 对于合成与继承这两种方法,大多数语法和行为都是类似的(因为它们都要根据现有的类型生成新类型)。 在本章,我们将深入学习这些代码再生或者重复使用的机制。

6. 1 合成的语法
就以前的学习情况来看,事实上已进行了多次“合成”操作。为进行合成,我们只需在新类里简单地置入对 象句柄即可。举个例子来说,假定需要在一个对象里容纳几个 St r i ng对象、两种基本数据类型以及属于另一 个类的一个对象。对于非基本类型的对象来说,只需将句柄置于新类即可;而对于基本数据类型来说,则需 在自己的类中定义它们。如下所示(若执行该程序时有麻烦,请参见第 3 章 3. 1. 2 小节“赋值”): //: Spr i nkl er Sys t em j ava . // C pos i t i on f or code r eus e om package c06; cl as s W er Sour ce { at pr i vat e St r i ng s ; W er Sour ce( ) { at Sys t em out . pr i nt l n( " W er Sour ce( ) " ) ; . at s = new St r i ng( " C t r uct ed" ) ; ons } publ i c St r i ng t oSt r i ng( ) { r et ur n s ; } } publ i c cl as s Spr i nkl er Sys t em { pr i vat e St r i ng val ve1, val ve2, val ve3, val ve4; W er Sour ce s our ce; at i nt i ; f l oat f ; voi d pr i nt ( ) { Sys t em out . pr i nt l n( " val ve1 = " + val ve1) ; . Sys t em out . pr i nt l n( " val ve2 = " + val ve2) ; . Sys t em out . pr i nt l n( " val ve3 = " + val ve3) ; . Sys t em out . pr i nt l n( " val ve4 = " + val ve4) ; . Sys t em out . pr i nt l n( " i = " + i ) ; . Sys t em out . pr i nt l n( " f = " + f ) ; . Sys t em out . pr i nt l n( " s our ce = " + s our ce) ; . 139

} publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Spr i nkl er Sys t em x = new Spr i nkl er Sys t em ) ; ( x. pr i nt ( ) ; } } ///: ~ W er Sour ce内定义的一个方法是比较特别的:t oSt r i ng( ) 。大家不久就会知道,每种非基本类型的对象都 at 有一个 t oSt r i ng( ) 方法。若编译器本来希望一个 St r i ng,但却获得某个这样的对象,就会调用这个方法。所 以在下面这个表达式中: Sys t em out . pr i nt l n( " s our ce = " + s our ce) ; . 编译器会发现我们试图向一个 W er Sour ce 添加一个 St r i ng对象(" s our ce =" )。这对它来说是不可接受 at 的,因为我们只能将一个字串“添加”到另一个字串,所以它会说:“我要调用 t oSt r i ng( ) ,把 s our ce 转 换成字串!”经这样处理后,它就能编译两个字串,并将结果字串传递给一个 Sys t em out . pr i nt l n( )。每次 . 随同自己创建的一个类允许这种行为的时候,都只需要写一个 t oSt r i ng( ) 方法。 如果不深究,可能会草率地认为编译器会为上述代码中的每个句柄都自动构造对象(由于 Java 的安全和谨慎 的形象)。例如,可能以为它会为 W er Sour ce调用默认构建器,以便初始化 s our ce at 。打印语句的输出事实 上是: val ve1 = val ve2 = val ve3 = val ve4 = i = 0 f = 0. 0 s our ce = nul nul nul nul l l l l

nul l

在类内作为字段使用的基本数据会初始化成零,就象第 2 章指出的那样。但对象句柄会初始化成 nul l 。而且 假若试图为它们中的任何一个调用方法,就会产生一次“违例”。这种结果实际是相当好的(而且很有 用),我们可在不丢弃一次违例的前提下,仍然把它们打印出来。 编译器并不只是为每个句柄创建一个默认对象,因为那样会在许多情况下招致不必要的开销。如希望句柄得 到初始化,可在下面这些地方进行: ( 1) 在对象定义的时候。这意味着它们在构建器调用之前肯定能得到初始化。 ( 2) 在那个类的构建器中。 ( 3) 紧靠在要求实际使用那个对象之前。这样做可减少不必要的开销——假如对象并不需要创建的话。 下面向大家展示了所有这三种方法: //: Bat h. j ava // C t r uct or i ni t i al i z at i on w t h com i t i on ons i pos cl as s Soap { pr i vat e St r i ng s ; Soap( ) { Sys t em out . pr i nt l n( " Soap( ) " ) ; . s = new St r i ng( " C t r uct ed" ) ; ons } publ i c St r i ng t oSt r i ng( ) { r et ur n s ; } } publ i c cl as s Bat h { pr i vat e St r i ng 140

// I ni t i al i zi ng at poi nt of def i ni t i on: s 1 = new St r i ng( " Happy" ) , s 2 = " Happy" , s 3, s 4; Soap cas t i l l e; i nt i ; f l oat t oy; Bat h( ) { Sys t em out . pr i nt l n( " I ns i de Bat h( ) " ) ; . s 3 = new St r i ng( " Joy" ) ; i = 47; t oy = 3. 14f ; cas t i l l e = new Soap( ) ; } voi d pr i nt ( ) { // D ayed i ni t i al i z at i on: el i f ( s 4 == nul l ) s 4 = new St r i ng( " Joy" ) ; Sys t em out . pr i nt l n( " s 1 = " + s 1) ; . Sys t em out . pr i nt l n( " s 2 = " + s 2) ; . Sys t em out . pr i nt l n( " s 3 = " + s 3) ; . Sys t em out . pr i nt l n( " s 4 = " + s 4) ; . Sys t em out . pr i nt l n( " i = " + i ) ; . Sys t em out . pr i nt l n( " t oy = " + t oy) ; . Sys t em out . pr i nt l n( " cas t i l l e = " + cas t i l l e) ; . } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Bat h b = new Bat h( ) ; b. pr i nt ( ) ; } } ///: ~ 请注意在 Bat h 构建器中,在所有初始化开始之前执行了一个语句。如果不在定义时进行初始化,仍然不能保 证能在将一条消息发给一个对象句柄之前会执行任何初始化——除非出现不可避免的运行期违例。 下面是该程序的输出: I ns i de Bat h( ) Soap( ) s 1 = Happy s 2 = Happy s 3 = Joy s 4 = Joy i = 47 t oy = 3. 14 cas t i l l e = C t r uct ed ons 调用 pr i nt ( )时,它会填充 s 4,使所有字段在使用之前都获得正确的初始化。

6. 2 继承的语法
继承与 Java(以及其他 O P 语言)非常紧密地结合在一起。我们早在第 1 章就为大家引入了继承的概念,并 O 在那章之后到本章之前的各章里不时用到,因为一些特殊的场合要求必须使用继承。除此以外,创建一个类 时肯定会进行继承,因为若非如此,会从 Java 的标准根类 O ect 中继承。 bj 141

用于合成的语法是非常简单且直观的。但为了进行继承,必须采用一种全然不同的形式。需要继承的时候, 我们会说:“这个新类和那个旧类差不多。”为了在代码里表面这一观念,需要给出类名。但在类主体的起 始花括号之前,需要放置一个关键字 ext ends ,在后面跟随“基础类”的名字。若采取这种做法,就可自动 获得基础类的所有数据成员以及方法。下面是一个例子: //: D er gent . j ava et // I nher i t ance s ynt ax & pr oper t i es cl as s C eans er { l pr i vat e St r i ng s = new St r i ng( " C eans er " ) ; l publ i c voi d append( St r i ng a) { s += a; } publ i c voi d di l ut e( ) { append( " di l ut e( ) " ) ; } publ i c voi d appl y( ) { append( " appl y( ) " ) ; } publ i c voi d s cr ub( ) { append( " s cr ub( ) " ) ; } publ i c voi d pr i nt ( ) { Sys t em out . pr i nt l n( s ) ; } . publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C eans er x = new C eans er ( ) ; l l x. di l ut e( ) ; x. appl y( ) ; x. s cr ub ) ; ( x. pr i nt ( ) ; } } publ i c cl as s D er gent ext ends C eans er { et l // C hange a m hod: et publ i c voi d s cr ub( ) { append( " D er gent . s cr ub( ) " ) ; et s uper . s cr ub( ) ; // C l bas e- cl as s ver s i on al } // A m hods t o t he i nt er f ace: dd et publ i c voi d f oam ) { append( " f oam ) " ) ; } ( ( // Tes t t he new cl as s : publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai D er gent x = new D er gent ( ) ; et et x. di l ut e( ) ; x. appl y( ) ; x. s cr ub( ) ; x. f oam ) ; ( x. pr i nt ( ) ; Sys t em out . pr i nt l n( " Tes t i ng bas e cl as s : " ) ; . C eans er . m n( ar gs ) ; l ai } } ///: ~ 这个例子向大家展示了大量特性。首先,在 C eans er append( )方法里,字串同一个 s 连接起来。这是用 l “+=”运算符实现的。同“+”一样,“+=”被 Java 用于对字串进行“过载”处理。 其次,无论 C eans er 还是 D er gent 都包含了一个 m n( ) 方法。我们可为自己的每个类都创建一个 l et ai m n( )。通常建议大家象这样进行编写代码,使自己的测试代码能够封装到类内。即便在程序中含有数量众 ai 多的类,但对于在命令行请求的 publ i c 类,只有 m n( ) 才会得到调用。所以在这种情况下,当我们使用 ai “j ava D er gent ”的时候,调用的是 D et eger gent . m n( )——即使 C eans er 并非一个 p l i c类。采用这种 ai l ub 将 m n( ) 置入每个类的做法,可方便地为每个类都进行单元测试。而且在完成测试以后,毋需将 m n( ) 删 ai ai 去;可把它保留下来,用于以后的测试。 在这里,大家可看到 D er egent . m n( )对 C eans er . m n( ) 的调用是明确进行的。 et ai l ai 142

需要着重强调的是 C eans er 中的所有类都是 publ i c属性。请记住,倘若省略所有访问指示符,则成员默认 l 为“友好的”。这样一来,就只允许对包成员进行访问。在这个包内,任何人都可使用那些没有访问指示符 的方法。例如,D er gent 将不会遇到任何麻烦。然而,假设来自另外某个包的类准备继承 C eans er ,它就 et l 只能访问那些 publ i c 成员。所以在计划继承的时候,一个比较好的规则是将所有字段都设为 pr i vat e,并将 所有方法都设为 publ i c(pr ot ect ed成员也允许衍生出来的类访问它;以后还会深入探讨这一问题)。当 然,在一些特殊的场合,我们仍然必须作出一些调整,但这并不是一个好的做法。 注意 C eans er 在它的接口中含有一系列方法:append( ) ,di l ut e( ) ,appl y( ),s cr ub( ) 以及p i nt ()。由于 l r D er gent 是从 C eans er 衍生出来的(通过 ext ends 关键字),所以它会自动获得接口内的所有这些方法— et l —即使我们在 D er gent 里并未看到对它们的明确定义。这样一来,就可将继承想象成“对接口的重复利 et 用”或者“接口的再生”(以后的实施细节可以自由设置,但那并非我们强调的重点)。 正如在 s cr ub( ) 里看到的那样,可以获得在基础类里定义的一个方法,并对其进行修改。在这种情况下,我 们通常想在新版本里调用来自基础类的方法。但在 s cr ub( )里,不可只是简单地发出对 s cr ub( ) 的调用。那样 便造成了递归调用,我们不愿看到这一情况。为解决这个问题,Java 提供了一个 s uper 关键字,它引用当前 类已从中继承的一个“超类”(Super cl as s )。所以表达式 s uper . s cr ub( ) 调用的是方法 s cr ub( )的基础类版 本。 进行继承时,我们并不限于只能使用基础类的方法。亦可在衍生出来的类里加入自己的新方法。这时采取的 做法与在普通类里添加其他任何方法是完全一样的:只需简单地定义它即可。ext ends 关键字提醒我们准备 将新方法加入基础类的接口里,对其进行“扩展”。f oam ) 便是这种做法的一个产物。 ( 在 D er gent . m n( ) 里,我们可看到对于 D er gent 对象,可调用 C eans er 以及 D er gent 内所有可用的 et ai et l et 方法(如 f oam ) )。 (

6 . 2 . 1 初始化基础类
由于这儿涉及到两个类——基础类及衍生类,而不再是以前的一个,所以在想象衍生类的结果对象时,可能 会产生一些迷惑。从外部看,似乎新类拥有与基础类相同的接口,而且可包含一些额外的方法和字段。但继 承并非仅仅简单地复制基础类的接口了事。创建衍生类的一个对象时,它在其中包含了基础类的一个“子对 象”。这个子对象就象我们根据基础类本身创建了它的一个对象。从外部看,基础类的子对象已封装到衍生 类的对象里了。 当然,基础类子对象应该正确地初始化,而且只有一种方法能保证这一点:在构建器中执行初始化,通过调 用基础类构建器,后者有足够的能力和权限来执行对基础类的初始化。在衍生类的构建器中,Java 会自动插 入对基础类构建器的调用。下面这个例子向大家展示了对这种三级继承的应用: //: C t oon. j ava ar // C t r uct or cal l s dur i ng i nher i t ance ons cl as s A t { r A t() { r Sys t em out . pr i nt l n( " A t cons t r uct or " ) ; . r } } cl as s D aw ng ext ends A t { r i r D aw ng( ) { r i Sys t em out . pr i nt l n( " D aw ng cons t r uct or " ) ; . r i } } publ i c cl as s C t oon ext ends D aw ng { ar r i C t oon( ) { ar Sys t em out . pr i nt l n( " C t o cons t r uct or " ) ; . ar on } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C t oon x = new C t oon( ) ; ar ar 143

} } ///: ~ 该程序的输出显示了自动调用: A t cons t r uct or r D aw ng cons t r uct or r i C t oon cons t r uct or ar 可以看出,构建是在基础类的“外部”进行的,所以基础类会在衍生类访问它之前得到正确的初始化。 即使没有为 C t oon( ) 创建一个构建器,编译器也会为我们自动合成一个默认构建器,并发出对基础类构建 ar 器的调用。 1. 含有自变量的构建器 上述例子有自己默认的构建器;也就是说,它们不含任何自变量。编译器可以很容易地调用它们,因为不存 在具体传递什么自变量的问题。如果类没有默认的自变量,或者想调用含有一个自变量的某个基础类构建 器,必须明确地编写对基础类的调用代码。这是用 s uper 关键字以及适当的自变量列表实现的,如下所示: //: C s . j ava hes // I nher i t ance, cons t r uct or s and ar gum s ent cl as s G e { am G e( i nt i ) { am Sys t em out . pr i nt l n( " G e cons t r uct or " ) ; . am } } cl as s Boar dG e ext ends G e { am am Boar dG e( i nt i ) { am s uper ( i ) ; Sys t em out . pr i nt l n( " Boar dG e cons t r uct or " ) ; . am } } publ i c cl as s C s ext ends Boar dG e { hes am C s() { hes s uper ( 11) ; Sys t em out . pr i nt l n( " C s cons t r uct or " ) ; . hes } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C s x = new C s ( ) ; hes hes } } ///: ~ 如果不调用 Boar dG es ( )内的基础类构建器,编译器就会报告自己找不到 G es ( ) 形式的一个构建器。除此 am am 以外,在衍生类构建器中,对基础类构建器的调用是必须做的第一件事情(如操作失当,编译器会向我们指 出)。 2. 捕获基本构建器的违例 正如刚才指出的那样,编译器会强迫我们在衍生类构建器的主体中首先设置对基础类构建器的调用。这意味 着在它之前不能出现任何东西。正如大家在第 9 章会看到的那样,这同时也会防止衍生类构建器捕获来自一 个基础类的任何违例事件。显然,这有时会为我们造成不便。 144

6. 3 合成与继承的 结合
许多时候都要求将合成与继承两种技术结合起来使用。下面这个例子展示了如何同时采用继承与合成技术, 从而创建一个更复杂的类,同时进行必要的构建器初始化工作: //: Pl aceSet t i ng. j ava // C bi ni ng com i t i on & i nher i t ance om pos cl as s Pl at e { Pl at e( i nt i ) { Sys t em out . pr i nt l n( " Pl at e cons t r uct or " ) ; . } } cl as s D nner Pl at e ext ends Pl at e { i D nner Pl at e( i nt i ) { i s uper ( i ) ; Sys t em out . pr i nt l n( . " D nner Pl at e cons t r uct or " ) ; i } } cl as s Ut ens i l { Ut ens i l ( i nt i ) { Sys t em out . pr i nt l n( " Ut ens i l cons t r uct or " ) ; . } } cl as s Spoon ext ends Ut ens i l { Spoon( i nt i ) { s uper ( i ) ; Sys t em out . pr i nt l n( " Spoon cons t r uct or " ) ; . } } cl as s For k ext ends Ut ens i l { For k( i nt i ) { s uper ( i ) ; Sys t em out . pr i nt l n( " For k cons t r uct or " ) ; . } } cl as s Kni f e ext ends Ut ens i l { Kni f e( i nt i ) { s uper ( i ) ; Sys t em out . pr i nt l n( " Kni f e const r uct or " ) ; . } } // A cul t ur al w of doi ng s om hi ng: ay et cl as s C t om { us C t om i nt i ) { us ( Sys t em out . pr i nt l n( " C t om cons t r uct or " ) ; . us 145

} } publ i c cl as s Pl aceSet t i ng ext ends C t om { us Spoon s p; For k f r k; Kni f e kn; D nner Pl at e pl ; i Pl aceSet t i ng( i nt i ) { s uper ( i + 1) ; s p = new Spoon( i + 2) ; f r k = new For k( i + 3) ; kn = new Kni f e( i + 4) ; pl = new D nner Pl at e( i + 5) ; i Sys t em out . pr i nt l n( . " Pl aceSet t i ng cons t r uct or " ) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Pl aceSet t i ng x = new Pl aceSet t i ng( 9) ; } } ///: ~ 尽管编译器会强迫我们对基础类进行初始化,并要求我们在构建器最开头做这一工作,但它并不会监视我们 是否正确初始化了成员对象。所以对此必须特别加以留意。

6 . 3 . 1 确保正确的清除
Java 不具备象 C ++的“破坏器”那样的概念。在 C ++中,一旦破坏(清除)一个对象,就会自动调用破坏器 方法。之所以将其省略,大概是由于在 Java 中只需简单地忘记对象,不需强行破坏它们。垃圾收集器会在必 要的时候自动回收内存。 垃圾收集器大多数时候都能很好地工作,但在某些情况下,我们的类可能在自己的存在时期采取一些行动, 而这些行动要求必须进行明确的清除工作。正如第 4 章已经指出的那样,我们并不知道垃圾收集器什么时候 才会显身,或者说不知它何时会调用。所以一旦希望为一个类清除什么东西,必须写一个特别的方法,明 确、专门地来做这件事情。同时,还要让客户程序员知道他们必须调用这个方法。而在所有这一切的后面, 就如第 9 章(违例控制)要详细解释的那样,必须将这样的清除代码置于一个 f i nal l y 从句中,从而防范任 何可能出现的违例事件。 下面介绍的是一个计算机辅助设计系统的例子,它能在屏幕上描绘图形: //: C D t em j ava A Sys . // Ens ur i ng pr oper cl eanup i m t j ava. ut i l . * ; por cl as s Shape { Shape( i nt i ) { Sys t em out . pr i nt l n( " Shape cons t r uct or " ) ; . } voi d cl eanup( ) { Sys t em out . pr i nt l n( " Shape cl eanup" ) ; . } } cl as s C r cl e ext ends Shape { i C r cl e( i nt i ) { i 146

s uper ( i ) ; Sys t em out . pr i nt l n( " D aw ng a C r cl e" ) ; . r i i } voi d cl eanup( ) { Sys t em out . pr i nt l n( " Er as i ng a C r cl e" ) ; . i s uper . cl eanup( ) ; } } cl as s Tr i angl e ext ends Shape { Tr i angl e( i nt i ) { s uper ( i ) ; Sys t em out . pr i nt l n( " D aw ng a Tr i angl e" ) ; . r i } voi d cl eanup( ) { Sys t em out . pr i nt l n( " Er as i ng a Tr i angl e" ) ; . s uper . cl eanup( ) ; } } cl as s Li ne ext ends Shape { pr i vat e i nt s t ar t , end; Li ne( i nt s t ar t , i nt end) { s uper ( s t ar t ) ; t hi s . s t ar t = s t ar t ; t hi s . end = end; Sys t em out . pr i nt l n( " D aw ng a Li ne: " + . r i s t ar t + " , " + end) ; } voi d cl eanup( ) { Sys t em out . pr i nt l n( " Er as i ng a Li ne: " + . s t ar t + " , " + end) ; s uper . cl eanup( ) ; } } publ i c cl as s C D t em ext ends Shape { A Sys pr i vat e C r cl e c; i pr i vat e Tr i angl e t ; pr i vat e Li ne[ ] l i nes = new Li ne[ 10] ; C D t em i nt i ) { A Sys ( s uper ( i + 1) ; f or ( i nt j = 0; j < 10; j ++) l i nes [ j ] = new Li ne( j , j * j ) ; c = new C r cl e( 1) ; i t = new Tr i angl e( 1) ; Sys t em out . pr i nt l n( " C bi ned cons t r uct or " ) ; . om } voi d cl eanup( ) { Sys t em out . pr i nt l n( " C D t em cl eanup( ) " ) ; . A Sys . t . cl eanup( ) ; c. cl eanup( ) ; 147

f or ( i nt i = 0; i < l i nes . l engt h; i ++) l i nes [ i ] . cl eanup( ) ; s uper . cl eanup( ) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C D t em x = new C D t em 47) ; A Sys A Sys ( try { // C ode and except i on handl i ng. . . } f i nal l y { x. cl eanup( ) ; } } } ///: ~ 这个系统中的所有东西都属于某种 Shape (几何形状)。Shape本身是一种 O ect (对象),因为它是从根 bj 类明确继承的。每个类都重新定义了 Shape 的 cl eanup( ) 方法,同时还要用 s uper 调用那个方法的基础类版 本。尽管对象存在期间调用的所有方法都可负责做一些要求清除的工作,但对于特定的 Shape 类——C r cl e i (圆)、Tr i angl e(三角形)以及 Li ne(直线),它们都拥有自己的构建器,能完成“作图”(dr aw)任 务。每个类都有它们自己的 cl eanup( ) 方法,用于将非内存的东西恢复回对象存在之前的景象。 在 m n( ) 中,可看到两个新关键字:t r y 和 f i nal l y。我们要到第 9 章才会向大家正式引荐它们。其中,t r y ai 关键字指出后面跟随的块(由花括号定界)是一个“警戒区”。也就是说,它会受到特别的待遇。其中一种 待遇就是:该警戒区后面跟随的 f i nal l y 从句的代码肯定会得以执行——不管 t r y 块到底存不存在(通过违 例控制技术,t r y 块可有多种不寻常的应用)。在这里,f i nal l y 从句的意思是“总是为 x 调用 cl eanup( ) , 无论会发生什么事情”。这些关键字将在第 9 章进行全面、完整的解释。 在自己的清除方法中,必须注意对基础类以及成员对象清除方法的调用顺序——假若一个子对象要以另一个 为基础。通常,应采取与 C ++编译器对它的“破坏器”采取的同样的形式:首先完成与类有关的所有特殊工 作(可能要求基础类元素仍然可见),然后调用基础类清除方法,就象这儿演示的那样。 许多情况下,清除可能并不是个问题;只需让垃圾收集器尽它的职责即可。但一旦必须由自己明确清除,就 必须特别谨慎,并要求周全的考虑。 1. 垃圾收集的顺序 不能指望自己能确切知道何时会开始垃圾收集。垃圾收集器可能永远不会得到调用。即使得到调用,它也可 能以自己愿意的任何顺序回收对象。除此以外,Java 1. 0 实现的垃圾收集器机制通常不会调用 f i nal i ze( ) 方 法。除内存的回收以外,其他任何东西都最好不要依赖垃圾收集器进行回收。若想明确地清除什么,请制作 自己的清除方法,而且不要依赖 f i nal i ze( ) 。然而正如以前指出的那样,可强迫 Java1. 1 调用所有收尾模块 (Fi nal i zer )。

6 . 3 . 2 名字的隐藏
只有 C ++程序员可能才会惊讶于名字的隐藏,因为它的工作原理与在 C ++里是完全不同的。如果 Java 基础类 有一个方法名被“过载”使用多次,在衍生类里对那个方法名的重新定义就不会隐藏任何基础类的版本。所 以无论方法在这一级还是在一个基础类中定义,过载都会生效: //: Hi de. j ava // O l oadi ng a bas e- cl as s m hod nam ver et e // i n a der i ved cl as s does not hi de t he / / bas e- cl as s ver s i ons cl as s Hom { er char doh( char c) { Sys t em out . pr i nt l n( " doh( char ) " ) ; . r et ur n ' d' ; } 148

f l oat doh( f l oat f ) { Sys t em out . pr i nt l n( " doh( f l oat ) " ) ; . r et ur n 1. 0f ; } } cl as s M l hous e { } i cl as s Bar t ext ends Hom { er voi d doh( M l hous e m { } i ) } cl as s Hi de { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Bar t b = new Bar t ( ) ; b. doh( 1) ; // doh( f l oat ) us ed b. doh( ' x' ) ; b. doh( 1. 0f ) ; b. doh( new M l hous e( ) ) ; i } } ///: ~ 正如下一章会讲到的那样,很少会用与基础类里完全一致的签名和返回类型来覆盖同名的方法,否则会使人 感到迷惑(这正是 C ++不允许那样做的原因,所以能够防止产生一些不必要的错误)。

6. 4 到底选择合成还是继承
无论合成还是继承,都允许我们将子对象置于自己的新类中。大家或许会奇怪两者间的差异,以及到底该如 何选择。 如果想利用新类内部一个现有类的特性,而不想使用它的接口,通常应选择合成。也就是说,我们可嵌入一 个对象,使自己能用它实现新类的特性。但新类的用户会看到我们已定义的接口,而不是来自嵌入对象的接 口。考虑到这种效果,我们需在新类里嵌入现有类的 pr i vat e 对象。 有些时候,我们想让类用户直接访问新类的合成。也就是说,需要将成员对象的属性变为 publ i c。成员对象 会将自身隐藏起来,所以这是一种安全的做法。而且在用户知道我们准备合成一系列组件时,接口就更容易 理解。car (汽车)对象便是一个很好的例子: //: C . j ava ar // C pos i t i on w t h publ i c obj ect s om i cl as s Engi ne { publ i c voi d s t ar t ( ) { } publ i c voi d r ev( ) { } publ i c voi d s t op( ) { } } cl as s W heel { publ i c voi d i nf l at e( i nt ps i ) { } } cl as s W ndow { i publ i c voi d r ol l up( ) { } publ i c voi d r ol l dow ) { } n( } 149

cl as s D oor { publ i c W ndow w ndow = new W ndow ) ; i i i ( publ i c voi d open( ) { } publ i c voi d cl os e( ) { } } publ i c cl as s C { ar publ i c Engi ne engi ne = new Engi ne( ) ; publ i c W heel [ ] w heel = new W heel [ 4] ; publ i c D oor l ef t = new D ( ) , oor r i ght = new D ( ) ; // 2- door oor C () { ar f or ( i nt i = 0; i < 4; i ++) w heel [ i ] = new W heel ( ) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C car = new C ( ) ; ar ar car . l ef t . w ndow r ol l up( ) ; i . car . w heel [ 0] . i nf l at e( 72) ; } } ///: ~ 由于汽车的装配是故障分析时需要考虑的一项因素(并非只是基础设计简单的一部分),所以有助于客户程 序员理解如何使用类,而且类创建者的编程复杂程度也会大幅度降低。 如选择继承,就需要取得一个现成的类,并制作它的一个特殊版本。通常,这意味着我们准备使用一个常规 用途的类,并根据特定的需求对其进行定制。只需稍加想象,就知道自己不能用一个车辆对象来合成一辆汽 车——汽车并不“包含”车辆;相反,它“属于”车辆的一种类别。“属于”关系是用继承来表达的,而 “包含”关系是用合成来表达的。

6. 5 pr ot ect ed
现在我们已理解了继承的概念,pr ot ect ed这个关键字最后终于有了意义。在理想情况下,pr i vat e 成员随时 都是“私有”的,任何人不得访问。但在实际应用中,经常想把某些东西深深地藏起来,但同时允许访问衍 生类的成员。pr ot ect ed关键字可帮助我们做到这一点。它的意思是“它本身是私有的,但可由从这个类继 承的任何东西或者同一个包内的其他任何东西访问”。也就是说,Java 中的 pr ot ect ed会成为进入“友好” 状态。 我们采取的最好的做法是保持成员的 pr i vat e 状态——无论如何都应保留对基 础的实施细节进行修改的权 利。在这一前提下,可通过 pr ot ect ed方法允许类的继承者进行受到控制的访问: //: O c. j ava r // T he pr ot ect ed keyw d or i m t j ava. ut i l . * ; por cl as s Vi l l ai n { pr i vat e i nt i ; pr ot ect ed i nt r ead( ) { r et ur n i ; pr ot ect ed voi d s et ( i nt i i ) { i = publ i c Vi l l ai n( i nt i i ) { i = i i ; publ i c i nt val ue( i nt m { r et ur n ) } publ i c cl as s O c ext ends Vi l l ai n { r 150

} ii; } } m ; } *i

pr i vat e i nt j ; publ i c O c( i nt j j ) { s uper ( j j ) ; j = j j ; } r publ i c voi d change( i nt x) { s et ( x) ; } } ///: ~ 可以看到,change( ) 拥有对 s et ( ) 的访问权限,因为它的属性是 pr ot ect ed(受到保护的)。

6. 6 累积开发
继承的一个好处是它支持“累积开发”,允许我们引入新的代码,同时不会为现有代码造成错误。这样可将 新错误隔离到新代码里。通过从一个现成的、功能性的类继承,同时增添成员新的数据成员及方法(并重新 定义现有方法),我们可保持现有代码原封不动(另外有人也许仍在使用它),不会为其引入自己的编程错 误。一旦出现错误,就知道它肯定是由于自己的新代码造成的。这样一来,与修改现有代码的主体相比,改 正错误所需的时间和精力就可以少很多。 类的隔离效果非常好,这是许多程序员事先没有预料到的。甚至不需要方法的源代码来实现代码的再生。最 多只需要导入一个包(这对于继承和合并都是成立的)。 大家要记住这样一个重点:程序开发是一个不断递增或者累积的过程,就象人们学习知识一样。当然可根据 要求进行尽可能多的分析,但在一个项目的设计之初,谁都不可能提前获知所有的答案。如果能将自己的项 目看作一个有机的、能不断进步的生物,从而不断地发展和改进它,就有望获得更大的成功以及更直接的反 馈。 尽管继承是一种非常有用的技术,但在某些情况下,特别是在项目稳定下来以后,仍然需要从新的角度考察 自己的类结构,将其收缩成一个更灵活的结构。请记住,继承是对一种特殊关系的表达,意味着“这个新类 属于那个旧类的一种类型”。我们的程序不应纠缠于一些细树末节,而应着眼于创建和操作各种类型的对 象,用它们表达出来自“问题空间”的一个模型。

6. 7 上溯造型
继承最值得注意的地方就是它没有为新类提供方法。继承是对新类和基础类之间的关系的一种表达。可这样 总结该关系:“新类属于现有类的一种类型”。 这种表达并不仅仅是对继承的一种形象化解释,继承是直接由语言提供支持的。作为一个例子,大家可考虑 一个名为 I ns t r um 的基础类,它用于表示乐器;另一个衍生类叫作 W nd。由于继承意味着基础类的所有 ent i 方法亦可在衍生出来的类中使用,所以我们发给基础类的任何消息亦可发给衍生类。若 I ns t r um 类有一个 ent pl ay( )方法,则 W nd 设备也会有这个方法。这意味着我们能肯定地认为一个 W nd 对象也是 I ns t r um 的一 i i ent 种类型。下面这个例子揭示出编译器如何提供对这一概念的支持: //: W nd. j ava i // I nher i t ance & upcas t i ng i m t j ava. ut i l . *; por cl as s I ns t r um ent { publ i c voi d pl ay( ) { } s t at i c voi d t une( I ns t r um ent i ) { // . . . i . pl ay( ) ; } } // W nd obj ect s ar e i ns t r um s i ent // becaus e t hey have t he s am i nt er f ace: e cl as s W nd ext ends I ns t r um i ent { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai W nd f l ut e = new W nd( ) ; i i I ns t r um . t une( f l ut e) ; // Upcas t i ng ent 151

} } ///: ~ 这个例子中最有趣的无疑是 t une( )方法,它能接受一个 I ns t r um 句柄。但在 W nd. m n( ) 中,t une ent i ai ()方法 是通过为其赋予一个 W nd 句柄来调用的。由于 Java 对类型检查特别严格,所以大家可能会感到很奇怪,为 i 什么接收一种类型的方法也能接收另一种类型呢?但是,我们一定要认识到一个 W nd 对象也是一个 i I ns t r um 对象。而且对于不在 W nd 中的一个 I ns t r um (乐器),没有方法可以由 t une( ) 调用。在 ent i ent t une( )中,代码适用于 I ns t r um 以及从 I ns t r um 衍生出来的任何东西。在这里,我们将从一个 Wnd 句 ent ent i 柄转换成一个 I ns t r um 句柄的行为叫作“上溯造型”。 ent

6 . 7 . 1 何谓“上溯造型”?
之所以叫作这个名字,除了有一定的历史原因外,也是由于在传统意义上,类继承图的画法是根位于最顶 部,再逐渐向下扩展(当然,可根据自己的习惯用任何方法描绘这种图)。因素,W nd. j ava的继承图就象 i 下面这个样子:

由于造型的方向是从衍生类到基础类,箭头朝上,所以通常把它叫作“上溯造型”,即 Upcas t i ng。上溯造 型肯定是安全的,因为我们是从一个更特殊的类型到一个更常规的类型。换言之,衍生类是基础类的一个超 集。它可以包含比基础类更多的方法,但它至少包含了基础类的方法。进行上溯造型的时候,类接口可能出 现的唯一一个问题是它可能丢失方法,而不是赢得这些方法。这便是在没有任何明确的造型或者其他特殊标 注的情况下,编译器为什么允许上溯造型的原因所在。 也可以执行下溯造型,但这时会面临第 11 章要详细讲述的一种困境。 1. 再论合成与继承 在面向对象的程序设计中,创建和使用代码最可能采取的一种做法是:将数据和方法统一封装到一个类里, 并且使用那个类的对象。有些时候,需通过“合成”技术用现成的类来构造新类。而继承是最少见的一种做 法。因此,尽管继承在学习 O P 的过程中得到了大量的强调,但并不意味着应该尽可能地到处使用它。相 O 反,使用它时要特别慎重。只有在清楚知道继承在所有方法中最有效的前提下,才可考虑它。为判断自己到 底应该选用合成还是继承,一个最简单的办法就是考虑是否需要从新类上溯造型回基础类。若必须上溯,就 需要继承。但如果不需要上溯造型,就应提醒自己防止继承的滥用。在下一章里(多形性),会向大家介绍 必须进行上溯造型的一种场合。但只要记住经常问自己“我真的需要上溯造型吗”,对于合成还是继承的选 择就不应该是个太大的问题。

6. 8 f i nal 关键字
由于语境(应用环境)不同,f i nal 关键字的含义可能会稍微产生一些差异。但它最一般的意思就是声明 “这个东西不能改变”。之所以要禁止改变,可能是考虑到两方面的因素:设计或效率。由于这两个原因颇 有些区别,所以也许会造成 f i nal 关键字的误用。 在接下去的小节里,我们将讨论 f i nal 关键字的三种应用场合:数据、方法以及类。

6 . 8 . 1 f i nal 数据
许多程序设计语言都有自己的办法告诉编译器某个数据是“常数”。常数主要应用于下述两个方面: ( 1) 编译期常数,它永远不会改变 ( 2) 在运行期初始化的一个值,我们不希望它发生变化 对于编译期的常数,编译器(程序)可将常数值“封装”到需要的计算过程里。也就是说,计算可在编译期 间提前执行,从而节省运行时的一些开销。在 Java 中,这些形式的常数必须属于基本数据类型 (Pr i m t i ves ),而且要用 f i nal 关键字进行表达。在对这样的一个常数进行定义的时候,必须给出一个 i 152

值。 无论 s t at i c还是 f i nal 字段,都只能存储一个数据,而且不得改变。 若随同对象句柄使用 f i nal ,而不是基本数据类型,它的含义就稍微让人有点儿迷糊了。对于基本数据类 型,f i nal 会将值变成一个常数;但对于对象句柄,f i nal 会将句柄变成一个常数。进行声明时,必须将句柄 初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。然而,对象本身是可以修改的。Java 对此未提供任何手段,可将一个对象直接变成一个常数(但是,我们可自己编写一个类,使其中的对象具有 “常数”效果)。这一限制也适用于数组,它也属于对象。 下面是演示 f i nal 字段用法的一个例子: //: Fi nal D a. j ava at // T he ef f ect of f i nal on f i el ds cl as s Val ue { i nt i = 1; } publ i c cl as s Fi nal D a { at // C be com l e- t i m cons t ant s an pi e f i nal i nt i 1 = 9; s t at i c f i nal i nt I 2 = 99; // Typi cal publ i c cons t ant : publ i c s t at i c f i nal i nt I 3 = 39; // C annot be com l e- t i m cons t ant s : pi e f i nal i nt i 4 = ( i nt ) ( M h. r andom ) *20) ; at ( s t at i c f i nal i nt i 5 = ( i nt ) ( M h. r andom ) *20) ; at ( Val ue v1 = new Val ue( ) ; f i nal Val ue v2 = new Val ue( ) ; st at i c f i nal Val ue v3 = new Val ue( ) ; //! f i nal Val ue v4; // Pr e- Java 1. 1 Er r or : // no i ni t i al i zer // A r ays : r f i nal i nt [ ] a = { 1, 2, 3, 4, 5, 6 } ; publ i c voi d pr i nt ( St r i ng i d) { Sys t em out . pr i nt l n( . i d + ": " + "i 4 = " + i 4 + " , i 5 = " + i 5) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Fi nal D a f d1 = new Fi nal D a( ) ; at at //! f d1. i 1++; // Er r or : can' t change val ue f d1. v2. i ++; // O ect i s n' t cons t ant ! bj f d1. v1 = new Val ue( ) ; // O - - not f i nal K f or ( i nt i = 0; i < f d1. a. l engt h; i ++) f d1. a[ i ] ++; // O ect i s n' t cons t ant ! bj //! f d1. v2 = new Val ue( ) ; // Er r or : C t an' //! f d1. v3 = new Val ue( ) ; // change handl e //! f d1. a = new i nt [ 3] ; f d1. pr i nt ( " f d1" ) ; Sys t em out . pr i nt l n( " C eat i ng new Fi nal D a" ) ; . r at 153

Fi nal D a f d2 = new Fi nal D a( ) ; at at f d1. pr i nt ( " f d1" ) ; f d2. pr i nt ( " f d2" ) ; } } ///: ~ 由于 i 1 和 I 2 都是具有 f i nal 属性的基本数据类型,并含有编译期的值,所以它们除了能作为编译期的常数 使用外,在任何导入方式中也不会出现任何不同。I 3 是我们体验此类常数定义时更典型的一种方式:publ i c 表示它们可在包外使用;St at i c 强调它们只有一个;而 f i nal 表明它是一个常数。注意对于含有固定初始化 值(即编译期常数)的 f i anl s t at i c基本数据类型,它们的名字根据规则要全部采用大写。也要注意 i 5 在 编译期间是未知的,所以它没有大写。 不能由于某样东西的属性是 f i nal ,就认定它的值能在编译时期知道。i 4 和 i 5 向大家证明了这一点。它们在 运行期间使用随机生成的数字。例子的这一部分也向大家揭示出将 f i nal 值设为 s t at i c 和非 s t at i c 之间的 差异。只有当值在运行期间初始化的前提下,这种差异才会揭示出来。因为编译期间的值被编译器认为是相 同的。这种差异可从输出结果中看出: f d i 4 = 15, i 5 = 9 1: C eat i ng new Fi nal D a r at f d1: i 4 = 15, i 5 = 9 f d2: i 4 = 10, i 5 = 9 注意对于 f d1 和 f d2 来说,i 4 的值是唯一的,但 i 5 的值不会由于创建了另一个 Fi nal D a对象而发生改 at 变。那是因为它的属性是 s t at i c,而且在载入时初始化,而非每创建一个对象时初始化。 从 v1 到 v4 的变量向我们揭示出 f i nal 句柄的含义。正如大家在 m n( ) 中看到的那样,并不能认为由于 v2 ai 属于 f i nal ,所以就不能再改变它的值。然而,我们确实不能再将 v2 绑定到一个新对象,因为它的属性是 f i nal 。这便是 f i nal 对于一个句柄的确切含义。我们会发现同样的含义亦适用于数组,后者只不过是另一种 类型的句柄而已。将句柄变成 f i nal 看起来似乎不如将基本数据类型变成 f i nal 那么有用。 2. 空白 f i nal Java 1. 1 允许我们创建“空白 f i nal ”,它们属于一些特殊的字段。尽管被声明成 f i nal ,但却未得到一个 初始值。无论在哪种情况下,空白 f i nal 都必须在实际使用前得到正确的初始化。而且编译器会主动保证这 一规定得以贯彻。然而,对于 f i nal 关键字的各种应用,空白 f i nal 具有最大的灵活性。举个例子来说,位 于类内部的一个 f i nal 字段现在对每个对象都可以有所不同,同时依然保持其“不变”的本质。下面列出一 个例子: //: Bl ankFi nal . j ava // " Bl ank" f i nal dat a m ber s em cl as s Poppet { } cl as s Bl ankFi nal { f i nal i nt i = 0; // I ni t i al i zed f i nal f i nal i nt j ; // Bl ank f i nal f i nal Poppet p; // Bl ank f i nal handl e // Bl ank f i nal s M UST be i ni t i al i zed // i n t he cons t r uct or : Bl ankFi nal ( ) { j = 1; // I ni t i al i ze bl ank f i nal p = new Poppet ( ) ; } Bl ankFi nal ( i nt x) { j = x; // I ni t i al i ze bl ank f i nal 154

p = new Poppet ( ) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Bl ankFi nal bf = new Bl ankFi nal ( ) ; } } ///: ~ 现在强行要求我们对 f i nal 进行赋值处理——要么在定义字段时使用一个表达 式,要么在每个构建器中。这 样就可以确保 f i nal 字段在使用前获得正确的初始化。 3. f i nal 自变量 Java 1. 1 允许我们将自变量设成 f i nal 属性,方法是在自变量列表中对它们进行适当的声明。这意味着在一 个方法的内部,我们不能改变自变量句柄指向的东西。如下所示: //: Fi nal A gum s . j ava r ent // Us i ng " f i nal " w t h m hod ar gum s i et ent cl as s G zm { i o publ i c voi d s pi n( ) { } } publ i c cl as s Fi nal A gum s { r ent voi d w t h( f i nal G zm g) { i i o //! g = new G zm ) ; // I l l egal - - g i s f i nal i o( g. s pi n( ) ; } voi d w t hout ( G z m g) { i i o g = new G zm ) ; // O - - g not f i nal i o( K g. s pi n( ) ; } // voi d f ( f i nal i nt i ) { i ++; } // C t change an' // You can onl y r ead f r om a f i nal pr i m t i ve: i i nt g( f i nal i nt i ) { r et ur n i + 1; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Fi nal A gum s bf = new Fi nal A gum s ( ) ; r ent r ent bf . w t hout ( nul l ) ; i bf . w t h( nul l ) ; i } } // / : ~ 注意此时仍然能为 f i nal 自变量分配一个 nul l (空)句柄,同时编译器不会捕获它。这与我们对非 f i nal 自 变量采取的操作是一样的。 方法 f ( )和 g( )向我们展示出基本类型的自变量为 f i nal 时会发生什么情况:我们只能读取自变量,不可改变 它。

6 . 8 . 2 f i nal 方法
之所以要使用 f i nal 方法,可能是出于对两方面理由的考虑。第一个是为方法“上锁”,防止任何继承类改 变它的本来含义。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可 以采取这种做法。 采用 f i nal 方法的第二个理由是程序执行的效率。将一个方法设成 f i nal 后,编译器就可以把对那个方法的 所有调用都置入“嵌入”调用里。只要编译器发现一个 f i nal 方法调用,就会(根据它自己的判断)忽略为 执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除 155

堆栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。这 样做可避免方法调用时的系统开销。当然,若方法体积太大,那么程序也会变得雍肿,可能受到到不到嵌入 代码所带来的任何性能提升。因为任何提升都被花在方法内部的时间抵消了。Java 编译器能自动侦测这些情 况,并颇为“明智”地决定是否嵌入一个 f i nal 方法。然而,最好还是不要完全相信编译器能正确地作出所 有判断。通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为 f i nal 。 类内所有 pr i vat e方法都自动成为 f i nal 。由于我们不能访问一个 pr i vat e方法,所以它绝对不会被其他方 法覆盖(若强行这样做,编译器会给出错误提示)。可为一个 pr i vat e方法添加 f i nal 指示符,但却不能为 那个方法提供任何额外的含义。

6 . 8 . 3 f i nal 类
如果说整个类都是 f i nal (在它的定义前冠以 f i nal 关键字),就表明自己不希望从这个类继承,或者不允 许其他任何人采取这种操作。换言之,出于这样或那样的原因,我们的类肯定不需要进行任何改变;或者出 于安全方面的理由,我们不希望进行子类化(子类处理)。 除此以外,我们或许还考虑到执行效率的问题,并想确保涉及这个类各对象的所有行动都要尽可能地有效。 如下所示: //: Jur as s i c. j ava // M ng an ent i r e cl as s f i nal aki cl as s Sm l Br ai n { } al f i nal cl as s D nos aur { i i nt i = 7; i nt j = 1; Sm l Br ai n x = new Sm l Br ai n( ) ; al al voi d f ( ) { } } //! cl as s Fur t her ext ends D nos aur { } i // er r or : C annot ext end f i nal cl as s ' D nos aur ' i publ i c cl as s Jur as s i c { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai D nos aur n = new D nos aur ( ) ; i i n. f ( ) ; n. i = 40; n. j ++; } } ///: ~ 注意数据成员既可以是 f i nal ,也可以不是,取决于我们具体选择。应用于 f i nal 的规则同样适用于数据成 员,无论类是否被定义成 f i nal 。将类定义成 f i nal 后,结果只是禁止进行继承——没有更多的限制。然 而,由于它禁止了继承,所以一个 f i nal 类中的所有方法都默认为 f i nal 。因为此时再也无法覆盖它们。所 以与我们将一个方法明确声明为 f i nal 一样,编译器此时有相同的效率选择。 可为 f i nal 类内的一个方法添加 f i nal 指示符,但这样做没有任何意义。

6 . 8 . 4 f i nal 的注意事项
设计一个类时,往往需要考虑是否将一个方法设为 f i nal 。可能会觉得使用自己的类时执行效率非常重要, 没有人想覆盖自己的方法。这种想法在某些时候是正确的。 但要慎重作出自己的假定。通常,我们很难预测一个类以后会以什么样的形式再生或重复利用。常规用途的 156

类尤其如此。若将一个方法定义成 f i nal ,就可能杜绝了在其他程序员的项目中对自己的类进行继承的途 径,因为我们根本没有想到它会象那样使用。 标准 Java 库是阐述这一观点的最好例子。其中特别常用的一个类是 Vect or 。如果我们考虑代码的执行效 率,就会发现只有不把任何方法设为 f i nal ,才能使其发挥更大的作用。我们很容易就会想到自己应继承和 覆盖如此有用的一个类,但它的设计者却否定了我们的想法。但我们至少可以用两个理由来反驳他们。首 先,St ack(堆栈)是从 Vect or 继承来的,亦即 St ack“是”一个 Vect or ,这种说法是不确切的。其次,对 于 Vect or 许多重要的方法,如 addEl em ( )以及 el em A ( )等,它们都变成了 s ynchr oni z ed ent ent t (同步 的)。正如在第 14 章要讲到的那样,这会造成显著的性能开销,可能会把 f i nal 提供的性能改善抵销得一干 二净。因此,程序员不得不猜测到底应该在哪里进行优化。在标准库里居然采用了如此笨拙的设计,真不敢 想象会在程序员里引发什么样的情绪。 另一个值得注意的是 Has ht abl e(散列表),它是另一个重要的标准类。该类没有采用任何 f i nal 方法。正 如我们在本书其他地方提到的那样,显然一些类的设计人员与其他设计人员有着全然不同的素质(注意比较 Has ht abl e 极短的方法名与 Vecor 的方法名)。对类库的用户来说,这显然是不应该如此轻易就能看出的。 一个产品的设计变得不一致后,会加大用户的工作量。这也从另一个侧面强调了代码设计与检查时需要很强 的责任心。

6. 9 初始化和类装载
在许多传统语言里,程序都是作为启动过程的一部分一次性载入的。随后进行的是初始化,再是正式执行程 序。在这些语言中,必须对初始化过程进行慎重的控制,保证 s t at i c数据的初始化不会带来麻烦。比如在一 个 s t at i c 数据获得初始化之前,就有另一个 s t at i c数据希望它是一个有效值,那么在 C ++中就会造成问 题。 Java 则没有这样的问题,因为它采用了不同的装载方法。由于 Java 中的一切东西都是对象,所以许多活动 变得更加简单,这个问题便是其中的一例。正如下一章会讲到的那样,每个对象的代码都存在于独立的文件 中。除非真的需要代码,否则那个文件是不会载入的。通常,我们可认为除非那个类的一个对象构造完毕, 否则代码不会真的载入。由于 s t at i c 方法存在一些细微的歧义,所以也能认为“类代码在首次使用的时候载 入”。 首次使用的地方也是 s t at i c 初始化发生的地方。装载的时候,所有 s t at i c对象和 s t at i c代码块都会按照本 来的顺序初始化(亦即它们在类定义代码里写入的顺序)。当然,s t at i c 数据只会初始化一次。

6 . 9 . 1 继承初始化
我们有必要对整个初始化过程有所认识,其中包括继承,对这个过程中发生的事情有一个整体性的概念。请 观察下述代码: //: Beet l e. j ava // T he f ul l pr oces s of i ni t i al i z at i on. cl as s I ns ect { i nt i = 9; i nt j ; I ns ect ( ) { pr t ( " i = " + i + " , j = " + j ) ; j = 39; } s t at i c i nt x1 = pr t ( " s t at i c I ns ect . x1 i ni t i al i zed" ) ; s t at i c i nt pr t ( St r i ng s ) { Sys t em out . pr i nt l n( s ) ; . r et ur n 47; } }

157

publ i c cl as s Beet l e ext ends I ns ect { i nt k = pr t ( " Beet l e. k i ni t i al i zed" ) ; Beet l e( ) { pr t ( " k = " + k) ; pr t ( " j = " + j ) ; } s t at i c i nt x2 = pr t ( " s t at i c Beet l e. x2 i ni t i al i zed" ) ; st at i c i nt pr t ( St r i ng s ) { Sys t em out . pr i nt l n( s ) ; . r et ur n 63; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai pr t ( " Beet l e cons t r uct or " ) ; Beet l e b = new Beet l e( ) ; } } ///: ~ 该程序的输出如下: s t at i c I ns ect . x i ni t i al i zed s t at i c Beet l e. x i ni t i al i zed Beet l e cons t r uct or i = 9, j = 0 Beet l e. k i ni t i al i zed k = 63 j = 39 对 Beet l e 运行 Java 时,发生的第一件事情是装载程序到外面找到那个类。在装载过程中,装载程序注意它 有一个基础类(即 ext ends 关键字要表达的意思),所以随之将其载入。无论是否准备生成那个基础类的一 个对象,这个过程都会发生(请试着将对象的创建代码当作注释标注出来,自己去证实)。 若基础类含有另一个基础类,则另一个基础类随即也会载入,以此类推。接下来,会在根基础类(此时是 I ns ect )执行 s t at i c 初始化,再在下一个衍生类执行,以此类推。保证这个顺序是非常关键的,因为衍生类 的初始化可能要依赖于对基础类成员的正确初始化。 此时,必要的类已全部装载完毕,所以能够创建对象。首先,这个对象中的所有基本数据类型都会设成它们 的默认值,而将对象句柄设为 nul l 。随后会调用基础类构建器。在这种情况下,调用是自动进行的。但也完 全可以用 s uper 来自行指定构建器调用(就象在 Beet l e( ) 构建器中的第一个操作一样)。基础类的构建采用 与衍生类构建器完全相同的处理过程。基础顺构建器完成以后,实例变量会按本来的顺序得以初始化。最 后,执行构建器剩余的主体部分。

6. 10 总结
无论继承还是合成,我们都可以在现有类型的基础上创建一个新类型。但在典型情况下,我们通过合成来实 现现有类型的“再生”或“重复使用”,将其作为新类型基础实施过程的一部分使用。但如果想实现接口的 “再生”,就应使用继承。由于衍生或派生出来的类拥有基础类的接口,所以能够将其“上溯造型”为基础 类。对于下一章要讲述的多形性问题,这一点是至关重要的。 尽管继承在面向对象的程序设计中得到了特别的强调,但在实际启动一个设计时,最好还是先考虑采用合成 技术。只有在特别必要的时候,才应考虑采用继承技术(下一章还会讲到这个问题)。合成显得更加灵活。 但是,通过对自己的成员类型应用一些继承技巧,可在运行期准确改变那些成员对象的类型,由此可改变它 们的行为。 尽管对于快速项目开发来说,通过合成和继承实现的代码再生具有很大的帮助作用。但在允许其他程序员完 全依赖它之前,一般都希望能重新设计自己的类结构。我们理想的类结构应该是每个类都有自己特定的用 158

途。它们不能过大(如集成的功能太多,则很难实现它的再生),也不能过小(造成不能由自己使用,或者 不能增添新功能)。最终实现的类应该能够方便地再生。

6. 11 练习
( 1) 用默认构建器(空自变量列表)创建两个类:A和 B,令它们自己声明自己。从 A继承一个名为 C的新 类,并在 C内创建一个成员 B。不要为 C创建一个构建器。创建类 C的一个对象,并观察结果。 ( 2) 修改练习 1,使 A和 B 都有含有自变量的构建器,则不是采用默认构建器。为 C写一个构建器,并在 C 的构建器中执行所有初始化工作。 ( 3) 使用文件 C t oon. j ava,将 C t oon类的构建器代码变成注释内容标注出去。解释会发生什么事情。 ar ar ( 4) 使用文件 C s . j ava,将 C s 类的构建器代码作为注释标注出去。同样解释会发生什么。 hes hes

159

第 7 章 多形性
“对于面向对象的程序设计语言,多型性是第三种最基本的特征(前两种是数据抽象和继承。” “多形性”(Pol ym phi s m or )从另一个角度将接口从具体的实施细节中分离出来,亦即实现了“是什么”与 “怎样做”两个模块的分离。利用多形性的概念,代码的组织以及可读性均能获得改善。此外,还能创建 “易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地“成 长”。 通过合并各种特征与行为,封装技术可创建出新的数据类型。通过对具体实施细节的隐藏,可将接口与实施 细节分离,使所有细节成为“pr i vat e”(私有)。这种组织方式使那些有程序化编程背景人感觉颇为舒适。 但多形性却涉及对“类型”的分解。通过上一章的学习,大家已知道通过继承可将一个对象当作它自己的类 型或者它自己的基础类型对待。这种能力是十分重要的,因为多个类型(从相同的基础类型中衍生出来)可 被当作同一种类型对待。而且只需一段代码,即可对所有不同的类型进行同样的处理。利用具有多形性的方 法调用,一种类型可将自己与另一种相似的类型区分开,只要它们都是从相同的基础类型中衍生出来的。这 种区分是通过各种方法在行为上的差异实现的,可通过基础类实现对那些方法的调用。 在这一章中,大家要由浅入深地学习有关多形性的问题(也叫作动态绑定、推迟绑定或者运行期绑定)。同 时举一些简单的例子,其中所有无关的部分都已剥除,只保留与多形性有关的代码。

7. 1 上溯造型
在第 6 章,大家已知道可将一个对象作为它自己的类型使用,或者作为它的基础类型的一个对象使用。取得 一个对象句柄,并将其作为基础类型句柄使用的行为就叫作“上溯造型”——因为继承树的画法是基础类位 于最上方。 但这样做也会遇到一个问题,如下例所示(若执行这个程序遇到麻烦,请参考第 3 章的 3. 1. 2 小节“赋 值”): //: M i c. j ava us // I nher i t ance & upcas t i ng package c07; cl as s N e { ot pr i vat e i nt val ue; pr i vat e N e( i nt val ) { val ue = val ; } ot publ i c s t at i c f i nal N e ot m ddl eC = new N e( 0) , i ot cShar p = new N e( 1) , ot cFl at = new N e( 2) ; ot } // Et c. cl as s I ns t r um ent { publ i c voi d pl ay( N e n) { ot Sys t em out . pr i nt l n( " I ns t r um . pl ay( ) " ) ; . ent } } // W nd obj ect s ar e i ns t r um s i ent // becaus e t hey have t he sam i nt er f ace: e cl as s W nd ext ends I ns t r um i ent { // Redef i ne i nt er f ace m hod: et publ i c voi d pl ay( N e n) { ot Sys t em out . pr i nt l n( " W nd. pl ay( ) " ) ; . i 160

} } publ i c cl as s M i c { us publ i c s t at i c voi d t une( I ns t r um ent i ) { // . . . i . pl ay( N e. m ddl eC ; ot i ) } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai W nd f l ut e = new W nd( ) ; i i t une( f l ut e) ; // Upcas t i ng } } ///: ~ 其中,方法 M i c. t une( )接收一个 I ns t r um 句柄,同时也接收从 I ns t r um 衍生出来的所有东西。当一 us ent ent 个 W nd 句柄传递给 t une( ) 的时候,就会出现这种情况。此时没有造型的必要。这样做是可以接受的; i I ns t r um 里的接口必须存在于 W nd 中,因为 W nd是从 I ns t r um 里继承得到的。从 W nd向I nstr umnt ent i i ent i e 的上溯造型可能“缩小”那个接口,但不可能把它变得比 I ns t r um 的完整接口还要小。 ent

7 . 1 . 1 为什么要上溯造型
这个程序看起来也许显得有些奇怪。为什么所有人都应该有意忘记一个对象的类型呢?进行上溯造型时,就 可能产生这方面的疑惑。而且如果让 t une( ) 简单地取得一个 W nd 句柄,将其作为自己的自变量使用,似乎 i 会更加简单、直观得多。但要注意:假如那样做,就需为系统内 I ns t r um 的每种类型写一个全新的 ent t une( )。假设按照前面的推论,加入 St r i nged(弦乐)和 Br as s (铜管)这两种 I ns t r um (乐器): ent //: M i c2. j ava us // O l oadi ng i ns t ead of upcas t i ng ver cl as s N e2 { ot pr i vat e i nt val ue; pr i vat e N e2( i nt val ) { val ue = val ; } ot publ i c s t at i c f i nal N e2 ot m ddl eC = new N e2( 0) , i ot cShar p = new Not e2( 1) , cFl at = new N e2( 2) ; ot } // Et c. cl as s I ns t r um 2 { ent publ i c voi d pl ay( N e2 n) { ot Sys t em out . pr i nt l n( " I ns t r um 2. pl ay( ) " ) ; . ent } } cl as s W nd2 ext ends I ns t r um 2 { i ent publ i c voi d pl ay( N e2 n) { ot Sys t em out . pr i nt l n( " W nd2. pl ay( ) " ) ; . i } } cl as s St r i nged2 ext ends I ns t r um 2 { ent publ i c voi d pl ay( N e2 n) { ot Sys t em out . pr i nt l n( " St r i nged2. pl ay( ) " ) ; . 161

} } cl as s Br as s 2 ext ends I ns t r um 2 { ent publ i c voi d pl ay( N e2 n) { ot Sys t em out . pr i nt l n( " Br as s 2. pl ay( ) " ) ; . } } publ i c cl as s M i c2 { us publ i c s t at i c voi d t une( W nd2 i ) { i i . pl ay( N e2. m ddl eC ; ot i ) } publ i c s t at i c voi d t une( St r i nged2 i ) { i . pl ay( N e2. m ddl eC ; ot i ) } publ i c s t at i c voi d t une( Br as s 2 i ) { i . pl ay( N e2. m ddl eC ; ot i ) } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai W nd2 f l ut e = new W nd2( ) ; i i St r i nged2 vi ol i n = new St r i nged2( ) ; Br as s 2 f r enchHor n = new Br as s 2( ) ; t une( f l ut e) ; // N upcas t i ng o t une( vi ol i n) ; t une( f r enchHor n) ; } } ///: ~ 这样做当然行得通,但却存在一个极大的弊端:必须为每种新增的 I ns t r um 2 类编写与类紧密相关的方 ent 法。这意味着第一次就要求多得多的编程量。以后,假如想添加一个象 t une( ) 那样的新方法或者为 I ns t r um 添加一个新类型,仍然需要进行大量编码工作。此外,即使忘记对自己的某个方法进行过载设 ent 置,编译器也不会提示任何错误。这样一来,类型的整个操作过程就显得极难管理,有失控的危险。 但假如只写一个方法,将基础类作为自变量或参数使用,而不是使用那些特定的衍生类,岂不是会简单得 多?也就是说,如果我们能不顾衍生类,只让自己的代码与基础类打交道,那么省下的工作量将是难以估计 的。 这正是“多形性”大显身手的地方。然而,大多数程序员(特别是有程序化编程背景的)对于多形性的工作 原理仍然显得有些生疏。

7. 2 深入理解
对于 M i c. j ava的困难性,可通过运行程序加以体会。输出是 W nd. pl ay( ) 。这当然是我们希望的输出,但 us i 它看起来似乎并不愿按我们的希望行事。请观察一下 t une( ) 方法: publ i c s t at i c voi d t une( I ns t r um ent i ) { // . . . i . pl ay( N e. m d eC ; ot i dl ) } 它接收 I ns t r um 句柄。所以在这种情况下,编译器怎样才能知道 I ns t r um 句柄指向的是一个 W nd,而 ent ent i 不是一个 Br as s 或 St r i nged 呢?编译器无从得知。为了深入了理解这个问题,我们有必要探讨一下“绑定” 这个主题。 162

7 . 2 . 1 方法调用的绑定
将一个方法调用同一个方法主体连接到一起就称为“绑定”(Bi ndi ng )。若在程序运行以前执行绑定(由编 译器和链接程序,如果有的话),就叫作“早期绑定”。大家以前或许从未听说过这个术语,因为它在任何 程序化语言里都是不可能的。C编译器只有一种方法调用,那就是“早期绑定”。 上述程序最令人迷惑不解的地方全与早期绑定有关,因为在只有一个 I ns t r um 句柄的前提下,编译器不知 ent 道具体该调用哪个方法。 解决的方法就是“后期绑定”,它意味着绑定在运行期间进行,以对象的类型为基础。后期绑定也叫作“动 态绑定”或“运行期绑定”。若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象 的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去 调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为: 它们都要在对象中安插某些特殊类型的信息。 Java 中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成 f i nal 。这意味着我们通常不必决定 是否应进行后期绑定——它是自动发生的。 为什么要把一个方法声明成 f i nal 呢?正如上一章指出的那样,它能防止其他人覆盖那个方法。但也许更重 要的一点是,它可有效地“关闭”动态绑定,或者告诉编译器不需要进行动态绑定。这样一来,编译器就可 为 f i nal 方法调用生成效率更高的代码。

7 . 2 . 2 产生正确的行为
知道 Java 里绑定的所有方法都通过后期绑定具有多形性以后,就可以相应地编写自己的代码,令其与基础类 沟通。此时,所有的衍生类都保证能用相同的代码正常地工作。或者换用另一种方法,我们可以“将一条消 息发给一个对象,让对象自行判断要做什么事情。” 在面向对象的程序设计中,有一个经典的“形状”例子。由于它很容易用可视化的形式表现出来,所以经常 都用它说明问题。但很不幸的是,它可能误导初学者认为 O P 只是为图形化编程设计的,这种认识当然是错 O 误的。 形状例子有一个基础类,名为 Shape;另外还有大量衍生类型:C r cl e(圆形),Squar e(方形), i T r i angl e(三角形)等等。大家之所以喜欢这个例子,因为很容易理解“圆属于形状的一种类型”等概念。 下面这幅继承图向我们展示了它们的关系:

上溯造型可用下面这个语句简单地表现出来: Shape s = new C r cl e( ) ; i 在这里,我们创建了 C r cl e 对象,并将结果句柄立即赋给一个 Shape。这表面看起来似乎属于错误操作(将 i 一种类型分配给另一个),但实际是完全可行的——因为按照继承关系,C r cl e属于 Shape 的一种。因此编 i 译器认可上述语句,不会向我们提示一条出错消息。 当我们调用其中一个基础类方法时(已在衍生类里覆盖): s . dr aw ) ; ( 同样地,大家也许认为会调用 Shape 的 dr aw ),因为这毕竟是一个 Shape句柄。那么编译器怎样才能知道该 ( 163

做其他任何事情呢?但此时实际调用的是 C r cl e. dr aw ) ,因为后期绑定已经介入(多形性)。 i ( 下面这个例子从一个稍微不同的角度说明了问题: //: Shapes . j ava // Pol ym phi s m i n Java or cl as s Shape { voi d dr aw ) { } ( voi d er as e( ) { } } cl as s C r cl e ext ends Shape { i voi d dr aw ) { ( Sys t em out . pr i nt l n( " C r cl e. dr aw ) " ) ; . i ( } voi d er as e( ) { Sys t em out . pr i nt l n( " C r cl e. er as e( ) " ) ; . i } } cl as s Squar e ext ends Shape { voi d dr aw ) { ( Sys t em out . pr i nt l n( " Squar e. dr aw ) " ) ; . ( } voi d er as e( ) { Sys t em out . pr i nt l n( " Squar e. er as e( ) " ) ; . } } cl as s Tr i angl e ext ends Shape { voi d dr aw ) { ( Sys t em out . pr i nt l n( " Tr i angl e. dr aw ) " ) ; . ( } voi d er as e( ) { Sys t em out . pr i nt l n( " Tr i angl e. er as e( ) " ) ; . } } publ i c cl as s Shapes { publ i c s t at i c Shape r andShape( ) { s w t ch( ( i nt ) ( M h. r andom ) * 3) ) { i at ( def aul t : // To qui et t he com l er pi cas e 0: r et ur n new C r cl e( ) ; i cas e 1: r et ur n new Squar e( ) ; cas e 2: r et ur n new Tr i angl e( ) ; } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Shape[ ] s = new Shape[ 9] ; // Fi l l up t he ar r ay w t h s hapes : i f or ( i nt i = 0; i < s . l engt h; i ++) s [ i ] = r andShape( ) ; 164

// M ake pol ym phi c m hod cal l s : or et f or ( i nt i = 0; i < s . l engt h; i ++) s [ i ] . dr aw ) ; ( } } ///: ~ 针对从 Shape 衍生出来的所有东西,Shape 建立了一个通用接口——也就是说,所有(几何)形状都可以描 绘和删除。衍生类覆盖了这些定义,为每种特殊类型的几何形状都提供了独一无二的行为。 在主类 Shapes 里,包含了一个 s t at i c 方法,名为 r andShape( )。它的作用是在每次调用它时为某个随机选 择的 Shape 对象生成一个句柄。请注意上溯造型是在每个 r et ur n 语句里发生的。这个语句取得指向一个 C r cl e i ,Squar e 或者 Tr i angl e 的句柄,并将其作为返回类型 Shape发给方法。所以无论什么时候调用这个 方法,就绝对没机会了解它的具体类型到底是什么,因为肯定会获得一个单纯的 Shape 句柄。 m n( )包含了 Shape 句柄的一个数组,其中的数据通过对 r andShape( ) 的调用填入。在这个时候,我们知道 ai 自己拥有 Shape,但不知除此之外任何具体的情况(编译器同样不知)。然而,当我们在这个数组里步进, 并为每个元素调用 dr aw ) 的时候,与各类型有关的正确行为会魔术般地发生,就象下面这个输出示例展示的 ( 那样: C r cl e. dr aw ) i ( Tr i angl e. dr aw ) ( C r cl e. dr aw ) i ( C r cl e. dr aw ) i ( C r cl e. dr aw ) i ( Squar e. dr aw ) ( Tr i angl e. dr aw ) ( Squar e. dr aw ) ( Squar e. dr aw ) ( 当然,由于几何形状是每次随机选择的,所以每次运行都可能有不同的结果。之所以要突出形状的随机选 择,是为了让大家深刻体会这一点:为了在编译的时候发出正确的调用,编译器毋需获得任何特殊的情报。 对 dr aw ) 的所有调用都是通过动态绑定进行的。 (

7 . 2 . 3 扩展性
现在,让我们仍然返回乐器(I ns t r um )示例。由于存在多形性,所以可根据自己的需要向系统里加入任 ent 意多的新类型,同时毋需更改 t r ue( ) 方法。在一个设计良好的 O P 程序中,我们的大多数或者所有方法都会 O 遵从 t une( ) 的模型,而且只与基础类接口通信。我们说这样的程序具有“扩展性”,因为可以从通用的基础 类继承新的数据类型,从而新添一些功能。如果是为了适应新类的要求,那么对基础类接口进行操纵的方法 根本不需要改变, 对于乐器例子,假设我们在基础类里加入更多的方法,以及一系列新类,那么会出现什么情况呢?下面是示 意图:

165

所有这些新类都能与老类——t une( ) 默契地工作,毋需对 t une( ) 作任何调整。即使 t une( )位于一个独立的文 件里,而将新方法添加到 I ns t r um 的接口,t une( ) 也能正确地工作,不需要重新编译。下面这个程序是对 ent 上述示意图的具体实现: //: M i c3. j ava us // A ext ens i bl e pr ogr am n i m t j ava. ut i l . * ; por cl as s I ns t r um 3 { ent publ i c voi d pl ay( ) { Sys t em out . pr i nt l n( " I ns t r um 3. pl ay( ) " ) ; . ent } publ i c St r i ng w ( ) { hat r et ur n " I ns t r um 3" ; ent } publ i c voi d adj us t ( ) { } } cl as s W nd3 ext ends I ns t r um 3 { i ent publ i c voi d pl ay( ) { Sys t em out . pr i nt l n( " W nd3. pl ay( ) " ) ; . i } publ i c St r i ng w ( ) { r et ur n " W nd3" ; } hat i publ i c voi d adj us t ( ) { } } cl as s Per cus s i on3 ext ends I ns t r um 3 { ent publ i c voi d pl ay( ) { 166

Sys t em out . pr i nt l n( " Per cus s i on3. pl ay( ) " ) ; . } publ i c St r i ng w ( ) { r et ur n " Per cus s i on3" ; } hat publ i c voi d adj us t ( ) { } } cl as s St r i nged3 ext ends I ns t r um 3 { ent publ i c voi d pl ay( ) { Sys t em out . pr i nt l n( " St r i nged3. pl ay( ) " ) ; . } publ i c St r i ng w ( ) { r et ur n " St r i nged3" ; } hat publ i c voi d adj us t ( ) { } } cl as s Br as s 3 ext ends W nd3 { i publ i c voi d pl ay( ) { Sys t em out . pr i nt l n( " Br as s 3. pl ay( ) " ) ; . } publ i c voi d adj us t ( ) { Sys t em out . pr i nt l n( " Br as s 3. adj us t ( ) " ) ; . } } cl as s W oodw nd3 ext ends W nd3 { i i publ i c voi d pl ay( ) { Sys t em out . pr i nt l n( " W . oodw nd3. pl ay( ) " ) ; i } publ i c St r i ng w ( ) { r et ur n " W hat oodw nd3" ; } i } publ i c cl as s M i c3 { us // D n' t car e about t ype, s o new t ypes oes // added t o t he s ys t em s t i l l w k r i ght : or s t at i c voi d t une( I ns t r um 3 i ) { ent // . . . i . pl ay( ) ; } s t at i c voi d t uneA l ( I ns t r um 3[ ] e) { l ent f or ( i nt i = 0; i < e. l engt h; i ++) t une( e[ i ] ) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai I ns t r um 3[ ] or ches t r a = new I ns t r um 3[ 5] ; ent ent i nt i = 0; // Upcas t i ng dur i ng addi t i on t o t he ar r ay: or ches t r a[ i ++] = new W nd3( ) ; i or ches t r a[ i ++] = new Per cus s i on3( ) ; or ches t r a[ i ++] = new St r i nged3( ) ; or ches t r a[ i ++] = new Br as s 3( ) ; or ches t r a[ i ++] = new W oodw nd3( ) ; i t uneA l ( or ches t r a) ; l } 167

} ///: ~ 新方法是 w ( ) 和 adj us t ( ) 。前者返回一个 St r i ng句柄,同时返回对那个类的说明;后者使我们能对每种 hat 乐器进行调整。 在 m n( ) 中,当我们将某样东西置入 I ns t r um 3 数组时,就会自动上溯造型到 I ns t r um 3。 ai ent ent 可以看到,在围绕 t une( ) 方法的其他所有代码都发生变化的同时,t une( )方法却丝毫不受它们的影响,依然 故我地正常工作。这正是利用多形性希望达到的目标。我们对代码进行修改后,不会对程序中不应受到影响 的部分造成影响。此外,我们认为多形性是一种至关重要的技术,它允许程序员“将发生改变的东西同没有 发生改变的东西区分开”。

7. 3 覆盖与过载
现在让我们用不同的眼光来看看本章的头一个例子。在下面这个程序中,方法 pl ay( ) 的接口会在被覆盖的过 程中发生变化。这意味着我们实际并没有“覆盖”方法,而是使其“过载”。编译器允许我们对方法进行过 载处理,使其不报告出错。但这种行为可能并不是我们所希望的。下面是这个例子: //: W ndEr r or . j ava i // A dent al l y changi ng t he i nt er f ace cci cl as s N eX { ot publ i c s t at i c f i nal i nt M D LE_C = 0, C I D _SHA = 1, C RP _FLA = 2; T } cl as s I ns t r um X { ent publ i c voi d pl ay( i nt N eX) { ot Sys t em out . pr i nt l n( " I ns t r um X. pl ay( ) " ) ; . ent } } cl as s W ndX ext ends I ns t r um X { i ent // O PS! C O hanges t he m hod i nt er f ace: et publ i c voi d pl ay( N eX n) { ot Sys t em out . pr i nt l n( " W ndX. pl ay( N eX n) " ) ; . i ot } } publ i c cl as s W nd r or { i Er publ i c s t at i c voi d t une( I ns t r um X i ) { ent // . . . i . pl ay( N eX. M D LE_C ; ot I D ) } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai W ndX f l ut e = new W ndX( ) ; i i t une( f l ut e) ; // N t he des i r ed behavi or ! ot } } ///: ~ 这里还向大家引入了另一个易于混淆的概念。在 I ns t r um X 中,pl ay( ) 方法采用了一个 i nt (整数)数 ent 值,它的标识符是 N eX。也就是说,即使 N eX 是一个类名,也可以把它作为一个标识符使用,编译器不 ot ot 会报告出错。但在 W ndX 中,pl ay( ) 采用一个 N eX 句柄,它有一个标识符 n。即便我们使用“pl ay( N eX i ot ot N eX)”,编译器也不会报告错误。这样一来,看起来就象是程序员有意覆盖 pl ay( )的功能,但对方法的类 ot 型定义却稍微有些不确切。然而,编译器此时假定的是程序员有意进行“过载”,而非“覆盖”。请仔细体 168

会这两个术语的区别。“过载”是指同一样东西在不同的地方具有多种含义;而“覆盖”是指它随时随地都 只有一种含义,只是原先的含义完全被后来的含义取代了。请注意如果遵守标准的 Java 命名规范,自变量标 识符就应该是 not eX,这样可把它与类名区分开。 在 t une 中,“I ns t r um X i ”会发出 pl ay( )消息,同时将某个 N eX 成员作为自变量使用(M D LE_C ent ot I D )。 由于 N eX 包含了 i nt 定义,过载的 pl ay( ) 方法的 i nt 版本会得到调用。同时由于它尚未被“覆盖”,所以 ot 会使用基础类版本。 输出是: I ns t r um X. pl ay( ) ent

7. 4 抽象类和方法
在我们所有乐器(I ns t r um )例子中,基础类 I ns t r um 内的方法都肯定是“伪”方法。若去调用这些方 ent ent 法,就会出现错误。那是由于 I ns t r um 的意图是为从它衍生出去的所有类都创建一个通用接口。 ent 之所以要建立这个通用接口,唯一的原因就是它能为不同的子类型作出不同的表示。它为我们建立了一种基 本形式,使我们能定义在所有衍生类里“通用”的一些东西。为阐述这个观念,另一个方法是把 I ns t r um ent 称为“抽象基础类”(简称“抽象类”)。若想通过该通用接口处理一系列类,就需要创建一个抽象类。对 所有与基础类声明的签名相符的衍生类方法,都可以通过动态绑定机制进行调用(然而,正如上一节指出的 那样,如果方法名与基础类相同,但自变量或参数不同,就会出现过载现象,那或许并非我们所愿意的)。 如果有一个象 I ns t r um 那样的抽象类,那个类的对象几乎肯定没有什么意义。换言之,I ns t r um 的作 ent ent 用仅仅是表达接口,而不是表达一些具体的实施细节。所以创建一个 I ns t r um 对象是没有意义的,而且我 ent 们通常都应禁止用户那样做。为达到这个目的,可令 I ns t r um 内的所有方法都显示出错消息。但这样做会 ent 延迟信息到运行期,并要求在用户那一面进行彻底、可靠的测试。无论如何,最好的方法都是在编译期间捕 捉到问题。 针对这个问题,Java 专门提供了一种机制,名为“抽象方法”。它属于一种不完整的方法,只含有一个声 明,没有方法主体。下面是抽象方法声明时采用的语法: abs t r act voi d X( ) ; 包含了抽象方法的一个类叫作“抽象类”。如果一个类里包含了一个或多个抽象方法,类就必须指定成 abs t r act (抽象)。否则,编译器会向我们报告一条出错消息。 若一个抽象类是不完整的,那么一旦有人试图生成那个类的一个对象,编译器又会采取什么行动呢?由于不 能安全地为一个抽象类创建属于它的对象,所以会从编译器那里获得一条出错提示。通过这种方法,编译器 可保证抽象类的“纯洁性”,我们不必担心会误用它。 如果从一个抽象类继承,而且想生成新类型的一个对象,就必须为基础类中的所有抽象方法提供方法定义。 如果不这样做(完全可以选择不做),则衍生类也会是抽象的,而且编译器会强迫我们用 abs t r act 关键字标 志那个类的“抽象”本质。 即使不包括任何 abs t r act 方法,亦可将一个类声明成“抽象类”。如果一个类没必要拥有任何抽象方法,而 且我们想禁止那个类的所有实例,这种能力就会显得非常有用。 I ns t r um 类可很轻松地转换成一个抽象类。只有其中一部分方法会变成抽象方法,因为使一个类抽象以 ent 后,并不会强迫我们将它的所有方法都同时变成抽象。下面是它看起来的样子:

169

下面是我们修改过的“管弦”乐器例子,其中采用了抽象类以及方法: //: M i c4. j ava us // A t r act cl as s es and m hods bs et i m t j ava. ut i l . * ; por abs t r act cl as s I ns t r um 4 { ent i nt i ; // s t or age al l ocat ed f or each publ i c abs t r act voi d pl ay( ) ; publ i c St r i ng w ( ) { hat r et ur n " I ns t r um 4" ; ent } publ i c abs t r act voi d adj us t ( ) ; } cl as s W nd4 ext ends I ns t r um 4 { i ent publ i c voi d pl ay( ) { Sys t em out . pr i nt l n( " W nd4. pl ay( ) " ) ; . i } publ i c St r i ng w ( ) { r et ur n " W nd4" ; } hat i publ i c voi d adj us t ( ) { } } cl as s Per cus s i on4 ext ends I ns t r um 4 { ent p i c voi d pl ay( ) { ubl Sys t em out . pr i nt l n( " Per cus s i on4. pl ay( ) " ) ; . } publ i c St r i ng w ( ) { r et ur n " Per cus s i on4" ; } hat 170

publ i c voi d adj us t ( ) { } } cl as s St r i nged4 ext ends I ns t r um 4 { ent publ i c voi d pl ay( ) { Sys t em out . pr i nt l n( " St r i nged4. pl ay( ) " ) ; . } publ i c St r i ng w ( ) { r et ur n " St r i nged4" ; } hat publ i c voi d adj us t ( ) { } } cl as s Br as s 4 ext ends W nd4 { i publ i c voi d pl ay( ) { Sys t em out . pr i nt l n( " Br as s 4. pl ay( ) " ) ; . } publ i c voi d adj us t ( ) { Sys t em out . pr i nt l n( " Br as s 4. adj us t ( ) " ) ; . } } cl as s W oodw nd4 ext ends W nd4 { i i publ i c voi d pl ay( ) { Sys t em out . pr i nt l n( " W . oodw nd4. pl ay( ) " ) ; i } publ i c St r i ng w ( ) { r et ur n " W hat oodw nd4" ; } i } publ i c cl as s M i c4 { us // D n' t car e about t ype, s o new t ypes oes // added t o t he s ys t em s t i l l w k r i ght : or s t at i c voi d t une( I ns t r um 4 i ) { ent // . . . i . pl ay( ) ; } s t at i c voi d t uneA l ( I ns t r um 4[ ] e) { l ent f or ( i nt i = 0; i < e. l engt h; i ++) t une( e[ i ] ) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai I ns t r um 4[ ] or ches t r a = new I ns t r um 4[ 5]; ent ent i nt i = 0; // Upcas t i ng dur i ng addi t i on t o t he ar r ay: or ches t r a[ i ++] = new W nd4( ) ; i or ches t r a[ i ++] = new Per cus s i on4( ) ; or ches t r a[ i ++] = new St r i nged4( ) ; or ches t r a[ i ++] = new Br as s 4( ) ; or ches t r a[ i ++] = new W oodw nd4( ) ; i t uneA l ( or ches t r a) ; l } } ///: ~ 可以看出,除基础类以外,实际并没有进行什么改变。 171

创建抽象类和方法有时对我们非常有用,因为它们使一个类的抽象变成明显的事实,可明确告诉用户和编译 器自己打算如何用它。

7. 5 接口
“i nt er f ace”(接口)关键字使抽象的概念更深入了一层。我们可将其想象为一个“纯”抽象类。它允许创 建者规定一个类的基本形式:方法名、自变量列表以及返回类型,但不规定方法主体。接口也包含了基本数 据类型的数据成员,但它们都默认为 s t at i c 和 f i nal 。接口只提供一种形式,并不提供实施的细节。 接口这样描述自己:“对于实现我的所有类,看起来都应该象我现在这个样子”。因此,采用了一个特定接 口的所有代码都知道对于那个接口可能会调用什么方法。这便是接口的全部含义。所以我们常把接口用于建 立类和类之间的一个“协议”。有些面向对象的程序设计语言采用了一个名为“pr ot ocol ”(协议)的关键 字,它做的便是与接口相同的事情。 为创建一个接口,请使用 i nt er f ace 关键字,而不要用 cl as s 。与类相似,我们可在 i nt er f ace关键字的前 面增加一个 publ i c关键字(但只有接口定义于同名的一个文件内);或者将其省略,营造一种“友好的”状 态。 为了生成与一个特定的接口(或一组接口)相符的类,要使用 i m em s (实现)关键字。我们要表达的意 pl ent 思是“接口看起来就象那个样子,这儿是它具体的工作细节”。除这些之外,我们其他的工作都与继承极为 相似。下面是乐器例子的示意图:

具体实现了一个接口以后,就获得了一个普通的类,可用标准方式对其进行扩展。 可决定将一个接口中的方法声明明确定义为“publ i c”。但即便不明确定义,它们也会默认为 publ i c。所以 在实现一个接口的时候,来自接口的方法必须定义成 publ i c。否则的话,它们会默认为“友好的”,而且会 限制我们在继承过程中对一个方法的访问——Java 编译器不允许我们那样做。 在 I ns t r um 例子的修改版本中,大家可明确地看出这一点。注意接口中的每个方法都严格地是一个声明, ent 它是编译器唯一允许的。除此以外,I ns t r um 5 中没有一个方法被声明为 publ i c,但它们都会自动获得 ent publ i c属性。如下所示: //: M i c5. j ava us // I nt er f aces 172

i m t j ava. ut i l . * ; por i nt er f ace I ns t r um 5 { ent // C pi l e t i m cons t ant : om e i nt i = 5; // s t at i c & f i nal // C annot have m hod def i ni t i ons : et voi d pl ay( ) ; // A om i cal l y publ i c ut at St r i ng w ( ) ; hat voi d adj us t ( ) ; } cl as s W nd5 i m em s I ns t r um 5 { i pl ent ent publ i c voi d pl ay( ) { Sys t em out . pr i nt l n( " W nd5. pl ay( ) " ) ; . i } publ i c St r i ng w ( ) { r et ur n " W nd5" ; } hat i publ i c voi d adj us t ( ) { } } cl as s Per cus s i on5 i m em s I ns t r um 5 { pl ent ent publ i c voi d pl ay( ) { Sys t em out . pr i nt l n( " Per cus s i on5. pl ay( ) " ) ; . } publ i c St r i ng w ( ) { r et ur n " Per cus s i on5" ; } hat publ i c voi d adj us t ( ) { } } cl as s St r i nged5 i m em s I ns t r um 5 { pl ent ent publ i c voi d pl ay( ) { Sys t em out . pr i nt l n( " St r i nged5. pl ay( ) " ) ; . } publ i c St r i ng w ( ) { r et ur n " St r i nged5" ; } hat publ i c voi d adj us t ( ) { } } cl as s Br as s 5 ext ends W nd5 { i publ i c voi d pl ay( ) { Sys t em out . pr i nt l n( " Br as s 5. pl ay( ) " ) ; . } publ i c voi d adj us t ( ) { Sys t em out . pr i nt l n( " Br as s 5. adj us t ( ) " ) ; . } } cl as s W oodw nd5 ext ends W nd5 { i i publ i c voi d pl ay( ) { Sys t em out . pr i nt l n( " W . oodw nd5. pl ay( ) " ) ; i } publ i c St r i ng w ( ) { r et ur n " W hat oodw nd5" ; } i } publ i c cl as s M i c5 { us 173

// D n' t car e about t ype, s o new t ypes oes // added t o t he s ys t em s t i l l w k r i ght : or s t at i c voi d t une( I ns t r um 5 i ) { ent // . . . i . pl ay( ) ; } s t at i c voi d t uneA l ( I ns t r um 5[ ] e) { l ent f or ( i nt i = 0; i < e. l engt h; i ++) t une( e[ i ] ) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai I ns t r um 5[ ] or ches t r a = new I ns t r um 5[ 5] ; ent ent i nt i = 0; // Upcas t i ng dur i ng addi t i on t o t he ar r ay: or ches t r a[ i ++] = new W nd5( ) ; i or ches t r a[ i ++] = new Per cus s i on5( ) ; or ches t r a[ i ++] = new St r i nged5( ) ; or ches t r a[ i ++] = new Br as s 5( ) ; or ches t r a[ i ++] = new W oodw nd5( ) ; i t uneA l ( or ches t r a) ; l } } ///: ~ 代码剩余的部分按相同的方式工作。我们可以自由决定上溯造型到一个名为 I ns t r um 5 的“普通”类,一 ent 个名为 I ns t r um 5 的“抽象”类,或者一个名为 I ns t r um 5 的“接口”。所有行为都是相同的。事实 ent ent 上,我们在 t une( )方法中可以发现没有任何证据显示 I ns t r um 5 到底是个“普通”类、“抽象”类还是一 ent 个“接口”。这是做是故意的:每种方法都使程序员能对对象的创建与使用进行不同的控制。

7 . 5 . 1 J av a 的“多重继承”
接口只是比抽象类“更纯”的一种形式。它的用途并不止那些。由于接口根本没有具体的实施细节——也就 是说,没有与存储空间与“接口”关联在一起——所以没有任何办法可以防止多个接口合并到一起。这一点 是至关重要的,因为我们经常都需要表达这样一个意思:“x 从属于 a ,也从属于 b,也从属于 c”。在 C ++ 中,将多个类合并到一起的行动称作“多重继承”,而且操作较为不便,因为每个类都可能有一套自己的实 施细节。在 Java 中,我们可采取同样的行动,但只有其中一个类拥有具体的实施细节。所以在合并多个接口 的时候,C ++的问题不会在 Java 中重演。如下所示:

在一个衍生类中,我们并不一定要拥有一个抽象或具体(没有抽象方法)的基础类。如果确实想从一个非接 口继承,那么只能从一个继承。剩余的所有基本元素都必须是“接口”。我们将所有接口名置于 i m em s pl ent 关键字的后面,并用逗号分隔它们。可根据需要使用多个接口,而且每个接口都会成为一个独立的类型,可 对其进行上溯造型。下面这个例子展示了一个“具体”类同几个接口合并的情况,它最终生成了一个新类:

174

//: A dvent ur e. j ava // M t i pl e i nt er f aces ul i m t j ava. ut i l . * ; por i nt er f ace C anFi ght { voi d f i ght ( ) ; } i nt er f ace C anSw m { i voi d s w m ) ; i ( } i nt er f ace C anFl y { voi d f l y( ) ; } cl as s A i onC act er { ct har publ i c voi d f i ght ( ) { } } cl as s Her o ext ends A i onC act er ct har i m em s C pl ent anFi ght , C anSw m C i , anFl y { publ i c voi d s w m ) { } i ( publ i c voi d f l y( ) { } } publ i c cl as s A dvent ur e { s t at i c voi d t ( C anFi ght x) { x. f i ght ( ) ; } s t at i c voi d u( C anSw m x) { x. s w m ) ; } i i ( s t at i c voi d v( C anFl y x) { x. f l y( ) ; } s t at i c voi d w A i onC act er x) { x. f i ght ( ) ; } ( ct har publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Her o i = new Her o( ) ; t ( i ) ; // Tr eat i t as a C anFi ght u( i ) ; // Tr eat i t as a C anSw m i v( i ) ; // Tr eat i t as a C anFl y w i ) ; // Tr eat i t as an A i onC act er ( ct har } } ///: ~ 从中可以看到,Her o 将具体类 A i onC act er 同接口 C ct har anFi ght ,C anSw m以及 C i anFl y 合并起来。按这种 形式合并一个具体类与接口的时候,具体类必须首先出现,然后才是接口(否则编译器会报错)。 请注意 f i ght ( ) 的签名在 C anFi ght 接口与 A i onC act er 类中是相同的,而且没有在 H o 中为f i g ()提 ct har er ht 供一个具体的定义。接口的规则是:我们可以从它继承(稍后就会看到),但这样得到的将是另一个接口。 如果想创建新类型的一个对象,它就必须是已提供所有定义的一个类。尽管 Her o 没有为 f i ght ( )明确地提供 一个定义,但定义是随同 A i onC act er 来的,所以这个定义会自动提供,我们可以创建 Her o 的对象。 ct har 在类 A dvent ur e 中,我们可看到共有四个方法,它们将不同的接口和具体类作为自己的自变量使用。创建一 个 Her o 对象后,它可以传递给这些方法中的任何一个。这意味着它们会依次上溯造型到每一个接口。由于接 口是用 Java 设计的,所以这样做不会有任何问题,而且程序员不必对此加以任何特别的关注。 注意上述例子已向我们揭示了接口最关键的作用,也是使用接口最重要的一个原因:能上溯造型至多个基础 类。使用接口的第二个原因与使用抽象基础类的原因是一样的:防止客户程序员制作这个类的一个对象,以 175

及规定它仅仅是一个接口。这样便带来了一个问题:到底应该使用一个接口还是一个抽象类呢?若使用接 口,我们可以同时获得抽象类以及接口的好处。所以假如想创建的基础类没有任何方法定义或者成员变量, 那么无论如何都愿意使用接口,而不要选择抽象类。事实上,如果事先知道某种东西会成为基础类,那么第 一个选择就是把它变成一个接口。只有在必须使用方法定义或者成员变量的时候,才应考虑采用抽象类。

7 . 5 . 2 通过继承扩展接口
利用继承技术,可方便地为一个接口添加新的方法声明,也可以将几个接口合并成一个新接口。在这两种情 况下,最终得到的都是一个新接口,如下例所示: //: Hor r or Show j ava . // Ext endi ng an i nt er f ace w t h i nher i t ance i i nt er f ace M t er { ons voi d m enace( ) ; } i nt er f ace D anger ous M t er ext ends M t er { ons ons voi d des t r oy( ) ; } i nt er f ace Let hal { voi d ki l l ( ) ; } cl as s D agonZi l l a i m em s D r pl ent anger ous M t er { ons publ i c voi d m enace( ) { } publ i c voi d des t r oy( ) { } } i nt er f ace Vam r e pi ext ends D anger ous M t er , Let hal { ons voi d dr i nkBl ood( ) ; } cl as s Hor r or Show { s t at i c voi d u( M t er b) { b. m ons enace( ) ; } s t at i c voi d v( D anger ous M t er d) { ons d. m enace( ) ; d. des t r oy( ) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai D agonZi l l a i f 2 = new D agonZi l l a( ) ; r r u( i f 2) ; v( i f 2) ; } } ///: ~ D anger ous M t er 是对 M t er 的一个简单的扩展,最终生成了一个新接口。这是在 D agonZi l l a里实现 ons ons r 的。 Vam r e的语法仅在继承接口时才可使用。通常,我们只能对单独一个类应用 ext ends(扩展)关键字。但由 pi 于接口可能由多个其他接口构成,所以在构建一个新接口时,ext ends 可能引用多个基础接口。正如大家看 到的那样,接口的名字只是简单地使用逗号分隔。 176

7 . 5 . 3 常数分组
由于置入一个接口的所有字段都自动具有 s t at i c 和 f i nal 属性,所以接口是对常数值进行分组的一个好工 具,它具有与 C或 C ++的 enum非常相似的效果。如下例所示: //: M hs . j ava ont // Us i ng i nt er f aces t o cr eat e gr oups of cons t ant s package c07; publ i c i nt er f ace M hs { ont i nt JA UA = 1, FEBRUA = 2, M RC = 3, N RY RY A H A L = 4, M Y = 5, JUN = 6, JULY = 7, PRI A E A UST = 8, SEPTEM UG BER = 9, O TO C BER = 10, N VEM O BER = 11, D EM EC BER = 12; } ///: ~ 注意根据 Java 命名规则,拥有固定标识符的 s t at i c f i nal 基本数据类型(亦即编译期常数)都全部采用大 写字母(用下划线分隔单个标识符里的多个单词)。 接口中的字段会自动具备 publ i c 属性,所以没必要专门指定。 现在,通过导入 c07. *或 c07. M hs ,我们可以从包的外部使用常数——就象对其他任何包进行的操作那 ont 样。此外,也可以用类似 M hs . JA UA 的表达式对值进行引用。当然,我们获得的只是一个 i nt ,所以不 ont N RY 象C ++的 enum那样拥有额外的类型安全性。但与将数字强行编码(硬编码)到自己的程序中相比,这种(常 用的)技术无疑已经是一个巨大的进步。我们通常把“硬编码”数字的行为称为“魔术数字”,它产生的代 码是非常难以维护的。 如确实不想放弃额外的类型安全性,可构建象下面这样的一个类(注释①): //: M h2. j ava ont // A m e r obus t enum at i on sys t em or er package c07; publ i c f i nal cl as s M h2 { ont pr i vat e St r i ng nam e; pr i vat e M h2( St r i ng nm { nam = nm } ont ) e ; publ i c St r i ng t oSt r i ng( ) { r et ur n nam } e; publ i c f i nal s t at i c M h2 ont JA = new M h2( " Januar y" ) , N ont FEB = new M h2( " Febr uar y" ) , ont M R = new M h2( " M ch" ) , A ont ar A = new M h2( " A i l " ) , PR ont pr M Y = new M h2( " M ) , A ont ay" JUN = new M h2( " June" ) , ont JUL = new M h2( " Jul y" ) , ont A = new M h2( " A UG ont ugus t " ) , SEP = new M h2( " Sept em " ) , ont ber O T = new M h2( " O ober " ) , C ont ct N V = new M h2( " N O ont ovem " ) , ber D = new M h2( " D EC ont ecem " ) ; ber publ i c f i nal s t at i c M h2[ ] m h = { ont ont JA , JA , FEB, M R, A N N A PR, M Y, JUN A , JUL, A , SEP, O T, N V, D UG C O EC }; publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai 177

M h2 m = M h2. JA ; ont ont N Sys t em out . pr i nt l n( m ; . ) m = M h2. m h[ 12] ; ont ont Sys t em out . pr i nt l n( m ; . ) Sys t em out . pr i nt l n( m == M h2. D ) ; . ont EC Sys t em out . pr i nt l n( m equal s ( M h2. D ) ) ; . . ont EC } } ///: ~ ①:是 Ri ch Hof f ar t h 的一封 E- m l 触发了我这样编写程序的灵感。 ai 这个类叫作 M h2,因为标准 Java 库里已经有一个 M h。它是一个 f i nal 类,并含有一个 pr i vat e构建 ont ont 器,所以没有人能从它继承,或制作它的一个实例。唯一的实例就是那些 f i nal s t at i c对象,它们是在类本 身内部创建的,包括:JA ,FEB,M R等等。这些对象也在 m h 数组中使用,后者让我们能够按数字挑选 N A ont 月份,而不是按名字(注意数组中提供了一个多余的 JA ,使偏移量增加了 1,也使 D N ecem 确实成为 12 ber 月)。在 m n( ) 中,我们可注意到类型的安全性:m ai 是一个 M h2 对象,所以只能将其分配给 M h2。在 ont ont 前面的 M hs . j ava例子中,只提供了 i nt 值,所以本来想用来代表一个月份的 i nt 变量可能实际获得一个 ont 整数值,那样做可能不十分安全。 这儿介绍的方法也允许我们交换使用==或者 equal s ( ) ,就象 m n( ) 尾部展示的那样。 ai

7 . 5 . 4 初始化接口中的字段
接口中定义的字段会自动具有 s t at i c 和 f i nal 属性。它们不能是“空白 f i nal ”,但可初始化成非常数表达 式。例如: //: RandVal s . j ava // I ni t i al i zi ng i nt er f ace f i el ds w t h i / / non- cons t ant i ni t i al i zer s i m t j ava. ut i l . * ; por publ i c i nt er f ace RandVal s { i nt r i nt = ( i nt ) ( M h. r andom ) * 10) ; at ( l ong r l ong = ( l ong) ( M h. r andom ) * 10) ; at ( f l oat r f l oat = ( f l oat ) ( M h. r andom ) * 10) ; at ( doubl e r doubl e = M h. r andom ) * 10; at ( } ///: ~ 由于字段是 s t at i c的,所以它们会在首次装载类之后、以及首次访问任何字段之前获得初始化。下面是一个 简单的测试: //: Tes t RandVal s . j ava publ i c cl as s Tes t RandVal s { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Sys t em out . pr i nt l n( RandVal s . r i nt ) ; . Sys t em out . pr i nt l n( RandVal s . r l ong) ; . Sys t em out . pr i nt l n( RandVal s . r f l oat ) ; . Sys t em out . pr i nt l n( RandVal s . r doubl e) ; . } } ///: ~ 当然,字段并不是接口的一部分,而是保存于那个接口的 s t at i c存储区域中。

178

7. 6 内部类
在 Java 1. 1 中,可将一个类定义置入另一个类定义中。这就叫作“内部类”。内部类对我们非常有用,因为 利用它可对那些逻辑上相互联系的类进行分组,并可控制一个类在另一个类里的“可见性”。然而,我们必 须认识到内部类与以前讲述的“合成”方法存在着根本的区别。 通常,对内部类的需要并不是特别明显的,至少不会立即感觉到自己需要使用内部类。在本章的末尾,介绍 完内部类的所有语法之后,大家会发现一个特别的例子。通过它应该可以清晰地认识到内部类的好处。 创建内部类的过程是平淡无奇的:将类定义置入一个用于封装它的类内部(若执行这个程序遇到麻烦,请参 见第 3 章的 3. 1. 2 小节“赋值”): //: Par cel 1. j ava // C eat i ng i nner cl as s es r package c07. par cel 1; publ i c cl as s Par cel 1 { cl as s C ent s { ont pr i vat e i nt i = 11; publ i c i nt val ue( ) { r et ur n i ; } } cl as s D t i nat i on { es pr i vat e St r i ng l abel ; D t i nat i on( St r i ng w eTo) { es her l abel = w eTo; her } St r i ng r eadLabel ( ) { r et ur n l abel ; } } // Us i ng i nner cl as s es l ooks j ust l i ke // us i ng any ot her cl as s , w t hi n Par cel 1: i publ i c voi d s hi p( St r i ng des t ) { C ent s c = new C ent s ( ) ; ont ont D t i nat i on d = new D t i nat i on( des t ) ; es es } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Par cel 1 p = new Par cel 1( ) ; p. s hi p( " Tanzani a" ) ; } } ///: ~ 若在 s hi p( ) 内部使用,内部类的使用看起来和其他任何类都没什么分别。在这里,唯一明显的区别就是它的 名字嵌套在 Par cel 1 里面。但大家不久就会知道,这其实并非唯一的区别。 更典型的一种情况是,一个外部类拥有一个特殊的方法,它会返回指向一个内部类的句柄。就象下面这样: //: Par cel 2. j ava // Ret ur ni ng a handl e t o an i nner cl as s package c07. par cel 2; publ i c cl as s Par cel 2 { cl as s C ent s { ont pr i vat e i nt i = 11; publ i c i nt val ue( ) { r et ur n i ; } } cl as s D t i nat i on { es pr i vat e St r i ng l abel ; 179

D t i nat i on( St r i ng w eTo) { es her l abel = w eTo; her } St r i ng r eadLabel ( ) { r et ur n l abel ; } } publ i c D i nat i on t o( St r i ng s ) { est r et ur n new D t i nat i on( s ) ; es } publ i c C ent s cont ( ) { ont r et ur n new C ent s ( ) ; ont } publ i c voi d s hi p( St r i ng des t ) { C ent s c = cont ( ) ; ont D t i nat i on d = t o( des t ) ; es } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Par cel 2 p = new Par cel 2( ) ; p. s hi p( " Tanzani a" ) ; Par cel 2 q = new Par cel 2( ) ; // D i ni ng handl es t o i nner cl as s es : ef Par cel 2. C ent s c = q. cont ( ) ; ont Par cel 2. D t i nat i on d = q. t o( " Bor neo" ) ; es } } ///: ~ 若想在除外部类非 s t at i c 方法内部之外的任何地方生成内部类的一个对象,必须将那个对象的类型设为“外 部类名. 内部类名”,就象 m n( ) 中展示的那样。 ai

7 . 6 . 1 内部类和上溯造型
迄今为止,内部类看起来仍然没什么特别的地方。毕竟,用它实现隐藏显得有些大题小做。Java 已经有一个 非常优秀的隐藏机制——只允许类成为“友好的”(只在一个包内可见),而不是把它创建成一个内部类。 然而,当我们准备上溯造型到一个基础类(特别是到一个接口)的时候,内部类就开始发挥其关键作用(从 用于实现的对象生成一个接口句柄具有与上溯造型至一个基础类相同的效果)。这是由于内部类随后可完全 进入不可见或不可用状态——对任何人都将如此。所以我们可以非常方便地隐藏实施细节。我们得到的全部 回报就是一个基础类或者接口的句柄,而且甚至有可能不知道准确的类型。就象下面这样: //: Par cel 3. j ava // Ret ur ni ng a handl e t o an i nner cl as s package c07. par cel 3; abs t r act cl as s C ent s { ont abs t r act publ i c i nt val ue( ) ; } i nt er f ace D t i nat i on { es St r i ng r eadLabel ( ) ; } publ i c cl as s Par cel 3 { pr i vat e cl as s PC ent s ext ends C ent s { ont ont pr i vat e i nt i = 11; publ i c i nt val ue( ) { r et ur n i ; } 180

} pr ot ect ed cl as s PD t i nat i on es i m em s D t i nat i on { pl ent es pr i vat e St r i ng l abel ; pr i vat e PD t i nat i on( St r i ng w eTo) { es her l abel = w eTo; her } publ i c St r i ng r eadLabel ( ) { r et ur n l abel ; } } publ i c D t i nat i on des t ( St r i ng s ) { es r et ur n new PD t i nat i on( s ) ; es } publ i c C ent s cont ( ) { ont r et ur n new PC ent s ( ) ; ont } } cl as s Tes t { p i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ubl ai Par cel 3 p = new Par cel 3( ) ; C ent s c = p. cont ( ) ; ont D t i nat i on d = p. des t ( " Tanzani a" ) ; es // I l l egal - - can' t acces s pr i vat e cl as s : //! Par cel 3. PC ent s c = p. new PC ent s ( ) ; ont ont } } ///: ~ 现在,C ent s 和 D t i nat i on 代表可由客户程序员使用的接口(记住接口会将自己的所有成员都变成 ont es publ i c属性)。为方便起见,它们置于单独一个文件里,但原始的 C ent s 和 D t i nat i on 在它们自己的 ont es 文件中是相互 publ i c 的。 在 Par cel 3 中,一些新东西已经加入:内部类 PC ent s 被设为 pr i vat e,所以除了 Par cel 3 之外,其他任 ont 何东西都不能访问它。PD t i nat i on被设为 pr ot ect ed,所以除了 Par cel 3,Par cel 3 包内的类(因为 es pr ot ect ed也为包赋予了访问权;也就是说,pr ot ect ed也是“友好的”),以及 Par cel 3 的继承者之外,其 他任何东西都不能访问 PD t i nat i on。这意味着客户程序员对这些成员的认识与访问将会受到限制。事实 es 上,我们甚至不能下溯造型到一个 pr i vat e内部类(或者一个 pr ot ect ed内部类,除非自己本身便是一个继 承者),因为我们不能访问名字,就象在 cl as s Tes t 里看到的那样。所以,利用 pr i vat e 内部类,类设计人 员可完全禁止其他人依赖类型编码,并可将具体的实施细节完全隐藏起来。除此以外,从客户程序员的角度 来看,一个接口的范围没有意义的,因为他们不能访问不属于公共接口类的任何额外方法。这样一来,Java 编译器也有机会生成效率更高的代码。 普通(非内部)类不可设为 pr i vat e或 pr ot ect ed——只允许 publ i c或者“友好的”。 注意 C ent s 不必成为一个抽象类。在这儿也可以使用一个普通类,但这种设计最典型的起点依然是一个 ont “接口”。

7 . 6 . 2 方法和作用域中的内部类
至此,我们已基本理解了内部类的典型用途。对那些涉及内部类的代码,通常表达的都是“单纯”的内部 类,非常简单,且极易理解。然而,内部类的设计非常全面,不可避免地会遇到它们的其他大量用法——假 若我们在一个方法甚至一个任意的作用域内创建内部类。有两方面的原因促使我们这样做: ( 1) 正如前面展示的那样,我们准备实现某种形式的接口,使自己能创建和返回一个句柄。 ( 2) 要解决一个复杂的问题,并希望创建一个类,用来辅助自己的程序方案。同时不愿意把它公开。 在下面这个例子里,将修改前面的代码,以便使用: ( 1) 在一个方法内定义的类 181

( 2) ( 3) ( 4) ( 5) ( 6)

在方法的一个作用域内定义的类 一个匿名类,用于实现一个接口 一个匿名类,用于扩展拥有非默认构建器的一个类 一个匿名类,用于执行字段初始化 一个匿名类,通过实例初始化进行构建(匿名内部类不可拥有构建器)

所有这些都在 i nner s copes 包内发生。首先,来自前述代码的通用接口会在它们自己的文件里获得定义,使 它们能在所有的例子里使用: //: D t i nat i on. j ava es package c07. i nner s copes ; i nt er f ace D t i nat i on { es St r i ng r eadLabel ( ) ; } ///: ~ 由于我们已认为 C ent s 可能是一个抽象类,所以可采取下面这种更自然的形式,就象一个接口那样: ont //: C ent s . j ava ont package c07. i nner s copes ; i nt er f ace C ent s { ont i nt val ue( ) ; } ///: ~ 尽管是含有具体实施细节的一个普通类,但 W appi ng 也作为它所有衍生类的一个通用“接口”使用: r //: W appi ng. j ava r package c07. i nner s copes ; publ i c cl as s W appi ng { r pr i vat e i nt i ; publ i c W appi ng( i nt x) { i = x; } r publ i c i nt val ue( ) { r et ur n i ; } } ///: ~ 在上面的代码中,我们注意到 W appi ng 有一个要求使用自变量的构建器,这就使情况变得更加有趣了。 r 第一个例子展示了如何在一个方法的作用域(而不是另一个类的作用域)中创建一个完整的类: //: Par cel 4. j ava // N t i ng a cl as s w t hi n a m hod es i et package c07. i nner s copes ; publ i c cl as s Par cel 4 { publ i c D t i nat i on des t ( St r i ng s ) { es cl as s PD t i nat i on es i m em s D t i nat i on { pl ent es pr i vat e St r i ng l abel ; pr i vat e PD t i nat i on( St r i ng w eTo) { es her l abel = w eTo; her } publ i c St r i ng r eadLabel ( ) { r et ur n l abel ; } 182

} r et ur n new PD t i nat i on( s ) ; es } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Par cel 4 p = new Par cel 4( ) ; D t i nat i on d = p. des t ( " Tanzani a" ) ; es } } ///: ~ PD t i nat i on类属于 des t ( )的一部分,而不是 Par cel 4 的一部分(同时注意可为相同目录内每个类内部的一 es 个内部类使用类标识符 PD t i nat i on,这样做不会发生命名的冲突)。因此,PD t i nat i on不可从 des t ( ) es es 的外部访问。请注意在返回语句中发生的上溯造型——除了指向基础类 D t i nat i on 的一个句柄之外,没有 es 任何东西超出 des t ( ) 的边界之外。当然,不能由于类 PD t i nat i on的名字置于 des t ( ) 内部,就认为在 es des t ( )返回之后 PD t i nat i on不是一个有效的对象。 es 下面这个例子展示了如何在任意作用域内嵌套一个内部类: //: Par cel 5. j ava // N t i ng a cl as s w t hi n a s cope es i package c07. i nner s copes ; publ i c cl as s Par cel 5 { pr i vat e voi d i nt er nal Tr acki ng( bool ean b) { i f ( b) { cl as s Tr acki ngSl i p { pr i vat e St r i ng i d; T r acki ngSl i p( St r i ng s ) { i d = s; } St r i ng get Sl i p( ) { r et ur n i d; } } Tr acki ngSl i p t s = new Tr acki ngSl i p( " s l i p" ) ; St r i ng s = t s . get Sl i p( ) ; } // C t us e i t her e! O of s cope: an' ut //! T r acki ngSl i p t s = new Tr acki ngSl i p( " x" ) ; } publ i c voi d t r ack( ) { i nt er nal Tr acki ng( t r ue) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Par cel 5 p = new Par cel 5( ) ; p. t r ack( ) ; } } ///: ~ Tr acki ngSl i p类嵌套于一个 i f 语句的作用域内。这并不意味着类是有条件创建的——它会随同其他所有东 西得到编译。然而,在定义它的那个作用域之外,它是不可使用的。除这些以外,它看起来和一个普通类并 没有什么区别。 下面这个例子看起来有些奇怪: //: Par cel 6. j ava // A m hod t hat r et ur ns an anonym et ous i nner cl as s package c07. i nner s copes ;

183

publ i c cl as s Par cel 6 { publ i c C ent s cont ( ) { ont r et ur n new C ent s ( ) { ont pr i vat e i nt i = 11; publ i c i nt val ue( ) { r et ur n i ; } } ; // Sem col on r equi r ed i n t hi s cas e i } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Par cel 6 p = new Par cel 6( ) ; C ent s c = p. cont ( ) ; ont } } ///: ~ cont ( )方法同时合并了返回值的创建代码,以及用于表示那个返回值的类。除此以外,这个类是匿名的—— 它没有名字。而且看起来似乎更让人摸不着头脑的是,我们准备创建一个 C ent s 对象: ont r et ur n new C ent s ( ) ont 但在这之后,在遇到分号之前,我们又说:“等一等,让我先在一个类定义里再耍一下花招”: r et ur n new C ent s ( ) { ont pr i vat e i nt i = 11; publ i c i nt val ue( ) { r et ur n i ; } }; 这种奇怪的语法要表达的意思是:“创建从 C ent s 衍生出来的匿名类的一个对象”。由 new表达式返回的 ont 句柄会自动上溯造型成一个 C ent s 句柄。匿名内部类的语法其实要表达的是: ont cl as s M ont ent s ext ends C ent s { yC ont pr i vat e i nt i = 11; publ i c i nt val ue( ) { r et ur n i ; } } r et ur n new M ont ent s ( ) ; yC 在匿名内部类中,C ent s 是用一个默认构建器创建的。下面这段代码展示了基础类需要含有自变量的一个 ont 构建器时做的事情: //: Par cel 7. j ava // A anonym n ous i nner cl as s t hat cal l s t he / / bas e- cl as s cons t r uct or package c07. i nner s copes ; publ i c cl as s Par cel 7 { publ i c W appi ng w ap( i nt x) { r r // Bas e cons t r uct or cal l : r et ur n new W appi ng( x) { r publ i c i nt val ue( ) { r et ur n s uper . val ue( ) * 47; } } ; // Sem col on r equi r ed i } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Par cel 7 p = new Par cel 7( ) ; W appi ng w = p. w ap( 10) ; r r 184

} } ///: ~ 也就是说,我们将适当的自变量简单地传递给基础类构建器,在这儿表现为在“new W appi ng( x)”中传递 r x。匿名类不能拥有一个构建器,这和在调用 s uper ( )时的常规做法不同。 在前述的两个例子中,分号并不标志着类主体的结束(和 C ++不同)。相反,它标志着用于包含匿名类的那 个表达式的结束。因此,它完全等价于在其他任何地方使用分号。 若想对匿名内部类的一个对象进行某种形式的初始化,此时会出现什么情况呢?由于它是匿名的,没有名字 赋给构建器,所以我们不能拥有一个构建器。然而,我们可在定义自己的字段时进行初始化: //: Par cel 8. j ava // A anonym n ous i nner cl as s t hat per f or m s // i ni t i al i zat i on. A br i ef er ver s i on // of Par cel 5. j ava. package c07. i nner s copes ; publ i c cl as s Par cel 8 { // A gum r ent m t be f i nal t o us e i ns i de us // anonym ous i nner cl as s : publ i c D t i nat i on des t ( f i nal St r i ng des t ) { es r et ur n new D t i nat i on( ) { es pr i vat e St r i ng l abel = des t ; publ i c St r i ng r eadLabel ( ) { r et ur n l abel ; } }; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Par cel 8 p = new Par cel 8( ) ; D t i nat i on d = p. des t ( " Tanzani a" ) ; es } } ///: ~ 若试图定义一个匿名内部类,并想使用在匿名内部类外部定义的一个对象,则编译器要求外部对象为 f i nal 属性。这正是我们将 des t ( ) 的自变量设为 f i nal 的原因。如果忘记这样做,就会得到一条编译期出错提示。 只要自己只是想分配一个字段,上述方法就肯定可行。但假如需要采取一些类似于构建器的行动,又应怎样 操作呢?通过 Java 1. 1 的实例初始化,我们可以有效地为一个匿名内部类创建一个构建器: //: Par cel 9. j ava // Us i ng " i ns t ance i ni t i al i zat i on" t o per f or m // cons t r uct i on on an anonym ous i nner cl as s package c07. i nner s copes ; publ i c cl as s Par cel 9 { publ i c D t i nat i on es des t ( f i nal St r i ng des t , f i nal f l oat pr i ce) { r et ur n new D t i nat i on( ) { es pr i vat e i nt cos t ; // I ns t ance i ni t i al i zat i on f or each obj ect : { cos t = M h. r ound( pr i ce) ; at i f ( cos t > 100) Sys t em out . pr i nt l n( " O . ver budget ! " ) ; } 185

pr i vat e St r i ng l abel = des t ; publ i c St r i ng r eadLabel ( ) { r et ur n l abel ; } }; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Par cel 9 p = new Par cel 9( ) ; D t i nat i on d = p. des t ( " Tanzani a" , 101. 395F) ; es } } ///: ~ 在实例初始化模块中,我们可看到代码不能作为类初始化模块(即 i f 语句)的一部分执行。所以实际上,一 个实例初始化模块就是一个匿名内部类的构建器。当然,它的功能是有限的;我们不能对实例初始化模块进 行过载处理,所以只能拥有这些构建器的其中一个。

7 . 6 . 3 链接到外部类
迄今为止,我们见到的内部类好象仅仅是一种名字隐藏以及代码组织方案。尽管这些功能非常有用,但似乎 并不特别引人注目。然而,我们还忽略了另一个重要的事实。创建自己的内部类时,那个类的对象同时拥有 指向封装对象(这些对象封装或生成了内部类)的一个链接。所以它们能访问那个封装对象的成员——毋需 取得任何资格。除此以外,内部类拥有对封装类所有元素的访问权限(注释②)。下面这个例子阐示了这个 问题: //: Sequence. j ava // Hol ds a s equence of O ect s bj i nt er f ace Sel ect or { b ean end( ) ; ool O ect cur r ent ( ) ; bj voi d next ( ) ; } publ i c cl as s Sequence { pr i vat e O ect [ ] o; bj pr i vat e i nt next = 0; publ i c Sequence( i nt s i ze) { o = new O ect [ s i z e] ; bj } publ i c voi d add( O ect x) { bj i f ( next < o. l engt h) { o[ next ] = x; next ++; } } pr i vat e cl as s SSel ect or i m em s Sel ect or { pl ent i nt i = 0; publ i c bool ean end( ) { r et ur n i == o. l engt h; } publ i c O ect cur r ent ( ) { bj r et ur n o[ i ] ; } publ i c voi d next ( ) { i f ( i < o. l engt h) i ++; 186

} } publ i c Sel ect or get Sel ect or ( ) { r et ur n new SSel ect or ( ) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Sequence s = new Sequence( 10) ; f or ( i nt i = 0; i < 10; i ++) s . add( I nt eger . t oSt r i ng( i ) ) ; Sel ect or s l = s . get Sel ect or ( ) ; w l e( ! s l . end( ) ) { hi Sys t em out . pr i nt l n( ( St r i ng) s l . cur r ent ( ) ) ; . s l . next ( ) ; } } } ///: ~ ②:这与 C ++“嵌套类”的设计颇有不同,后者只是一种单纯的名字隐藏机制。在 C ++中,没有指向一个封装 对象的链接,也不存在默认的访问权限。 其中,Sequence 只是一个大小固定的对象数组,有一个类将其封装在内部。我们调用 add( ) ,以便将一个新 对象添加到 Sequence 末尾(如果还有地方的话)。为了取得 Sequence 中的每一个对象,要使用一个名为 Sel ect or 的接口,它使我们能够知道自己是否位于最末尾(end( ) ),能观看当前对象(cur r ent ( ) O ect ),以及能够移至 Sequence 内的下一个对象(next ( ) O ect )。由于 Sel ect or 是一个接口,所以其 bj bj 他许多类都能用它们自己的方式实现接口,而且许多方法都能将接口作为一个自变量使用,从而创建一般的 代码。 在这里,SSel ect or 是一个私有类,它提供了 Sel ect or 功能。在 m n( ) 中,大家可看到 Sequence 的创建过 ai 程,在它后面是一系列字串对象的添加。随后,通过对 get Sel ect or ( ) 的一个调用生成一个 Sel ect or 。并用 它在 Sequence 中移动,同时选择每一个项目。 从表面看,SSel ect or 似乎只是另一个内部类。但不要被表面现象迷惑。请注意观察 end( ) ,cur r ent ( ) 以及 next ( ),它们每个方法都引用了 o。o 是个不属于 SSel ect or 一部分的句柄,而是位于封装类里的一个 pr i vat e字段。然而,内部类可以从封装类访问方法与字段,就象已经拥有了它们一样。这一特征对我们来 说是非常方便的,就象在上面的例子中看到的那样。 因此,我们现在知道一个内部类可以访问封装类的成员。这是如何实现的呢?内部类必须拥有对封装类的特 定对象的一个引用,而封装类的作用就是创建这个内部类。随后,当我们引用封装类的一个成员时,就利用 那个(隐藏)的引用来选择那个成员。幸运的是,编译器会帮助我们照管所有这些细节。但我们现在也可以 理解内部类的一个对象只能与封装类的一个对象联合创建。在这个创建过程中,要求对封装类对象的句柄进 行初始化。若不能访问那个句柄,编译器就会报错。进行所有这些操作的时候,大多数时候都不要求程序员 的任何介入。

7 . 6 . 4 s t at i c 内部类
为正确理解 s t at i c在应用于内部类时的含义,必须记住内部类的对象默认持有创建它的那个封装类的一个对 象的句柄。然而,假如我们说一个内部类是 s t at i c 的,这种说法却是不成立的。s t at i c内部类意味着: ( 1) 为创建一个 s t at i c内部类的对象,我们不需要一个外部类对象。 ( 2) 不能从 s t at i c内部类的一个对象中访问一个外部类对象。 但在存在一些限制:由于 s t at i c 成员只能位于一个类的外部级别,所以内部类不可拥有 s t at i c 数据或 s t at i c内部类。 倘若为了创建内部类的对象而不需要创建外部类的一个对象,那么可将所有东西都设为 s t at i c。为了能正常 工作,同时也必须将内部类设为 s t at i c。如下所示: //: Par cel 10. j ava // St at i c i nner cl as s es 187

package c07. par cel 10; abs t r act cl as s C ent s { ont abs t r act publ i c i nt val ue( ) ; } i nt er f ace D t i nat i on { es St r i ng r eadLabel ( ) ; } publ i c cl as s Par cel 10 { pr i vat e s t at i c cl as s PC ent s ont ext ends C ent s { ont pr i vat e i nt i = 11; publ i c i nt val ue( ) { r et ur n i ; } } pr ot ect ed s t at i c cl as s PD t i nat i on es i m em s D t i nat i on { pl ent es pr i vat e St r i ng l abel ; pr i vat e PD t i nat i on( St r i ng w eTo) { es her l abel = w eTo; her } publ i c St r i ng r eadLabel ( ) { r et ur n l abel ; } } publ i c s t a i c D t i nat i on des t ( St r i ng s ) { t es r et ur n new PD t i nat i on( s ) ; es } publ i c s t at i c C ent s cont ( ) { ont r et ur n new PC ent s ( ) ; ont } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C ent s c = cont ( ) ; ont D t i nat i on d = des t ( " Tanzani a" ) ; es } } ///: ~ 在 m n( ) 中,我们不需要 Par cel 10 的对象;相反,我们用常规的语法来选择一个 s t at i c 成员,以便调用将 ai 句柄返回 C ent s 和 D t i nat i on的方法。 ont es 通常,我们不在一个接口里设置任何代码,但 s t at i c内部类可以成为接口的一部分。由于类是“静态”的, 所以它不会违反接口的规则——s t at i c 内部类只位于接口的命名空间内部: //: I I nt er f ace. j ava // St at i c i nner cl as s es i ns i de i nt er f aces i nt er f ace I I nt er f ace { s t at i c cl as s I nner { i nt i , j , k; publ i c I nner ( ) { } voi d f ( ) { } } } ///: ~

188

在本书早些时候,我建议大家在每个类里都设置一个 m n( ) ,将其作为那个类的测试床使用。这样做的一个 ai 缺点就是额外代码的数量太多。若不愿如此,可考虑用一个 s t at i c 内部类容纳自己的测试代码。如下所示: //: Tes t Bed. j ava // Put t i ng t es t code i n a s t at i c i nner cl as s cl as s Tes t Bed { Tes t Bed( ) { } voi d f ( ) { Sys t em out . pr i nt l n( " f ( ) " ) ; } . publ i c s t at i c cl as s Tes t er { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Tes t Bed t = new Tes t Bed( ) ; t. f (); } } } ///: ~ 这样便生成一个独立的、名为 Tes t Bed$Tes t er 的类(为运行程序,请使用“j ava Tes t Bed$Tes t er ”命 令)。可将这个类用于测试,但不需在自己的最终发行版本中包含它。

7 . 6 . 5 引用外部类对象
若想生成外部类对象的句柄,就要用一个点号以及一个 t hi s 来命名外部类。举个例子来说,在 Sequence. SSel ect or 类中,它的所有方法都能产生外部类 Sequence 的存储句柄,方法是采用 Seq uence. t hi s 的形式。结果获得的句柄会自动具备正确的类型(这会在编译期间检查并核实,所以不会出现运行期的开 销)。 有些时候,我们想告诉其他某些对象创建它某个内部类的一个对象。为达到这个目的,必须在 new表达式中 提供指向其他外部类对象的一个句柄,就象下面这样: //: Par cel 11. j ava // C eat i ng i nner cl as s es r package c07. par cel 11; publ i c cl as s Par cel 11 { cl as s C ent s { ont pr i vat e i nt i = 11; publ i c i nt val ue( ) { r et ur n i ; } } cl as s D t i nat i on { es pr i vat e St r i ng l abel ; D t i nat i on( St r i ng w eTo) { es her l abel = w eTo; her } St r i ng r eadLabel ( ) { r et ur n l abel ; } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Par cel 11 p = new Par cel 11( ) ; // M t us e i ns t ance of out er cl as s us // t o cr eat e an i ns t ances of t he i nner cl as s : Par cel 11. C ent s c = p. new C ent s ( ) ; ont ont Par cel 11. D t i nat i on d = es p. new D t i nat i on( " Tanzani a" ) ; es } 189

} ///: ~ 为直接创建内部类的一个对象,不能象大家或许猜想的那样——采用相同的形式,并引用外部类名 Par cel 11。此时,必须利用外部类的一个对象生成内部类的一个对象: Par cel 11. C ent s c = p. new C ent s ( ) ; ont ont 因此,除非已拥有外部类的一个对象,否则不可能创建内部类的一个对象。这是由于内部类的对象已同创建 它的外部类的对象“默默”地连接到一起。然而,如果生成一个 s t at i c 内部类,就不需要指向外部类对象的 一个句柄。

7 . 6 . 6 从内部类继承
由于内部类构建器必须同封装类对象的一个句柄联系到一起,所以从一个内部类继承的时候,情况会稍微变 得有些复杂。这儿的问题是封装类的“秘密”句柄必须获得初始化,而且在衍生类中不再有一个默认的对象 可以连接。解决这个问题的办法是采用一种特殊的语法,明确建立这种关联: //: I nher i t I nner . j ava // I nher i t i ng an i nner cl as s cl as s W t hI nner { i cl as s I nner { } } publ i c cl as s I nher i t I nner ext ends W t hI nner . I nner { i //! I nher i t I nner ( ) { } // W t com l e on' pi I nher i t I nner ( W t hI nner w ) { i i w . s uper ( ) ; i } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai W t hI nner w = new W t hI nner ( ) ; i i i I nher i t I nner i i = new I nher i t I nner ( w ) ; i } } ///: ~ 从中可以看到,I nher i t I nner 只对内部类进行了扩展,没有扩展外部类。但在需要创建一个构建器的时候, 默认对象已经没有意义,我们不能只是传递封装对象的一个句柄。此外,必须在构建器中采用下述语法: encl os i ngC as s Handl e. s uper ( ) ; l 它提供了必要的句柄,以便程序正确编译。

7 . 6 . 7 内部类可以覆盖吗?
若创建一个内部类,然后从封装类继承,并重新定义内部类,那么会出现什么情况呢?也就是说,我们有可 能覆盖一个内部类吗?这看起来似乎是一个非常有用的概念,但“覆盖”一个内部类——好象它是外部类的 另一个方法——这一概念实际不能做任何事情: //: Bi gEgg. j ava // A i nner cl as s cannot be over r i den n // l i ke a m hod et cl as s Egg { pr ot ect ed cl as s Yol k { publ i c Yol k( ) { Sys t em out . pr i nt l n( " Egg. Yol k( ) " ) ; . 190

} } pr i vat e Yol k y; publ i c Egg( ) { Sys t em out . pr i nt l n( " N Egg( ) " ) ; . ew y = new Yol k( ) ; } } publ i c cl as s Bi gEgg ext ends Egg { publ i c cl as s Yol k { publ i c Yol k( ) { Sys t em out . pr i nt l n( " Bi gEgg. Yol k( ) " ) ; . } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai new Bi gEgg( ) ; } } ///: ~ 默认构建器是由编译器自动合成的,而且会调用基础类的默认构建器。大家或许会认为由于准备创建一个 Bi gEgg ,所以会使用 Yol k 的“被覆盖”版本。但实际情况并非如此。输出如下: N Egg( ) ew Egg. Yol k( ) 这个例子简单地揭示出当我们从外部类继承的时候,没有任何额外的内部类继续下去。然而,仍然有可能 “明确”地从内部类继承: //: Bi gEgg2. j ava // Pr oper i nher i t ance of an i nner cl as s cl as s Egg2 { pr ot ect ed cl a s Yol k { s publ i c Yol k( ) { Sys t em out . pr i nt l n( " Egg2. Yol k( ) " ) ; . } publ i c voi d f ( ) { Sys t em out . pr i nt l n( " Egg2. Yol k. f ( ) " ) ; . } } pr i vat e Yol k y = new Yol k( ) ; publ i c Egg2( ) { Sys t em out . pr i nt l n( " N Egg2( ) " ) ; . ew } publ i c voi d i ns er t Yol k( Yol k yy) { y = yy; } publ i c voi d g( ) { y. f ( ) ; } } publ i c cl as s Bi gEgg2 ext ends Egg2 { publ i c cl as s Yol k ext ends Egg2. Yol k { publ i c Yol k( ) { Sys t em out . pr i nt l n( " Bi gEgg2. Yol k( ) " ) ; . } 191

publ i c voi d f ( ) { Sys t em out . pr i nt l n( " Bi gEgg2. Yol k. f ( ) " ) ; . } } publ i c Bi gEgg2( ) { i ns er t Yol k( new Yol k( ) ) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Egg2 e2 = new Bi gEgg2( ) ; e2. g( ) ; } } ///: ~ 现在,Bi gEgg2. Yol k 明确地扩展了 Egg2. Yol k,而且覆盖了它的方法。方法 i ns er t Yol k( )允许 Bi g g 将它 Eg 2 自己的某个 Yol k 对象上溯造型至 Egg2 的 y 句柄。所以当 g( ) 调用 y. f ( ) 的时候,就会使用 f ( ) 被覆盖版本。 输出结果如下: Egg2. Yol k( ) N Egg2( ) ew Egg2. Yol k( ) Bi gEgg2. Yol k( ) Bi gEgg2. Yol k. f ( ) 对 Egg2. Yol k( ) 的第二个调用是 Bi gEgg2. Yol k 构建器的基础类构建器调用。调用 g( )的时候,可发现使用的是 f ( )的被覆盖版本。

7 . 6 . 8 内部类标识符
由于每个类都会生成一个. cl as s 文件,用于容纳与如何创建这个类型的对象有关的所有信息(这种信息产生 了一个名为 C as s 对象的元类),所以大家或许会猜到内部类也必须生成相应的. cl as s 文件,用来容纳与它 l 们的 C as s 对象有关的信息。这些文件或类的名字遵守一种严格的形式:先是封装类的名字,再跟随一个$, l 再跟随内部类的名字。例如,由 I nher i t I nner . j ava创建的. cl as s 文件包括: I nher i t I nner . cl as s W t hI nner $I nner . cl as s i W t hI nner . cl as s i 如果内部类是匿名的,那么编译器会简单地生成数字,把它们作为内部类标识符使用。若内部类嵌套于其他 内部类中,则它们的名字简单地追加在一个$以及外部类标识符的后面。 这种生成内部名称的方法除了非常简单和直观以外,也非常“健壮”,可适应大多数场合的要求(注释 ③)。由于它是 Java 的标准命名机制,所以产生的文件会自动具备“与平台无关”的能力(注意 Java 编译 器会根据情况改变内部类,使其在不同的平台中能正常工作)。 ③:但在另一方面,由于“$”也是 Uni x 外壳的一个元字符,所以有时会在列出. cl as s 文件时遇到麻烦。对 一家以 Uni x 为基础的公司——Sun——来说,采取这种方案显得有些奇怪。我的猜测是他们根本没有仔细考 虑这方面的问题,而是认为我们会将全部注意力自然地放在源码文件上。

7 . 6 . 9 为什么要用内部类:控制框架
到目前为止,大家已接触了对内部类的运作进行描述的大量语法与概念。但这些并不能真正说明内部类存在 的原因。为什么 Sun要如此麻烦地在 Java 1. 1 里添加这样的一种基本语言特性呢?答案就在于我们在这里要 学习的“控制框架”。 一个“应用程序框架”是指一个或一系列类,它们专门设计用来解决特定类型的问题。为应用应用程序框 架,我们可从一个或多个类继承,并覆盖其中的部分方法。我们在覆盖方法中编写的代码用于定制由那些应 用程序框架提供的常规方案,以便解决自己的实际问题。“控制框架”属于应用程序框架的一种特殊类型, 受到对事件响应的需要的支配;主要用来响应事件的一个系统叫作“由事件驱动的系统”。在应用程序设计 语言中,最重要的问题之一便是“图形用户界面”(G ),它几乎完全是由事件驱动的。正如大家会在第 UI 13 章学习的那样,Java 1. 1 A T 属于一种控制框架,它通过内部类完美地解决了 G 的问题。 W UI 为理解内部类如何简化控制框架的创建与使用,可认为一个控制框架的工作就是在事件“就绪”以后执行它 192

们。尽管“就绪”的意思很多,但在目前这种情况下,我们却是以计算机时钟为基础。随后,请认识到针对 控制框架需要控制的东西,框架内并未包含任何特定的信息。首先,它是一个特殊的接口,描述了所有控制 事件。它可以是一个抽象类,而非一个实际的接口。由于默认行为是根据时间控制的,所以部分实施细节可 能包括: //: Event . j ava // T he com on m hods f or any cont r ol event m et package c07. cont r ol l er ; abs t r act publ i c cl as s Event { pr i vat e l ong e T i m vt e; publ i c Event ( l ong event Ti m { e) evt Ti m = event Ti m e e; } publ i c bool ean r eady( ) { r et ur n Sys t em cur r ent Ti m i l l i s ( ) >= evt Ti m . eM e; } abs t r act publ i c voi d act i on( ) ; abs t r act publ i c St r i ng des cr i pt i on( ) ; } ///: ~ 希望 Event (事件)运行的时候,构建器即简单地捕获时间。同时 r eady( )告诉我们何时该运行它。当然, r eady( )也可以在一个衍生类中被覆盖,将事件建立在除时间以外的其他东西上。 act i on( ) 是事件就绪后需要调用的方法,而 des cr i pt i on( ) 提供了与事件有关的文字信息。 下面这个文件包含了实际的控制框架,用于管理和触发事件。第一个类实际只是一个“助手”类,它的职责 是容纳 Event 对象。可用任何适当的集合替换它。而且通过第 8 章的学习,大家会知道另一些集合可简化我 们的工作,不需要我们编写这些额外的代码: //: C r ol l er . j ava ont // A ong w t h Event , t he gener i c l i // f r am or k f or al l cont r ol s ys t em : ew s package c07. cont r ol l er ; // Thi s i s j us t a w t o hol d Event obj ect s . ay cl as s Event Set { pr i vat e Event [ ] event s = new Event [ 100] ; pr i vat e i nt i ndex = 0; pr i vat e i nt next = 0; p i c voi d add( Event e) { ubl i f ( i ndex >= event s . l engt h) r et ur n; // ( I n r eal l i f e, t hr ow except i on) event s [ i ndex++] = e; } publ i c Event get N ( ) { ext bool ean l ooped = f al s e; i nt s t ar t = next ; do { next = ( next + 1) % event s . l engt h; // See i f i t has l ooped t o t he begi nni ng: i f ( s t ar t == next ) l ooped = t r ue; // I f i t l oops pas t s t ar t , t he l i s t // i s em y: pt 193

i f ( ( next == ( s t ar t + 1) % event s . l engt h) & l ooped) & r et ur n nul l ; } w l e( event s [ next ] == nul l ) ; hi r et ur n event s [ next ] ; } publ i c voi d r em oveC r ent ( ) { ur event s [ next ] = nul l ; } } publ i c cl as s C r ol l er { ont pr i vat e Event Set es = new Event Set ( ) ; publ i c voi d addEvent ( Event c) { es . add( c) ; } publ i c voi d r un( ) { Event e; w l e( ( e = es . get N ( ) ) ! = nul l ) { hi ext i f ( e. r eady( ) ) { e. act i on( ) ; Sys t em out . pr i nt l n( e. des cr i pt i on( ) ) ; . es . r em oveC r ent ( ) ; ur } } } } ///: ~ Event Set 可容纳 100 个事件(若在这里使用来自第 8 章的一个“真实”集合,就不必担心它的最大尺寸,因 为它会根据情况自动改变大小)。i ndex (索引)在这里用于跟踪下一个可用的空间,而 next (下一个)帮 助我们寻找列表中的下一个事件,了解自己是否已经循环到头。在对 get N ( ) 的调用中,这一点是至关重 ext 要的,因为一旦运行,Event 对象就会从列表中删去(使用 r em oveC r ent ( ) )。所以 get N ( ) 会在列表中 ur ext 向前移动时遇到“空洞”。 注意 r em oveC r ent ( ) 并不只是指示一些标志,指出对象不再使用。相反,它将句柄设为 nul l 。这一点是非 ur 常重要的,因为假如垃圾收集器发现一个句柄仍在使用,就不会清除对象。若认为自己的句柄可能象现在这 样被挂起,那么最好将其设为 nul l ,使垃圾收集器能够正常地清除它们。 C r ol l er 是进行实际工作的地方。它用一个 Event Set 容纳自己的 Event 对象,而且 addEvent ( ) 允许我们 ont 向这个列表加入新事件。但最重要的方法是 r un( ) 。该方法会在 Event Set 中遍历,搜索一个准备运行的 Event 对象——r eady( )。对于它发现 r eady( )的每一个对象,都会调用 act i on( ) 方法,打印出 des cr i pt i on( ) ,然后将事件从列表中删去。 注意在迄今为止的所有设计中,我们仍然不能准确地知道一个“事件”要做什么。这正是整个设计的关键; 它怎样“将发生变化的东西同没有变化的东西区分开”?或者用我的话来讲,“改变的意图”造成了各类 Event 对象的不同行动。我们通过创建不同的 Event 子类,从而表达出不同的行动。 这里正是内部类大显身手的地方。它们允许我们做两件事情: ( 1) 在单独一个类里表达一个控制框架应用的全部实施细节,从而完整地封装与那个实施有关的所有东西。 内部类用于表达多种不同类型的 act i on( ) ,它们用于解决实际的问题。除此以外,后续的例子使用了 pr i vat e内部类,所以实施细节会完全隐藏起来,可以安全地修改。 ( 2) 内部类使我们具体的实施变得更加巧妙,因为能方便地访问外部类的任何成员。若不具备这种能力,代 码看起来就可能没那么使人舒服,最后不得不寻找其他方法解决。 现在要请大家思考控制框架的一种具体实施方式,它设计用来控制温室(G eenhous e)功能(注释④)。每 r 个行动都是完全不同的:控制灯光、供水以及温度自动调节的开与关,控制响铃,以及重新启动系统。但控 制框架的设计宗旨是将不同的代码方便地隔离开。对每种类型的行动,都要继承一个新的 Event 内部类,并 在 act i on( ) 内编写相应的控制代码。 194

④:由于某些特殊原因,这对我来说是一个经常需要解决的、非常有趣的问题;原来的例子在《C I ns i de ++ & O 》一书里也出现过,但 Java 提供了一种更令人舒适的解决方案。 ut 作为应用程序框架的一种典型行为,G eenhous eC r ol s 类是从 C r ol l er 继承的: r ont ont //: G eenhous eC r ol s . j ava r ont // Thi s pr oduces a s peci f i c appl i cat i on of t he // cont r ol s ys t em al l i n a s i ngl e cl as s . I nner , // cl as s es al l ow you t o encaps ul at e di f f er ent // f unct i onal i t y f or each t ype of event . package c07. cont r ol l er ; publ i c cl as s G eenhous eC r ol s r ont ext ends C r ol l er { ont pr i vat e bool ean l i ght = f al s e; pr i vat e bool ean w er = f al s e; at pr i vat e St r i ng t her m t at = " D ; os ay" pr i vat e cl as s Li ght O ext ends Event { n publ i c Li ght O l ong event Ti m { n( e) s uper ( event Ti m ; e) } publ i c voi d act i on( ) { // Put har dw e cont r ol code her e t o ar // phys i cal l y t ur n on t he l i ght . l i ght = t r ue; } publ i c St r i ng des cr i pt i on( ) { r et ur n " Li ght i s on" ; } } pr i vat e cl as s Li ght O f ext ends Event { f publ i c Li ght O f ( l ong event Ti m { f e) s uper ( event Ti m ; e) } publ i c voi d act i on( ) { // Put har dw e cont r ol code her e t o ar // phys i cal l y t ur n of f t he l i ght . l i ght = f al s e; } publ i c St r i ng des cr i pt i on( ) { r et ur n " Li ght i s of f " ; } } pr i vat e cl as s W er O ext ends Event { at n publ i c W er O l ong event Ti m { at n( e) s uper ( event Ti m ; e) } publ i c voi d act i on( ) { // Put har dw e cont r ol code her e ar w er = t r ue; at } 195

publ i c St r i ng des cr i pt i on( ) { r et ur n " G eenhous e w er i s on" ; r at } } pr i vat e cl as s W er O f ext ends Event { at f publ i c W er O f ( l ong event Ti m { at f e) s uper ( event Ti m ; e) } publ i c voi d act i on( ) { // Put har dw e cont r ol code her e ar w er = f al s e; at } publ i c St r i ng des cr i pt i on( ) { r et ur n " G eenhous e w er i s of f " ; r at } } pr i vat e cl as s Ther m t at N ght ext ends Event { os i publ i c Ther m t at N ght ( l ong event Ti m { os i e) s uper ( event Ti m ; e) } publ i c voi d act i on( ) { // Put har dw e cont r ol code her e ar t her m t at = " N ght " ; os i } publ i c St r i ng des cr i pt i on( ) { r et ur n " Ther m t at on ni ght s et t i ng" ; os } } pr i vat e cl as s Ther m t at D ext ends Event { os ay publ i c Ther m t at D l ong event Ti m { os ay( e) s uper ( event Ti m ; e) } publ i c voi d act i on( ) { // Put har dw e cont r ol code her e ar t her m t at = " D ; os ay" } publ i c St r i ng des cr i pt i on( ) { r et ur n " Ther m t at on day s et t i ng" ; os } } // A exam e of an act i on( ) t hat i ns er t s a n pl // new one of i t s el f i nt o t he event l i s t : pr i vat e i nt r i ngs ; pr i vat e cl as s Bel l ext ends Event { publ i c Bel l ( l ong event Ti m { e) s uper ( event Ti m ; e) } publ i c voi d act i on( ) { // Ri ng bel l ever y 2 s econds , r i ngs t i m : es Sys t em out . pr i nt l n( " Bi ng! " ) ; . i f ( - - r i ngs > 0) addEvent ( new Bel l ( 196

Sys t em cur r ent Ti m i l l i s ( ) + 2000) ) ; . eM } publ i c St r i ng des cr i pt i on( ) { r et ur n " Ri ng bel l " ; } } pr i vat e cl as s Res t ar t ext ends Event { publ i c Res t ar t ( l ong event Ti m { e) s uper ( event Ti m ; e) } publ i c voi d act i on( ) { l ong t m = Sys t em cur r ent Ti m i l l i s ( ) ; . eM // I ns t ead of har d- w r i ng, you coul d par s e i // conf i gur at i on i nf or m i on f r om a t ext at // f i l e her e: r i ngs = 5; addEvent ( new Ther m t at N ght ( t m ) ; os i ) addEvent ( new Li ght O t m + 1000) ) ; n( addEvent ( new Li ght O f ( t m + 2000) ) ; f addEvent ( new W er O t m + 3000) ) ; at n( addEvent ( new W er O f ( t m + 8000) ) ; at f addEvent ( new Bel l ( t m + 9000) ) ; addEvent ( new Ther m t at D t m + 10000) ) ; os ay( // C even add a Res t ar t obj ect ! an addEvent ( new Res t ar t ( t m + 20000) ) ; } publ i c St r i ng des cr i pt i on( ) { r et ur n " Res t ar t i ng s ys t em ; " } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai G eenhous eC r ol s gc = r ont new G eenhous eC r ol s ( ) ; r ont l ong t m = Sys t em cur r ent Ti m i l l i s ( ) ; . eM gc. addEvent ( gc. new Res t ar t ( t m ) ; ) gc. r un( ) ; } } ///: ~ 注意 l i ght (灯光)、w er (供水)、t her m t at (调温)以及 r i ngs 都隶属于外部类 at os G eenhous eC r ol s ,所以内部类可以毫无阻碍地访问那些字段。此外,大多数 act i on( ) 方法也涉及到某些 r ont 形式的硬件控制,这通常都要求发出对非 Java 代码的调用。 大多数 Event 类看起来都是相似的,但 Bel l (铃)和 Res t ar t (重启)属于特殊情况。Bel l 会发出响声,若 尚未响铃足够的次数,它会在事件列表里添加一个新的 Bel l 对象,所以以后会再度响铃。请注意内部类看起 来为什么总是类似于多重继承:Bel l 拥有 Event 的所有方法,而且也拥有外部类 G eenhous eC r ol s 的所 r ont 有方法。 Res t ar t 负责对系统进行初始化,所以会添加所有必要的事件。当然,一种更灵活的做法是避免进行“硬编 码”,而是从一个文件里读入它们(第 10 章的一个练习会要求大家修改这个例子,从而达到这个目标)。由 于 Res t ar t ( ) 仅仅是另一个 Event 对象,所以也可以在 Res t ar t . act i on( )里添加一个 Res t ar t 对象,使系统 能够定期重启。在 m n( ) 中,我们需要做的全部事情就是创建一个 G eenhous eC r ol s 对象,并添加一个 ai r ont Res t ar t 对象,令其工作起来。 这个例子应该使大家对内部类的价值有一个更加深刻的认识,特别是在一个控制框架里使用它们的时候。此 197

外,在第 13 章的后半部分,大家还会看到如何巧妙地利用内部类描述一个图形用户界面的行为。完成那里的 学习后,对内部类的认识将上升到一个前所未有的新高度。

7. 7 构建器和多形性
同往常一样,构建器与其他种类的方法是有区别的。在涉及到多形性的问题后,这种方法依然成立。尽管构 建器并不具有多形性(即便可以使用一种“虚拟构建器”——将在第 11 章介绍),但仍然非常有必要理解构 建器如何在复杂的分级结构中以及随同多形性使用。这一理解将有助于大家避免陷入一些令人不快的纠纷。

7 . 7 . 1 构建器的调用顺序
构建器调用的顺序已在第 4 章进行了简要说明,但那是在继承和多形性问题引入之前说的话。 用于基础类的构建器肯定在一个衍生类的构建器中调用,而且逐渐向上链接,使每个基础类使用的构建器都 能得到调用。之所以要这样做,是由于构建器负有一项特殊任务:检查对象是否得到了正确的构建。一个衍 生类只能访问它自己的成员,不能访问基础类的成员(这些成员通常都具有 pr i vat e 属性)。只有基础类的 构建器在初始化自己的元素时才知道正确的方法以及拥有适当的权限。所以,必须令所有构建器都得到调 用,否则整个对象的构建就可能不正确。那正是编译器为什么要强迫对衍生类的每个部分进行构建器调用的 原因。在衍生类的构建器主体中,若我们没有明确指定对一个基础类构建器的调用,它就会“默默”地调用 默认构建器。如果不存在默认构建器,编译器就会报告一个错误(若某个类没有构建器,编译器会自动组织 一个默认构建器)。 下面让我们看看一个例子,它展示了按构建顺序进行合成、继承以及多形性的效果: //: Sandw ch. j ava i // O der of cons t r uct or cal l s r cl as s M eal { M ( ) { Sys t em out . pr i nt l n( " M ( ) " ) ; } eal . eal } cl as s Br ead { Br ead( ) { Sys t em out . pr i nt l n( " Br ead( ) " ) ; } . } cl as s C hees e { C hees e( ) { Sys t em out . pr i nt l n( " C . hees e( ) " ) ; } } cl as s Let t uce { Let t uce( ) { Sys t em out . pr i nt l n( " Let t uce( ) " ) ; } . } cl as s Lunch ext ends M eal { Lunch( ) { Sys t em out . pr i nt l n( " Lunch( ) " ) ; } . } cl as s Por t abl eLunch ext ends Lunch { Por t abl eLunch( ) { Sys t em out . pr i nt l n( " Por t abl eLunch( ) " ) ; . } } cl as s Sandw ch ext ends Por t abl eLunch { i Br ead b = new Br ead( ) ; 198

C hees e c = new C hees e( ) ; Let t uce l = new Let t uce( ) ; Sandw ch( ) { i Sys t em out . pr i nt l n( " Sandw ch( ) " ) ; . i } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai new Sandw ch( ) ; i } } ///: ~ 这个例子在其他类的外部创建了一个复杂的类,而且每个类都有一个构建器对自己进行了宣布。其中最重要 的类是 Sandw ch,它反映出了三个级别的继承(若将从 O ect 的默认继承算在内,就是四级)以及三个成 i bj 员对象。在 m n( )里创建了一个 Sandw ch 对象后,输出结果如下: ai i M () eal Lunch( ) Por t abl eLunch( ) Br ead( ) C hees e( ) Let t uce( ) Sandw ch( ) i 这意味着对于一个复杂的对象,构建器的调用遵照下面的顺序: ( 1) 调用基础类构建器。这个步骤会不断重复下去,首先得到构建的是分级结构的根部,然后是下一个衍生 类,等等。直到抵达最深一层的衍生类。 ( 2) 按声明顺序调用成员初始化模块。 ( 3) 调用衍生构建器的主体。 构建器调用的顺序是非常重要的。进行继承时,我们知道关于基础类的一切,并且能访问基础类的任何 publ i c和 pr ot ect ed成员。这意味着当我们在衍生类的时候,必须能假定基础类的所有成员都是有效的。采 用一种标准方法,构建行动已经进行,所以对象所有部分的成员均已得到构建。但在构建器内部,必须保证 使用的所有成员都已构建。为达到这个要求,唯一的办法就是首先调用基础类构建器。然后在进入衍生类构 建器以后,我们在基础类能够访问的所有成员都已得到初始化。此外,所有成员对象(亦即通过合成方法置 于类内的对象)在类内进行定义的时候(比如上例中的 b,c 和 l ),由于我们应尽可能地对它们进行初始 化,所以也应保证构建器内部的所有成员均为有效。若坚持按这一规则行事,会有助于我们确定所有基础类 成员以及当前对象的成员对象均已获得正确的初始化。但不幸的是,这种做法并不适用于所有情况,这将在 下一节具体说明。

7 . 7 . 2 继承和 f i nal i z e( )
通过“合成”方法创建新类时,永远不必担心对那个类的成员对象的收尾工作。每个成员都是一个独立的对 象,所以会得到正常的垃圾收集以及收尾处理——无论它是不是不自己某个类一个成员。但在进行初始化的 时候,必须覆盖衍生类中的 f i nal i ze( ) 方法——如果已经设计了某个特殊的清除进程,要求它必须作为垃圾 收集的一部分进行。覆盖衍生类的 f i nal i ze( )时,务必记住调用 f i nal i ze( ) 的基础类版本。否则,基础类的 初始化根本不会发生。下面这个例子便是明证: //: Fr og. j ava // Tes t i ng f i nal i ze w t h i nher i t ance i cl as s D oBas eFi nal i zat i on { publ i c s t at i c bool ean f l ag = f al s e; }

199

cl as s C act er i s t i c { har St r i ng s ; C act er i s t i c( St r i ng c) { har s = c; Sys t em out . pr i nt l n( . " C eat i ng C act er i s t i c " + s ) ; r har } pr ot ect ed voi d f i nal i z e( ) { Sys t em out . pr i nt l n( . " f i nal i zi ng C act er i s t i c " + s ) ; har } } cl as s Li vi ngC eat ur e { r C act er i s t i c p = har new C act er i s t i c( " i s al i ve" ) ; har Li vi ngC eat ur e( ) { r Sys t em out . pr i nt l n( " Li vi ngC eat ur e( ) " ) ; . r } pr ot ect ed voi d f i nal i z e( ) { Sys t em out . pr i nt l n( . " Li vi ngC eat ur e f i nal i ze" ) ; r / / C l bas e- cl as s ver s i on LA al ST! i f (D oBas eFi nal i zat i on. f l ag) try { s uper . f i nal i z e( ) ; } cat ch( Thr ow e t ) { } abl } } cl as s A m ext ends Li vi ngC eat ur e { ni al r C act er i s t i c p = har new C act er i s t i c( " has hear t " ) ; har A m () { ni al Syst em out . pr i nt l n( " A m ( ) " ) ; . ni al } pr ot ect ed voi d f i nal i z e( ) { Sys t em out . pr i nt l n( " A m f i nal i ze" ) ; . ni al i f (D oBas eFi nal i zat i on. f l ag) try { s uper . f i nal i z e( ) ; } cat ch( Thr ow e t ) { } abl } } cl as s A phi bi an ext ends A m { m ni al C act er i s t i c p = har new C act er i s t i c( " can l i ve i n w er " ) ; har at A phi bi an( ) { m Sys t em out . pr i nt l n( " A phi bi an( ) " ) ; . m } pr ot ect ed voi d f i nal i z e( ) { 200

Sys t em out . pr i nt l n( " A phi bi an f i nal i ze" ) ; . m i f (D oBas eFi nal i zat i on. f l ag) try { s uper . f i nal i ze( ) ; } cat ch( Thr ow e t ) { } abl } } publ i c cl as s Fr og ext ends A phi bi an { m Fr og( ) { Sys t em out . pr i nt l n( " Fr og( ) " ) ; . } pr ot ect ed voi d f i nal i z e( ) { Sys t em out . pr i nt l n( " Fr og f i nal i ze" ) ; . i f (D oBas eFi nal i zat i on. f l ag) try { s up . f i nal i ze( ) ; er } cat ch( Thr ow e t ) { } abl } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai i f ( ar gs . l engt h ! = 0 & & ar gs [ 0] . equal s ( " f i nal i ze" ) ) D oBas eFi nal i zat i on. f l ag = t r ue; el s e Sys t em out . pr i nt l n( " not f i nal i zi ng bas es " ) ; . new Fr og( ) ; // I ns t ant l y becom gar bage es Sys t em out . pr i nt l n( " bye! " ) ; . // M t do t hi s t o guar ant ee t hat al l us // f i nal i z er s w l l be cal l ed: i Sys t em r unFi nal i zer s O . nExi t ( t r ue) ; } } ///: ~ D oBas ef i nal i zat i on 类只是简单地容纳了一个标志,向分级结构中的每个类指出是否应调用 s uper . f i nal i ze( )。这个标志的设置建立在命令行参数的基础上,所以能够在进行和不进行基础类收尾工作 的前提下查看行为。 分级结构中的每个类也包含了 C act er i s t i c 类的一个成员对象。大家可以看到,无论是否调用了基础类收 har 尾模块,C act er i s t i c 成员对象都肯定会得到收尾(清除)处理。 har 每个被覆盖的 f i nal i ze( ) 至少要拥有对 pr ot ect ed成员的访问权力,因为 O ect 类中的 f i nal i ze( )方法具 bj 有 pr ot ect ed属性,而编译器不允许我们在继承过程中消除访问权限(“友好的”比“受到保护的”具有更 小的访问权限)。 在 Fr og. m n( ) 中,D ai oBas eFi nal i zat i on 标志会得到配置,而且会创建单独一个 Fr og 对象。请记住垃圾收集 (特别是收尾工作)可能不会针对任何特定的对象发生,所以为了强制采取这一行动, Sys t em r unFi nal i zer s O . nExi t ( t r ue) 添加了额外的开销,以保证收尾工作的正常进行。若没有基础类初始 化,则输出结果是: not f i nal i zi ng bas es C eat i ng C act er i s t i c i s al i ve r har Li vi ngC eat ur e( ) r C eat i ng C act er i s t i c has hear t r har A m () ni al C eat i ng C act er i s t i c can l i ve i n w er r har at 201

A phi bi an( ) m Fr og( ) bye! Fr og f i nal i ze f i nal i zi ng C act er i s t i c i s al i ve har f i nal i z i ng C act er i s t i c has hear t har f i nal i z i ng C act er i s t i c can l i ve i n w er har at 从中可以看出确实没有为基础类 Fr og 调用收尾模块。但假如在命令行加入“f i nal i ze”自变量,则会获得下 述结果: C eat i ng C act er i s t i c i s al i ve r har Li vi ngC eat ur e( ) r C eat i ng C act er i s t i c has hear t r har A m () ni al C eat i ng C act er i s t i c can l i ve i n w er r har at A phi bi an( ) m Fr og( ) bye! Fr og f i nal i ze A phi bi an f i nal i ze m A m f i nal i ze ni al Li vi ngC eat ur e f i nal i ze r f i nal i zi ng C act er i s t i c i s al i ve har f i nal i z i ng C act er i s t i c has hear t har f i nal i z i ng C act er i s t i c can l i ve i n w er har at 尽管成员对象按照与它们创建时相同的顺序进行收尾,但从技术角度说,并没有指定对象收尾的顺序。但对 于基础类,我们可对收尾的顺序进行控制。采用的最佳顺序正是在这里采用的顺序,它与初始化顺序正好相 反。按照与 C ++中用于“破坏器”相同的形式,我们应该首先执行衍生类的收尾,再是基础类的收尾。这是 由于衍生类的收尾可能调用基础类中相同的方法,要求基础类组件仍然处于活动状态。因此,必须提前将它 们清除(破坏)。

7 . 7 . 3 构建器内部的多形性方法的行为
构建器调用的分级结构(顺序)为我们带来了一个有趣的问题,或者说让我们进入了一种进退两难的局面。 若当前位于一个构建器的内部,同时调用准备构建的那个对象的一个动态绑定方法,那么会出现什么情况 呢?在原始的方法内部,我们完全可以想象会发生什么——动态绑定的调用会在运行期间进行解析,因为对 象不知道它到底从属于方法所在的那个类,还是从属于从它衍生出来的某些类。为保持一致性,大家也许会 认为这应该在构建器内部发生。 但实际情况并非完全如此。若调用构建器内部一个动态绑定的方法,会使用那个方法被覆盖的定义。然而, 产生的效果可能并不如我们所愿,而且可能造成一些难于发现的程序错误。 从概念上讲,构建器的职责是让对象实际进入存在状态。在任何构建器内部,整个对象可能只是得到部分组 织——我们只知道基础类对象已得到初始化,但却不知道哪些类已经继承。然而,一个动态绑定的方法调用 却会在分级结构里“向前”或者“向外”前进。它调用位于衍生类里的一个方法。如果在构建器内部做这件 事情,那么对于调用的方法,它要操纵的成员可能尚未得到正确的初始化——这显然不是我们所希望的。 通过观察下面这个例子,这个问题便会昭然若揭: //: Pol yC t r uct or s . j ava ons // C t r uct or s and pol ym phi s m ons or // don' t pr oduce w hat you m ght expect . i abs t r act cl as s G yph { l 202

abs t r act voi d dr aw ) ; ( G yph( ) { l Sys t em out . pr i nt l n( " G yph( ) bef or e dr aw ) " ) ; . l ( dr aw ) ; ( Sys t em out . pr i nt l n( " G yph( ) af t er dr aw ) " ) ; . l ( } } cl as s RoundG yph ext ends G yph { l l i nt r adi us = 1; RoundG yph( i nt r ) { l r adi us = r ; Sys t em out . pr i nt l n( . " RoundG yph. RoundG yph( ) , r adi us = " l l + r adi us ) ; } voi d dr aw ) { ( Sys t em out . pr i nt l n( . " RoundG yph. dr aw ) , r adi us = " + r adi us ) ; l ( } } publ i c cl as s Pol yC t r uct or s { ons publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai new RoundG yph( 5) ; l } } ///: ~ 在 G yph 中,dr aw ) 方法是“抽象的”(abs t r act ),所以它可以被其他方法覆盖。事实上,我们在 l ( RoundG yph中不得不对其进行覆盖。但 G yph构建器会调用这个方法,而且调用会在 RoundG yph. dr aw )中 l l l ( 止,这看起来似乎是有意的。但请看看输出结果: G yph( ) bef or e dr aw ) l ( RoundG yph. dr aw ) , r adi us = 0 l ( G yph( ) af t er dr aw ) l ( RoundG yph. RoundG yph( ) , r adi us = 5 l l 当 G yph 的构建器调用 dr aw )时,r adi us 的值甚至不是默认的初始值 1,而是 0。这可能是由于一个点号或 l ( 者屏幕上根本什么都没有画而造成的。这样就不得不开始查找程序中的错误,试着找出程序不能工作的原 因。 前一节讲述的初始化顺序并不十分完整,而那是解决问题的关键所在。初始化的实际过程是这样的: ( 1) 在采取其他任何操作之前,为对象分配的存储空间初始化成二进制零。 ( 2) 就象前面叙述的那样,调用基础类构建器。此时,被覆盖的 dr aw ) 方法会得到调用(的确是在 ( RoundG yph构建器调用之前),此时会发现 r adi us 的值为 0,这是由于步骤( 1)造成的。 l ( 3) 按照原先声明的顺序调用成员初始化代码。 ( 4) 调用衍生类构建器的主体。 采取这些操作要求有一个前提,那就是所有东西都至少要初始化成零(或者某些特殊数据类型与“零”等价 的值),而不是仅仅留作垃圾。其中包括通过“合成”技术嵌入一个类内部的对象句柄。如果假若忘记初始 化那个句柄,就会在运行期间出现违例事件。其他所有东西都会变成零,这在观看结果时通常是一个严重的 警告信号。 在另一方面,应对这个程序的结果提高警惕。从逻辑的角度说,我们似乎已进行了无懈可击的设计,所以它 203

的错误行为令人非常不可思议。而且没有从编译器那里收到任何报错信息(C ++在这种情况下会表现出更合理 的行为)。象这样的错误会很轻易地被人忽略,而且要花很长的时间才能找出。 因此,设计构建器时一个特别有效的规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调 用任何方法。在构建器内唯一能够安全调用的是在基础类中具有 f i nal 属性的那些方法(也适用于 pr i vat e 方法,它们自动具有 f i nal 属性)。这些方法不能被覆盖,所以不会出现上述潜在的问题。

7. 8 通过继承进行设计
学习了多形性的知识后,由于多形性是如此“聪明”的一种工具,所以看起来似乎所有东西都应该继承。但 假如过度使用继承技术,也会使自己的设计变得不必要地复杂起来。事实上,当我们以一个现成类为基础建 立一个新类时,如首先选择继承,会使情况变得异常复杂。 一个更好的思路是首先选择“合成”——如果不能十分确定自己应使用哪一个。合成不会强迫我们的程序设 计进入继承的分级结构中。同时,合成显得更加灵活,因为可以动态选择一种类型(以及行为),而继承要 求在编译期间准确地知道一种类型。下面这个例子对此进行了阐释: //: Tr ans m i f y. j ava ogr // D ynam cal l y changi ng t he behavi or of i // an obj ect vi a com i t i on. pos i nt er f ace A or { ct voi d act ( ) ; } cl as s HappyA or i m em s A or { ct pl ent ct publ i c voi d act ( ) { Sys t em out . pr i nt l n( " HappyA or " ) ; . ct } } cl as s SadA or i m em s A or { ct pl ent ct publ i c voi d act ( ) { Sys t em out . pr i nt l n( " SadA or " ) ; . ct } } cl as s St age { A or a = new HappyA or ( ) ; ct ct voi d change( ) { a = new SadA or ( ) ; } ct voi d go( ) { a. act ( ) ; } } publ i c cl as s Tr ans m i f y { ogr publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai St age s = new St age( ) ; s . go( ) ; // Pr i nt s " HappyA or " ct s . change( ) ; s . go( ) ; // Pr i nt s " SadA or " ct } } ///: ~ 在这里,一个 St age 对象包含了指向一个 A or 的句柄,后者被初始化成一个 HappyA or 对象。这意味着 ct ct go( ) 会产生特定的行为。但由于句柄在运行期间可以重新与一个不同的对象绑定或结合起来,所以 SadA or ct 对象的句柄可在 a中得到替换,然后由 go( ) 产生的行为发生改变。这样一来,我们在运行期间就获得了很大 204

的灵活性。与此相反,我们不能在运行期间换用不同的形式来进行继承;它要求在编译期间完全决定下来。 一条常规的设计准则是:用继承表达行为间的差异,并用成员变量表达状态的变化。在上述例子中,两者都 得到了应用:继承了两个不同的类,用于表达 act ( )方法的差异;而 St age通过合成技术允许它自己的状态 发生变化。在这种情况下,那种状态的改变同时也产生了行为的变化。

7 . 8 . 1 纯继承与扩展
学习继承时,为了创建继承分级结构,看来最明显的方法是采取一种“纯粹”的手段。也就是说,只有在基 础类或“接口”中已建立的方法才可在衍生类中被覆盖,如下面这张图所示:

可将其描述成一种纯粹的“属于”关系,因为一个类的接口已规定了它到底“是什么”或者“属于什么”。 通过继承,可保证所有衍生类都只拥有基础类的接口。如果按上述示意图操作,衍生出来的类除了基础类的 接口之外,也不会再拥有其他什么。 可将其想象成一种“纯替换”,因为衍生类对象可为基础类完美地替换掉。使用它们的时候,我们根本没必 要知道与子类有关的任何额外信息。如下所示:

也就是说,基础类可接收我们发给衍生类的任何消息,因为两者拥有完全一致的接口。我们要做的全部事情 就是从衍生上溯造型,而且永远不需要回过头来检查对象的准确类型是什么。所有细节都已通过多形性获得 了完美的控制。 若按这种思路考虑问题,那么一个纯粹的“属于”关系似乎是唯一明智的设计方法,其他任何设计方法都会 导致混乱不清的思路,而且在定义上存在很大的困难。但这种想法又属于另一个极端。经过细致的研究,我 们发现扩展接口对于一些特定问题来说是特别有效的方案。可将其称为“类似于”关系,因为扩展后的衍生 类“类似于”基础类——它们有相同的基础接口——但它增加了一些特性,要求用额外的方法加以实现。如 下所示:

205

尽管这是一种有用和明智的做法(由具体的环境决定),但它也有一个缺点:衍生类中对接口扩展的那一部 分不可在基础类中使用。所以一旦上溯造型,就不可再调用新方法:

若在此时不进行上溯造型,则不会出现此类问题。但在许多情况下,都需要重新核实对象的准确类型,使自 己能访问那个类型的扩展方法。在后面的小节里,我们具体讲述了这是如何实现的。

7 . 8 . 2 下溯造型与运行期类型标识
由于我们在上溯造型(在继承结构中向上移动)期间丢失了具体的类型信息,所以为了获取具体的类型信 息——亦即在分级结构中向下移动——我们必须使用 “下溯造型”技术。然而,我们知道一个上溯造型肯定 是安全的;基础类不可能再拥有一个比衍生类更大的接口。因此,我们通过基础类接口发送的每一条消息都 肯定能够接收到。但在进行下溯造型的时候,我们(举个例子来说)并不真的知道一个几何形状实际是一个 圆,它完全可能是一个三角形、方形或者其他形状。

206

为解决这个问题,必须有一种办法能够保证下溯造型正确进行。只有这样,我们才不会冒然造型成一种错误 的类型,然后发出一条对象不可能收到的消息。这样做是非常不安全的。 在某些语言中(如 C ++),为了进行保证“类型安全”的下溯造型,必须采取特殊的操作。但在 Java 中,所 有造型都会自动得到检查和核实!所以即使我们只是进行一次普通的括弧造型,进入运行期以后,仍然会毫 无留情地对这个造型进行检查,保证它的确是我们希望的那种类型。如果不是,就会得到一个 C as s C t Except i on(类造型违例)。在运行期间对类型进行检查的行为叫作“运行期类型标识” l as (RTTI )。下面这个例子向大家演示了 RTTI 的行为: //: RTTI . j ava // D ncas t i ng & Run- Ti m Type ow e // I dent i f i cat i on ( RTTI ) i m t j ava. ut i l . * ; por cl as s Us ef ul { publ i c voi d f ( ) { } publ i c voi d g( ) { } } cl as s M eUs ef ul ext ends Us ef ul { or publ i c voi d f ( ) { } publ i c voi d g( ) { } publ i c voi d u( ) { } publ i c voi d v( ) { } publ i c voi d w ) { } ( } publ i c cl as s RTTI { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Us ef ul [ ] x = { new Us ef ul ( ) , new M eUs ef ul ( ) or }; x[ 0] . f ( ) ; x[ 1] . g( ) ; // C pi l e- t i m m hod not f ound i n Us ef ul : om e: et 207

//! x[ 1] . u( ) ; ( ( M eUs ef ul ) x[ 1] ) . u( ) ; // D ncas t /RTTI or ow ( ( M eUs ef ul ) x[ 0] ) . u( ) ; // Except i on t hr ow or n } } ///: ~ 和在示意图中一样,M eUs ef ul (更有用的)对 Us ef ul (有用的)的接口进行了扩展。但由于它是继承来 or 的,所以也能上溯造型到一个 Us ef ul 。我们可看到这会在对数组 x(位于 m n( )中)进行初始化的时候发 ai 生。由于数组中的两个对象都属于 Us ef ul 类,所以可将 f ( )和 g( ) 方法同时发给它们两个。而且假如试图调 用 u( ) (它只存在于 M eUs ef ul ),就会收到一条编译期出错提示。 or 若想访问一个 M eUs ef ul 对象的扩展接口,可试着进行下溯造型。如果它是正确的类型,这一行动就会成 or 功。否则,就会得到一个 C as s C t Except i on。我们不必为这个违例编写任何特殊的代码,因为它指出的是 l as 一个可能在程序中任何地方发生的一个编程错误。 RTTI 的意义远不仅仅反映在造型处理上。例如,在试图下溯造型之前,可通过一种方法了解自己处理的是什 么类型。整个第 11 章都在讲述 Java 运行期类型标识的方方面面。

7. 9 总结
“多形性”意味着“不同的形式”。在面向对象的程序设计中,我们有相同的外观(基础类的通用接口)以 及使用那个外观的不同形式:动态绑定或组织的、不同版本的方法。 通过这一章的学习,大家已知道假如不利用数据抽象以及继承技术,就不可能理解、甚至去创建多形性的一 个例子。多形性是一种不可独立应用的特性(就象一个 s w t ch 语句),只可与其他元素协同使用。我们应将 i 其作为类总体关系的一部分来看待。人们经常混淆 Java 其他的、非面向对象的特性,比如方法过载等,这些 特性有时也具有面向对象的某些特征。但不要被愚弄:如果以后没有绑定,就不成其为多形性。 为使用多形性乃至面向对象的技术,特别是在自己的程序中,必须将自己的编程视野扩展到不仅包括单独一 个类的成员和消息,也要包括类与类之间的一致性以及它们的关系。尽管这要求学习时付出更多的精力,但 却是非常值得的,因为只有这样才可真正有效地加快自己的编程速度、更好地组织代码、更容易做出包容面 广的程序以及更易对自己的代码进行维护与扩展。

7. 10 练习
( 1) 创建 Rodent (啮齿动物): M e(老鼠), G bi l (鼹鼠), Ham t er (大颊鼠)等的一个继承分级结 ous er s 构。在基础类中,提供适用于所有 Rodent 的方法,并在衍生类中覆盖它们,从而根据不同类型的 Rodent 采 取不同的行动。创建一个 Rodent 数组,在其中填充不同类型的 Rodent ,然后调用自己的基础类方法,看看 会有什么情况发生。 ( 2) 修改练习 1,使 Rodent 成为一个接口。 ( 3) 改正 W ndEr r or . j ava中的问题。 i ( 4) 在 G eenhous eC r ol s . j ava中,添加 Event 内部类,使其能打开和关闭风扇。 r ont

208

第 8 章 对象的容纳
“如果一个程序只含有数量固定的对象,而且已知它们的存在时间,那么这个程序可以说是相当简单的。” 通常,我们的程序需要根据程序运行时才知道的一些标准创建新对象。若非程序正式运行,否则我们根本不 知道自己到底需要多少数量的对象,甚至不知道它们的准确类型。为了满足常规编程的需要,我们要求能在 任何时候、任何地点创建任意数量的对象。所以不可依赖一个已命名的句柄来容纳自己的每一个对象,就象 下面这样: M bj ect m yO yHandl e; 因为根本不知道自己实际需要多少这样的东西。 为解决这个非常关键的问题,Java 提供了容纳对象(或者对象的句柄)的多种方式。其中内建的类型是数 组,我们之前已讨论过它,本章准备加深大家对它的认识。此外,Java 的工具(实用程序)库提供了一些 “集合类”(亦称作“容器类”,但该术语已由 A T 使用,所以这里仍采用“集合”这一称呼)。利用这些 W 集合类,我们可以容纳乃至操纵自己的对象。本章的剩余部分会就此进行详细讨论。

8. 1 数组
对数组的大多数必要的介绍已在第 4 章的最后一节进行。通过那里的学习,大家已知道自己该如何定义及初 始化一个数组。对象的容纳是本章的重点,而数组只是容纳对象的一种方式。但由于还有其他大量方法可容 纳数组,所以是哪些地方使数组显得如此特别呢? 有两方面的问题将数组与其他集合类型区分开来:效率和类型。对于 Java 来说,为保存和访问一系列对象 (实际是对象的句柄)数组,最有效的方法莫过于数组。数组实际代表一个简单的线性序列,它使得元素的 访问速度非常快,但我们却要为这种速度付出代价:创建一个数组对象时,它的大小是固定的,而且不可在 那个数组对象的“存在时间”内发生改变。可创建特定大小的一个数组,然后假如用光了存储空间,就再创 建一个新数组,将所有句柄从旧数组移到新数组。这属于“矢量”(Vect or )类的行为,本章稍后还会详细 讨论它。然而,由于为这种大小的灵活性要付出较大的代价,所以我们认为矢量的效率并没有数组高。 C ++的矢量类知道自己容纳的是什么类型的对象,但同 Java 的数组相比,它却有一个明显的缺点:C ++矢量类 的 oper at or [ ] 不能进行范围检查,所以很容易超出边界(然而,它可以查询 vect or 有多大,而且 at ( ) 方法 确实能进行范围检查)。在 Java 中,无论使用的是数组还是集合,都会进行范围检查——若超过边界,就会 获得一个 Runt i m eExcept i on(运行期违例)错误。正如大家在第 9 章会学到的那样,这类违例指出的是一个 程序员错误,所以不需要在代码中检查它。在另一方面,由于 C ++的 vect or 不进行范围检查,所以访问速度 较快——在 Java 中,由于对数组和集合都要进行范围检查,所以对性能有一定的影响。 本章还要学习另外几种常见的集合类:Vect or (矢量)、St ack(堆栈)以及 Has ht abl e(散列表)。这些类 都涉及对对象的处理——好象它们没有特定的类型。换言之,它们将其当作 O ect 类型处理(O ect 类型 bj bj 是 Java 中所有类的“根”类)。从某个角度看,这种处理方法是非常合理的:我们仅需构建一个集合,然后 任何 Java 对象都可以进入那个集合(除基本数据类型外——可用 Java 的基本类型封装类将其作为常数置入 集合,或者将其封装到自己的类内,作为可以变化的值使用)。这再一次反映了数组优于常规集合:创建一 个数组时,可令其容纳一种特定的类型。这意味着可进行编译期类型检查,预防自己设置了错误的类型,或 者错误指定了准备提取的类型。当然,在编译期或者运行期,Java 会防止我们将不当的消息发给一个对象。 所以我们不必考虑自己的哪种做法更加危险,只要编译器能及时地指出错误,同时在运行期间加快速度,目 的也就达到了。此外,用户很少会对一次违例事件感到非常惊讶的。 考虑到执行效率和类型检查,应尽可能地采用数组。然而,当我们试图解决一个更常规的问题时,数组的局 限也可能显得非常明显。在研究过数组以后,本章剩余的部分将把重点放到 Java 提供的集合类身上。

8 . 1 . 1 数组和第一类对象
无论使用的数组属于什么类型,数组标识符实际都是指向真实对象的一个句柄。那些对象本身是在内存 “堆”里创建的。堆对象既可“隐式”创建(即默认产生),亦可“显式”创建(即明确指定,用一个 new 表达式)。堆对象的一部分(实际是我们能访问的唯一字段或方法)是只读的 l engt h(长度)成员,它告诉 我们那个数组对象里最多能容纳多少元素。对于数组对象,“[ ] ”语法是我们能采用的唯一另类访问方法。 下面这个例子展示了对数组进行初始化的不同方式,以及如何将数组句柄分配给不同的数组对象。它也揭示 209

出对象数组和基本数据类型数组在使用方法上几乎是完全一致的。唯一的差别在于对象数组容纳的是句柄, 而基本数据类型数组容纳的是具体的数值(若在执行此程序时遇到困难,请参考第 3 章的“赋值”小节): //: A r aySi ze. j ava r // I ni t i al i zat i on & r e- as s i gnm ent of ar r ays package c08; cl as s W eebl e { } // A s m l m hi cal cr eat ur e al yt publ i c cl as s A r aySi ze { r publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai // A r ays of obj ect s : r W eebl e[ ] a; // N l handl e ul W eebl e[ ] b = new W eebl e[ 5] ; // N l handl es ul W eebl e[ ] c = new W eebl e[ 4] ; f or ( i nt i = 0; i < c. l engt h; i ++) c[ i ] = new W eebl e( ) ; W eebl e[ ] d = { new W eebl e( ) , new W eebl e( ) , new W eebl e( ) }; // C pi l e er r or : var i abl e a not i ni t i al i zed: om //! Sys t em out . pr i nt l n( " a. l engt h=" + a. l engt h) ; . Sys t em out . pr i nt l n( " b. l engt h = " + b. l engt h) ; . // The handl es i ns i de t he ar r ay ar e // aut om i cal l y i ni t i al i zed t o nul l : at f or ( i nt i = 0; i < b. l engt h; i ++) Sys t em out . pr i nt l n( " b[ " + i + " ] =" + b[ i ] ) ; . Sys t em out . pr i nt l n( " c. l engt h = " + c. l engt h) ; . Sys t em out . pr i nt l n( " d. l engt h = " + d. l engt h) ; . a = d; Sys t em out . pr i nt l n( " a. l engt h = " + a. l engt h) ; . // Java 1. 1 i ni t i al i z at i on s ynt ax: a = new W eebl e[ ] { new W eebl e( ) , new W eebl e( ) }; Sys t em out . pr i nt l n( " a. l engt h = " + a. l engt h) ; . / / A r ays of pr i m t i ves : r i i nt [ ] e; // N l handl e ul i nt [ ] f = new i nt [ 5] ; i nt [ ] g = new i nt [ 4] ; f or ( i nt i = 0; i < g. l engt h; i ++) g[ i ] = i *i ; i nt [ ] h = { 11, 47, 93 } ; // C pi l e er r or : var i abl e e not i ni t i al i zed: om //! Sys t em out . pr i nt l n( " e. l engt h=" + e. l engt h) ; . Sys t em out . pr i nt l n( " f . l engt h = " + f . l engt h) ; . // The pr i m t i ves i ns i de t he ar r ay ar e i // aut om i cal l y i ni t i al i zed t o zer o: at f or ( i nt i = 0; i < f . l engt h; i ++) Sys t em out . pr i nt l n( " f [ " + i + " ] =" + f [ i ] ) ; . Sys t em out . pr i nt l n( " g. l engt h = " + g. l engt h) ; . 210

Sys t em out . pr i nt l n( " h. l engt h = " + h. l engt h) ; . e = h; Sys t em out . pr i nt l n( " e. l engt h = " + e. l engt h) ; . // Java 1. 1 i ni t i al i z at i on s ynt ax: e = new i nt [ ] { 1, 2 } ; Sys t em out . pr i nt l n( " e. l engt h = " + e. l engt h) ; . } } ///: ~ Her e’s t he out put f r om t he pr ogr am :

b. l engt h b[ 0] =nul l b[ 1] =nul l b[ 2] =nul l b[ 3] =nul l b[ 4] =nul l c. l engt h d. l engt h a. l engt h a. l engt h f . l engt h f [ 0] =0 f [ 1] =0 f [ 2] =0 f [ 3] =0 f [ 4] =0 g. l engt h h. l engt h e. l engt h e. l engt h

= 5

= = = = =

4 3 3 2 5

= = = =

4 3 3 2

其中,数组 a只是初始化成一个 nul l 句柄。此时,编译器会禁止我们对这个句柄作任何实际操作,除非已正 确地初始化了它。数组 b被初始化成指向由 W eebl e 句柄构成的一个数组,但那个数组里实际并未放置任何 W eebl e对象。然而,我们仍然可以查询那个数组的大小,因为 b指向的是一个合法对象。这也为我们带来了 一个难题:不可知道那个数组里实际包含了多少个元素,因为 l engt h只告诉我们可将多少元素置入那个数 组。换言之,我们只知道数组对象的大小或容量,不知其实际容纳了多少个元素。尽管如此,由于数组对象 在创建之初会自动初始化成 nul l ,所以可检查它是否为 nul l ,判断一个特定的数组“空位”是否容纳一个对 象。类似地,由基本数据类型构成的数组会自动初始化成零(针对数值类型)、nul l (字符类型)或者 f al s e(布尔类型)。 数组 c 显示出我们首先创建一个数组对象,再将 W eebl e 对象赋给那个数组的所有“空位”。数组 d揭示出 “集合初始化”语法,从而创建数组对象(用 new命令明确进行,类似于数组 c),然后用 W eebl e 对象进行 初始化,全部工作在一条语句里完成。 下面这个表达式: a = d; 向我们展示了如何取得同一个数组对象连接的句柄,然后将其赋给另一个数组对象,就象我们针对对象句柄 的其他任何类型做的那样。现在,a和 d都指向内存堆内同样的数组对象。 Java 1. 1 加入了一种新的数组初始化语法,可将其想象成“动态集合初始化”。由 d采用的 Java 1. 0 集合 初始化方法则必须在定义 d的同时进行。但若采用 Java 1. 1 的语法,却可以在任何地方创建和初始化一个数 组对象。例如,假设 hi de( ) 方法用于取得一个 W eebl e 对象数组,那么调用它时传统的方法是: 211

hi de( d) ; 但在 Java 1. 1 中,亦可动态创建想作为参数传递的数组,如下所示: hi de( new W eebl e[ ] { new W eebl e( ) , new W eebl e( ) } ) ; 这一新式语法使我们在某些场合下写代码更方便了。 上述例子的第二部分揭示出这样一个问题:对于由基本数据类型构成的数组,它们的运作方式与对象数组极 为相似,只是前者直接包容了基本类型的数据值。 1. 基本数据类型集合 集合类只能容纳对象句柄。但对一个数组,却既可令其直接容纳基本类型的数据,亦可容纳指向对象的句 柄。利用象 I nt eger 、D oubl e之类的“封装器”类,可将基本数据类型的值置入一个集合里。但正如本章后 面会在 W dC or ount . j ava例子中讲到的那样,用于基本数据类型的封装器类只是在某些场合下才能发挥作用。 无论将基本类型的数据置入数组,还是将其封装进入位于集合的一个类内,都涉及到执行效率的问题。显 然,若能创建和访问一个基本数据类型数组,那么比起访问一个封装数据的集合,前者的效率会高出许多。 当然,假如准备一种基本数据类型,同时又想要集合的灵活性(在需要的时候可自动扩展,腾出更多的空 间),就不宜使用数组,必须使用由封装的数据构成的一个集合。大家或许认为针对每种基本数据类型,都 应有一种特殊类型的 Vect or 。但 Java 并未提供这一特性。某些形式的建模机制或许会在某一天帮助 Java 更 好地解决这个问题(注释①)。 ①:这儿是 C ++比 Java 做得好的一个地方,因为 C ++通过 t em at e 关键字提供了对“参数化类型”的支持。 pl

8 . 1 . 2 数组的返回
假定我们现在想写一个方法,同时不希望它仅仅返回一样东西,而是想返回一系列东西。此时,象 C和 C ++ 这样的语言会使问题复杂化,因为我们不能返回一个数组,只能返回指向数组的一个指针。这样就非常麻 烦,因为很难控制数组的“存在时间”,它很容易造成内存“漏洞”的出现。 Java 采用的是类似的方法,但我们能“返回一个数组”。当然,此时返回的实际仍是指向数组的指针。但在 Java 里,我们永远不必担心那个数组的是否可用——只要需要,它就会自动存在。而且垃圾收集器会在我们 完成后自动将其清除。 作为一个例子,请思考如何返回一个字串数组: //: I ceC eam j ava r . // Ret ur ni ng ar r ays f r om m hods et publ i c cl as s I ceC eam { r s t at i c St r i ng[ ] f l av = { "C hocol at e" , " St r aw r y" , ber " Vani l l a Fudge Sw r l " , " M nt C p" , i i hi "M ocha A m l ond Fudge" , " Rum Rai s i n" , " Pr al i ne C eam , " M Pi e" r " ud }; s t at i c St r i ng[ ] f l avor Set ( i nt n) { // For ce i t t o be pos i t i ve & w t hi n bounds : i n = M h. abs ( n) % ( f l av. l engt h + 1) ; at St r i ng[ ] r es ul t s = new St r i ng[ n] ; i nt [ ] pi cks = new i nt [ n] ; f or ( i nt i = 0; i < pi cks . l engt h; i ++) pi cks [ i ] = - 1; f or ( i nt i = 0; i < pi cks . l engt h; i ++) { r et r y: w l e( t r ue) { hi i nt t = ( i nt ) ( M h. r andom ) * f l av. l engt h) ; at ( f or ( i nt j = 0; j < i ; j ++) 212

i f ( pi cks [ j ] == t ) cont i nue r et r y; pi cks [ i ] = t ; r es ul t s [ i ] = f l av[ t ] ; br eak; } } r et ur n r es ul t s ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai f or ( i nt i = 0; i < 20; i ++) { Sys t em out . pr i nt l n( . " f l avor Set ( " + i + " ) = " ) ; St r i ng[ ] f l = f l avor Set ( f l av. l engt h) ; f or ( i nt j = 0; j < f l . l engt h; j ++) Sys t em out . pr i nt l n( " \t " + f l [ j ] ) ; . } } } ///: ~ f l avor Set ( ) 方法创建了一个名为 r es ul t s 的 St r i ng数组。该数组的大小为 n——具体数值取决于我们传递 给方法的自变量。随后,它从数组 f l av 里随机挑选一些“香料”(Fl avor ),并将它们置入 r es ul t s 里,并 最终返回 r es ul t s 。返回数组与返回其他任何对象没什么区别——最终返回的都是一个句柄。至于数组到底 是在 f l avor Set ( )里创建的,还是在其他什么地方创建的,这个问题并不重要,因为反正返回的仅是一个句 柄。一旦我们的操作完成,垃圾收集器会自动关照数组的清除工作。而且只要我们需要数组,它就会乖乖地 听候调遣。 另一方面,注意当 f l avor Set ( ) 随机挑选香料的时候,它需要保证以前出现过的一次随机选择不会再次出 现。为达到这个目的,它使用了一个无限 w l e 循环,不断地作出随机选择,直到发现未在 pi cks 数组里出 hi 现过的一个元素为止(当然,也可以进行字串比较,检查随机选择是否在 r es ul t s 数组里出现过,但字串比 较的效率比较低)。若成功,就添加这个元素,并中断循环(br eak),再查找下一个(i 值会递增)。但假 若 t 是一个已在 pi cks 里出现过的数组,就用标签式的 cont i nue 往回跳两级,强制选择一个新 t 。用一个调 试程序可以很清楚地看到这个过程。 m n( )能显示出 20 个完整的香料集合,所以我们看到 f l avor Set ( ) 每次都用一个随机顺序选择香料。为体会 ai 这一点,最简单的方法就是将输出重导向进入一个文件,然后直接观看这个文件的内容。

8. 2 集合
现在总结一下我们前面学过的东西:为容纳一组对象,最适宜的选择应当是数组。而且假如容纳的是一系列 基本数据类型,更是必须采用数组。在本章剩下的部分,大家将接触到一些更常规的情况。当我们编写程序 时,通常并不能确切地知道最终需要多少个对象。有些时候甚至想用更复杂的方式来保存对象。为解决这个 问题,Java 提供了四种类型的“集合类”:Vect or (矢量)、Bi t Set (位集)、St ack(堆栈)以及 Has ht abl e(散列表)。与拥有集合功能的其他语言相比,尽管这儿的数量显得相当少,但仍然能用它们解决 数量惊人的实际问题。 这些集合类具有形形色色的特征。例如,St ack 实现了一个 LI FO (先入先出)序列,而 Has ht abl e 是一种 “关联数组”,允许我们将任何对象关联起来。除此以外,所有 Java 集合类都能自动改变自身的大小。所 以,我们在编程时可使用数量众多的对象,同时不必担心会将集合弄得有多大。

8 . 2 . 1 缺点:类型未知
使用 Java 集合的“缺点”是在将对象置入一个集合时丢失了类型信息。之所以会发生这种情况,是由于当初 编写集合时,那个集合的程序员根本不知道用户到底想把什么类型置入集合。若指示某个集合只允许特定的 类型,会妨碍它成为一个“常规用途”的工具,为用户带来麻烦。为解决这个问题,集合实际容纳的是类型 为 O ect 的一些对象的句柄。这种类型当然代表 Java 中的所有对象,因为它是所有类的根。当然,也要注 bj 意这并不包括基本数据类型,因为它们并不是从“任何东西”继承来的。这是一个很好的方案,只是不适用 213

下述场合: ( 1) 将一个对象句柄置入集合时,由于类型信息会被抛弃,所以任何类型的对象都可进入我们的集合——即 便特别指示它只能容纳特定类型的对象。举个例子来说,虽然指示它只能容纳猫,但事实上任何人都可以把 一条狗扔进来。 ( 2) 由于类型信息不复存在,所以集合能肯定的唯一事情就是自己容纳的是指向一个对象的句柄。正式使用 它之前,必须对其进行造型,使其具有正确的类型。 值得欣慰的是,Java 不允许人们滥用置入集合的对象。假如将一条狗扔进一个猫的集合,那么仍会将集合内 的所有东西都看作猫,所以在使用那条狗时会得到一个“违例”错误。在同样的意义上,假若试图将一条狗 的句柄“造型”到一只猫,那么运行期间仍会得到一个“违例”错误。 下面是个例子: //: C s A ogs . j ava at ndD // Si m e col l ect i on exam e ( Vect or ) pl pl i m t j ava. ut i l . * ; por cl as s C { at pr i vat e i nt cat N ber ; um C ( i nt i ) { at cat N ber = i ; um } voi d pr i nt ( ) { Sys t em o . pr i nt l n( " C #" + cat N ber ) ; . ut at um } } cl as s D { og pr i vat e i nt dogN ber ; um D i nt i ) { og( dogN ber = i ; um } voi d pr i nt ( ) { Sys t em out . pr i nt l n( " D #" + dogN ber ) ; . og um } } publ i c cl as s C s A ogs { at ndD publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Vect or cat s = new Vect or ( ) ; f or ( i nt i = 0; i < 7; i ++) cat s . addEl em ( new C ( i ) ) ; ent at // N a pr obl em t o add a dog t o cat s : ot cat s . addEl em ( new D 7) ) ; ent og( f or ( i nt i = 0; i < cat s . s i ze( ) ; i ++) ( ( C ) cat s . el em A ( i ) ) . pr i nt ( ) ; at ent t // D i s det ect ed onl y at r un- t i m og e } } ///: ~ 可以看出,Vect or 的使用是非常简单的:先创建一个,再用 addEl em ( )置入对象,以后用 el em A ( ) 取 ent ent t 得那些对象(注意 Vect or 有一个 s i z e( )方法,可使我们知道已添加了多少个元素,以便防止误超边界,造 成违例错误)。 214

C 和D at og类都非常浅显——除了都是“对象”之外,它们并无特别之处(倘若不明确指出从什么类继承, 就默认为从 O ect 继承。所以我们不仅能用 Vect or 方法将 C 对象置入这个集合,也能添加 D bj at og对象,同 时不会在编译期和运行期得到任何出错提示。用 Vect or 方法 el em A ( )获取原本认为是 C 的对象时,实 ent t at 际获得的是指向一个 O ect 的句柄,必须将那个对象造型为 C 。随后,需要将整个表达式用括号封闭起 bj at 来,在为 C 调用 pr i nt ( ) 方法之前进行强制造型;否则就会出现一个语法错误。在运行期间,如果试图将 at D og对象造型为 C ,就会得到一个违例。 at 这些处理的意义都非常深远。尽管显得有些麻烦,但却获得了安全上的保证。我们从此再难偶然造成一些隐 藏得深的错误。若程序的一个部分(或几个部分)将对象插入一个集合,但我们只是通过一次违例在程序的 某个部分发现一个错误的对象置入了集合,就必须找出插入错误的位置。当然,可通过检查代码达到这个目 的,但这或许是最笨的调试工具。另一方面,我们可从一些标准化的集合类开始自己的编程。尽管它们在功 能上存在一些不足,且显得有些笨拙,但却能保证没有隐藏的错误。 1. 错误有时并不显露出来 在某些情况下,程序似乎正确地工作,不造型回我们原来的类型。第一种情况是相当特殊的:St r i ng类从编 译器获得了额外的帮助,使其能够正常工作。只要编译器期待的是一个 St r i ng对象,但它没有得到一个,就 会自动调用在 O ect 里定义、并且能够由任何 Java 类覆盖的 t oSt r i ng( )方法。这个方法能生成满足要求的 bj St r i ng对象,然后在我们需要的时候使用。 因此,为了让自己类的对象能显示出来,要做的全部事情就是覆盖 t oSt r i ng( ) 方法,如下例所示: //: W ks A ay. j ava or nyw // I n s peci al cas es , t hi ngs j us t s eem // t o w k cor r ect l y. or i m t j ava. ut i l . * ; por cl as s M e { ous pr i vat e i nt m eN ber ; ous um M e( i nt i ) { ous m eN ber = i ; ous um } // M c m hod: agi et publ i c St r i ng t oSt r i ng( ) { r et ur n " Thi s i s M e #" + m eN ber ; ous ous um } voi d pr i nt ( St r i ng m g) { s i f ( m g ! = nul l ) Sys t em out . pr i nt l n( m g) ; s . s Sys t em out . pr i nt l n( . " M e num ous ber " + m eN ber ) ; ous um } } cl as s M eTr ap { ous s t at i c voi d caught Ya( O ect m { bj ) M e m e = ( M e) m // C t f r om O ect ous ous ous ; as bj m e. pr i nt ( " C ous aught one! " ) ; } } publ i c cl as s W ks A ay { or nyw publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Vect or m ce = new Vect or ( ) ; i f or ( i nt i = 0; i < 3; i ++) m ce. addEl em ( new M e( i ) ) ; i ent ous 215

f or ( i nt i = 0; i < m ce. s i ze( ) ; i ++) { i // N cas t neces s ar y, aut om i c cal l o at // t o O ect . t oSt r i ng( ) : bj Sys t em out . pr i nt l n( . " Fr ee m e: " + m ce. el em A ( i ) ) ; ous i ent t M eTr ap. caught Ya( m ce. el em A ( i ) ) ; ous i ent t } } } ///: ~ 可在 M e 里看到对 t oSt r i ng( ) 的重定义代码。在 m n( ) 的第二个 f or 循环中,可发现下述语句: ous ai Sys t em out . pr i nt l n( " Fr ee m e: " + . ous m ce. el em A ( i ) ) ; i ent t 在“+”后,编译器预期看到的是一个 St r i ng对象。el em A ( ) 生成了一个 O ect ,所以为获得希望的 ent t bj St r i ng ,编译器会默认调用 t oSt r i ng( ) 。但不幸的是,只有针对 St r i ng才能得到象这样的结果;其他任何 类型都不会进行这样的转换。 隐藏造型的第二种方法已在 M et r ap 里得到了应用。caught Ya( ) 方法接收的不是一个 M e,而是一个 ous ous O ect 。随后再将其造型为一个 M e。当然,这样做是非常冒失的,因为通过接收一个 O ect ,任何东西 bj ous bj 都可以传递给方法。然而,假若造型不正确——如果我们传递了错误的类型——就会在运行期间得到一个违 例错误。这当然没有在编译期进行检查好,但仍然能防止问题的发生。注意在使用这个方法时毋需进行造 型: M eTr ap. caught Ya( m ce. el em A ( i ) ) ; ous i ent t 2. 生成能自动判别类型的 Vect or 大家或许不想放弃刚才那个问题。一个更“健壮”的方案是用 Vect or 创建一个新类,使其只接收我们指定的 类型,也只生成我们希望的类型。如下所示: //: G opher Vect or . j ava // A t ype- cons ci ous Vect or i m t j ava. ut i l . * ; por cl as s G opher { pr i vat e i nt gopher N ber ; um G opher ( i nt i ) { gopher N ber = i ; um } voi d pr i nt ( St r i ng m g) { s i f ( m g ! = nul l ) Sys t em out . pr i nt l n( m g) ; s . s Sys t em out . pr i nt l n( . "G opher num ber " + gopher N ber ) ; um } } cl as s G opher Tr ap { s t at i c voi d caught Ya( G opher g) { g. pr i nt ( " C aught one! " ) ; } } cl as s G opher Vect or { 216

pr i vat e Vect or v = new Vect or ( ) ; publ i c voi d addEl em ( G ent opher m { ) v. addEl em ( m ; ent ) } publ i c G opher el em A ( i nt i ndex) { ent t r et ur n ( G opher ) v. el em A ( i ndex) ; ent t } publ i c i nt s i ze( ) { r et ur n v. s i ze( ) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai G opher Vect or gopher s = new G opher Vect or ( ) ; f or ( i nt i = 0; i < 3; i ++) gopher s . addEl em ( new G ent opher ( i ) ) ; f or ( i nt i = 0; i < gopher s . s i z e( ) ; i ++) G opher Tr ap. caught Ya( gopher s . el em A ( i ) ) ; ent t } } ///: ~ 这前一个例子类似,只是新的 G opher Vect or 类有一个类型为 Vect or 的 pr i vat e成员(从 Vect o 继承有些麻 r 烦,理由稍后便知),而且方法也和 Vect or 类似。然而,它不会接收和产生普通 O ect ,只对 G bj opher 对象 感兴趣。 由于 G opher Vect or 只接收一个 G opher (地鼠),所以假如我们使用: gopher s . addEl em ( new Pi geon( ) ) ; ent 就会在编译期间获得一条出错消息。采用这种方式,尽管从编码的角度看显得更令人沉闷,但可以立即判断 出是否使用了正确的类型。 注意在使用 el em A ( )时不必进行造型——它肯定是一个 G ent t opher 。 3. 参数化类型 这类问题并不是孤立的——我们许多时候都要在其他类型的基础上创建新类型。此时,在编译期间拥有特定 的类型信息是非常有帮助的。这便是“参数化类型”的概念。在 C ++中,它由语言通过“模板”获得了直接 支持。至少,Java 保留了关键字 gener i c,期望有一天能够支持参数化类型。但我们现在无法确定这一天何 时会来临。

8. 3 枚举器(反复器)
在任何集合类中,必须通过某种方法在其中置入对象,再用另一种方法从中取得对象。毕竟,容纳各种各样 的对象正是集合的首要任务。在 Vect or 中,addEl em ( )便是我们插入对象采用的方法,而 el em A ( ) 是 ent ent t 提取对象的唯一方法。Vect or 非常灵活,我们可在任何时候选择任何东西,并可使用不同的索引选择多个元 素。 若从更高的角度看这个问题,就会发现它的一个缺陷:需要事先知道集合的准确类型,否则无法使用。乍看 来,这一点似乎没什么关系。但假若最开始决定使用 Vect or ,后来在程序中又决定(考虑执行效率的原因) 改变成一个 Li s t (属于 Java1. 2 集合库的一部分),这时又该如何做呢? 可利用“反复器”(I t er at or )的概念达到这个目的。它可以是一个对象,作用是遍历一系列对象,并选择 那个序列中的每个对象,同时不让客户程序员知道或关注那个序列的基础结构。此外,我们通常认为反复器 是一种“轻量级”对象;也就是说,创建它只需付出极少的代价。但也正是由于这个原因,我们常发现反复 器存在一些似乎很奇怪的限制。例如,有些反复器只能朝一个方向移动。 Java 的 Enum at i on(枚举,注释②)便是具有这些限制的一个反复器的例子。除下面这些外,不可再用它 er 做其他任何事情: ( 1) 用一个名为 el em s( ) 的方法要求集合为我们提供一个 Enum at i on。我们首次调用它的 next El em ( ) ent er ent 时,这个 Enum at i on 会返回序列中的第一个元素。 er ( 2) 用 next El em ( ) 获得下一个对象。 ent ( 3) 用 has M eEl em s ( ) 检查序列中是否还有更多的对象。 or ent ②:“反复器”这个词在 C ++和 O P 的其他地方是经常出现的,所以很难确定为什么 Java 的开发者采用了这 O 217

样一个奇怪的名字。Java 1. 2 的集合库修正了这个问题以及其他许多问题。 只可用 Enum at i on 做这些事情,不能再有更多。它属于反复器一种简单的实现方式,但功能依然十分强 er 大。为体会它的运作过程,让我们复习一下本章早些时候提到的 C s A ogs . j ava程序。在原始版本中, at ndD el em A ( )方法用于选择每一个元素,但在下述修订版中,可看到使用了一个“枚举”: ent t //: C s A ogs 2. j ava at ndD // Si m e col l ect i on w t h Enum at i on pl i er i m t j ava. ut i l . * ; por cl as s C 2 { at pr i vat e i nt cat N ber ; um C 2( i nt i ) { at cat N ber = i ; um } voi d pr i nt ( ) { Sys t em out . pr i nt l n( " C num . at ber " +cat N ber ) ; um } } cl as s D og2 { pr i vat e i nt dogN ber ; um D og2( i nt i ) { dogN ber = i ; um } voi d pr i nt ( ) { Sys t em out . pr i nt l n( " D num . og ber " +dogN ber ) ; um } } publ i c cl as s C s A ogs 2 { at ndD publ i c s t at i c voi d m n(St r i ng[ ] ar gs ) { ai Vect or cat s = new Vect or ( ) ; f or ( i nt i = 0; i < 7; i ++) cat s . addEl em ( new C 2( i ) ) ; ent at // N a pr obl em t o add a dog t o cat s : ot cat s . addEl em ( new D ent og2( 7) ) ; Enum at i on e = cat s . el em s ( ) ; er ent w l e( e. has M eEl em s ( ) ) hi or ent ( ( C 2) e. next El em ( ) ) . pr i nt ( ) ; at ent // D i s det ect ed onl y at r un- t i m og e } } ///: ~ 我们看到唯一的改变就是最后几行。不再是: f or ( i nt i = 0; i < cat s . s i ze( ) ; i ++) ( ( C ) cat s . el em A ( i ) ) . pr i nt ( ) ; at ent t 而是用一个 Enum at i on遍历整个序列: er w l e( e. has M eEl em s ( ) ) hi or ent 218

( ( C 2) e. next El em ( ) ) . pr i nt ( ) ; at ent 使用 Enum at i on,我们不必关心集合中的元素数量。所有工作均由 has M eEl em s ( ) 和 next El em ( ) 自 er or ent ent 动照管了。 下面再看看另一个例子,让我们创建一个常规用途的打印方法: //: Ham t er M s aze. j ava // Us i ng an Enum at i on er i m t j ava. ut i l . * ; por cl as s Ham t er { s pr i vat e i nt ham t er N ber ; s um Ham t er ( i nt i ) { s ham t er N ber = i ; s um } publ i c St r i ng t oSt r i ng( ) { r et ur n " Thi s i s Ham t er #" + ham t er N ber ; s s um } } cl as s Pr i nt er { s t at i c voi d pr i nt A l ( Enum at i on e) { l er w l e( e. has M eEl em s ( ) ) hi or ent Sys t em out . pr i nt l n( . e. next El em ( ) . t oSt r i ng( )) ; ent } } publ i c cl as s Ham t er M e { s az publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Vect or v = new Vect or ( ) ; f or ( i nt i = 0; i < 3; i ++) v. addEl em ( new Ham t er ( i ) ) ; ent s Pr i nt er . pr i nt A l ( v. el em s ( ) ) ; l ent } } ///: ~ 仔细研究一下打印方法: s t at i c voi d pr i nt A l ( Enum at i on e) { l er w l e( e. has M eEl em s ( ) ) hi or ent Sys t em out . pr i nt l n( . e. next El em ( ) . t oSt r i ng( ) ) ; ent } 注意其中没有与序列类型有关的信息。我们拥有的全部东西便是 Enum at i on。为了解有关序列的情况,一 er 个 Enum at i on 便足够了:可取得下一个对象,亦可知道是否已抵达了末尾。取得一系列对象,然后在其中 er 遍历,从而执行一个特定的操作——这是一个颇有价值的编程概念,本书许多地方都会沿用这一思路。 这个看似特殊的例子甚至可以更为通用,因为它使用了常规的 t oSt r i ng( ) 方法(之所以称为常规,是由于它 属于 O ect 类的一部分)。下面是调用打印的另一个方法(尽管在效率上可能会差一些): bj Sys t em out . pr i nt l n( " " + e. next El em ( ) ) ; . ent 它采用了封装到 Java 内部的“自动转换成字串”技术。一旦编译器碰到一个字串,后面跟随一个“+”,就 219

会希望后面又跟随一个字串,并自动调用 t oSt r i ng( ) 。在 Java 1. 1 中,第一个字串是不必要的;所有对象 都会转换成字串。亦可对此执行一次造型,获得与调用 t oSt r i ng( ) 同样的效果: Sys t em out . pr i nt l n( ( St r i ng) e. next El em ( ) ) . ent 但我们想做的事情通常并不仅仅是调用 O ect 方法,所以会再度面临类型造型的问题。对于自己感兴趣的类 bj 型,必须假定自己已获得了一个 Enum at i on,然后将结果对象造型成为那种类型(若操作错误,会得到运 er 行期违例)。

8. 4 集合的类型
标准 Java 1. 0 和 1. 1 库配套提供了非常少的一系列集合类。但对于自己的大多数编程要求,它们基本上都能 胜任。正如大家到本章末尾会看到的,Java 1. 2 提供的是一套重新设计过的大型集合库。

8 . 4 . 1 Vect or
Vect or 的用法很简单,这已在前面的例子中得到了证明。尽管我们大多数时候只需用 addEl em ( ) 插入对 ent 象,用 el em A ( ) 一次提取一个对象,并用 el em s ( ) 获得对序列的一个“枚举”。但仍有其他一系列方 ent t ent 法是非常有用的。同我们对于 Java 库惯常的做法一样,在这里并不使用或讲述所有这些方法。但请务必阅读 相应的电子文档,对它们的工作有一个大概的认识。 1. 崩溃 Java Java 标准集合里包含了 t oSt r i ng( ) 方法,所以它们能生成自己的 St r i ng表达方式,包括它们容纳的对象。 例如在 Vect or 中,t oSt r i ng( ) 会在 Vect or 的各个元素中步进和遍历,并为每个元素调用 t oSt r i ng( )。假定 我们现在想打印出自己类的地址。看起来似乎简单地引用 t hi s 即可(特别是 C ++程序员有这样做的倾向): //: C as hJava. j ava r // O w t o cr as h Java ne ay i m t j ava. ut i l . * ; por publ i c cl as s C as hJava { r publ i c St r i ng t oSt r i ng( ) { r et ur n " C as hJava addr es s : " + t hi s + " \n" ; r } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Vect or v = new Vect or ( ) ; f or ( i nt i = 0; i < 10; i ++) v. addEl em ( new C as hJava( ) ) ; ent r Sys t em out . pr i nt l n( v) ; . } } ///: ~ 若只是简单地创建一个 C as hJava对象,并将其打印出来,就会得到无穷无尽的一系列违例错误。然而,假 r 如将 C as hJava对象置入一个 Vect or ,并象这里演示的那样打印 Vect or ,就不会出现什么错误提示,甚至连 r 一个违例都不会出现。此时 Java 只是简单地崩溃(但至少它没有崩溃我的操作系统)。这已在 Java 1. 1 中 测试通过。 此时发生的是字串的自动类型转换。当我们使用下述语句时: " C as hJava addr es s : " + t hi s r 编译器就在一个字串后面发现了一个“+”以及好象并非字串的其他东西,所以它会试图将 t hi s 转换成一个 字串。转换时调用的是 t oSt r i ng( ),后者会产生一个递归调用。若在一个 Vect or 内出现这种事情,看起来 堆栈就会溢出,同时违例控制机制根本没有机会作出响应。 若确实想在这种情况下打印出对象的地址,解决方案就是调用 O ect 的 t oSt r i ng 方法。此时就不必加入 bj t hi s ,只需使用 s uper . t oSt r i ng( )。当然,采取这种做法也有一个前提:我们必须从 O ect 直接继承,或 bj 者没有一个父类覆盖了 t oSt r i ng 方法。

220

8 . 4 . 2 Bi t S et
Bi t Set 实际是由“二进制位”构成的一个 Vect or 。如果希望高效率地保存大量“开-关”信息,就应使用 Bi t Set 。它只有从尺寸的角度看才有意义;如果希望的高效率的访问,那么它的速度会比使用一些固有类型 的数组慢一些。 此外,Bi t Set 的最小长度是一个长整数(Long)的长度:64 位。这意味着假如我们准备保存比这更小的数 据,如 8 位数据,那么 Bi t Set 就显得浪费了。所以最好创建自己的类,用它容纳自己的标志位。 在一个普通的 Vect or 中,随我们加入越来越多的元素,集合也会自我膨胀。在某种程度上,Bi t Set 也不例 外。也就是说,它有时会自行扩展,有时则不然。而且 Java 的 1. 0 版本似乎在这方面做得最糟,它的 Bi t Set 表现十分差强人意(Java1. 1 已改正了这个问题)。下面这个例子展示了 Bi t Set 是如何运作的,同时 演示了 1. 0 版本的错误: //: Bi t s . j ava // D ons t r at i on of Bi t Set em i m t j ava. ut i l . * ; por publ i c cl as s Bi t s { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Random r and = new Random ) ; ( // Take t he LSB of next I nt ( ) : byt e bt = ( byt e) r and. next I nt ( ) ; Bi t Set bb = new Bi t Set ( ) ; f or ( i nt i = 7; i >=0; i - - ) i f ( ( ( 1 =0; i - - ) i f ( ( ( 1 =0; i - - ) i f ( ( ( 1 = 64 bi t s : Bi t Set b127 = new Bi t Set ( ) ; b127. s et ( 127) ; 221

Sys t em out . pr i nt l n( " s et bi t 127: " + b127) ; . Bi t Set b255 = new Bi t Set ( 65) ; b255. s et ( 255) ; Sys t em out . pr i nt l n( " s et bi t 255: " + b255) ; . Bi t Set b1023 = new Bi t Set ( 512) ; // W t hout t he f ol l ow ng, an except i on i s t hr ow i i n // i n t he Java 1. 0 i m em at i on of Bi t Set : pl ent // b1023. s et ( 1023) ; b1023. s et ( 1024) ; Sys t em out . pr i nt l n( " s et bi t 1023: " + b1023) ; . } s t at i c voi d pr i nt Bi t Set ( Bi t Set b) { Sys t em out . pr i nt l n( " bi t s : " + b) ; . St r i ng bbi t s = new St r i ng( ) ; f or ( i nt j = 0; j < b. s i ze( ) ; j ++) bbi t s += ( b. get ( j ) ? " 1" : " 0" ) ; Sys t em out . pr i nt l n( " bi t pat t er n: " + bbi t s ) ; . } } ///: ~ 随机数字生成器用于创建一个随机的 byt e、s hor t 和 i nt 。每一个都会转换成 Bi t Set 内相应的位模型。此时 一切都很正常,因为 Bi t Set 是 64 位的,所以它们都不会造成最终尺寸的增大。但在 Java 1. 0 中,一旦 Bi t Set 大于 64 位,就会出现一些令人迷惑不解的行为。假如我们设置一个只比 Bi t Set 当前分配存储空间大 出 1 的一个位,它能够正常地扩展。但一旦试图在更高的位置设置位,同时不先接触边界,就会得到一个恼 人的违例。这正是由于 Bi t Set 在 Java 1. 0 里不能正确扩展造成的。本例创建了一个 512 位的 Bi t Set 。构建 器分配的存储空间是位数的两倍。所以假如设置位 1024 或更高的位,同时没有先设置位 1023,就会在 Java 1. 0 里得到一个违例。但幸运的是,这个问题已在 Java 1. 1 得到了改正。所以如果是为 Java 1. 0 写代码, 请尽量避免使用 Bi t Set 。

8 . 4 . 3 S t ack
St ack 有时也可以称为“后入先出”(LI FO )集合。换言之,我们在堆栈里最后“压入”的东西将是以后第 一个“弹出”的。和其他所有 Java 集合一样,我们压入和弹出的都是“对象”,所以必须对自己弹出的东西 进行“造型”。 一种很少见的做法是拒绝使用 Vect or 作为一个 St ack 的基本构成元素,而是从 Vect or 里“继承”一个 St ack。这样一来,它就拥有了一个 Vect or 的所有特征及行为,另外加上一些额外的 St ack 行为。很难判断 出设计者到底是明确想这样做,还是属于一种固有的设计。 下面是一个简单的堆栈示例,它能读入数组的每一行,同时将其作为字串压入堆栈。 //: St acks . j ava // D ons t r at i on of St ack C as s em l i m t j ava. ut i l . * ; por publ i c cl as s St acks { s t at i c St r i ng[ ] m hs = { ont " Januar y" , " Febr uar y" , " M ch" , " A i l " , ar pr " M , " June" , " Jul y" , " A ay" ugus t " , " Sept em " , ber " O ober " , " N ct ovem " , " D ber ecem " } ; ber publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai St ack s t k = new St ack( ) ; f or ( i nt i = 0; i < m hs . l engt h; i ++) ont s t k. pus h( m hs [ i ] + " " ) ; ont Sys t em out . pr i nt l n( " s t k = " + s t k) ; . 222

// Tr eat i ng a s t ack as a Vect or : s t k. addEl em ( " The l as t l i ne" ) ; ent Sys t em out . pr i nt l n( . " el em ent 5 = " + s t k. el em A ( 5) ) ; ent t Sys t em out . pr i nt l n( " poppi ng el em s : " ) ; . ent w l e( ! s t k. em y( ) ) hi pt Sys t em out . pr i nt l n( s t k. pop( ) ) ; . } } ///: ~ m hs 数组的每一行都通过 pus h( )继承进入堆栈,稍后用 pop( ) 从堆栈的顶部将其取出。要声明的一点是, ont Vect or 操作亦可针对 St ack 对象进行。这可能是由继承的特质决定的——St ack“属于”一种 Vect or 。因 此,能对 Vect or 进行的操作亦可针对 St ack 进行,例如 el em A ( )方法。 ent t

8 . 4 . 4 Has ht abl e
Vect or 允许我们用一个数字从一系列对象中作出选择,所以它实际是将数字同对象关联起来了。但假如我们 想根据其他标准选择一系列对象呢?堆栈就是这样的一个例子:它的选择标准是“最后压入堆栈的东西”。 这种“从一系列对象中选择”的概念亦可叫作一个“映射”、“字典”或者“关联数组”。从概念上讲,它 看起来象一个 Vect or ,但却不是通过数字来查找对象,而是用另一个对象来查找它们!这通常都属于一个程 序中的重要进程。 在 Java 中,这个概念具体反映到抽象类 D ct i onar y 身上。该类的接口是非常直观的 s i z e( ) 告诉我们其中包 i 含了多少元素;i s Em y( ) 判断是否包含了元素(是则为 t r ue);put ( O ect key, O ect val ue) 添加一个 pt bj bj 值(我们希望的东西),并将其同一个键关联起来(想用于搜索它的东西);get ( O ect key) 获得与某个键 bj 对应的值;而 r em ove( O ect Key) 用于从列表中删除“键-值”对。还可以使用枚举技术:keys ( ) 产生对键 bj 的一个枚举(Enum at i on);而 el em s ( )产生对所有值的一个枚举。这便是一个 D ct i onar y(字典)的 er ent i 全部。 D ct i onar y 的实现过程并不麻烦。下面列出一种简单的方法,它使用了两个 Vect or ,一个用于容纳键,另一 i 个用来容纳值: //: A s ocA r ay. j ava s r // Si m e ver s i on of a D ct i onar y pl i i m t j ava. ut i l . * ; por publ i c cl as s A s ocA r ay ext ends D ct i onar y { s r i pr i vat e Vect or keys = new Vect or ( ) ; pr i vat e Vect or val ues = new Vect or ( ) ; publ i c i nt s i ze( ) { r et ur n keys . s i ze( ) ; } publ i c bool ean i s Em y( ) { pt r et ur n keys . i s Em y( ) ; pt } publ i c O ect put ( O ect key, O ect val ue) { bj bj bj keys . addEl em ( key) ; ent val ues . addEl em ( val ue) ; ent r et ur n key; } publ i c O ect get ( O ect key) { bj bj i nt i ndex = keys . i ndexO ( key) ; f // i ndexO ( ) Ret ur ns - 1 i f key not f ound: f i f ( i ndex == - 1) r et ur n nul l ; r et ur n val ues . el em A ( i ndex) ; ent t } publ i c O ect r em bj ove( O ect key) { bj 223

i nt i ndex = keys . i ndexO ( key) ; f i f ( i ndex == - 1) r et ur n nul l ; keys . r em oveEl em A ( i ndex) ; ent t O ect r et ur nval = val ues . el em A ( i ndex) ; bj ent t val ues . r em oveEl em A ( i ndex) ; ent t r et ur n r et ur nval ; } publ i c Enum at i on keys ( ) { er r et ur n keys . el em s ( ) ; ent } publ i c Enum at i on el em s ( ) { er ent r et ur n val ues . el em s ( ) ; ent } // Tes t i t : publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai A s ocA r ay aa = new A s ocA r ay( ) ; s r s r f or ( char c = ' a' ; c l ef t ) { O ect o1 = el em A ( r i ght ) ; bj ent t i nt i = l ef t - 1; i nt j = r i ght ; w l e( t r ue) { hi w l e( com e. l es s Than( hi par el em A ( ++i ) , o1) ) ent t ; w l e( j > 0) hi i f ( com e. l es s ThanO Equal ( par r el em A ( - - j ) , o1) ) ent t 229

br eak; // out of w l e hi i f ( i >= j ) br eak; sw i , j ); ap( } s w i , r i ght ) ; ap( qui ckSor t ( l ef t , i - 1) ; qui ckSor t ( i +1, r i ght ) ; } } pr i vat e voi d s w i nt l oc1, i nt l oc2) { ap( O ect t m = el em A ( l oc1) ; bj p ent t s et El em A ( el em A ( l oc2) , l oc1) ; ent t ent t s et El em A ( t m l oc2) ; ent t p, } } ///: ~ 现在,大家可以明白“回调”一词的来历,这是由于 qui ckSor t ( ) 方法“往回调用”了 C par e 中的方法。 om 从中亦可理解这种技术如何生成通用的、可重复利用(再生)的代码。 为使用 Sor t Vect or ,必须创建一个类,令其为我们准备排序的对象实现 C par e。此时内部类并不显得特别 om 重要,但对于代码的组织却是有益的。下面是针对 St r i ng对象的一个例子: //: St r i // Tes t i package im t j por ngSor t Tes t . j ava ng t he gener i c s or t i ng Vect or c08; ava ut i l . *; .

publ i c cl as s St r i ngSor t Tes t { s t at i c cl as s St r i ngC par e i m em s C par e { om pl ent om publ i c bool ean l es s Than( O ect l , O ect r ) { bj bj r et ur n ( ( St r i ng) l ) . t oLow C e( ) . com eTo( er as par ( ( St r i ng) r ) . t oLow C e( ) ) < 0; er as } publ i c bool ean l es s ThanO Equal ( O ect l , O ect r ) { r bj bj r et ur n ( ( St r i ng) l ) . t oLow C e( ) . com eTo( er as par ( ( St r i ng) r ) . t oLow C e( ) ) 0) next = new W m i , ( char ) ( x + 1) ) ; or ( } W m) { or ( Sys t em out . pr i nt l n( " D aul t cons t r uct or " ) ; . ef } 316

publ i c St r i ng t oSt r i ng( ) { St r i ng s = " : " + c + " ( " ; f or ( i nt i = 0; i < d. l engt h; i ++) s += d[ i ] . t oSt r i ng( ) ; s += " ) " ; i f ( next ! = nul l ) s += next . t oSt r i ng( ) ; r et ur n s ; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai W m w = new W m 6, ' a' ) ; or or ( Sys t em out . pr i nt l n( " w = " + w ; . ) try { O ect O put St r eam out = bj ut new O ect O put St r eam bj ut ( new Fi l eO put St r eam " w m out " ) ) ; ut ( or . out . w i t eO ect ( " W m s t or age" ) ; r bj or out . w i t eO ect ( w ; r bj ) out . cl os e( ) ; // A s o f l us hes out put l O ect I nput St r eam i n = bj new O ect I nput St r eam bj ( new Fi l eI nput St r eam " w m out " ) ) ; ( or . St r i ng s = ( St r i ng) i n. r eadO ect ( ) ; bj W m w = ( W m i n. r eadO ect ( ) ; or 2 or ) bj Sys t em out . pr i nt l n( s + " , w = " + w ; . 2 2) } cat ch( Except i on e) { e. pr i nt St ackTr ace( ) ; } try { Byt eA r ayO put St r eam bout = r ut new Byt eA r ayO put St r eam ) ; r ut ( O ect O put St r eam out = bj ut new O ect O put St r eam bout ) ; bj ut ( out . w i t eO ect ( " W m s t or age" ) ; r bj or out . w i t eO ect ( w ; r bj ) out . f l us h( ) ; O ect I nput St r eam i n = bj new O ect I nput St r eam bj ( new Byt eA r ayI nput St r eam r ( bout . t oByt eA r ay( ) ) ) ; r St r i ng s = ( St r i ng) i n. r eadO ect ( ) ; bj W m w = ( W m i n. r eadO ect ( ) ; or 3 or ) bj Sys t em out . pr i nt l n( s + " , w = " + w ; . 3 3) } cat ch( Except i on e) { e. pr i nt St ackTr ace( ) ; } } } ///: ~ 更有趣的是,W m内的 D a 对象数组是用随机数字初始化的(这样便不用怀疑编译器保留了某种原始信 or at 息)。每个 W m段都用一个 C 标记。这个 C 是在重复生成链接的 W m列表时自动产生的。创建一个 or har har or W m时,需告诉构建器希望它有多长。为产生下一个句柄(next ),它总是用减去 1 的长度来调用 W m构 or or 317

建器。最后一个 next 句柄则保持为 nul l (空),表示已抵达 W m的尾部。 or 上面的所有操作都是为了加深事情的复杂程度,加大对象序列化的难度。然而,真正的序列化过程却是非常 简单的。一旦从另外某个流里创建了 O ect O put St r eam r i t eO ect ( ) 就会序列化对象。注意也可以为 bj ut ,w bj 一个 St r i ng调用 w i t eO ect ( ) 。亦可使用与 D aO put St r eam相同的方法写入所有基本数据类型(它们 r bj at ut 有相同的接口)。 有两个单独的 t r y 块看起来是类似的。第一个读写的是文件,而另一个读写的是一个 Byt eA r ay(字节数 r 组)。可利用对任何 D aI nput St r eam或者 D aO put St r eam at at ut 的序列化来读写特定的对象;正如在关于连网 的那一章会讲到的那样,这些对象甚至包括网络。一次循环后的输出结果如下: W m cons t r uct or : 6 or W m cons t r uct or : 5 or W m cons t r uct or : 4 or W m cons t r uct or : 3 or W m cons t r uct or : 2 or W m cons t r uct or : 1 or w = : a( 262) : b( 100) : c( 396) : d( 480) : e( 316) : f ( 398) W m s t or age, w = : a( 262) : b( 100) : c( 396) : d( 480) : e( 316) : f ( 398) or 2 W m s t or age, w = : a( 262) : b( 100) : c( 396) : d( 480) : e( 316) : f ( 398) or 3 可以看出,装配回原状的对象确实包含了原来那个对象里包含的所有链接。 注意在对一个 Ser i al i z abl e(可序列化)对象进行重新装配的过程中,不会调用任何构建器(甚至默认构建 器)。整个对象都是通过从 I nput St r eam 中取得数据恢复的。 作为 Java 1. 1 特性的一种,我们注意到对象的序列化并不属于新的 Reader 和 W i t er 层次结构的一部分,而 r 是沿用老式的 I nput St r eam和 O put St r eam结构。所以在一些特殊的场合下,不得不混合使用两种类型的层 ut 次结构。

1 0 . 9 . 1 寻找类
读者或许会奇怪为什么需要一个对象从它的序列化状态中恢复。举个例子来说,假定我们序列化一个对象, 并通过网络将其作为文件传送给另一台机器。此时,位于另一台机器的程序可以只用文件目录来重新构造这 个对象吗? 回答这个问题的最好方法就是做一个实验。下面这个文件位于本章的子目录下: //: A i en. j ava l // A s er i al i zabl e cl as s i m t j ava. i o. * ; por publ i c cl as s A i en i m em s Ser i al i zabl e { l pl ent } ///: ~ 用于创建和序列化一个 A i en对象的文件位于相同的目录下: l //: Fr eezeA i en. j ava l // C eat e a s er i al i z ed out put f i l e r i m t j ava. i o. * ; por publ i c cl as s Fr eez eA i en { l publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) ai t hr ow Except i on { s O ect O put out = bj ut new O ect O put St r eam bj ut ( new Fi l eO put St r eam " f i l e. x" ) ) ; ut ( A i en zor con = new A i en( ) ; l l 318

out . w i t eO ect ( zor con) ; r bj } } ///: ~ 该程序并不是捕获和控制违例,而是将违例简单、直接地传递到 m n( ) 外部,这样便能在命令行报告它们。 ai 程序编译并运行后,将结果产生的 f i l e. x 复制到名为 xf i l es 的子目录,代码如下: //: Thaw l i en. j ava A // Tr y t o r ecover a s er i al i zed f i l e w t hout t he i // cl as s of obj ect t hat ' s s t or ed i n t hat f i l e. package c10. xf i l es ; i m t j ava. i o. * ; por publ i c cl as s Thaw l i en { A publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) ai t hr ow Except i on { s O ect I nput St r ea i n = bj m new O ect I nput St r eam bj ( new Fi l eI nput St r eam " f i l e. x" ) ) ; ( O ect m t er y = i n. r eadO ect ( ) ; bj ys bj Sys t em out . pr i nt l n( . m t er y. get C as s ( ) . t oSt r i ng( ) ) ; ys l } } ///: ~ 该程序能打开文件,并成功读取 m t er y 对象中的内容。然而,一旦尝试查找与对象有关的任何资料——这 ys 要求 A i en 的 C as s 对象——Java 虚拟机(JVM l l )便找不到 A i en. cl as s (除非它正好在类路径内,而本例理 l 应相反)。这样就会得到一个名叫 C as s N FoundExcept i on 的违例(同样地,若非能够校验 A i en 存在的证 l ot l 据,否则它等于消失)。 恢复了一个序列化的对象后,如果想对其做更多的事情,必须保证 JVM 能在本地类路径或者因特网的其他什 么地方找到相关的. cl as s 文件。

1 0 . 9 . 2 序列化的控制
正如大家看到的那样,默认的序列化机制并不难操纵。然而,假若有特殊要求又该怎么办呢?我们可能有特 殊的安全问题,不希望对象的某一部分序列化;或者某一个子对象完全不必序列化,因为对象恢复以后,那 一部分需要重新创建。 此时,通过实现 Ext er nal i zabl e 接口,用它代替 Ser i al i z abl e接口,便可控制序列化的具体过程。这个 Ext er nal i zabl e 接口扩展了 Ser i al i z abl e,并增添了两个方法:w i t eExt er nal ( ) 和 r ea Ext er na ()。在序 r d l 列化和重新装配的过程中,会自动调用这两个方法,以便我们执行一些特殊操作。 下面这个例子展示了 Ext er nal i zabl e 接口方法的简单应用。注意 Bl i p1 和 Bl i p2 几乎完全一致,除了极微小 的差别(自己研究一下代码,看看是否能发现): //: Bl i ps . j ava // Si m e us e of Ext er nal i zabl e & a pi t f al l pl i m t j ava. i o. * ; por i m t j ava. ut i l . * ; por cl as s Bl i p1 i m em s Ext er nal i zabl e { pl ent publ i c Bl i p1( ) { Sys t em out . pr i nt l n( " Bl i p1 C t r uct or " ) ; . ons } publ i c voi d w i t eExt er nal ( O ect O put out ) r bj ut 319

t hr ow I O s Except i on { Sys t em out . pr i nt l n( " Bl i p1. w i t eExt er nal " ) ; . r } publ i c voi d r eadExt er nal ( O ect I nput i n) bj t hr ow I O s Except i on, C as s Not FoundExcept i on { l Sys t em out . pr i nt l n( " Bl i p1. r eadExt er nal " ) ; . } } cl as s Bl i p2 i m em s Ext er nal i zabl e { pl ent Bl i p2( ) { Sys t em out . pr i nt l n( " Bl i p2 C t r uct or " ) ; . ons } publ i c voi d w i t eExt er nal ( O ect O put out ) r bj ut t hr ow I O s Except i on { Sys t em out . pr i nt l n( " Bl i p2. w i t eExt er nal " ) ; . r } publ i c voi d r eadExt er nal ( O ect I nput i n) bj t hr ow I O s Except i on, C as s N FoundExcept i on { l ot Sys t em out . pr i nt l n( " Bl i p2. r eadExt er nal " ) ; . } } publ i c cl as s Bl i ps { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Sys t em out . pr i nt l n( " C t r uct i ng obj ect s : " ) ; . ons Bl i p1 b1 = new Bl i p1( ) ; Bl i p2 b2 = new Bl i p2( ) ; try { O ect O put St r eam o = bj ut new O ect O put St r eam bj ut ( new Fi l eO put St r eam " Bl i ps . out " ) ) ; ut ( Sys t em out . pr i nt l n( " Savi ng obj ect s : " ) ; . o. w i t eO ect ( b1) ; r bj o. w i t eO ect ( b2) ; r bj o. cl os e( ) ; // N get t hem back: ow O ect I nput St r eam i n = bj new O ect I nput St r eam bj ( new Fi l eI nput St r eam " Bl i ps . out " ) ) ; ( Sys t em out . pr i nt l n( " Recover i ng b1: " ) ; . b1 = ( Bl i p1) i n. r eadO ect ( ) ; bj // O PS! Thr ow an except i on: O s //! Sys t em out . pr i nt l n( " Recover i ng b2: " ) ; . //! b2 = ( Bl i p2) i n. r eadO ect ( ) ; bj } cat ch( Except i on e) { e. pr i nt St ackTr ace( ) ; } } } ///: ~

320

该程序输出如下: C t r uct i ng obj ect s : ons Bl i p1 C t r uct or ons Bl i p2 C t r uct or ons Savi ng obj ect s : Bl i p1. w i t eExt er nal r Bl i p2. w i t eExt er nal r Recover i ng b1: Bl i p1 C t r uct or ons Bl i p1. r eadExt er nal 未恢复 Bl i p2 对象的原因是那样做会导致一个违例。你找出了 Bl i p1 和 Bl i p2 之间的区别吗?Bl i p1 的构建 器是“公共的”(publ i c),Bl i p2 的构建器则不然,这样便会在恢复时造成违例。试试将 Bl i p2 的构建器 属性变成“publ i c”,然后删除/ / ! 注释标记,看看是否能得到正确的结果。 恢复 b1 后,会调用 Bl i p1 默认构建器。这与恢复一个 Ser i al i z abl e (可序列化)对象不同。在后者的情况 下,对象完全以它保存下来的二进制位为基础恢复,不存在构建器调用。而对一个 Ext er nal i zabl e 对象,所 有普通的默认构建行为都会发生(包括在字段定义时的初始化),而且会调用 r eadExt er nal ( ) 。必须注意这 一事实——特别注意所有默认的构建行为都会进行——否则很难在自己的 Ext er nal i zabl e 对象中产生正确的 行为。 下面这个例子揭示了保存和恢复一个 Ext er nal i zabl e 对象必须做的全部事情: //: Bl i p3. j ava // Recons t r uct i ng an ext er nal i zabl e obj ect i m t j ava. i o. * ; por i m t j ava. ut i l . * ; por cl as s Bl i p3 i m em s Ext er nal i zabl e { pl ent i nt i ; St r i ng s ; // N i ni t i al i zat i on o publ i c Bl i p3( ) { Sys t em out . pr i nt l n( " Bl i p3 C t r uct or " ) ; . ons // s , i not i ni t i al i zed } publ i c Bl i p3( St r i ng x, i nt a) { Sys t em out . pr i nt l n( " Bl i p3( St r i ng x, i nt a) " ) ; . s = x; i = a; // s & i i ni t i al i zed onl y i n non- def aul t // cons t r uct or . } publ i c St r i ng t oSt r i ng( ) { r et ur n s + i ; } publ i c voi d w i t eExt er nal ( O ect O put out ) r bj ut t hr ow I O s Except i on { Sys t em out . pr i nt l n( " Bl i p3. w i t eExt er nal " ) ; . r // You m t do t hi s : us out . w i t eO ect ( s ) ; out . w i t eI nt ( i ) ; r bj r } publ i c voi d r eadExt er nal ( O ect I nput i n) bj t hr ow I O s Except i on, C as s N FoundExcept i on { l ot Sys t em out . pr i nt l n( " Bl i p3. r eadExt er nal " ) ; . // You m t do t hi s : us s = ( St r i ng) i n. r eadO ect ( ) ; bj 321

i =i n. r eadI nt ( ) ; } publ i c s t at i c voi d m n( St r i ng[ ] ar g ) { ai s Sys t em out . pr i nt l n( " C t r uct i ng obj ect s : " ) ; . ons Bl i p3 b3 = new Bl i p3( " A St r i ng " , 47) ; Sys t em out . pr i nt l n( b3. t oSt r i ng( ) ) ; . try { O ect O put St r eam o = bj ut new O ect O put St r eam bj ut ( new Fi l eO put St r eam " Bl i p3. out " ) ) ; ut ( Sys t em out . pr i nt l n( " Savi ng obj ect : " ) ; . o. w i t eO ect ( b3) ; r bj o. cl os e( ) ; // N get i t back: ow O ect I nput St r eam i n = bj new O ect I nput St r eam bj ( new Fi l eI nput St r eam " Bl i p3. out " ) ) ; ( Sys t em out . pr i nt l n( " Recover i ng b3: " ) ; . b3 = ( Bl i p3) i n. r eadO ect ( ) ; bj Sys t em out . pr i nt l n( b3. t oSt r i ng( ) ) ; . } cat ch( Except i on e) { e. pr i nt St ackTr ace( ) ; } } } ///: ~ 其中,字段 s 和 i 只在第二个构建器中初始化,不关默认构建器的事。这意味着假如不在 r eadExt er nal 中初 始化 s 和 i ,它们就会成为 nul l (因为在对象创建的第一步中已将对象的存储空间清除为 1)。若注释掉跟 随于“You m t do t hi s”后面的两行代码,并运行程序,就会发现当对象恢复以后,s 是 nul l ,而 i 是 us 零。 若从一个 Ext er nal i zabl e 对象继承,通常需要调用 w i t eExt er nal ( ) 和 r eadExt er nal ( ) 的基础类版本,以便 r 正确地保存和恢复基础类组件。 所以为了让一切正常运作起来,千万不可仅在 w i t eExt er nal ( )方法执行期间写入对象的重要数据(没有默 r 认的行为可用来为一个 Ext er nal i zabl e对象写入所有成员对象)的,而是必须在 r eadExt er nal ( ) 方法中也 恢复那些数据。初次操作时可能会有些不习惯,因为 Ext er nal i zabl e 对象的默认构建行为使其看起来似乎正 在进行某种存储与恢复操作。但实情并非如此。 1. t r ans i ent (临时)关键字 控制序列化过程时,可能有一个特定的子对象不愿让 Java 的序列化机制自动保存与恢复。一般地,若那个子 对象包含了不想序列化的敏感信息(如密码),就会面临这种情况。即使那种信息在对象中具有“pr i vat e” (私有)属性,但一旦经序列化处理,人们就可以通过读取一个文件,或者拦截网络传输得到它。 为防止对象的敏感部分被序列化,一个办法是将自己的类实现为 Ext er nal i zabl e,就象前面展示的那样。这 样一来,没有任何东西可以自动序列化,只能在 w i t eExt er nal ( ) 明确序列化那些需要的部分。 r 然而,若操作的是一个 Ser i al i z abl e对象,所有序列化操作都会自动进行。为解决这个问题,可以用 t r ans i ent (临时)逐个字段地关闭序列化,它的意思是“不要麻烦你(指自动机制)保存或恢复它了——我 会自己处理的”。 例如,假设一个 Logi n 对象包含了与一个特定的登录会话有关的信息。校验登录的合法性时,一般都想将数 据保存下来,但不包括密码。为做到这一点,最简单的办法是实现 Ser i al i z abl e ,并将 pas s w d 字段设为 or t r ans i ent 。下面是具体的代码: //: Logon. j ava // D ons t r at es t he " t r ans i ent " keyw d em or 322

i m t j ava. i o. * ; por i m t j ava. ut i l . *; por cl as s Logon i m em s Ser i al i zabl e { pl ent pr i vat e D e dat e = new D e( ) ; at at pr i vat e St r i ng us er nam e; pr i vat e t r ans i ent St r i ng pas s w d; or Logon( St r i ng nam St r i ng pw { e, d) us er nam = nam e e; pas s w d = pw or d; } publ i c St r i ng t oSt r i ng( ) { St r i ng pw = d ( pas s w d == nul l ) ? " ( n/a) " : pas s w d; or or r et ur n " l ogon i nf o: \n " + " us er nam " + us er nam + e: e " \n dat e: " + dat e. t oSt r i ng( ) + " \n pas s w d: " + pw or d; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Logon a = new Logon( " Hul k" , " m t t l ePony" ) ; yLi Sys t em out . pr i nt l n( " l ogon a = " + a) ; . try { O ect O put St r eam o = bj ut new O ect O put St r eam bj ut ( new Fi l eO put St r eam " Logon. out " ) ) ; ut ( o. w i t eO ect ( a) ; r bj o. cl os e( ) ; // D ay: el i nt s econds = 5; l ong t = Sys t em cur r ent Ti m i l l i s ( ) . eM + s econds * 1000; w l e( Sys t em cur r ent Ti m i l l i s ( ) < t ) hi . eM ; // N get t hem back: ow O ect I nput St r eam i n = bj new O ect I nput St r eam bj ( new Fi l eI nput St r eam " Logon. out " ) ) ; ( Sys t em out . pr i nt l n( . " Recover i ng obj ect at " + new D e( ) ) ; at a = ( Logon) i n. r eadO ect ( ) ; bj Sys t em out . pr i nt l n( " l ogon a = " + a) ; . } cat ch( Except i on e) { e. pr i nt St ackTr ace( ) ; } } } ///: ~ 可以看到,其中的 dat e 和 us er nam 字段保持原始状态(未设成 t r ans i ent ),所以会自动序列化。然而, e pas s w d 被设为 t r ans i ent ,所以不会自动保存到磁盘;另外,自动序列化机制也不会作恢复它的尝试。输 or 出如下:

323

l ogon a = l ogon i nf o: us er nam Hul k e: dat e: Sun M 23 18: 25: 53 PST 1997 ar pas s w d: m t t l ePony or yLi Recover i ng obj ect at Sun M 23 18: 25: 59 PST 1997 ar l ogon a = l ogon i nf o: us er nam Hul k e: dat e: Sun M 23 18: 25: 53 PST 1997 ar pas s w d: ( n/a) or 一旦对象恢复成原来的样子,pas s w d 字段就会变成 nul l 。注意必须用 t oSt r i ng( ) 检查 pas s w d 是否为 or or nul l ,因为若用过载的“+”运算符来装配一个 St r i ng对象,而且那个运算符遇到一个 nul l 句柄,就会造成 一个名为 N l Poi nt er Except i on 的违例(新版 Java可能会提供避免这个问题的代码)。 ul 我们也发现 dat e 字段被保存到磁盘,并从磁盘恢复,没有重新生成。 由于 Ext er nal i zabl e 对象默认时不保存它的任何字段,所以 t r ans i ent 关键字只能伴随 Ser i al i z abl e使 用。 2. Ext er nal i z abl e的替代方法 若不是特别在意要实现 Ext er nal i zabl e接口,还有另一种方法可供选用。我们可以实现 Ser i al i z abl e接 口,并添加(注意是“添加”,而非“覆盖”或者“实现”)名为 w i t eO ect ( ) 和 r eadO ect ( )的方法。 r bj bj 一旦对象被序列化或者重新装配,就会分别调用那两个方法。也就是说,只要提供了这两个方法,就会优先 使用它们,而不考虑默认的序列化机制。 这些方法必须含有下列准确的签名: pr i vat e voi d w i t eO ect ( O ect O put St r eam s t r eam r bj bj ut ) t hr ow I O s Except i on; pr i vat e voi d r eadO ect ( O ect I nput St r eam s t r eam bj bj ) t hr ow I O s Except i on, C as s N FoundExcept i on l ot 从设计的角度出发,情况变得有些扑朔迷离。首先,大家可能认为这些方法不属于基础类或者 Ser i al i z abl e 接口的一部分,它们应该在自己的接口中得到定义。但请注意它们被定义成“pr i vat e”,这意味着它们只能 由这个类的其他成员调用。然而,我们实际并不从这个类的其他成员中调用它们,而是由 O ect O put St r eam和 O ect I nput St r eam w i t eO ect ( ) 及 r eadO ect ( )方法来调用我们对象的 bj ut bj 的 r bj bj w i t eO ect ( ) 和 r eadO ect ( ) 方法(注意我在这里用了很大的抑制力来避免使用相同的方法名——因为怕 r bj bj 混淆)。大家可能奇怪 O ect O put St r eam和 O ect I nput St r eam bj ut bj 如何有权访问我们的类的 p i va e方法— r t —只能认为这是序列化机制玩的一个把戏。 在任何情况下,接口中的定义的任何东西都会自动具有 publ i c 属性,所以假若 w i t eO ect ( ) 和 r bj r eadO ect ( )必须为 pr i vat e,那么它们不能成为接口(i nt er f ace)的一部分。但由于我们准确地加上了签 bj 名,所以最终的效果实际与实现一个接口是相同的。 看起来似乎我们调用 O ect O put St r eam w i t eO ect ( ) 的时候,我们传递给它的 Ser i al i z abl e对象似乎 bj ut . r bj 会被检查是否实现了自己的 w i t eO ect ( ) 。若答案是肯定的是,便会跳过常规的序列化过程,并调用 r bj w i t eO ect ( ) 。r eadO ect ( )也会遇到同样的情况。 r bj bj 还存在另一个问题。在我们的 w i t eO ect ( ) 内部,可以调用 def aul t W i t eO ect ( ),从而决定采取默认的 r bj r bj w i t eO ect ( ) 行动。类似地,在 r eadO ect ( )内部,可以调用 def aul t ReadO ect ( ) 。下面这个简单的例子 r bj bj bj 演示了如何对一个 Ser i al i z abl e 对象的存储与恢复进行控制: //: Ser i al C l . j ava t // C r ol l i ng s er i al i z at i on by addi ng your ow ont n 324

// w i t eO ect ( ) and r eadO ect ( ) m hods . r bj bj et i m t j ava. i o. * ; por publ i c cl as s Ser i al C l i m em s Ser i al i zabl e { t pl ent St r i ng a; t r ans i ent St r i ng b; publ i c Ser i al C l ( St r i ng aa, St r i ng bb) { t a = " N Tr ans i ent : " + aa; ot b = " Tr ans i ent : " + bb; } publ i c St r i ng t oSt r i ng( ) { r et ur n a + " \n" + b; } pr i vat e voi d w i t eO ect ( O ect O put St r eam s t r eam r bj bj ut ) t hr ow I O s Except i on { s t r eam def aul t W i t eO ect ( ) ; . r bj s t r eam w i t eO ect ( b) ; . r bj } pr i vat e voi d r eadO ect ( O ect I nput St r eam s t r eam bj bj ) t hr ow I O s Except i on, C as s N FoundExcept i on { l ot s t r eam def aul t ReadO ect ( ) ; . bj b = ( St r i ng) s t r eam r eadO ect ( ) ; . bj } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Ser i al C l s c = t new Ser i al C l ( " Tes t 1" , " Tes t 2" ) ; t Sys t em out . pr i nt l n( " Bef or e: \n" + s c) ; . Byt eA r ayO put St r eam buf = r ut new Byt eA r ayO put St r eam ) ; r ut ( try { O ect O put St r eam o = bj ut new O ect O put St r eam buf ) ; bj ut ( o. w i t eO ect ( s c) ; r bj // N get i t back: ow O ect I nput St r eam i n = bj new O ect I nput St r eam bj ( new Byt eA r ayI nput St r eam r ( buf . t oByt eA r ay( ) ) ) ; r Ser i al C l s c2 = ( Ser i al C l ) i n. r ea bj ect ( ) ; t t dO Sys t em out . pr i nt l n( " A t er : \n" + s c2) ; . f } cat ch( Except i on e) { e. pr i nt St ackTr ace( ) ; } } } ///: ~ 在这个例子中,一个 St r i ng保持原始状态,其他设为 t r ans i ent (临时),以便证明非临时字段会被 def aul t W i t eO ect ( )方法自动保存,而 t r ans i ent 字段必须在程序中明确保存和恢复。字段是在构建器内 r bj 部初始化的,而不是在定义的时候,这证明了它们不会在重新装配的时候被某些自动化机制初始化。 若准备通过默认机制写入对象的非 t r ans i ent 部分,那么必须调用 def aul t W i t eO ect ( ),令其作为 r bj 325

w i t eO ect ( ) 中的第一个操作;并调用 def aul t ReadO ect ( ) ,令其作为 r eadO ect ( ) 的第一个操作。这些 r bj bj bj 都是不常见的调用方法。举个例子来说,当我们为一个 O ect O put St r eam调用 def aul t W i t eO ect ( )的 bj ut r bj 时候,而且没有为其传递参数,就需要采取这种操作,使其知道对象的句柄以及如何写入所有非 t r ans i ent 的部分。这种做法非常不便。 t r ans i ent 对象的存储与恢复采用了我们更熟悉的代码。现在考虑一下会发生一些什么事情。在 m n( )中会 ai 创建一个 Ser i al C l 对象,随后会序列化到一个 O ect O put St r eam里(注意这种情况下使用的是一个缓冲 t bj ut 区,而非文件——与 O ect O put St r eam完全一致)。正式的序列化操作是在下面这行代码里发生的: bj ut o. w i t eO ect ( s c) ; r bj 其中,w i t eO ect ( ) 方法必须核查 s c,判断它是否有自己的 w i t eO ect ( ) 方法(不是检查它的接口——它 r bj r bj 根本就没有,也不是检查类的类型,而是利用反射方法实际搜索方法)。若答案是肯定的,就使用那个方 法。类似的情况也会在 r eadO ect ( )上发生。或许这是解决问题唯一实际的方法,但确实显得有些古怪。 bj 3. 版本问题 有时候可能想改变一个可序列化的类的版本(比如原始类的对象可能保存在数据库中)。尽管这种做法得到 了支持,但一般只应在非常特殊的情况下才用它。此外,它要求操作者对背后的原理有一个比较深的认识, 而我们在这里还不想达到这种深度。JD 1. 1 的 HTM 文档对这一主题进行了非常全面的论述(可从 Sun公司 K L 下载,但可能也成了 Java 开发包联机文档的一部分)。

1 0 . 9 . 3 利用“持久性”
一个比较诱人的想法是用序列化技术保存程序的一些状态信息,从而将程序方便地恢复到以前的状态。但在 具体实现以前,有些问题是必须解决的。如果两个对象都有指向第三个对象的句柄,该如何对这两个对象序 列化呢?如果从两个对象序列化后的状态恢复它们,第三个对象的句柄只会出现在一个对象身上吗?如果将 这两个对象序列化成独立的文件,然后在代码的不同部分重新装配它们,又会得到什么结果呢? 下面这个例子对上述问题进行了很好的说明: //: M or l d. j ava yW i m t j ava. i o. * ; por i m t j ava. ut i l . * ; por cl as s Hous e i m em s Ser i al i zabl e { } pl ent cl as s A m i m em s Ser i al i zabl e { ni al pl ent St r i ng nam e; Hous e pr ef er r edHous e; A m ( St r i ng nm Hous e h) { ni al , nam = nm e ; pr ef er r edHous e = h; } publ i c St r i ng t oSt r i ng( ) { r et ur n nam + " [ " + s uper . t oSt r i ng( ) + e " ] , " + pr ef er r edHous e + " \n" ; } } publ i c cl as s M or l d { yW publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Hous e hous e = new Hous e( ) ; Vect or ani m s = new Vect or ( ); al ani m s . addEl em ( al ent new A m ( " Bos co t he dog" , hous e) ) ; ni al ani m s . addEl em ( al ent new A m ( " Ral ph t he ham t er " , hous e) ) ; ni al s 326

ani m s . addEl em ( al ent new A m ( " Fr onk t he cat " , hous e) ) ; ni al Sys t em out . pr i nt l n( " ani m s : " + ani m s ) ; . al al try { Byt eA r ayO put St r eam buf 1 = r ut new Byt eA r ayO put St r eam ) ; r ut ( O ect O put St r eam o1 = bj ut new O ect O put St r eam buf 1) ; bj ut ( o1. w i t eO ect ( ani m s ) ; r bj al o1. w i t eO ect ( ani m s ) ; // W i t e a 2nd s et r bj al r // W i t e t o a di f f e ent s t r eam r r : Byt eA r ayO put St r eam buf 2 = r ut new Byt eA r ayO put St r eam ) ; r ut ( O ect O put St r eam o2 = bj ut new O ect O put St r eam buf 2) ; bj ut ( o2. w i t eO ect ( ani m s ) ; r bj al // N get t hem back: ow O ect I nput St r eam i n1 = bj new O ect I nput St r eam bj ( new Byt eA r ayI nput St r eam r ( buf 1. t oByt eA r ay( ) ) ) ; r O ect I nput St r eam i n2 = bj new O ect I nput St r eam bj ( new Byt eA r ayI nput St r eam r ( buf 2. t oByt eA r ay( ) ) ) ; r Vect or ani m s 1 = ( Vect or ) i n1. r eadO ect ( ) ; al bj Vect or ani m s 2 = ( Vect or ) i n1. r eadO ect ( ) ; al bj Vect or ani m s 3 = ( Vect or ) i n2. r eadO ect ( ) ; al bj Sys t em out . pr i nt l n( " ani m s 1: " + ani m s 1) ; . al al Sys t em out . pr i nt l n( " ani m s 2: " + ani m s 2) ; . al al Sys t em out . pr i nt l n( " ani m s 3: " + ani m s 3) ; . al al } cat ch( Except i on e) { e. pr i nt St ackTr ace( ) ; } } } ///: ~ 这里一件有趣的事情是也许是能针对一个字节数组应用对象的序列化,从而实现对任何 Ser i al i z abl e(可序 列化)对象的一个“全面复制”(全面复制意味着复制的是整个对象网,而不仅是基本对象和它的句柄)。 复制问题将在第 12 章进行全面讲述。 A m 对象包含了类型为 Hous e 的字段。在 m n( )中,会创建这些 A m 的一个 Vect or ,并对其序列化两 ni al ai ni al 次,分别送入两个不同的数据流内。这些数据重新装配并打印出来后,可看到下面这样的结果(对象在每次 运行时都会处在不同的内存位置,所以每次运行的结果有区别): ani m s : [ Bos co t he dog[ A m @ al ni al 1cc76c] , Hous e@ 1cc769 , Ral ph t he ham t er [ A m @ s ni al 1cc76d] , Hous e@ 1cc769 , Fr onk t he cat [ A m @ ni al 1cc76e] , Ho e@ us 1cc769 ] ani m s 1: [ Bos co t he dog[ A m @ al ni al 1cca0c] , Hous e@ 1cca16 , Ral ph t he ham t er [ A m @ s ni al 1cca17] , Hous e@ 1cca16 , Fr onk t he cat [ A m @ ni al 1cca1b] , Hous e@ 1cca16 327

] ani m s 2: [ Bos co t he dog[ A m @ al ni al 1cca0c] , Hous e@ 1cca16 , Ral ph t he ham t er [ A m @ s ni al 1cca17] , Hous e@ 1cca16 , Fr onk t he cat [ A m @ ni al 1cca1b] , Hous e@ 1cca16 ] ani m s 3: [ Bos co t he dog[ A m @ al ni al 1cca52] , Hous e@ 1cca5c , Ral ph t he ham t er [ A m @ s ni al 1cca5d], Hous e@ 1cca5c , Fr onk t he cat [ A m @ ni al 1cca61] , Hous e@ 1cca5c ] 当然,我们希望装配好的对象有与原来不同的地址。但注意在 ani m s 1 和 ani m s 2 中出现了相同的地址, al al 其中包括共享的、对 Hous e 对象的引用。在另一方面,当 ani m s 3 恢复以后,系统没有办法知道另一个流内 al 的对象是第一个流内对象的化身,所以会产生一个完全不同的对象网。 只要将所有东西都序列化到单独一个数据流里,就能恢复获得与以前写入时完全一样的对象网,不会不慎造 成对象的重复。当然,在写第一个和最后一个对象的时间之间,可改变对象的状态,但那必须由我们明确采 取操作——序列化时,对象会采用它们当时的任何状态(包括它们与其他对象的连接关系)写入。 若想保存系统状态,最安全的做法是当作一种“微观”操作序列化。如果序列化了某些东西,再去做其他一 些工作,再来序列化更多的东西,以此类推,那么最终将无法安全地保存系统状态。相反,应将构成系统状 态的所有对象都置入单个集合内,并在一次操作里完成那个集合的写入。这样一来,同样只需一次方法调 用,即可成功恢复之。 下面这个例子是一套假想的计算机辅助设计(C D A )系统,对这一方法进行了很好的演示。此外,它还为我们 引入了 s t at i c 字段的问题——如留意联机文档,就会发现 C as s 是“Ser i al i z abl e”(可序列化)的,所以 l 只需简单地序列化 C as s 对象,就能实现 s t at i c 字段的保存。这无论如何都是一种明智的做法。 l //: C D at e. j ava A St // Savi ng and r es t or i ng t he s t at e of a // pr et end C D s ys t em A . i m t j ava. i o. * ; por i m t j ava. ut i l . * ; por abs t r act cl as s Shape i m em s Ser i al i zabl e { pl ent publ i c s t at i c f i nal i nt RED = 1, BLUE = 2, G REEN = 3; pr i vat e i nt xPos , yPos , di m i on; ens pr i vat e s t at i c Random r = new Random ) ; ( pr i vat e s t at i c i nt count er = 0; abs t r act publ i c voi d s et C or ( i nt new ol or ) ; ol C abs t r act publ i c i nt get C or ( ) ; ol publ i c Shape( i nt xVal , i nt yVal , i nt di m { ) xPos = xVal ; yPos = yVal ; di m i on = di m ens ; } publ i c St r i ng t oSt r i ng( ) { r et ur n get C as s ( ) . t oSt r i ng( ) + l " col or [ " + get C or ( ) + ol " ] xPos [ " + xPos + " ] yPos [ " + yPos + " ] di m " + di m i on + " ] \n" ; [ ens } publ i c s t at i c Shape r andom Fact or y( ) { i nt xVal = r . next I nt ( ) % 100; 328

i nt yVal = r . next I nt ( ) % 100; i nt di m = r . next I nt ( ) % 100; s w t ch( count er ++ % 3) { i d aul t : ef cas e 0: r et ur n new C r cl e( xVal , yVal , di m ; i ) cas e 1: r et ur n new Squar e( xVal , yVal , di m ; ) cas e 2: r et ur n new Li ne( xVal , yVal , di m ; ) } } } cl as s C r cl e ext ends Shape i pr i vat e s t at i c i nt col or publ i c C r cl e( i nt xVal , i i s uper ( xVal , yVal , di m ; ) } publ i c voi d s et C or ( i nt ol col or = new ol or ; C } publ i c i nt get C or ( ) { ol r et ur n col or ; } } { = RED ; nt yVal , i nt di m { )

new ol or ) { C

cl as s Squar e ext ends Shape { pr i vat e s t at i c i nt col or ; publ i c Squar e( i nt xVal , i nt yVal , i nt di m { ) s uper ( xVal , yVal , di m ; ) col or = RED ; } publ i c voi d s et C or ( i nt new ol or ) { ol C col or = new ol or ; C } publ i c i nt get C or ( ) { ol r et ur n col or ; } } cl as s Li ne ext ends Shape { pr i vat e s t at i c i nt col or = RED ; publ i c s t at i c voi d s er i al i zeSt at i cSt at e( O ect O put St r eam os ) bj ut t hr ow I O s Except i on { os . w i t eI nt ( col or ) ; r } publ i c s t at i c voi d des er i al i zeSt at i cSt at e( O ect I nput St r eam os ) bj t hr ow I O s Except i on { col or = os . r eadI nt ( ) ; } publ i c Li ne( i nt xVal , i nt yVal , i nt di m { ) s uper ( xVal , yVal , di m ; ) 329

} publ i c voi d s et C or ( i nt new ol or ) { ol C col or = new ol or ; C } publ i c i nt get C or ( ) { ol r et ur n col or ; } } publ i c cl as s C D at e { A St publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) ai t hr ow Except i on { s Vect or s hapeTypes , s hapes ; i f ( ar gs . l engt h == 0) { s hapeTypes = new Vect or ( ) ; s hapes = new Vect or ( ) ; // A handl es t o t he cl as s obj ect s : dd s hapeTypes . addEl em ( C r cl e. cl as s ) ; ent i s hapeTypes . addEl em ( Squar e. cl as s ) ; ent s hapeTypes . addEl em ( Li ne. cl as s ) ; ent // M ake s om s hapes : e f or ( i nt i = 0; i < 10; i ++) s hapes . addEl em ( Shape. r andom ent Fact or y( ) ) ; // Set al l t he s t at i c col or s t o G REEN : f or ( i nt i = 0; i < 10; i ++) ( ( Shape) s hapes . el em A ( i ) ) ent t . s et C or ( Shape. G ol REEN ; ) // Save t he s t at e vect or : O ect O put St r eam out = bj ut new O ect O put St r eam bj ut ( new Fi l eO put St r eam " C D at e. out " ) ) ; ut ( A St out . w i t eO ect ( s hapeTypes ) ; r bj Li ne. s er i al i zeSt at i cSt at e( out ) ; out . w i t eO ect ( s hapes ) ; r bj } el s e { // Ther e' s a com and- l i ne ar gum m ent O ect I nput St r eam i n = bj new O ect I nput St r eam bj ( new Fi l eI nput St r eam ar gs [ 0] ) ) ; ( // Read i n t he s am or der t hey w e w i t t en: e er r shapeTypes = ( Vect or ) i n. r eadO ect ( ) ; bj Li ne. des er i al i zeSt at i cSt at e( i n) ; s hapes = ( Vect or ) i n. r eadO ect ( ) ; bj } // D s pl ay t he s hapes : i Sys t em out . pr i nt l n( s hapes ) ; . } } ///: ~ Shape(几何形状)类“实现了可序列化”(i m em s Ser i al i zabl e),所以从 Shape继承的任何东西也 pl ent 都会自动“可序列化”。每个 Shape 都包含了数据,而且每个衍生的 Shape 类都包含了一个特殊的 s t at i c 字 段,用于决定所有那些类型的 Shape 的颜色(如将一个 s t at i c字段置入基础类,结果只会产生一个字段,因 为 s t at i c 字段未在衍生类中复制)。可对基础类中的方法进行覆盖处理,以便为不同的类型设置颜色 330

(s t at i c 方法不会动态绑定,所以这些都是普通的方法)。每次调用 r andom Fact or y( ) 方法时,它都会创建 一个不同的 Shape(Shape 值采用随机值)。 C r cl e i (圆)和 Squar e (矩形)属于对 Shape 的直接扩展;唯一的差别是 C r cl e 在定义时会初始化颜色, i 而 Squar e 在构建器中初始化。Li ne(直线)的问题将留到以后讨论。 在 m n( ) 中,一个 Vect or 用于容纳 C as s 对象,而另一个用于容纳形状。若不提供相应的命令行参数,就 ai l 会创建 s hapeTypes Vect or ,并添加 C as s 对象。然后创建 s hapes Vect or ,并添加 Shape对象。接下来,所 l 有 s t at i c col or 值都会设成 G REEN ,而且所有东西都会序列化到文件 C D at e. out 。 A St 若提供了一个命令行参数(假设 C D at e. out ),便会打开那个文件,并用它恢复程序的状态。无论在哪种 A St 情况下,结果产生的 Shape 的 Vect or 都会打印出来。下面列出它某一次运行的结果: >j ava C D at e A St [ cl as s C r cl e col or [ 3] xPos [ - 51] yPos [ - 99] di m 38] i [ , cl as s Squar e col or [ 3] xPos [ 2] yPos [ 61] di m - 46] [ , cl as s Li ne col or [ 3] xPos [ 51] yPos [ 73] di m 64] [ , cl as s C r cl e col or [ 3] xPos [ - 70] yPos [ 1] di m 16] i [ , cl as s Squar e col or [ 3] xPos [ 3] yPos [ 94] di m - 36] [ , cl as s Li ne col or [ 3] xPos [ - 84] yPos [ - 21] di m - 35] [ , cl as s C r cl e col or [ 3] xPos [- 75] yPos [ - 43] di m 22] i [ , cl as s Squar e col or [ 3] xPos [ 81] yPos [ 30] di m - 45] [ , cl as s Li ne col or [ 3] xPos [ - 29] yPos [ 92] di m 17] [ , cl as s C r cl e col or [ 3] xPos [ 17] yPos [ 90] di m - 76] i [ ] >j ava C D at e C D at e. out A St A St [ cl as s C r cl e col or [ 1] xPos [ - 51] yPos [ - 99] di m 38] i [ , cl as s Squar e col or [ 0] xPos [ 2] yPos [ 61] di m - 46] [ , cl as s Li ne col or [ 3] xPos [ 51] yPos [ 73] di m 64] [ , cl as s C r cl e col or [ 1] xPos [- 70] yPos [ 1] di m 16] i [ , cl as s Squar e col or [ 0] xPos [ 3] yPos [ 94] di m - 36] [ , cl as s Li ne col or [ 3] xPos [ - 84] yPos [ - 21] di m - 35] [ , cl as s C r cl e col or [ 1] xPos [- 75] yPos [ - 43] di m 22] i [ , cl as s Squar e col or [ 0] xPos [ 81] yPos [30] di m - 45] [ , cl as s Li ne col or [ 3] xPos [ - 29] yPos [ 92] di m 17] [ , cl as s C r cl e col or [ 1] xPos [ 17] yPos [ 90] di m - 76] i [ ] 从中可以看出,xPos ,yPos 以及 di m 的值都已成功保存和恢复出来。但在获取 s t at i c信息时却出现了问 题。所有“3”都已进入,但没有正常地出来。C r cl e 有一个 1 值(定义为 RED i ),而 Squar e 有一个 0 值 (记住,它们是在构建器里初始化的)。看上去似乎 s t at i c 根本没有得到初始化!实情正是如此——尽管类 C as s 是“可以序列化的”,但却不能按我们希望的工作。所以假如想序列化 s t at i c值,必须亲自动手。 l 这正是 Li ne 中的 s er i al i zeSt at i cSt at e( ) 和 des er i al i zeSt at i cSt at e( ) 两个 s t at i c 方法的用途。可以看 到,这两个方法都是作为存储和恢复进程的一部分明确调用的(注意写入序列化文件和从中读回的顺序不能 改变)。所以为了使 C D at e. j ava 正确运行起来,必须采用下述三种方法之一: A St ( 1) 为几何形状添加一个 s er i al i zeSt at i cSt at e( ) 和 des er i al i zeSt at i cSt at e( ) 。 ( 2) 删除 Vect or s hapeT ypes 以及与之有关的所有代码 ( 3) 在几何形状内添加对新序列化和撤消序列化静态方法的调用 要注意的另一个问题是安全,因为序列化处理也会将 pr i vat e 数据保存下来。若有需要保密的字段,应将其 标记成 t r ans i ent 。但在这之后,必须设计一种安全的信息保存方法。这样一来,一旦需要恢复,就可以重 设那些 pr i vat e 变量。

331

10. 10 总结
Java I O流库能满足我们的许多基本要求:可以通过控制台、文件、内存块甚至因特网(参见第 15 章)进行 读写。可以创建新的输入和输出对象类型(通过从 I nput St r eam O put St r eam 和 ut 继承)。向一个本来预期为 收到字串的方法传递一个对象时,由于 Java 已限制了“自动类型转换”,所以会自动调用 t oSt r i ng( )方 法。而我们可以重新定义这个 t oSt r i ng( ) ,扩展一个数据流能接纳的对象种类。 在 I O数据流库的联机文档和设计过程中,仍有些问题没有解决。比如当我们打开一个文件以便输出时,完全 可以指定一旦有人试图覆盖该文件就“掷”出一个违例——有的编程系统允许我们自行指定想打开一个输出 文件,但唯一的前提是它尚不存在。但在 Java 中,似乎必须用一个 Fi l e 对象来判断某个文件是否存在,因 为假如将其作为 Fi l eO put St r eam ut 或者 Fi l eW i t er 打开,那么肯定会被覆盖。若同时指定文件和目录路 r 径,Fi l e 类设计上的一个缺陷就会暴露出来,因为它会说“不要试图在单个类里做太多的事情”! I O流库易使我们混淆一些概念。它确实能做许多事情,而且也可以移植。但假如假如事先没有吃透装饰器方 案的概念,那么所有的设计都多少带有一点盲目性质。所以不管学它还是教它,都要特别花一些功夫才行。 而且它并不完整:没有提供对输出格式化的支持,而其他几乎所有语言的 I O包都提供了这方面的支持(这一 点没有在 Java 1. 1 里得以纠正,它完全错失了改变库设计方案的机会,反而增添了更特殊的一些情况,使复 杂程度进一步提高)。Java 1. 1 转到那些尚未替换的 I O库,而不是增加新库。而且库的设计人员似乎没有 很好地指出哪些特性是不赞成的,哪些是首选的,造成库设计中经常都会出现一些令人恼火的反对消息。 然而,一旦掌握了装饰器方案,并开始在一些较为灵活的环境使用库,就会认识到这种设计的好处。到那个 时候,为此多付出的代码行应该不至于使你觉得太生气。

10. 11 练习
( 1) 打开一个文本文件,每次读取一行内容。将每行作为一个 St r i ng读入,并将那个 St r i ng对象置入一个 Vect or 里。按相反的顺序打印出 Vect or 中的所有行。 ( 2) 修改练习 1,使读取那个文件的名字作为一个命令行参数提供。 ( 3) 修改练习 2,又打开一个文本文件,以便将文字写入其中。将 Vect or 中的行随同行号一起写入文件。 ( 4) 修改练习 2,强迫 Vect or 中的所有行都变成大写形式,将结果发给 Sys t em out 。 . ( 5) 修改练习 2,在文件中查找指定的单词。打印出包含了欲找单词的所有文本行。 ( 6) 在 Bl i ps . j ava中复制文件,将其重命名为 Bl i pC heck. j ava。然后将类 Bl i p2 重命名为 Bl i p heck(在进 C 程中将其标记为 publ i c)。删除文件中的/ / ! 记号,并执行程序。接下来,将 Bl i pC heck 的默认构建器变成 注释信息。运行它,并解释为什么仍然能够工作。 ( 7) 在 Bl i p3. j ava中,将接在“You m t do t hi s : ”字样后的两行变成注释,然后运行程序。解释得到的 us 结果为什么会与执行了那两行代码不同。 ( 8) 转换 Sor t edW dC or ount . j ava程序,以便使用 Java 1. 1 I O流。 ( 9) 根据本章正文的说明修改程序 C D at e. j ava。 A St ( 10) 在第 7 章(中间部分)找到 G eenhous eC r ol s . j ava 示例,它应该由三个文件构成。在 r ont G eenhous eC r ol s . j ava中,Res t ar t ( ) 内部类有一个硬编码的事件集。请修改这个程序,使其能从一个文 r ont 本文件里动态读取事件以及它们的相关时间。

332

第 11 章 运行期类型鉴定
运行期类型鉴定(RTTI )的概念初看非常简单——手上只有基础类型的一个句柄时,利用它判断一个对象的 正确类型。 然而,对 RTTI 的需要暴露出了面向对象设计许多有趣(而且经常是令人困惑的)的问题,并把程序的构造问 题正式摆上了桌面。 本章将讨论如何利用 Java 在运行期间查找对象和类信息。这主要采取两种形式:一种是“传统”RTTI ,它假 定我们已在编译和运行期拥有所有类型;另一种是 Java1. 1 特有的“反射”机制,利用它可在运行期独立查 找类信息。首先讨论“传统”的 RTTI ,再讨论反射问题。

11. 1 对 RT T I 的需要
请考虑下面这个熟悉的类结构例子,它利用了多形性。常规类型是 Shape 类,而特别衍生出来的类型是 C r cl e i ,Squar e 和 Tr i angl e。

这是一个典型的类结构示意图,基础类位于顶部,衍生类向下延展。面向对象编程的基本目标是用大量代码 控制基础类型(这里是 Shape)的句柄,所以假如决定添加一个新类(比如 Rhom d,从 Shape衍生),从 boi 而对程序进行扩展,那么不会影响到原来的代码。在这个例子中,Shape 接口中的动态绑定方法是 dr aw ) , ( 所以客户程序员要做的是通过一个普通 Shape 句柄调用 dr aw ) 。dr aw ) 在所有衍生类里都会被覆盖。而且由 ( ( 于它是一个动态绑定方法,所以即使通过一个普通的 Shape 句柄调用它,也有表现出正确的行为。这正是多 形性的作用。 所以,我们一般创建一个特定的对象(C r cl e,Squar e,或者 Tr i angl e),把它上溯造型到一个 Shape(忽 i 略对象的特殊类型),以后便在程序的剩余部分使用匿名 Shape句柄。 作为对多形性和上溯造型的一个简要回顾,可以象下面这样为上述例子编码(若执行这个程序时出现困难, 请参考第 3 章 3. 1. 2 小节“赋值”): //: Shapes . j ava package c11; i m t j ava. ut i l . * ; por i nt er f ace Shape { voi d dr aw ) ; ( } cl as s C r cl e i m em s Shape { i pl ent publ i c voi d dr aw ) { ( Sys t em out . pr i nt l n( " C r cl e. dr aw ) " ) ; . i ( } } cl as s Squar e i m em s Shape { pl ent 333

publ i c voi d dr aw ) { ( Sys t em out . pr i nt l n( " Squar e. dr aw ) " ) ; . ( } } cl as s Tr i angl e i m em s Shape { pl ent publ i c voi d dr aw ) { ( Sys t em out . pr i nt l n( " Tr i angl e. dr aw ) " ) ; . ( } } publ i c cl as s Shapes { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Vect or s = new Vect or ( ) ; s . addEl em ( new C r cl e( ) ) ; ent i s . addEl em ( new Squar e( ) ) ; ent s . addEl em ( new Tr i angl e( ) ) ; ent Enum at i on e = s . el em s ( ) ; er ent w l e( e. has M eEl em s ( ) ) hi or ent ( ( Shape) e. next El em ( ) ) . dr aw ) ; ent ( } } ///: ~ 基础类可编码成一个 i nt er f ace(接口)、一个 abs t r act (抽象)类或者一个普通类。由于 Shape 没有真正 的成员(亦即有定义的成员),而且并不在意我们创建了一个纯粹的 Shape 对象,所以最适合和最灵活的表 达方式便是用一个接口。而且由于不必设置所有那些 abs t r act 关键字,所以整个代码也显得更为清爽。 每个衍生类都覆盖了基础类 dr aw方法,所以具有不同的行为。在 m n( ) 中创建了特定类型的 Shape,然后将 ai 其添加到一个 Vect or 。这里正是上溯造型发生的地方,因为 Vect or 只容纳了对象。由于 Java 中的所有东西 (除基本数据类型外)都是对象,所以 Vect or 也能容纳 Shape对象。但在上溯造型至 O ect 的过程中,任 bj 何特殊的信息都会丢失,其中甚至包括对象是几何形状这一事实。对 V or 来说,它们只是 O ect 。 ect bj 用 next El em ( ) 将一个元素从 Vect or 提取出来的时候,情况变得稍微有些复杂。由于 Vect or 只容纳 ent O ect ,所以 next El em ( ) 会自然地产生一个 O ect 句柄。但我们知道它实际是个 Shape 句柄,而且希望 bj ent bj 将 Shape 消息发给那个对象。所以需要用传统的" ( Shape) " 方式造型成一个 Shape。这是 RTTI 最基本的形 式,因为在 Java 中,所有造型都会在运行期间得到检查,以确保其正确性。那正是 RTTI 的意义所在:在运 行期,对象的类型会得到鉴定。 在目前这种情况下,RTTI 造型只实现了一部分:O ect 造型成 Shape,而不是造型成 C r cl e,Squar e或者 bj i Tr i angl e。那是由于我们目前能够肯定的唯一事实就是 Vect or 里充斥着几何形状,而不知它们的具体类别。 在编译期间,我们肯定的依据是我们自己的规则;而在编译期间,却是通过造型来肯定这一点。 现在的局面会由多形性控制,而且会为 Shape 调用适当的方法,以便判断句柄到底是提供 C r cl e i ,Squar e, 还是提供给 Tr i angl e。而且在一般情况下,必须保证采用多形性方案。因为我们希望自己的代码尽可能少知 道一些与对象的具体类型有关的情况,只将注意力放在某一类对象(这里是 Shape)的常规信息上。只有这 样,我们的代码才更易实现、理解以及修改。所以说多形性是面向对象程序设计的一个常规目标。 然而,若碰到一个特殊的程序设计问题,只有在知道常规句柄的确切类型后,才能最容易地解决这个问题, 这个时候又该怎么办呢?举个例子来说,我们有时候想让自己的用户将某一具体类型的几何形状(如三角 形)全都变成紫色,以便突出显示它们,并快速找出这一类型的所有形状。此时便要用到 RTTI 技术,用它查 询某个 Shape 句柄引用的准确类型是什么。

1 1 . 1 . 1 Cl as s 对象
为理解 RTTI 在 Java 里如何工作,首先必须了解类型信息在运行期是如何表示的。这时要用到一个名为 “C as s 对象”的特殊形式的对象,其中包含了与类有关的信息(有时也把它叫作“元类”)。事实上,我 l 们要用 C as s 对象创建属于某个类的全部“常规”或“普通”对象。 l 对于作为程序一部分的每个类,它们都有一个 C as s 对象。换言之,每次写一个新类时,同时也会创建一个 l 334

C as s 对象(更恰当地说,是保存在一个完全同名的. cl as s 文件中)。在运行期,一旦我们想生成那个类的 l 一个对象,用于执行程序的 Java 虚拟机(JVM )首先就会检查那个类型的 C as s 对象是否已经载入。若尚未 l 载入,JVM 就会查找同名的. cl as s 文件,并将其载入。所以 Java 程序启动时并不是完全载入的,这一点与许 多传统语言都不同。 一旦那个类型的 C as s 对象进入内存,就用它创建那一类型的所有对象。 l 若这种说法多少让你产生了一点儿迷惑,或者并没有真正理解它,下面这个示范程序或许能提供进一步的帮 助: //: Sw Shop. j ava eet // Exam nat i on of t he w t he cl as s l oader w ks i ay or cl as s C andy { s t at i c { Sys t em out . pr i nt l n( " Loadi ng C . andy" ) ; } } cl as s G { um s t at i c { Sys t em out . pr i nt l n( " Loadi ng G " ) ; . um } } cl as s C ooki e { s t at i c { Sys t em out . pr i nt l n( " Loadi ng C . ooki e" ) ; } } publ i c cl as s Sw Shop { eet p i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ubl ai Sys t em out . pr i nt l n( " i ns i de m n" ) ; . ai new C andy( ) ; Sys t em out . pr i nt l n( " A t er cr eat i ng C . f andy" ) ; try { C as s . f or N e( " G " ) ; l am um } cat ch( C as s N FoundExcept i on e) { l ot e. pr i nt St ackTr ace( ) ; } Sys t em out . pr i nt l n( . " A t er C as s . f or N e( \" G \" ) " ) ; f l am um new C ooki e( ) ; Sys t em out . pr i nt l n( " A t er cr eat i ng C . f ooki e" ) ; } } ///: ~ 对每个类来说(C andy,G um和 C ooki e),它们都有一个 s t at i c从句,用于在类首次载入时执行。相应的信 息会打印出来,告诉我们载入是什么时候进行的。在 m n( ) 中,对象的创建代码位于打印语句之间,以便侦 ai 测载入时间。 特别有趣的一行是: C as s . f or N e( " G " ) ; l am um 该方法是 C as s (即全部 C as s 所从属的)的一个 s t at i c成员。而 C as s 对象和其他任何对象都是类似的, l l l 335

所以能够获取和控制它的一个句柄(装载模块就是干这件事的)。为获得 C as s 的一个句柄,一个办法是使 l 用 f or N e( ) 。它的作用是取得包含了目标类文本名字的一个 St r i ng am (注意拼写和大小写)。最后返回的是 一个 C as s 句柄。 l 该程序在某个 JVM 中的输出如下: i ns i de m n ai Loadi ng C andy A t er cr eat i ng C f andy Loadi ng G um A t er C as s . f or N e( " G " ) f l am um Loadi ng C ooki e A t er cr eat i ng C f ooki e 可以看到,每个 C as s 只有在它需要的时候才会载入,而 s t at i c 初始化工作是在类载入时执行的。 l 非常有趣的是,另一个 JVM 的输出变成了另一个样子: Loadi ng C andy Loadi ng C ooki e i ns i de m n ai A t er cr eat i ng C f andy Loadi ng G um A t er C as s . f or N e(" G " ) f l am um A t er cr eat i ng C f ooki e 看来 JVM 通过检查 m n( ) 中的代码,已经预测到了对 C ai andy 和 C ooki e 的需要,但却看不到 G ,因为它是通 um 过对 f or N e( ) 的一个调用创建的,而不是通过更典型的 new调用。尽管这个 JVM am 也达到了我们希望的效 果,因为确实会在我们需要之前载入那些类,但却不能肯定这儿展示的行为百分之百正确。 1. 类标记 在 Java 1. 1 中,可以采用第二种方式来产生 C as s 对象的句柄:使用“类标记”。对上述程序来说,看起来 l 就象下面这样: G . cl a s ; um s 这样做不仅更加简单,而且更安全,因为它会在编译期间得到检查。由于它取消了对方法调用的需要,所以 执行的效率也会更高。 类标记不仅可以应用于普通类,也可以应用于接口、数组以及基本数据类型。除此以外,针对每种基本数据 类型的封装器类,它还存在一个名为 TYPE 的标准字段。TYPE 字段的作用是为相关的基本数据类型产生 C as s l 对象的一个句柄,如下所示: . . . i s equi v al ent t o . . . bool ean. cl as s Bool ean. TYPE char . cl as s byt e. cl as s s hor t . cl as s i nt . cl as s l ong. cl as s f l oat . cl as s voi d. cl as s C act er . TYPE har Byt e. TYPE Shor t . TYPE I nt eger . TYPE Long. TYPE Fl oat . TYPE Voi d. TYPE

doubl e. cl as s D oubl e. TYPE

336

1 1 . 1 . 2 造型前的检查
迄今为止,我们已知的 RTTI 形式包括: ( 1) 经典造型,如" ( Shape) " ,它用 RTTI 确保造型的正确性,并在遇到一个失败的造型后产生一个 C as s C t Except i on 违例。 l as ( 2) 代表对象类型的 C as s 对象。可查询 C as s 对象,获取有用的运行期资料。 l l 在C ++中,经典的" ( Shape) " 造型并不执行 RTTI 。它只是简单地告诉编译器将对象当作新类型处理。而 Java 要执行类型检查,这通常叫作“类型安全”的下溯造型。之所以叫“下溯造型”,是由于类分层结构的历史 排列方式造成的。若将一个 C r cl e i (圆)造型到一个 Shape(几何形状),就叫做上溯造型,因为圆只是几 何形状的一个子集。反之,若将 Shape 造型至 C r cl e,就叫做下溯造型。然而,尽管我们明确知道 C r cl e i i 也是一个 Shape,所以编译器能够自动上溯造型,但却不能保证一个 Shape肯定是一个 C r cl e。因此,编译 i 器不允许自动下溯造型,除非明确指定一次这样的造型。 RTTI 在 Java 中存在三种形式。关键字 i ns t anceof 告诉我们对象是不是一个特定类型的实例(I ns t ance 即 “实例”)。它会返回一个布尔值,以便以问题的形式使用,就象下面这样: i f ( x i ns t anceof D og) ( ( D x) . bar k( ) ; og) 将 x 造型至一个 D og前,上面的 i f 语句会检查对象 x 是否从属于 D og类。进行造型前,如果没有其他信息可 以告诉自己对象的类型,那么 i ns t anceof 的使用是非常重要的——否则会得到一个 C as s C t Except i on 违 l as 例。 我们最一般的做法是查找一种类型(比如要变成紫色的三角形),但下面这个程序却演示了如何用 i ns t anceof 标记出所有对象。 //: Pet C ount . j ava // Us i ng i ns t anceof package c11. pet count ; i m t j ava. ut i l . * ; por cl cl cl cl cl cl cl as s as s as s as s as s as s as s Pet { } D ext ends Pet { } og Pug ext ends D { } og C ext ends Pet { } at Rodent ext ends Pet { } G bi l ext ends Rodent { } er Ham t er ext ends Rodent { } s

cl as s C ount er { i nt i ; } publ i c cl as s Pet C ount { s t at i c St r i ng[ ] t ypenam = { es " Pet " , " D , " Pug" , " C " , og" at " Rodent " , " G bi l " , " Ham t er " , er s }; publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Vect or pet s = new Vect or ( ) ; try { C as s [ ] pet Types = { l C as s . f or N e( " c11. pet count . D ) , l am og" C as s . f or N e( " c11. pet count . Pug" ) , l am C a s . f or N e( " c11. pet count . C " ) , l s am at C as s . f or N e( " c11. pet count . Rodent " ) , l am C as s . f or N e( " c11. pet count . G bi l " ) , l am er C as s . f or N e( " c11. pet count . Ham t er " ) , l am s 337

}; f or ( i nt i = 0; i < 15; i ++) pet s . addEl em ( ent pet T ypes [ ( i nt ) ( M h. r andom ) *pet Types . l engt h) ] at ( . new ns t ance( ) ) ; I } cat ch( I ns t ant i at i onExcept i on e) { } cat ch( I l l egal A cces s Except i on e) { } cat ch( C as s N FoundExcept i on e) { } l ot Has ht abl e h = new Has ht abl e( ) ; f or ( i nt i = 0; i < t ypenam . l engt h; i ++) es h. put ( t ypenam [ i ] , new C es ount er ( ) ) ; f or ( i nt i = 0; i < pet s . s i ze( ) ; i ++) { O ect o = pet s . el em A ( i ) ; bj ent t i f ( o i ns t anceof Pet ) ((C ount er ) h. get ( " Pet " ) ) . i ++; i f ( o i ns t anceof D og) ((C ount er ) h. get ( " D ) ) . i ++; og" i f ( o i ns t anceof Pug) ((C ount er ) h. get ( " Pug" ) ) . i ++; i f ( o i ns t anceof C ) at ((C ount er ) h. get ( " C " ) ) . i ++; at i f ( o i ns t anceof Rodent ) ((C ount er ) h. get ( " Rodent " ) ) . i ++; i f ( o i ns t anceof G bi l ) er ((C ount er ) h. get ( " G bi l " ) ) . i ++; er i f ( o i ns t anceof Ham t er ) s ((C ount er ) h. get ( " Ham t er " ) ) . i ++; s } f or ( i nt i = 0; i < pet s . s i ze( ) ; i ++) Sys t em out . pr i nt l n( . pet s . el em A ( i ) . get C as s ( ) . t oSt r i ng( ) ) ; ent t l f or ( i nt i = 0; i < t yp enam . l engt h; i ++) es Sys t em out . pr i nt l n( . t ypenam [ i ] + " quant i t y: " + es ((C ount er ) h. get ( t ypenam [ i ] ) ) . i ) ; es } } ///: ~ 在 Java 1. 0 中,对 i ns t anceof 有一个比较小的限制:只可将其与一个已命名的类型比较,不能同 C as s 对 l 象作对比。在上述例子中,大家可能觉得将所有那些 i ns t anceof 表达式写出来是件很麻烦的事情。实际情况 正是这样。但在 Java 1. 0 中,没有办法让这一工作自动进行——不能创建 C as s 的一个 Vect or ,再将其与 l 之比较。大家最终会意识到,如编写了数量众多的 i ns t anceof 表达式,整个设计都可能出现问题。 当然,这个例子只是一个构想——最好在每个类型里添加一个 s t at i c数据成员,然后在构建器中令其增值, 以便跟踪计数。编写程序时,大家可能想象自己拥有类的源码控制权,能够自由改动它。但由于实际情况并 非总是这样,所以 RTTI 显得特别方便。 1. 使用类标记 Pet C ount . j ava 示例可用 Java 1. 1 的类标记重写一遍。得到的结果显得更加明确易懂: //: Pet C ount 2. j ava // Us i ng Java 1. 1 cl as s l i t er al s 338

package c11. pet count 2; i m t j ava. ut i l . * ; por cl cl cl cl cl cl cl as s as s as s as s as s as s as s Pet { } D e ends Pet { } og xt Pug ext ends D { } og C ext ends Pet { } at Rodent ext ends Pet { } G bi l ext ends Rodent { } er Ham t er ext ends Rodent { } s

cl as s C ount er { i nt i ; } publ i c cl as s Pet C ount 2 { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Vect or pet s = new Vect or ( ) ; C as s [ ] pet T ypes = { l // C as s l i t er al s w k i n Java 1. 1+ onl y: l or Pet . cl as s , D cl as s , og. Pug. cl as s , C . cl as s , at Rodent . cl as s , G bi l . cl as s , er Ham t er . cl as s , s }; try { f or ( i nt i = 0; i < 15; i ++) { // O f s et by one t o el i m nat e Pet . cl as s : f i i nt r nd = 1 + ( i nt ) ( M h. r andom ) * ( pet Types . l engt h - 1) ) ; at ( pet s . addEl em ( ent pet Types [ r nd] . new ns t ance( ) ) ; I } } cat ch( I ns t ant i at i onExcep i on e) { } t cat ch( I l l egal A cces s Except i on e) { } Has ht abl e h = new Has ht abl e( ) ; f or ( i nt i = 0; i < pet Types . l engt h; i ++) h. put ( pet Types [ i ] . t oSt r i ng( ) , new C ount er ( ) ) ; f or ( i nt i = 0; i < pet s . s i ze( ) ; i ++) { O ect o = pet s. el em A ( i ) ; bj ent t i f ( o i ns t anceof Pet ) ((C ount er ) h. get ( " cl as s c11. pet count 2. Pet " ) ) . i ++; i f ( o i ns t anceof D og) ((C ount er ) h. get ( " cl as s c11. pet count 2. D ) ) . i ++; og" i f ( o i ns t anceof Pug) ((C ount er ) h. get ( " cl as s c11. pet count 2. Pug" ) ) . i ++; i f ( o i ns t anceof C ) at 339

((C ount er ) h. get ( " cl as s c11. pet count 2. C " ) ) . i ++; at i f ( o i ns t anceof Rodent ) ((C ount er ) h. get ( " cl as s c11. pet count 2. Rodent " ) ) . i ++; i f ( o i ns t a nceof G bi l ) er ((C ount er ) h. get ( " cl as s c11. pet count 2. G bi l " ) ) . i ++; er i f ( o i ns t anceof Ham t er ) s ((C ount er ) h. get ( " cl as s c11. pet count 2. Ham t er " ) ) . i ++; s } f or ( i nt i = 0; i < pet s . s i ze( ) ; i ++) Sys t em out . pr i nt l n( . pet s . el em A ( i ) . get C as s ( ) . t oSt r i ng( ) ) ; ent t l Enum at i on keys = h. keys ( ) ; er w l e( keys . has M eEl em s ( ) ) { hi or ent St r i ng nm = ( St r i ng) keys . next El em ( ) ; ent C ount er cnt = ( C ount er ) h. get ( nm ; ) Sys t em out . pr i nt l n( . nm s ubs t r i ng nm l as t I ndexO ( ' . ' ) + 1) + . ( . f " quant i t y: " + cnt . i ) ; } } } ///: ~ 在这里,t ypenam (类型名)数组已被删除,改为从 C as s 对象里获取类型名称。注意为此而额外做的工 es l 作:例如,类名不是 G bi l ,而是 c11. pet count 2. G bi l ,其中已包含了包的名字。也要注意系统是能够区 et et 分类和接口的。 也可以看到,pet Types 的创建模块不需要用一个 t r y 块包围起来,因为它会在编译期得到检查,不会象 C as s . f or N e( )那样“掷”出任何违例。 l am Pet 动态创建好以后,可以看到随机数字已得到了限制,位于 1 和 pet Types . l engt h 之间,而且不包括零。 那是由于零代表的是 Pet . cl as s ,而且一个普通的 Pet 对象可能不会有人感兴趣。然而,由于 Pet . cl as s 是 pet Types 的一部分,所以所有 Pet (宠物)都会算入计数中。 2. 动态的 i ns t anceof Java 1. 1 为 C as s 类添加了 i s I ns t ance方法。利用它可以动态调用 i ns t anceof 运算符。而在 Java 1. 0 l 中,只能静态地调用它(就象前面指出的那样)。因此,所有那些烦人的 i ns t anceof 语句都可以从 Pet C ount 例子中删去了。如下所示: //: Pet C ount 3. j ava // Us i ng Java 1. 1 i s I ns t ance( ) package c11. pet count 3; i m t j ava. ut i l . * ; por cl cl cl cl cl cl cl as s as s as s as s as s as s as s Pet { } D ext ends Pet { } og Pug ext ends D { } og C ext ends Pet { } at Rodent ext ends Pet { } G bi l ext ends Rodent { } er Ham t er ext ends Rodent { } s 340

cl as s C ount er { i nt i ; } publ i c cl as s Pet C ount 3 { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Vect or pet s = new Vect or ( ) ; C as s [ ] pet T ypes = { l Pet . cl as s , D cl as s , og. Pug. cl as s , C . cl as s , at Rodent . cl as s , G bi l . cl as s , er Ham t er . cl as s , s }; try { f or ( i nt i = 0; i < 15; i ++) { // O f s et by one t o el i m nat e Pet . cl as s : f i i nt r nd = 1 + ( i nt ) ( M h. r andom ) * ( pet Types . l engt h - 1) ) ; at ( pet s . addEl em ( ent pet Types [ r nd] . new ns t ance( ) ) ; I } } cat ch( I ns t ant i at i onExcept i on e) { } cat ch( I l l egal A cces s Except i on e) { } Has ht abl e h = new Has ht abl e( ) ; f or ( i nt i = 0; i < pet Types . l engt h; i ++) h. put ( pet Types [ i ] . t oSt r i ng( ) , new C ount er ( ) ) ; f or ( i nt i = 0; i < pet s . s i ze( ) ; i ++) { O ect o = pet s . el em A ( i ) ; bj ent t // Us i ng i s I ns t ance t o el i m nat e i ndi vi dual i // i ns t anceof expr es s i ons : f or ( i nt j = 0; j < pet T ypes . l engt h; ++j ) i f ( pet Types [ j ] . i s I ns t ance( o) ) { St r i ng key = pet Types [ j ] . t oSt r i ng( ) ; ((C ount er ) h. get ( key) ) . i ++; } } f or ( i nt i = 0; i < pet s . s i ze( ) ; i ++) Sys t em out . pr i nt l n( . pet s . el em A ( i ) . get C as s ( ) . t oSt r i ng( ) ) ; ent t l Enum at i on keys = h. keys ( ) ; er w l e( keys . has M eEl em s ( ) ) { hi or ent St r i ng nm = ( St r i ng) keys . next El em ( ) ; ent C ount er cnt = ( C ount er ) h. get ( nm ; ) Sys t em out . pr i nt l n( . nm s ubs t r i ng( nm l as t I ndexO ( ' . ' ) + 1) + . . f " quant i t y: " + cnt . i ) ; } }

341

} ///: ~ 可以看到,Java 1. 1 的 i s I ns t ance( ) 方法已取消了对 i ns t anceof 表达式的需要。此外,这也意味着一旦要 求添加新类型宠物,只需简单地改变 pet Types 数组即可;毋需改动程序剩余的部分(但在使用 i ns t anceof 时却是必需的)。

11. 2 RT T I 语法
Java 用 C as s 对象实现自己的 RTTI 功能——即便我们要做的只是象造型那样的一些工作。C as s 类也提供了 l l 其他大量方式,以方便我们使用 RT T I 。 首先必须获得指向适当 C as s 对象的的一个句柄。就象前例演示的那样,一个办法是用一个字串以及 l C as s . f or N e( )方法。这是非常方便的,因为不需要那种类型的一个对象来获取 C as s 句柄。然而,对于自 l am l 己感兴趣的类型,如果已有了它的一个对象,那么为了取得 C as s 句柄,可调用属于 O ect 根类一部分的一 l bj 个方法:get C as s ( ) 。它的作用是返回一个特定的 C as s 句柄,用来表示对象的实际类型。C as s 提供了几 l l l 个有趣且较为有用的方法,从下例即可看出: //: ToyTes t . j ava / / Tes t i ng cl as s C as s l i nt er f ace Has Bat t er i es { } i nt er f ace W er pr oof { } at i nt er f ace Shoot s Thi ngs { } cl as s Toy { // C m om ent out t he f ol l ow ng def aul t i // cons t r uct or t o s ee // N oSuchM hodEr r or f r om ( *1*) et Toy( ) { } Toy( i nt i ) { } } cl as s FancyToy ext ends Toy i m em s Has Bat t er i es , pl ent W er pr oof , Shoot s Thi ngs { at FancyToy( ) { s uper ( 1) ; } } publ i c cl as s ToyTes t { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C as s c = nul l ; l try { c = C as s . f or N e( " FancyToy" ) ; l am } cat ch( C as sN FoundExcept i on e) { } l ot pr i nt I nf o( c) ; C as s [ ] f aces = c. get I nt er f aces ( ) ; l f or ( i nt i = 0; i < f aces . l engt h; i ++) pr i nt I nf o( f aces [ i ] ) ; C as s cy = c. get Super cl as s ( ) ; l O ect o = nul l ; bj try { // Requi r es def aul t cons t r uct or : o = cy. new ns t ance( ) ; // ( *1*) I } cat ch( I ns t ant i at i onExcept i on e) { } cat ch( I l l egal A cces s Except i on e) { } 342

pr i nt I nf o( o. get C as s ( ) ) ; l } s t at i c voi d pr i nt I nf o( C as s cc) { l Sys t em out . pr i nt l n( . " C as s nam " + cc. get N e( ) + l e: am " i s i nt er f ace? [ " + cc. i s I nt er f ace( ) + " ] " ) ; } } ///: ~ 从中可以看出,cl as s FancyToy 相当复杂,因为它从 T oy 中继承,并实现了 Has Bat t er i es,W er pr oof 以 at 及 Shoot s Thi ngs 的接口。在 m n( ) 中创建了一个 C as s 句柄,并用位于相应 t r y 块内的 f or N m ) 初始化成 ai l a e( FancyToy。 C as s . get I nt er f aces 方法会返回 C as s 对象的一个数组,用于表示包含在 C as s 对象内的接口。 l l l 若有一个 C as s 对象,也可以用 get Super cl as s ( ) 查询该对象的直接基础类是什么。当然,这种做会返回一 l 个 C as s 句柄,可用它作进一步的查询。这意味着在运行期的时候,完全有机会调查到对象的完整层次结 l 构。 若从表面看,C as s 的 new ns t ance( ) 方法似乎是克隆(cl one( ))一个对象的另一种手段。但两者是有区别 l I 的。利用 new ns t ance( ) ,我们可在没有现成对象供“克隆”的情况下新建一个对象。就象上面的程序演示 I 的那样,当时没有 T oy 对象,只有 cy——即 y 的 C as s 对象的一个句柄。利用它可以实现“虚拟构建器”。 l 换言之,我们表达:“尽管我不知道你的准确类型是什么,但请你无论如何都正确地创建自己。”在上述例 子中,cy 只是一个 C as s 句柄,编译期间并不知道进一步的类型信息。一旦新建了一个实例后,可以得到 l O ect 句柄。但那个句柄指向一个 T oy 对象。当然,如果要将除 O ect 能够接收的其他任何消息发出去, bj bj 首先必须进行一些调查研究,再进行造型。除此以外,用 new ns t ance( ) 创建的类必须有一个默认构建器。 I 没有办法用 new ns t ance( ) 创建拥有非默认构建器的对象,所以在 Java 1. 0 中可能存在一些限制。然而, I Java 1. 1 的“反射”A (下一节讨论)却允许我们动态地使用类里的任何构建器。 PI 程序中的最后一个方法是 pr i nt I nf o( ) ,它取得一个 C as s 句柄,通过 get N e( ) 获得它的名字,并用 l am i nt er f ace( )调查它是不是一个接口。 该程序的输出如下: C as s l C as s l C as s l C as s l C as s l nam e: nam e: nam e: nam e: nam e: FancyToy i s i nt er f ace? [ f al s e] Has Bat t er i es i s i nt er f ace? [ t r ue] W e pr oof i s i nt er f ace? [ t r ue] at r Shoot s Thi ngs i s i nt er f ace? [ t r ue] Toy i s i nt er f ace? [ f al s e]

所以利用 C as s 对象,我们几乎能将一个对象的祖宗十八代都调查出来。 l

11. 3 反射:运行期类信息
如果不知道一个对象的准确类型,RTTI 会帮助我们调查。但却有一个限制:类型必须是在编译期间已知的, 否则就不能用 RTTI 调查它,进而无法展开下一步的工作。换言之,编译器必须明确知道 RTTI 要处理的所有 类。 从表面看,这似乎并不是一个很大的限制,但假若得到的是一个不在自己程序空间内的对象的句柄,这时又 会怎样呢?事实上,对象的类即使在编译期间也不可由我们的程序使用。例如,假设我们从磁盘或者网络获 得一系列字节,而且被告知那些字节代表一个类。由于编译器在编译代码时并不知道那个类的情况,所以怎 样才能顺利地使用这个类呢? 在传统的程序设计环境中,出现这种情况的概率或许很小。但当我们转移到一个规模更大的编程世界中,却 必须对这个问题加以高度重视。第一个要注意的是基于组件的程序设计。在这种环境下,我们用“快速应用 开发”(RA )模型来构建程序项目。RA D D一般是在应用程序构建工具中内建的。这是编制程序的一种可视途 径(在屏幕上以窗体的形式出现)。可将代表不同组件的图标拖曳到窗体中。随后,通过设定这些组件的属 性或者值,进行正确的配置。设计期间的配置要求任何组件都是可以“例示”的(即可以自由获得它们的实 例)。这些组件也要揭示出自己的一部分内容,允许程序员读取和设置各种值。此外,用于控制 G 事件的 UI 343

组件必须揭示出与相应的方法有关的信息,以便 RA D环境帮助程序员用自己的代码覆盖这些由事件驱动的方 法。“反射”提供了一种特殊的机制,可以侦测可用的方法,并产生方法名。通过 Java Beans (第 13 章将 详细介绍),Java 1. 1 为这种基于组件的程序设计提供了一个基础结构。 在运行期查询类信息的另一个原动力是通过网络创建与执行位于远程系统上的对象。这就叫作“远程方法调 用”(RM ),它允许 Java 程序(版本 1. 1 以上)使用由多台机器发布或分布的对象。这种对象的分布可能 I 是由多方面的原因引起的:可能要做一件计算密集型的工作,想对它进行分割,让处于空闲状态的其他机器 分担部分工作,从而加快处理进度。某些情况下,可能需要将用于控制特定类型任务(比如多层客户/服务 器架构中的“运作规则”)的代码放置在一台特殊的机器上,使这台机器成为对那些行动进行描述的一个通 用储藏所。而且可以方便地修改这个场所,使其对系统内的所有方面产生影响(这是一种特别有用的设计思 路,因为机器是独立存在的,所以能轻易修改软件!)。分布式计算也能更充分地发挥某些专用硬件的作 用,它们特别擅长执行一些特定的任务——例如矩阵逆转——但对常规编程来说却显得太夸张或者太昂贵 了。 在 Java 1. 1 中,C as s 类(本章前面已有详细论述)得到了扩展,可以支持“反射”的概念。针对 Fi el d, l M hod以及 C t r uct or 类(每个都实现了 M ber i nt er f ace et ons em ——成员接口),它们都新增了一个库: j ava. l ang. r ef l ect 。这些类型的对象都是 JVM 在运行期创建的,用于代表未知类里对应的成员。这样便可用 构建器创建新对象,用 get ( ) 和 s et ( ) 方法读取和修改与 Fi el d对象关联的字段,以及用 i nvoke( ) 方法调用 与 M hod对象关联的方法。此外,我们可调用方法 get Fi el ds ( ) ,get M hods ( ),get C t r uct or s ( ),分 et et ons 别返回用于表示字段、方法以及构建器的对象数组(在联机文档中,还可找到与 C as s 类有关的更多的资 l 料)。因此,匿名对象的类信息可在运行期被完整的揭露出来,而在编译期间不需要知道任何东西。 大家要认识的很重要的一点是“反射”并没有什么神奇的地方。通过“反射”同一个未知类型的对象打交道 时,JVM 只是简单地检查那个对象,并调查它从属于哪个特定的类(就象以前的 RTTI 那样)。但在这之后, 在我们做其他任何事情之前,C as s 对象必须载入。因此,用于那种特定类型的. cl as s 文件必须能由 JVM l 调 用(要么在本地机器内,要么可以通过网络取得)。所以 RTT I 和“反射”之间唯一的区别就是对 RTTI 来 说,编译器会在编译期打开和检查. cl as s 文件。换句话说,我们可以用“普通”方式调用一个对象的所有方 法;但对“反射”来说,. cl as s 文件在编译期间是不可使用的,而是由运行期环境打开和检查。

1 1 . 3 . 1 一个类方法提取器
很少需要直接使用反射工具;之所以在语言中提供它们,仅仅是为了支持其他 Java 特性,比如对象序列化 (第 10 章介绍)、Java Beans 以及 RM (本章后面介绍)。但是,我们许多时候仍然需要动态提取与一个类 I 有关的资料。其中特别有用的工具便是一个类方法提取器。正如前面指出的那样,若检视类定义源码或者联 机文档,只能看到在那个类定义中被定义或覆盖的方法,基础类那里还有大量资料拿不到。幸运的是,“反 射”做到了这一点,可用它写一个简单的工具,令其自动展示整个接口。下面便是具体的程序: //: Show et hods . j ava M // Us i ng Java 1. 1 r ef l ect i on t o s how al l t he // m hods of a cl as s , even i f t he m hods ar e et et // def i ned i n t he bas e cl as s . i m t j ava. l ang. r ef l ect . *; por publ i c cl as s Show et hods { M s t at i c f i nal St r i ng us age = " us age: \n" + " Show et hods qual i f i ed. cl as s . nam M e\n" + " To s how al l m hods i n cl as s or : \n" + et " Show et hods qual i f i ed. cl as s . nam w d\n" + M e or " To s ear ch f or m hods i nvol vi ng ' w d' " ; et or publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai i f ( ar gs . l engt h < 1) { Sys t em out . pr i nt l n( us age) ; . Sys t em exi t ( 0) ; . } try { 344

C as s c = C as s . f or N e( ar gs [ 0] ) ; l l am M hod[ ] m = c. get M hods ( ) ; et et C t r uct or [ ] ct or = c. get C t r uct or s ( ) ; ons ons i f ( ar gs . l engt h == 1) { f or ( i nt i = 0; i < m l engt h; i ++) . Sys t em out . pr i nt l n( m i ] . t oSt r i ng( ) ) ; . [ f or ( i nt i = 0; i < ct or . l engt h; i ++) Sys t em out . pr i nt l n( ct or [ i ] . t oSt r i ng( ) ); . } el s e { f or ( i nt i = 0; i < m l engt h; i ++) . i f ( m i ] . t oSt r i ng( ) [ . i ndexO ( ar gs [ 1] ) ! = - 1) f Sys t em out . pr i nt l n( m i ] . t oSt r i ng( ) ) ; . [ f or ( i nt i = 0; i < ct or . l engt h; i ++) i f ( ct or [ i ] . t oSt r i ng( ) . i ndexO ( ar gs [ 1] ) ! = - 1) f Sys t em out . pr i nt l n( ct or [ i ] . t oSt r i ng( ) ) ; . } } cat ch ( C as s N FoundExcept i on e) { l ot Sys t em out . pr i nt l n( " N s uch cl as s : " + e) ; . o } } } ///: ~ C as s 方法 get M hods ( )和 get C t r uct or s ( )可以分别返回 M hod和 C t r uct or 的一个数组。每个类都 l et ons et ons 提供了进一步的方法,可解析出它们所代表的方法的名字、参数以及返回值。但也可以象这样一样只使用 t oSt r i ng( ) ,生成一个含有完整方法签名的字串。代码剩余的部分只是用于提取命令行信息,判断特定的签 名是否与我们的目标字串相符(使用 i ndexO ( ) ),并打印出结果。 f 这里便用到了“反射”技术,因为由 C as s . f or N e( ) 产生的结果不能在编译期间获知,所以所有方法签名 l am 信息都会在运行期间提取。若研究一下联机文档中关于“反射”(Ref l ect i on)的那部分文字,就会发现它 已提供了足够多的支持,可对一个编译期完全未知的对象进行实际的设置以及发出方法调用。同样地,这也 属于几乎完全不用我们操心的一个步骤——Java 自己会利用这种支持,所以程序设计环境能够控制 Java Beans ——但它无论如何都是非常有趣的。 一个有趣的试验是运行 j ava Show ehods Show et hods 。这样做可得到一个列表,其中包括一个 publ i c默认 M M 构建器,尽管我们在代码中看见并没有定义一个构建器。我们看到的是由编译器自动合成的那一个构建器。 如果随之将 Show et hods 设为一个非 publ i c 类(即换成“友好”类),合成的默认构建器便不会在输出结果 M 中出现。合成的默认构建器会自动获得与类一样的访问权限。 Show et hods 的输出仍然有些“不爽”。例如,下面是通过调用 j ava Show et hods j ava. l ang. St r i ng得到 M M 的输出结果的一部分: publ i c bool ean j ava. l ang. St r i ng. s t ar t s W t h( j ava. l ang. St r i ng, i nt ) i publ i c bool ean j ava. l ang. St r i ng. s t ar t s W t h(j ava. l ang. St r i ng) i publ i c bool ean j ava. l ang. St r i ng. ends W t h( j ava. l ang. St r i ng) i 若能去掉象 j ava. l ang这样的限定词,结果显然会更令人满意。有鉴于此,可引入上一章介绍的 St r eam Tokeni zer 类,解决这个问题: //: Show et hods C ean. j ava M l 345

// Show et hods w t h t he qual i f i er s s t r i pped M i // t o m ake t he r es ul t s eas i er t o r ead i m t j ava. l ang. r ef l ect . *; por i m t j ava. i o. * ; por publ i c cl as s Show et hods C ean { M l s t at i c f i nal St r i ng us age = " us age: \n" + " Show et hods C ean qual i f i ed. cl as s . nam M l e\n" + " To s how al l m hods i n cl as s or : \n" + et " Show et hods C ean qual i f . cl as s . nam w d M l e or \n" + " To s ear ch f or m hods i nvol vi ng ' w d' " ; et or publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai i f ( ar gs . l engt h < 1) { Sys t em out . pr i nt l n( us age) ; . Sys t em exi t ( 0) ; . } try { C as s c = C as s . f or N e( ar gs [ 0] ) ; l l am M hod[ ] m = c. get M hods ( ) ; et et C t r uct or [ ] ct or = c. get C t r uct or s ( ) ; ons ons // C onver t t o an ar r ay of cl eaned St r i ngs : St r i ng[ ] n = new St r i ng[ m l engt h + ct or . l engt h] ; . f or ( i nt i = 0; i < m l engt h; i ++) { . St r i ng s = m i ] . t oSt r i ng( ) ; [ n[ i ] = St r i pQ i f i er s . s t r i p( s ) ; ual } f or ( i nt i = 0; i < ct or . l engt h; i ++) { St r i ng s = ct or [ i ] . t oSt r i ng( ) ; n[ i + m l engt h] = . St r i pQ i f i er s . s t r i p( s ) ; ual } i f ( ar gs . l engt h == 1) f or ( i nt i = 0; i < n. l engt h; i ++) Sys t em out . pr i nt l n( n[ i ] ) ; . el s e f or ( i nt i = 0; i < n. l engt h; i ++) i f ( n[ i ] . i ndexO ( ar gs [ 1] ) ! = - 1) f Sys t em out . pr i nt l n( n[ i ] ) ; . } cat ch ( C as s N FoundExcept i on e) { l ot Sys t em out . pr i nt l n( " N s uch cl as s : " + e) ; . o } } } cl as s St r i pQ i f i er s { ual pr i vat e St r eam Tokeni zer s t ; publ i c St r i pQ i f i er s ( St r i ng qual i f i ed) { ual s t = new St r eam Tokeni zer ( new St r i ngReader ( qual i f i ed) ) ; s t . or di nar yC ( ' ' ) ; // Keep t he s paces har 346

} publ i c St r i ng get N ( ) { ext St r i ng s = nul l ; try { i f ( s t . next Token( ) ! = St r eam Tokeni zer . TT_EO { F) s w t ch( s t . t t ype) { i cas e St r eam Tokeni zer . TT_EO L: s = nul l ; br eak; cas e St r eam okeni zer . TT_N BER: T UM s = D oubl e. t oSt r i ng( s t . nval ) ; br eak; cas e St r eam Tokeni zer . TT_W RD O : s = new St r i ng( s t . s val ) ; br eak; def aul t : // s i ngl e char act er i n t t ype s = St r i ng. val ueO ( ( char ) s t . t t ype) ; f } } } cat ch( I O Except i on e) { Sys t em out . pr i nt l n( e) ; . } r et ur n s ; } publ i c s t at i c St r i ng s t r i p( St r i ng qual i f i ed) { St r i pQ i f i er s s q = ual new St r i pQ i f i er s ( qual i f i ed) ; ual St r i ng s = " " , s i ; w l e( ( s i = s q. get N ( ) ) ! = nul l ) { hi ext i nt l as t D = s i . l as t I ndexO ( ' . ' ) ; ot f i f ( l as t D ! = - 1) ot s i = s i . s ubs t r i ng( l as t D + 1) ; ot s += s i ; } r et ur n s ; } } ///: ~ Show et hods C ean方法非常接近前一个 Show et hods ,只是它取得了 M hod和 C M l M et onst r uct or 数组,并将它们 转换成单个 St r i ng数组。随后,每个这样的 St r i ng对象都在 St r i pQ i f i er s . St r i p( ) 里“过”一遍,删 ual 除所有方法限定词。正如大家看到的那样,此时用到了 St r eam Tokeni zer 和 St r i ng来完成这个工作。 假如记不得一个类是否有一个特定的方法,而且不想在联机文档里逐步检查类结构,或者不知道那个类是否 能对某个对象(如 C or 对象)做某件事情,该工具便可节省大量编程时间。 ol 第 17 章提供了这个程序的一个 G 版本,可在自己写代码的时候运行它,以便快速查找需要的东西。 UI

11. 4 总结
利用 RTTI 可根据一个匿名的基础类句柄调查出类型信息。但正是由于这个原因,新手们极易误用它,因为有 些时候多形性方法便足够了。对那些以前习惯程序化编程的人来说,极易将他们的程序组织成一系列 s w t ch i 语句。他们可能用 RTTI 做到这一点,从而在代码开发和维护中损失多形性技术的重要价值。Java 的要求是 让我们尽可能地采用多形性,只有在极特别的情况下才使用 RTTI 。 但为了利用多形性,要求我们拥有对基础类定义的控制权,因为有些时候在程序范围之内,可能发现基础类 347

并未包括我们想要的方法。若基础类来自一个库,或者由别的什么东西控制着,RTTI 便是一种很好的解决方 案:可继承一个新类型,然后添加自己的额外方法。在代码的其他地方,可以侦测自己的特定类型,并调用 那个特殊的方法。这样做不会破坏多形性以及程序的扩展能力,因为新类型的添加不要求查找程序中的 s w t ch语句。但在需要新特性的主体中添加新代码时,就必须用 RTTI 侦测自己特定的类型。 i 从某个特定类的利益的角度出发,在基础类里加入一个特性后,可能意味着从那个基础类衍生的其他所有类 都必须获得一些无意义的“鸡肋”。这使得接口变得含义模糊。若有人从那个基础类继承,且必须覆盖抽象 方法,这一现象便会使他们陷入困扰。比如现在用一个类结构来表示乐器(I ns t r um )。假定我们想清洁 ent 管弦乐队中所有适当乐器的通气音栓(Spi t Val ve),此时的一个办法是在基础类 I ns t r um 中置入一个 ent C ear Spi t Val ve( )方法。但这样做会造成一个误区,因为它暗示着打击乐器和电子乐器中也有音栓。针对这 l 种情况,RTTI 提供了一个更合理的解决方案,可将方法置入特定的类中(此时是 W nd,即“通气口”)—— i 这样做是可行的。但事实上一种更合理的方案是将 pr epar eI ns t r um ( ) 置入基础类中。初学者刚开始时往 ent 往看不到这一点,一般会认定自己必须使用 RTTI 。 最后,RTTI 有时能解决效率问题。若代码大量运用了多形性,但其中的一个对象在执行效率上很有问题,便 可用 RTTI 找出那个类型,然后写一段适当的代码,改进其效率。

11. 5 练习
( 1) 写一个方法,向它传递一个对象,循环打印出对象层次结构中的所有类。 ( 2) 在 ToyTes t . j ava中,将 T oy 的默认构建器标记成注释信息,解释随之发生的事情。 ( 3) 新建一种类型的集合,令其使用一个 Vect or 。捕获置入其中的第一个对象的类型,然后从那时起只允许 用户插入那种类型的对象。 ( 4) 写一个程序,判断一个 C 数组属于基本数据类型,还是一个真正的对象。 har ( 5) 根据本章的说明,实现 cl ear Spi t Val ve( )。 ( 6) 实现本章介绍的 r ot at e( Shape) 方法,令其检查是否已经旋转了一个圆(若已旋转,就不再执行旋转操 作)。

348

第 12 章 传递和返回对象
到目前为止,读者应对对象的“传递”有了一个较为深刻的认识,记住实际传递的只是一个句柄。 在许多程序设计语言中,我们可用语言的“普通”方式到处传递对象,而且大多数时候都不会遇到问题。但 有些时候却不得不采取一些非常做法,使得情况突然变得稍微复杂起来(在 C ++中则是变得非常复杂)。 Java 亦不例外,我们十分有必要准确认识在对象传递和赋值时所发生的一切。这正是本章的宗旨。 若读者是从某些特殊的程序设计环境中转移过来的,那么一般都会问到:“Java 有指针吗?”有些人认为指 针的操作很困难,而且十分危险,所以一厢情愿地认为它没有好处。同时由于 Java 有如此好的口碑,所以应 该很轻易地免除自己以前编程中的麻烦,其中不可能夹带有指针这样的“危险品”。然而准确地说,Java 是 有指针的!事实上,Java 中每个对象(除基本数据类型以外)的标识符都属于指针的一种。但它们的使用受 到了严格的限制和防范,不仅编译器对它们有“戒心”,运行期系统也不例外。或者换从另一个角度说, Java 有指针,但没有传统指针的麻烦。我曾一度将这种指针叫做“句柄”,但你可以把它想像成“安全指 针”。和预备学校为学生提供的安全剪刀类似——除非特别有意,否则不会伤着自己,只不过有时要慢慢 来,要习惯一些沉闷的工作。

12. 1 传递句柄
将句柄传递进入一个方法时,指向的仍然是相同的对象。一个简单的实验可以证明这一点(若执行这个程序 时有麻烦,请参考第 3 章 3. 1. 2 小节“赋值”): //: Pas s Handl es . j ava // Pas s i ng handl es ar ound package c12; publ i c cl as s Pas s Handl es { s t at i c voi d f ( Pas s Handl es h) { Sys t em out . pr i nt l n( " h i ns i de f ( ) : " + h) ; . } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Pas s Handl es p = new Pas s Handl es ( ) ; Sys t em out . pr i nt l n( " p i ns i de m n( ) : " + p) ; . ai f ( p) ; } } ///: ~ t oSt r i ng 方法会在打印语句里自动调用,而 Pas s Handl es 直接从 O ect 继承,没有 t oSt r i ng 的重新定义。 bj 因此,这里会采用 t oSt r i ng 的 O ect 版本,打印出对象的类,接着是那个对象所在的位置(不是句柄,而 bj 是对象的实际存储位置)。输出结果如下: p i ns i de m n( ) : Pas s Handl es @ ai 1653748 h i ns i de f ( ) : Pas s Handl es @ 1653748 可以看到,无论 p还是 h引用的都是同一个对象。这比复制一个新的 Pas s Handl es 对象有效多了,使我们能 将一个参数发给一个方法。但这样做也带来了另一个重要的问题。

1 2 . 1 . 1 别名问题
“别名”意味着多个句柄都试图指向同一个对象,就象前面的例子展示的那样。若有人向那个对象里写入一 点什么东西,就会产生别名问题。若其他句柄的所有者不希望那个对象改变,恐怕就要失望了。这可用下面 这个简单的例子说明: //: A i as 1. j ava l // A i as i ng t w handl es t o one obj ect l o 349

publ i c cl as s A i as 1 { l i nt i ; A i as 1( i nt i i ) { i = i i ; } l publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai A i as 1 x = new A i as 1( 7) ; l l A i as 1 y = x; // A s i gn t he handl e l s Sys t em out . pr i nt l n( " x: " + x. i ) ; . Sys t em out . pr i nt l n( " y: " + y. i ) ; . Sys t em out . pr i nt l n( " I ncr em i ng x" ) ; . ent x. i ++; Sys t em out . pr i nt l n( " x: " + x. i ) ; . Sys t em out . pr i nt l n( " y: " + y. i ) ; . } } ///: ~ 对下面这行: A i as 1 y = x; // A s i gn t he handl e l s 它会新建一个 A i as 1 句柄,但不是把它分配给由 new创建的一个新鲜对象,而是分配给一个现有的句柄。所 l 以句柄 x 的内容——即对象 x 指向的地址——被分配给 y,所以无论 x 还是 y 都与相同的对象连接起来。这 样一来,一旦 x 的 i 在下述语句中增值: x. i ++; y 的 i 值也必然受到影响。从最终的输出就可以看出: x: 7 y: 7 I ncr em i ng x ent x: 8 y: 8 此时最直接的一个解决办法就是干脆不这样做:不要有意将多个句柄指向同一个作用域内的同一个对象。这 样做可使代码更易理解和调试。然而,一旦准备将句柄作为一个自变量或参数传递——这是 Java 设想的正常 方法——别名问题就会自动出现,因为创建的本地句柄可能修改“外部对象”(在方法作用域之外创建的对 象)。下面是一个例子: //: A i as 2. j ava l // M hod cal l s i m i ci t l y al i as t hei r et pl // ar gum s . ent publ i c cl as s A i as 2 { l i nt i ; A i as 2( i nt i i ) { i = i i ; } l s t at i c voi d f ( A i as 2 handl e) { l handl e. i ++; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai A i as 2 x = new A i as 2( 7) ; l l Sys t em out . pr i nt l n( " x: " + x. i ) ; . Sys t em out . pr i nt l n( " C l i ng f ( x) " ) ; . al f ( x) ; Sys t em out . pr i nt l n( " x: " + x. i ) ; . } } ///: ~

350

输出如下: x: 7 C l i ng f ( x) al x: 8 方法改变了自己的参数——外部对象。一旦遇到这种情况,必须判断它是否合理,用户是否愿意这样,以及 是不是会造成问题。 通常,我们调用一个方法是为了产生返回值,或者用它改变为其调用方法的那个对象的状态(方法其实就是 我们向那个对象“发一条消息”的方式)。很少需要调用一个方法来处理它的参数;这叫作利用方法的“副 作用”(Si de Ef f ect )。所以倘若创建一个会修改自己参数的方法,必须向用户明确地指出这一情况,并警 告使用那个方法可能会有的后果以及它的潜在威胁。由于存在这些混淆和缺陷,所以应该尽量避免改变参 数。 若需在一个方法调用期间修改一个参数,且不打算修改外部参数,就应在自己的方法内部制作一个副本,从 而保护那个参数。本章的大多数内容都是围绕这个问题展开的。

12. 2 制作本地副本
稍微总结一下:Java 中的所有自变量或参数传递都是通过传递句柄进行的。也就是说,当我们传递“一个对 象”时,实际传递的只是指向位于方法外部的那个对象的“一个句柄”。所以一旦要对那个句柄进行任何修 改,便相当于修改外部对象。此外: ■参数传递过程中会自动产生别名问题 ■不存在本地对象,只有本地句柄 ■句柄有自己的作用域,而对象没有 ■对象的“存在时间”在 Java 里不是个问题 ■没有语言上的支持(如常量)可防止对象被修改(以避免别名的副作用) 若只是从对象中读取信息,而不修改它,传递句柄便是自变量传递中最有效的一种形式。这种做非常恰当; 默认的方法一般也是最有效的方法。然而,有时仍需将对象当作“本地的”对待,使我们作出的改变只影响 一个本地副本,不会对外面的对象造成影响。许多程序设计语言都支持在方法内自动生成外部对象的一个本 地副本(注释①)。尽管 Java 不具备这种能力,但允许我们达到同样的效果。 ①:在 C语言中,通常控制的是少量数据位,默认操作是按值传递。C ++也必须遵照这一形式,但按值传递对 象并非肯定是一种有效的方式。此外,在 C ++中用于支持按值传递的代码也较难编写,是件让人头痛的事 情。

1 2 . 2 . 1 按值传递
首先要解决术语的问题,最适合“按值传递”的看起来是自变量。“按值传递”以及它的含义取决于如何理 解程序的运行方式。最常见的意思是获得要传递的任何东西的一个本地副本,但这里真正的问题是如何看待 自己准备传递的东西。对于“按值传递”的含义,目前存在两种存在明显区别的见解: ( 1) Java 按值传递任何东西。若将基本数据类型传递进入一个方法,会明确得到基本数据类型的一个副本。 但若将一个句柄传递进入方法,得到的是句柄的副本。所以人们认为“一切”都按值传递。当然,这种说法 也有一个前提:句柄肯定也会被传递。但 Java 的设计方案似乎有些超前,允许我们忽略(大多数时候)自己 处理的是一个句柄。也就是说,它允许我们将句柄假想成“对象”,因为在发出方法调用时,系统会自动照 管两者间的差异。 ( 2) Java 主要按值传递(无自变量),但对象却是按引用传递的。得到这个结论的前提是句柄只是对象的一 个“别名”,所以不考虑传递句柄的问题,而是直接指出“我准备传递对象”。由于将其传递进入一个方法 时没有获得对象的一个本地副本,所以对象显然不是按值传递的。Sun公司似乎在某种程度上支持这一见 解,因为它“保留但未实现”的关键字之一便是 byval ue(按值)。但没人知道那个关键字什么时候可以发 挥作用。 尽管存在两种不同的见解,但其间的分歧归根到底是由于对“句柄”的不同解释造成的。我打算在本书剩下 的部分里回避这个问题。大家不久就会知道,这个问题争论下去其实是没有意义的——最重要的是理解一个 句柄的传递会使调用者的对象发生意外的改变。

351

1 2 . 2 . 2 克隆对象
若需修改一个对象,同时不想改变调用者的对象,就要制作该对象的一个本地副本。这也是本地副本最常见 的一种用途。若决定制作一个本地副本,只需简单地使用 cl one( )方法即可。C one 是“克隆”的意思,即制 l 作完全一模一样的副本。这个方法在基础类 O ect 中定义成“pr ot ect ed”(受保护)模式。但在希望克隆 bj 的任何衍生类中,必须将其覆盖为“publ i c”模式。例如,标准库类 Vect or 覆盖了 cl one( ),所以能为 Vect or 调用 cl one( ) ,如下所示: //: C oni ng. j ava l // T he cl one( ) oper at i on w ks f or onl y a f ew or // i t em i n t he s t andar d Java l i br ar y. s i m t j ava. ut i l . * ; por cl as s I nt { pr i vat e i nt i ; publ i c I nt ( i nt i i ) { i = i i ; } publ i c voi d i ncr em ( ) { i ++; } ent publ i c St r i ng t oSt r i ng( ) { r et ur n I nt eger . t oSt r i ng( i ) ; } } publ i c cl as s C oni ng { l publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Vect or v = new Vect or ( ) ; f or ( i nt i = 0; i < 10; i ++ ) v. addEl em ( new I nt ( i ) ) ; ent Sys t em out . pr i nt l n( " v: " + v) ; . Vect or v2 = ( Vect or ) v. cl one( ) ; // I ncr em ent al l v2' s el em s : ent f or ( Enum at i on e = v2. el em s ( ) ; er ent e. has M eEl em s ( ) ; ) or ent ( ( I nt ) e. next El em ( ) ) . i ncr em ( ) ; ent ent // See i f i t changed v' s el em s : ent Sys t em out . pr i nt l n( " v: " + v) ; . } } ///: ~ cl one( )方法产生了一个 O ect ,后者必须立即重新造型为正确类型。这个例子指出 Vect or 的 cl one( )方法 bj 不能自动尝试克隆 Vect or 内包含的每个对象——由于别名问题,老的 Vect or 和克隆的 Vect or 都包含了相同 的对象。我们通常把这种情况叫作“简单复制”或者“浅层复制”,因为它只复制了一个对象的“表面”部 分。实际对象除包含这个“表面”以外,还包括句柄指向的所有对象,以及那些对象又指向的其他所有对 象,由此类推。这便是“对象网”或“对象关系网”的由来。若能复制下所有这张网,便叫作“全面复制” 或者“深层复制”。 在输出中可看到浅层复制的结果,注意对 v2 采取的行动也会影响到 v: v: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] v: [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 一般来说,由于不敢保证 Vect or 里包含的对象是“可以克隆”(注释②)的,所以最好不要试图克隆那些对 象。 ②:“可以克隆”用英语讲是 cl oneabl e,请留意 Java 库中专门保留了这样的一个关键字。 352

1 2 . 2 . 3 使类具有克隆能力
尽管克隆方法是在所有类最基本的 O ect 中定义的,但克隆仍然不会在每个类里自动进行。这似乎有些不可 bj 思议,因为基础类方法在衍生类里是肯定能用的。但 Java 确实有点儿反其道而行之;如果想在一个类里使用 克隆方法,唯一的办法就是专门添加一些代码,以便保证克隆的正常进行。 1. 使用 pr ot ect ed时的技巧 为避免我们创建的每个类都默认具有克隆能力,cl one( ) 方法在基础类 O ect 里得到了“保留”(设为 bj pr ot ect ed)。这样造成的后果就是:对那些简单地使用一下这个类的客户程序员来说,他们不会默认地拥有 这个方法;其次,我们不能利用指向基础类的一个句柄来调用 cl one( )(尽管那样做在某些情况下特别有 用,比如用多形性的方式克隆一系列对象)。在编译期的时候,这实际是通知我们对象不可克隆的一种方 式——而且最奇怪的是,Java 库中的大多数类都不能克隆。因此,假如我们执行下述代码: I nt eger x = new I nt eger ( l ) ; x = x. cl one( ) ; 那么在编译期,就有一条讨厌的错误消息弹出,告诉我们不可访问 cl one( )——因为 I nt eger 并没有覆盖 它,而且它对 pr ot ect ed版本来说是默认的)。 但是,假若我们是在一个从 O ect 衍生出来的类中(所有类都是从 O ect 衍生的),就有权调用 bj bj O ect . cl one( ) ,因为它是“pr ot ect ed”,而且我们在一个继承器中。基础类 cl one( ) 提供了一个有用的功 bj 能——它进行的是对衍生类对象的真正“按位”复制,所以相当于标准的克隆行动。然而,我们随后需要将 自己的克隆操作设为 publ i c,否则无法访问。总之,克隆时要注意的两个关键问题是:几乎肯定要调用 s uper . cl one( ) ,以及注意将克隆设为 publ i c。 有时还想在更深层的衍生类中覆盖 cl one( ),否则就直接使用我们的 cl one( ) (现在已成为 publ i c),而那 并不一定是我们所希望的(然而,由于 O ect . cl one( ) 已制作了实际对象的一个副本,所以也有可能允许这 bj 种情况)。pr ot ect ed的技巧在这里只能用一次:首次从一个不具备克隆能力的类继承,而且想使一个类变 成“能够克隆”。而在从我们的类继承的任何场合,cl one( ) 方法都是可以使用的,因为 Java 不可能在衍生 之后反而缩小方法的访问范围。换言之,一旦对象变得可以克隆,从它衍生的任何东西都是能够克隆的,除 非使用特殊的机制(后面讨论)令其“关闭”克隆能力。 2. 实现 C oneabl e 接口 l 为使一个对象的克隆能力功成圆满,还需要做另一件事情:实现 C oneabl e 接口。这个接口使人稍觉奇怪, l 因为它是空的! i nt er f ace C oneabl e { } l 之所以要实现这个空接口,显然不是因为我们准备上溯造型成一个 C oneabl e,以及调用它的某个方法。有 l 些人认为在这里使用接口属于一种“欺骗”行为,因为它使用的特性打的是别的主意,而非原来的意思。 C oneabl e i nt er f ace 的实现扮演了一个标记的角色,封装到类的类型中。 l 两方面的原因促成了 C oneabl e i nt er f ace 的存在。首先,可能有一个上溯造型句柄指向一个基础类型,而 l 且不知道它是否真的能克隆那个对象。在这种情况下,可用 i ns t anceof 关键字(第 11 章有介绍)调查句柄 是否确实同一个能克隆的对象连接: i f (m yHandl e i ns t anceof C oneabl e) // . . . l 第二个原因是考虑到我们可能不愿所有对象类型都能克隆。所以 O ect . cl one( ) 会验证一个类是否真的是实 bj 现了 C oneabl e 接口。若答案是否定的,则“掷”出一个 C oneN Suppor t edExcept i on违例。所以在一般情 l l ot 况下,我们必须将“i m em pl ent C oneabl e”作为对克隆能力提供支持的一部分。 l

12 . 2 . 4 成功的克隆
理解了实现 cl one( )方法背后的所有细节后,便可创建出能方便复制的类,以便提供了一个本地副本: //: Local C opy. j ava // C eat i ng l ocal copi es w t h cl one( ) r i i m t j ava. ut i l . * ; por cl as s M bj ect i m em s C oneabl e { yO pl ent l i nt i ; 353

M bj ect ( i nt i i ) { i = i i ; } yO publ i c O ect cl one( ) { bj O ect o = nul l ; bj try { o = s uper . cl one( ) ; } cat ch ( C oneN Suppor t edExcept i on e) { l ot Sys t em out . pr i nt l n( " M bj ect can' t cl one" ) ; . yO } r et ur n o; } publ i c St r i ng t oSt r i ng( ) { r et ur n I nt eger . t oSt r i ng( i ) ; } } publ i c cl as s Local C opy { s t at i c M bj ect g( M bj ect v) { yO yO // Pas s i ng a handl e, m f i es out s i de obj ect : odi v. i ++; r et ur n v; } s t at i c M bj ect f ( M bj ect v) { yO yO v = ( M bj ect ) v. cl one( ) ; // Local copy yO v. i ++; r et ur n v; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai M bj ect a = new M bj ect ( 11) ; yO yO M bj ect b = g( a) ; yO // Tes t i ng handl e equi val ence, // not obj ect equi val ence: i f ( a == b) Sys t em out . pr i nt l n( " a == b" ) ; . el s e Sys t em out . pr i nt l n( " a ! = b" ) ; . Sys t em out . pr i nt l n( " a = " + a) ; . Sys t em out . pr i nt l n( " b = " + b) ; . M bj ect c = new M bj ect ( 47) ; yO yO M bj ect d = f ( c) ; yO i f ( c == d) Sys t em out . pr i nt l n( " c == d" ) ; . el s e Sys t em out . pr i nt l n( " c ! = d" ) ; . Sys t em out . pr i nt l n( " c = " + c) ; . Sys t em out . pr i nt l n( " d = " + d) ; . } } ///: ~ 不管怎样,cl one( )必须能够访问,所以必须将其设为 publ i c(公共的)。其次,作为 cl one( ) 的初期行动, 应调用 cl one( ) 的基础类版本。这里调用的 cl one( )是 O ect 内部预先定义好的。之所以能调用它,是由于 bj 它具有 pr ot ect ed(受到保护的)属性,所以能在衍生的类里访问。 O ect . cl one( ) 会检查原先的对象有多大,再为新对象腾出足够多的内存,将所有二进制位从原来的对象复 bj 354

制到新对象。这叫作“按位复制”,而且按一般的想法,这个工作应该是由 cl one( ) 方法来做的。但在 O ect . cl one( ) 正式开始操作前,首先会检查一个类是否 C oneabl e,即是否具有克隆能力——换言之,它 bj l 是否实现了 C oneabl e 接口。若未实现,O ect . cl one( ) 就掷出一个 C oneN Suppor t edExcept i on 违例,指 l bj l ot 出我们不能克隆它。因此,我们最好用一个 t r y- cat ch 块将对 s uper . cl one( ) 的调用代码包围(或封装)起 来,试图捕获一个应当永不出现的违例(因为这里确实已实现了 C oneabl e 接口)。 l 在 Local C opy 中,两个方法 g( )和 f ( )揭示出两种参数传递方法间的差异。其中,g( )演示的是按引用传递, 它会修改外部对象,并返回对那个外部对象的一个引用。而 f ( )是对自变量进行克隆,所以将其分离出来, 并让原来的对象保持独立。随后,它继续做它希望的事情。甚至能返回指向这个新对象的一个句柄,而且不 会对原来的对象产生任何副作用。注意下面这个多少有些古怪的语句: v = ( M bj ect ) v. cl one( ) ; yO 它的作用正是创建一个本地副本。为避免被这样的一个语句搞混淆,记住这种相当奇怪的编码形式在 Java 中 是完全允许的,因为有一个名字的所有东西实际都是一个句柄。所以句柄 v 用于克隆一个它所指向的副本, 而且最终返回指向基础类型 O ect 的一个句柄(因为它在 O ect . cl one( ) 中是那样被定义的),随后必须 bj bj 将其造型为正确的类型。 在 m n( ) 中,两种不同参数传递方式的区别在于它们分别测试了一个不同的方法。输出结果如下: ai a a b c c d == b = 12 = 12 != d = 47 = 48

大家要记住这样一个事实:Java 对“是否等价”的测试并不对所比较对象的内部进行检查,从而核实它们的 值是否相同。==和! =运算符只是简单地对比句柄的内容。若句柄内的地址相同,就认为句柄指向同样的对 象,所以认为它们是“等价”的。所以运算符真正检测的是“由于别名问题,句柄是否指向同一个对象?”

1 2 . 2 . 5 Obj ect . cl one( ) 的效果
调用 O ect . cl one( ) 时,实际发生的是什么事情呢?当我们在自己的类里覆盖 cl one( )时,什么东西对于 bj s uper . cl one( ) 来说是最关键的呢?根类中的 cl one( )方法负责建立正确的存储容量,并通过“按位复制”将 二进制位从原始对象中复制到新对象的存储空间。也就是说,它并不只是预留存储空间以及复制一个对象— —实际需要调查出欲复制之对象的准确大小,然后复制那个对象。由于所有这些工作都是在由根类定义之 cl one( )方法的内部代码中进行的(根类并不知道要从自己这里继承出去什么),所以大家或许已经猜到,这 个过程需要用 RTTI 判断欲克隆的对象的实际大小。采取这种方式,cl one( )方法便可建立起正确数量的存储 空间,并对那个类型进行正确的按位复制。 不管我们要做什么,克隆过程的第一个部分通常都应该是调用 s uper . cl one( ) 。通过进行一次准确的复制, 这样做可为后续的克隆进程建立起一个良好的基础。随后,可采取另一些必要的操作,以完成最终的克隆。 为确切了解其他操作是什么,首先要正确理解 O ect . cl one( )为我们带来了什么。特别地,它会自动克隆所 bj 有句柄指向的目标吗?下面这个例子可完成这种形式的检测: //: Snake. j ava // Tes t s cl oni ng t o s ee i f des t i nat i on of // handl es ar e al s o cl oned. publ i c cl as s Snake i m em s C oneabl e { pl ent l pr i vat e Snake next ; pr i vat e char c; // Val ue of i == num ber of s egm s ent Snake( i nt i , char x) { c = x; i f (- - i > 0) next = new Snake( i , ( char ) ( x + 1) ) ; 355

} voi d i ncr em ( ) { ent c++; i f ( next ! = nul l ) next . i ncr em ( ) ; ent } publ i c St r i ng t oSt r i ng( ) { St r i ng s = " : " + c; i f ( next ! = nul l ) s += next . t oSt r i ng( ) ; r et ur n s ; } publ i c O ect cl one( ) { bj O ect o = nul l ; bj try { o = s uper . cl one( ) ; } cat ch ( C oneN Suppor t edExcept i on e) { } l ot r et ur n o; } p i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ubl ai Snake s = new Snake( 5, ' a' ) ; Sys t em out . pr i nt l n( " s = " + s ) ; . Snake s 2 = ( Snake) s . cl one( ) ; Sys t em out . pr i nt l n( " s 2 = " + s 2) ; . s . i ncr em ( ) ; ent Sys t em out . pr i nt l n( . " af t er s . i ncr em , s 2 = " + s 2) ; ent } } ///: ~ 一条 Snake(蛇)由数段构成,每一段的类型都是 Snake。所以,这是一个一段段链接起来的列表。所有段都 是以循环方式创建的,每做好一段,都会使第一个构建器参数的值递减,直至最终为零。而为给每段赋予一 个独一无二的标记,第二个参数(一个 C )的值在每次循环构建器调用时都会递增。 har i ncr em ( )方法的作用是循环递增每个标记,使我们能看到发生的变化;而 t oSt r i ng 则循环打印出每个标 ent 记。输出如下: s = : a: b: c: d: e s 2 = : a: b: c: d: e af t er s . i ncr em , s 2 = : a: c: d: e: f ent 这意味着只有第一段才是由 O ect . cl one( ) 复制的,所以此时进行的是一种“浅层复制”。若希望复制整条 bj 蛇——即进行“深层复制”——必须在被覆盖的 cl one( ) 里采取附加的操作。 通常可在从一个能克隆的类里调用 s uper . cl one( ) ,以确保所有基础类行动(包括 O ect . cl one( ) )能够进 bj 行。随着是为对象内每个句柄都明确调用一个 cl one( );否则那些句柄会别名变成原始对象的句柄。构建器 的调用也大致相同——首先构造基础类,然后是下一个衍生的构建器⋯⋯以此类推,直到位于最深层的衍生 构建器。区别在于 cl one( ) 并不是个构建器,所以没有办法实现自动克隆。为了克隆,必须由自己明确进 行。

1 2 . 2 . 6 克隆合成对象
试图深层复制合成对象时会遇到一个问题。必须假定成员对象中的 cl one( )方法也能依次对自己的句柄进行 深层复制,以此类推。这使我们的操作变得复杂。为了能正常实现深层复制,必须对所有类中的代码进行控 制,或者至少全面掌握深层复制中需要涉及的类,确保它们自己的深层复制能正确进行。 356

下面这个例子总结了面对一个合成对象进行深层复制时需要做哪些事情: //: D eepC opy. j ava // C oni ng a com ed obj ect l pos cl as s D hReadi ng i m em s C oneabl e { ept pl ent l pr i vat e doubl e dept h; publ i c D hReadi ng( doubl e dept h) { ept t hi s . dept h = dept h; } publ i c O ect cl one( ) { bj O ect o = nul l ; bj try { o = s uper . cl one( ) ; } cat ch ( C oneN Suppor t edExcept i on e) { l ot e. pr i nt St ackTr ace( ) ; } r et ur n o; } } cl as s Tem at ur eReadi ng i m em s C oneabl e { per pl ent l pr i vat e l ong t i m e; pr i vat e doubl e t em at ur e; per publ i c Tem at ur eReadi ng( doubl e t em at ur e) { per per t i m = Sys t em cur r ent Ti m i l l i s ( ) ; e . eM t hi s . t em at ur e = t em at ur e; per per } publ i c O ect cl one( ) { bj O ect o = nul l ; bj try { o = s uper . cl one( ) ; } cat ch ( C oneN Suppor t edExcept i on e) { l ot e. pr i nt St ackTr ace( ) ; } r et ur n o; } } cl as s O ceanReadi ng i m em s C oneabl e { pl ent l pr i vat e D hReadi ng dept h; ept pr i vat e Tem at ur eReadi ng t em at ur e; per per publ i c O ceanReadi ng( doubl e t dat a, doubl e ddat a) { t em at ur e = new Tem at ur eReadi ng( t dat a) ; per per dept h = new D hReadi ng( ddat a) ; ept } publ i c O ect cl one( ) { bj O ceanReadi ng o = nul l ; try { o = (O ceanReadi ng) s uper . cl one( ) ; } cat ch ( C oneN Suppor t edExcept i on e) { l ot e. pr i nt St ackTr ace( ) ; 357

} // M t cl one handl es : us o. dept h = ( D hReadi ng) o. dept h. cl one( ) ; ept o. t em at ur e = per ( Tem at ur eReadi ng) o. t em at ur e. cl one( ) ; per per r et ur n o; // Upcas t s back t o O ect bj } } publ i c cl as s D eepC opy { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai O ceanReadi ng r eadi ng = new O ceanReadi ng( 33. 9, 100. 5) ; // N cl one i t : ow O ceanReadi ng r = (O ceanReadi ng) r eadi ng. cl one( ) ; } } ///: ~ D hReadi ng和 Tem at ur eReadi ng 非常相似;它们都只包含了基本数据类型。所以 cl one( ) 方法能够非常 ept per 简单:调用 s uper . cl one( ) 并返回结果即可。注意两个类使用的 cl one( ) 代码是完全一致的。 O ceanReadi ng是由 D hReadi ng 和 Tem at ur eReadi ng 对象合并而成的。为了对其进行深层复制,cl one( ) ept per 必须同时克隆 O ceanReadi ng内的句柄。为达到这个目标,s uper . cl one( ) 的结果必须造型成一个 O ceanReadi ng对象(以便访问 dept h 和 t em at ur e 句柄)。 per

1 2 . 2 . 7 用 Vect or 进行深层复制
下面让我们复习一下本章早些时候提出的 Vect or 例子。这一次 I nt 2 类是可以克隆的,所以能对 Vect or 进行 深层复制: //: A ngC one. j ava ddi l // You m t go t hr ough a f ew gyr at i ons t o us / / add cl oni ng t o your ow cl as s . n i m t j ava. ut i l . * ; por cl as s I nt 2 i m em s C oneabl e { pl ent l pr i vat e i nt i ; publ i c I nt 2( i nt i i ) { i = i i ; } publ i c voi d i ncr em ( ) { i ++; } ent publ i c St r i ng t oSt r i ng( ) { r et ur n I nt eger . t oSt r i ng( i ) ; } publ i c O ect cl one( ) { bj O ect o = nul l ; bj try { o = s uper . cl one( ) ; } cat ch ( C oneN Suppor t edExcept i on e) { l ot Sys t em out . pr i nt l n( " I nt 2 can' t cl one" ) ; . } r et ur n o; } }

358

// O nce i t ' s cl oneabl e, i nher i t ance // does n' t r em ove cl oneabi l i t y: cl as s I nt 3 ex t ends I nt 2 { pr i vat e i nt j ; // A om i cal l y dupl i cat ed ut at publ i c I nt 3( i nt i ) { s uper ( i ) ; } } publ i c cl as s A ngC one { ddi l publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai I nt 2 x = new I nt 2( 10) ; I nt 2 x2 = ( I nt 2) x. cl one( ) ; x2. i ncr em ( ) ; ent Sys t em o . pr i nt l n( . ut " x = " + x + " , x2 = " + x2) ; // A hi ng i nher i t ed i s al s o cl oneabl e: nyt I nt 3 x3 = new I nt 3( 7) ; x3 = ( I nt 3) x3. cl one( ) ; Vect or v = new Vect or ( ) ; f or ( i nt i = 0; i < 10; i ++ ) v. addEl em ( new I nt 2( i ) ) ; ent Sys t em out . pr i nt l n( " v: " + v) ; . Vect or v2 = ( Vect or ) v. cl one( ) ; // N cl one each el em : ow ent f or ( i nt i = 0; i < v. s i ze( ) ; i ++) v2. s et El em A ( ent t ( ( I nt 2) v2. el em A ( i ) ) . cl one( ) , i ) ; ent t // I ncr em ent al l v2' s el em s : ent f or ( Enum at i on e = v2. el em s ( ) ; er ent e. has M eEl em s ( ) ; ) or ent ( ( I nt 2) e. next El em ( ) ) . i ncr em ( ) ; ent ent // See i f i t changed v' s el em s : ent Sys t em out . pr i nt l n( " v: " + v) ; . Sys t em out . pr i nt l n( " v2: " + v2) ; . } } ///: ~ I nt 3 自 I nt 2 继承而来,并添加了一个新的基本类型成员 i nt j 。大家也许认为自己需要再次覆盖 cl one( ) , 以确保 j 得到复制,但实情并非如此。将 I nt 2 的 cl one( )当作 I nt 3 的 cl one( )调用时,它会调用 O ect . cl one( ) ,判断出当前操作的是 I nt 3,并复制 I nt 3 内的所有二进制位。只要没有新增需要克隆的句 bj 柄,对 O ect . cl one( ) 的一个调用就能完成所有必要的复制——无论 cl one( ) 是在层次结构多深的一级定义 bj 的。 至此,大家可以总结出对 Vect or 进行深层复制的先决条件:在克隆了 Vect or 后,必须在其中遍历,并克隆 由 Vect or 指向的每个对象。为了对 Has ht abl e(散列表)进行深层复制,也必须采取类似的处理。 这个例子剩余的部分显示出克隆已实际进行——证据就是在克隆了对象以后,可以自由改变它,而原来那个 对象不受任何影响。

1 2 . 2 . 8 通过序列化进行深层复制
若研究一下第 10 章介绍的那个 Java 1. 1 对象序列化示例,可能发现若在一个对象序列化以后再撤消对它的 序列化,或者说进行装配,那么实际经历的正是一个“克隆”的过程。 那么为什么不用序列化进行深层复制呢?下面这个例子通过计算执行时间对比了这两种方法:

359

//: C pet e. j ava om i m t j ava. i o. * ; por cl as s Thi ng1 i m em s Ser i al i zabl e { } pl ent cl as s Thi ng2 i m em s Ser i al i zabl e { pl ent Thi ng1 o1 = new Thi ng1( ) ; } cl as s Thi ng3 i m em s C oneabl e { pl ent l publ i c O ect cl one( ) { bj O ect o = nul l ; bj try { o = s uper . cl one( ) ; } cat ch ( C oneN Suppor t edExcept i on e) { l ot Sys t em out . pr i nt l n( " Thi ng3 can' t cl one" ) ; . } r et ur n o; } } cl as s Thi ng4 i m em s C oneabl e { pl ent l Thi ng3 o3 = new Thi ng3( ) ; publ i c O ect cl one( ) { bj Thi ng4 o = nul l ; try { o = ( Thi ng4) s uper . cl one( ) ; } cat ch ( C oneN Suppor t edExcept i on e) { l ot Sys t em out . pr i nt l n( " Thi ng4 can' t cl one" ) ; . } // C one t he f i el d, t oo: l o. o3 = ( Thi ng3) o3. cl one( ) ; r et ur n o; } } publ i c cl as s C pet e { om s t at i c f i nal i nt SI ZE = 5000; publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai T hi ng2[ ] a = new T hi ng2[ SI ZE] ; f or ( i nt i = 0; i < a. l engt h; i ++) a[ i ] = new Thi ng2( ) ; T hi ng4[ ] b = new T hi ng4[ SI ZE] ; f or ( i nt i = 0; i < b. l engt h; i ++) b[ i ] = ne Thi ng4( ) ; w try { l ong t 1 = Sys t em cur r ent Ti m i l l i s ( ) ; . eM Byt eA r ayO put St r eam buf = r ut new Byt eA r ayO put St r eam ) ; r ut ( O ect O put St r eam o = bj ut new O ect O put St r eam buf ) ; bj ut ( f or ( i nt i = 0; i < a. l engt h; i ++) 360

o. w i t eO ect ( a[ i ] ) ; r bj // N get copi es : ow O ect I nput St r eam i n = bj new O ect I nput St r eam bj ( new Byt eA r ayI nput St r eam r ( buf . t oByt eA r ay( ) ) ) ; r Thi ng2[ ] c = new Thi ng2[ SI ZE] ; f or ( i nt i = 0; i < c. l engt h; i ++) c[ i ] = ( Thi ng2) i n. r eadO ect ( ) ; bj l ong t 2 = Sys t em cur r ent Ti m i l l i s ( ) ; . eM Sys t em out . pr i nt l n( . " D i cat i on vi a s er i al i zat i on: " + upl ( t 2 - t 1) + " M l l i s econds " ) ; i // N t r y cl oni ng: ow t 1 = Sys t em cur r ent Ti m i l l i s ( ) ; . eM Thi ng4[ ] d = new Thi ng4[ SI ZE] ; f or ( i nt i = 0; i < d. l engt h; i ++) d[ i ] = ( Thi ng4) b[ i ] . cl one( ) ; t 2 = Sys t em cur r ent Ti m i l l i s ( ) ; . eM Sys t em out . pr i nt l n( . " D i cat i on vi a cl oni ng: " + upl ( t 2 - t 1) + " M l l i s econds " ) ; i } cat ch( Except i on e) { e. pr i nt St ackTr ace( ) ; } } } ///: ~ 其中,Thi ng2 和 Thi ng4 包含了成员对象,所以需要进行一些深层复制。一个有趣的地方是尽管 Ser i al i z abl e类很容易设置,但在复制它们时却要做多得多的工作。克隆涉及到大量的类设置工作,但实际 的对象复制是相当简单的。结果很好地说明了一切。下面是几次运行分别得到的结果: 的确 D i cat i on vi a s er i al i zat i on: 3400 M l l i s econds upl i D i cat i on vi a cl oni ng: 110 M l l i s econds upl i D i cat i on vi a s er i a i zat i on: 3410 M l l i s econds upl l i D i cat i on vi a cl oni ng: 110 M l l i s econds upl i D i cat i on vi a s er i al i zat i on: 3520 M l l i s econds upl i D i cat i on vi a cl oni ng: 110 M l l i s econds upl i 除了序列化和克隆之间巨大的时间差异以外,我们也注意到序列化技术的运行结果并不稳定,而克隆每一次 花费的时间都是相同的。

1 2 . 2 . 9 使克隆具有更大的深度
若新建一个类,它的基础类会默认为 O ect ,并默认为不具备克隆能力(就象在下一节会看到的那样)。只 bj 要不明确地添加克隆能力,这种能力便不会自动产生。但我们可以在任何层添加它,然后便可从那个层开始 向下具有克隆能力。如下所示: //: Hor r or Fl i ck. j ava // You can i ns er t C oneabi l i t y at any l // l evel of i nher i t ance. i m t j ava. ut i l . * ; por

361

cl as s Per s on { } cl as s Her o ext ends Per s on { } cl as s Sci ent i s t ext ends Per s on i m em s C oneabl e { pl ent l publ i c O ect cl one( ) { bj try { r et ur n s uper . cl one( ) ; } cat ch ( C oneN Suppor t edExcept i on e) { l ot // t hi s s houl d never happen: // I t ' s C oneabl e al r eady! l t hr ow new I nt er nal Er r or () ; } } } cl as s M adSci ent i s t ext ends Sci ent i s t { } publ i c cl as s Hor r or Fl i ck { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Per s on p = new Per s on( ) ; Her o h = new Her o( ) ; Sci ent i s t s = new Sci ent i s t ( ) ; M adSci ent i s t m = new M adSci ent i s t ( ) ; // p = ( Per s on) p. cl one( ) ; // C pi l e er r or om // h = ( Her o) h. cl one( ) ; // C pi l e er r or om s = ( Sci ent i s t ) s . cl one( ) ; m= ( M adSci ent i s t ) m cl one( ) ; . } } ///: ~ 添加克隆能力之前,编译器会阻止我们的克隆尝试。一旦在 Sci ent i s t 里添加了克隆能力,那么 Sci ent i s t 以及它的所有“后裔”都可以克隆。

1 2 . 2 . 1 0 为什么有这个奇怪的设计
之所以感觉这个方案的奇特,因为它事实上的确如此。也许大家会奇怪它为什么要象这样运行,而该方案背 后的真正含义是什么呢?后面讲述的是一个未获证实的故事——大概是由于围绕 Java 的许多买卖使其成为一 种设计优良的语言——但确实要花许多口舌才能讲清楚这背后发生的所有事情。 最初,Java 只是作为一种用于控制硬件的语言而设计,与因特网并没有丝毫联系。象这样一类面向大众的语 言一样,其意义在于程序员可以对任意一个对象进行克隆。这样一来,cl one( ) 就放置在根类 O ect 里面, bj 但因为它是一种公用方式,因而我们通常能够对任意一个对象进行克隆。看来这是最灵活的方式了,毕竟它 不会带来任何害处。 正当 Java 看起来象一种终级因特网程序设计语言的时候,情况却发生了变化。突然地,人们提出了安全问 题,而且理所当然,这些问题与使用对象有关,我们不愿望任何人克隆自己的保密对象。所以我们最后看到 的是为原来那个简单、直观的方案添加的大量补丁:cl one( ) 在 O ect 里被设置成“pr ot ect ed”。必须将其 bj 覆盖,并使用“i m em pl ent C oneabl e”,同时解决违例的问题。 l 只有在准备调用 O ect 的 cl one( )方法时,才没有必要使用 C oneabl e 接口,因为那个方法会在运行期间得 bj l 到检查,以确保我们的类实现了 C oneabl e。但为了保持连贯性(而且由于 C oneabl e 无论如何都是空 l l 的),最好还是由自己实现 C oneabl e。 l

362

12. 3 克隆的控制
为消除克隆能力,大家也许认为只需将 cl one( )方法简单地设为 pr i vat e(私有)即可,但这样是行不通的, 因为不能采用一个基础类方法,并使其在衍生类中更“私有”。所以事情并没有这么简单。此外,我们有必 要控制一个对象是否能够克隆。对于我们设计的一个类,实际有许多种方案都是可以采取的: ( 1) 保持中立,不为克隆做任何事情。也就是说,尽管不可对我们的类克隆,但从它继承的一个类却可根据 实际情况决定克隆。只有 O ect . cl one( ) 要对类中的字段进行某些合理的操作时,才可以作这方面的决定。 bj ( 2) 支持 cl one( ),采用实现 C oneabl e(可克隆)能力的标准操作,并覆盖 cl one( )。在被覆盖的 cl one( ) l 中,可调用 s uper . cl one( ) ,并捕获所有违例(这样可使 cl one( ) 不“掷”出任何违例)。 ( 3) 有条件地支持克隆。若类容纳了其他对象的句柄,而那些对象也许能够克隆(集合类便是这样的一个例 子),就可试着克隆拥有对方句柄的所有对象;如果它们“掷”出了违例,只需让这些违例通过即可。举个 例子来说,假设有一个特殊的 Vect or ,它试图克隆自己容纳的所有对象。编写这样的一个 Vect or 时,并不 知道客户程序员会把什么形式的对象置入这个 Vect or 中,所以并不知道它们是否真的能够克隆。 ( 4) 不实现 C oneabl e( ),但是将 cl one( ) 覆盖成 pr ot ect ed,使任何字段都具有正确的复制行为。这样一 l 来,从这个类继承的所有东西都能覆盖 cl one( ),并调用 s uper . cl one( ) 来产生正确的复制行为。注意在我们 实现方案里,可以而且应该调用 s uper . cl one( ) ——即使那个方法本来预期的是一个 C oneabl e对象(否则 l 会掷出一个违例),因为没有人会在我们这种类型的对象上直接调用它。它只有通过一个衍生类调用;对那 个衍生类来说,如果要保证它正常工作,需实现 C oneabl e。 l ( 5) 不实现 C oneabl e 来试着防止克隆,并覆盖 cl one( ),以产生一个违例。为使这一设想顺利实现,只有 l 令从它衍生出来的任何类都调用重新定义后的 cl one( )里的 s uepr . cl one( )。 ( 6) 将类设为 f i nal ,从而防止克隆。若 cl one( ) 尚未被我们的任何一个上级类覆盖,这一设想便不会成功。 若已被覆盖,那么再一次覆盖它,并“掷”出一个 C oneN Suppor t edExcept i on(克隆不支持)违例。为担 l ot 保克隆被禁止,将类设为 f i nal 是唯一的办法。除此以外,一旦涉及保密对象或者遇到想对创建的对象数量 进行控制的其他情况,应该将所有构建器都设为 pr i vat e,并提供一个或更多的特殊方法来创建对象。采用 这种方式,这些方法就可以限制创建的对象数量以及它们的创建条件——一种特殊情况是第 16 章要介绍的 s i ngl et on(独子)方案。 下面这个例子总结了克隆的各种实现方法,然后在层次结构中将其“关闭”: //: C heckC oneabl e. j ava l // C hecki ng t o s ee i f a handl e can be cl oned // C t cl one t hi s becaus e i t does n' t an' // over r i de cl one( ) : cl as s O di nar y { } r // O r i des cl one, but does n' t i m em ver pl ent / / C oneabl e: l cl as s W ongC one ext ends O di nar y { r l r publ i c O ect cl one( ) bj t hr ow C oneN Suppor t edExcept i on { s l ot r et ur n s uper . cl one( ) ; // Thr ow except i on s } } // D oes al l t he r i ght t hi ngs f or cl oni ng: cl as s I s C oneabl e ext ends O di nar y l r i m em s C oneabl e { pl ent l publ i c O ect cl one( ) bj t hr ow C oneN Suppor t edExcept i on { s l ot r et ur n s uper . cl one( ) ; } } 363

// Tur n of f cl oni ng by t hr ow ng t he except i on: i cl as s N or e ext ends I s C oneabl e { oM l publ i c O ect cl one( ) bj t hr ow C oneN Suppor t edExcept i on { s l ot t hr ow new C oneN Suppor t edExcept i on( ) ; l ot } } cl as s Tr yM e ext ends N or e { or oM publ i c O ect cl one( ) bj t hr ow C oneN Suppor t edExcept i on { s l ot // C l s N or e. cl one( ) , t hr ow except i on: al oM s r et ur n s uper . cl one( ) ; } } cl as s BackO ext ends N or e { n oM pr i vat e BackO dupl i cat e( BackO b) { n n // Som ehow m ake a copy of b // and r et ur n t hat copy. Thi s i s a dum y m // copy, j us t t o m ake t he poi nt : r et ur n new BackO ) ; n( } publ i c O ect cl one( ) { bj // D n' t cal l N or e. cl one( ) : oes oM r et ur n dupl i cat e( t hi s ) ; } } // C t i nher i t f r om t hi s , s o can' t over r i de an' // t he cl one m hod l i ke i n BackO et n: f i nal cl as s Real l yN or e ext ends N or e { } oM oM publ i c cl as s C heckC oneabl e { l s t at i c O di nar y t r yToC one( O di nar y or d) { r l r St r i ng i d = or d. get C as s ( ) . get N e( ) ; l am O di nar y x = nul l ; r i f ( or d i ns t anceof C oneabl e) { l try { Sys t em out . pr i nt l n( " A t em i ng " + i d) ; . t pt x = ( O di nar y) ( ( I s C oneabl e) or d) . cl one( ) ; r l Sys t em out . pr i nt l n( " C oned " + i d) ; . l } cat ch( C oneN Suppor t edExcept i on e) { l ot Sys t em out . pr i nt l n( . " C d not cl one " + i d) ; oul } } r et ur n x; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai // Upcas t i ng: 364

O di nar y[ ] or d = { r new I s C oneabl e( ) , l new W ongC one( ) , r l new N or e( ) , oM new Tr yM e( ) , or new BackO ) , n( new Real l yN or e( ) , oM }; O di nar y x = new O di nar y( ) ; r r // Thi s w t com l e, s i nce cl one( ) i s on' pi // pr ot ect ed i n O ect : bj //! x = ( O di nar y) x. cl one( ) ; r // t r yT oC one( ) checks f i r s t t o s ee i f l // a cl as s i m em s C oneabl e: pl ent l f or ( i nt i = 0; i < or d. l engt h; i ++) t r yToC one( or d[ i ] ) ; l } } ///: ~ 第一个类 O di nar y 代表着大家在本书各处最常见到的类:不支持克隆,但在它正式应用以后,却也不禁止对 r 其克隆。但假如有一个指向 O di nar y 对象的句柄,而且那个对象可能是从一个更深的衍生类上溯造型来的, r 便不能判断它到底能不能克隆。 W ongC one 类揭示了实现克隆的一种不正确途径。它确实覆盖了 O ect . cl one( ) ,并将那个方法设为 r l bj publ i c,但却没有实现 C oneabl e。所以一旦发出对 s uper . cl one( ) 的调用(由于对 O ect . cl one( )的一个 l bj 调用造成的),便会无情地掷出 C oneN Suppor t edExcept i on违例。 l ot 在 I s C oneabl e 中,大家看到的才是进行克隆的各种正确行动:先覆盖 cl one( ),并实现了 C oneabl e。但 l l 是,这个 cl one( ) 方法以及本例的另外几个方法并不捕获 C oneN Suppor t edExcept i on 违例,而是任由它通 l ot 过,并传递给调用者。随后,调用者必须用一个 t r y- cat ch 代码块把它包围起来。在我们自己的 cl one( ) 方 法中,通常需要在 cl one( ) 内部捕获 C oneN Suppor t edExcept i on违例,而不是任由它通过。正如大家以后 l ot 会理解的那样,对这个例子来说,让它通过是最正确的做法。 类 N or e 试图按照 Java 设计者打算的那样“关闭”克隆:在衍生类 cl one( ) 中,我们掷出 oM C oneN Suppor t edExcept i on违例。T r yM e类中的 cl one( )方法正确地调用 s uper . cl one( ) ,并解析成 l ot or N or e. cl one( ) ,后者掷出一个违例并禁止克隆。 oM 但在已被覆盖的 cl one( )方法中,假若程序员不遵守调用 s uper . cl one( ) 的“正确”方法,又会出现什么情况 呢?在 BackO 中,大家可看到实际会发生什么。这个类用一个独立的方法 dupl i cat e( ) 制作当前对象的一个 n 副本,并在 cl one( )内部调用这个方法,而不是调用 s uper . cl one( ) 。违例永远不会产生,而且新类是可以克 隆的。因此,我们不能依赖“掷”出一个违例的方法来防止产生一个可克隆的类。唯一安全的方法在 Real l yN or e中得到了演示,它设为 f i nal ,所以不可继承。这意味着假如 cl one( )在 f i nal 类中掷出了一 oM 个违例,便不能通过继承来进行修改,并可有效地禁止克隆(不能从一个拥有任意继承级数的类中明确调用 O ect . cl one( ) ;只能调用 s uper . cl one( ) ,它只可访问直接基础类)。因此,只要制作一些涉及安全问题 bj 的对象,就最好把那些类设为 f i nal 。 在类 C heckC oneabl e 中,我们看到的第一个类是 t r yToC one( ),它能接纳任何 O di nar y 对象,并用 l l r i ns t anceof 检查它是否能够克隆。若答案是肯定的,就将对象造型成为一个 I s C oneabl e,调用 cl one( ) , l 并将结果造型回 O di nar y,最后捕获有可能产生的任何违例。请注意用运行期类型鉴定(见第 11 章)打印 r 出类名,使自己看到发生的一切情况。 在 m n( ) 中,我们创建了不同类型的 O di nar y 对象,并在数组定义中上溯造型成为 O di nar y。在这之后的 ai r r 头两行代码创建了一个纯粹的 O di nar y 对象,并试图对其克隆。然而,这些代码不会得到编译,因为 r cl one( )是 O j ect 中的一个 pr ot ect ed(受到保护的)方法。代码剩余的部分将遍历数组,并试着克隆每个 b 对象,分别报告它们的成功或失败。输出如下: A t em i ng I s C oneabl e t pt l C oned I s C oneabl e l l 365

A t em i ng N or e t pt oM C d not cl one N or e oul oM A t em i ng Tr yM e t pt or C d not cl one Tr yM e oul or A t em i ng BackO t pt n C oned BackO l n A t em i ng Real l yN or e t pt oM C d not cl one Real l yN or e oul oM 总之,如果希望一个类能够克隆,那么: ( 1) 实现 C oneabl e 接口 l ( 2) 覆盖 cl one( ) ( 3) 在自己的 cl one( ) 中调用 s uper . cl one( ) ( 4) 在自己的 cl one( ) 中捕获违例 这一系列步骤能达到最理想的效果。

1 2 . 3 . 1 副本构建器
克隆看起来要求进行非常复杂的设置,似乎还该有另一种替代方案。一个办法是制作特殊的构建器,令其负 责复制一个对象。在 C ++中,这叫作“副本构建器”。刚开始的时候,这好象是一种非常显然的解决方案 (如果你是 C ++程序员,这个方法就更显亲切)。下面是一个实际的例子: //: C opyC t r uct or . j ava ons // A cons t r uct or f or copyi ng an obj ect // of t he s am t ype, as an at t em t o cr eat e e pt // a l ocal copy. cl as s Fr ui t Q i t i es { ual pr i vat e i nt w ght ; ei pr i vat e i nt col or ; pr i vat e i nt f i r m s ; nes pr i vat e i nt r i penes s ; pr i vat e i nt s m l ; el // et c. Fr ui t Q i t i es ( ) { // D aul t cons t r uct or ual ef // do s om hi ng m et eani ngf ul . . . } // O her cons t r uct or s : t // . . . // C opy cons t r uct or : Fr ui t Q i t i es ( Fr ui t Q i t i es f ) { ual ual w ght = f . w ght ; ei ei col or = f . col or ; f i r m s = f . f i r m s; nes nes r i penes s = f . r i penes s ; sm l = f . sm l ; el el / / et c. } } cl as s Seed { // M ber s . . . em Seed( ) { /* D aul t cons t r uct or */ } ef 366

Seed( Seed s ) { /* C opy cons t r uct or */ } } cl as s Fr ui t { pr i vat e Fr ui t Q i t i es f q; ual pr i vat e i nt s eeds ; pr i vat e Seed[ ] s ; Fr ui t ( Fr ui t Q i t i es q, i nt s eedC ual ount ) { f q = q; s eeds = s eedC ount ; s = new Seed[ s eeds ] ; f or ( i nt i = 0; i < s eeds ; i ++) s [ i ] = new Seed( ) ; } // O her cons t r uct or s : t // . . . // C opy cons t r uct or : Fr ui t ( Fr ui t f ) { f q = new Fr ui t Q i t i es ( f . f q) ; ual s eeds = f . s eeds ; // C l al l Seed copy- cons t r uct or s : al f or ( i nt i = 0; i < s eeds ; i ++) s [ i ] = new Seed( f . s [ i ] ) ; // O her copy- cons t r uct i on act i vi t i es . . . t } // To al l ow der i ved cons t r uct or s ( or ot her // m hods ) t o put i n di f f er ent qual i t i es : et pr ot ect ed voi d addQ i t i es ( Fr ui t Q i t i es q) { ual ual f q = q; } pr ot ect ed Fr ui t Q i t i es get Q i t i es ( ) { ual ual r et ur n f q; } } cl as s Tom o ext ends Fr ui t { at Tom o( ) { at s uper ( new Fr ui t Q i t i es ( ) , 100) ; ual } Tom o( Tom o t ) { // C at at opy- cons t r uct or s uper ( t ) ; // Upcas t f or bas e copy- cons t r uct or // O her copy- cons t r uct i on act i vi t i es . . . t } } cl as s Zebr aQ i t i es ext ends Fr ui t Q i t i es { ual ual pr i vat e i nt s t r i pednes s ; Zebr aQ i t i es ( ) { // D aul t cons t r uct or ual ef // do s om hi ng m et eani ngf ul . . . } Zebr aQ i t i es ( Zebr aQ i t i es z) { ual ual s uper ( z) ; 367

s t r i pednes s = z. s t r i pednes s ; } } cl as s G eenZebr a ext ends Tom o { r at G eenZebr a( ) { r addQ i t i es ( new Zebr aQ l i t i es ( ) ) ; ual ua } G eenZebr a( G eenZebr a g) { r r s uper ( g) ; // C l s Tom o( Tom o) al at at // Res t or e t he r i ght qual i t i es : addQ i t i es ( new Zebr aQ i t i es ( ) ) ; ual ual } voi d eval uat e( ) { Zebr aQ i t i es zq = ual ( Zebr aQ i t i es ) get Q i t i es ( ) ; ual ual / / D som hi ng w t h t he qual i t i es o et i // . . . } } publ i c cl as s C opyC t r uct or { ons publ i c s t at i c voi d r i pen( Tom o t ) { at // Us e t he " copy cons t r uct or " : t = new Tom o( t ) ; at Sys t em out . pr i nt l n( " I n r i pen, t i s a . t . get C as s ( ) . get N e( ) ) ; l am } publ i c s t at i c voi d s l i ce( Fr ui t f ) { f = new Fr ui t ( f ) ; // Hm m . . w l l t hi m. i Sys t em out . pr i nt l n( " I n s l i ce, f i s a . f . get C as s ( ) . get N e( ) ) ; l am } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) ai Tom o t om o = new Tom o( ) ; at at at r i pen( t om o) ; // O at K s l i ce( t om o) ; // O PS! at O G eenZebr a g = new G eenZebr a( ) ; r r r i pen( g) ; // O PS! O s l i ce( g) ; // O PS! O g. eval uat e( ) ; } } ///: ~

" +

s w k? or " +

{

这个例子第一眼看上去显得有点奇怪。不同水果的质量肯定有所区别,但为什么只是把代表那些质量的数据 成员直接置入 Fr ui t (水果)类?有两方面可能的原因。第一个是我们可能想简便地插入或修改质量。注意 Fr ui t 有一个 pr ot ect ed(受到保护的)addQ i t i es ( ) 方法,它允许衍生类来进行这些插入或修改操作(大 ual 家或许会认为最合乎逻辑的做法是在 Fr ui t 中使用一个 pr ot ect ed构建器,用它获取 Fr ui t Q i t i es 参数, ual 但构建器不能继承,所以不可在第二级或级数更深的类中使用它)。通过将水果的质量置入一个独立的类, 可以得到更大的灵活性,其中包括可以在特定 Fr ui t 对象的存在期间中途更改质量。 之所以将 Fr ui t Q i t i es 设为一个独立的对象,另一个原因是考虑到我们有时希望添加新的质量,或者通过 ual 继承与多形性改变行为。注意对 G eenZebr a来说(这实际是西红柿的一类——我已栽种成功,它们简直令人 r 368

难以置信),构建器会调用 addQ i t i es ( ) ,并为其传递一个 Zebr aQ i t i es 对象。该对象是从 ual ual Fr ui t Q i t i es 衍生出来的,所以能与基础类中的 Fr ui t Q i t i es 句柄联系在一起。当然,一旦 ual ual G eenZebr a使用 Fr ui t Q i t i es ,就必须将其下溯造型成为正确的类型(就象 eval uat e( ) 中展示的那 r ual 样),但它肯定知道类型是 Zebr aQ i t i es 。 ual 大家也看到有一个 Seed(种子)类,Fr ui t (大家都知道,水果含有自己的种子)包含了一个 Seed 数组。 最后,注意每个类都有一个副本构建器,而且每个副本构建器都必须关心为基础类和成员对象调用副本构建 器的问题,从而获得“深层复制”的效果。对副本构建器的测试是在 C opyC t r uct or 类内进行的。方法 ons r i pen( )需要获取一个 Tom o参数,并对其执行副本构建工作,以便复制对象: at t = new Tom o( t ) ; at 而 s l i ce( ) 需要获取一个更常规的 Fr ui t 对象,而且对它进行复制: f = new Fr ui t ( f ) ; 它们都在 m n( ) 中伴随不同种类的 Fr ui t 进行测试。下面是输出结果: ai In In In In ri sl ri sl pen, i ce, pen, i ce, t f t f is is is is a T om o at a Fr ui t a T om o at a Fr ui t

从中可以看出一个问题。在 s l i ce( )内部对 Tom o 进行了副本构建工作以后,结果便不再是一个 Tom o 对 at at 象,而只是一个 Fr ui t 。它已丢失了作为一个 Tom o at (西红柿)的所有特征。此外,如果采用一个 G eenZebr a,r i pen( ) 和 s l i ce( ) 会把它分别转换成一个 Tom o 和一个 Fr ui t 。所以非常不幸,假如想制作对 r at 象的一个本地副本,Java 中的副本构建器便不是特别适合我们。 1. 为什么在 C ++的作用比在 Java 中大? 副本构建器是 C ++的一个基本构成部分,因为它能自动产生对象的一个本地副本。但前面的例子确实证明了 它不适合在 Java 中使用,为什么呢?在 Java 中,我们操控的一切东西都是句柄,而在 C ++中,却可以使用 类似于句柄的东西,也能直接传递对象。这时便要用到 C ++的副本构建器:只要想获得一个对象,并按值传 递它,就可以复制对象。所以它在 C ++里能很好地工作,但应注意这套机制在 Java 里是很不通的,所以不要 用它。

12. 4 只读类
尽管在一些特定的场合,由 cl one( )产生的本地副本能够获得我们希望的结果,但程序员(方法的作者)不 得不亲自禁止别名处理的副作用。假如想制作一个库,令其具有常规用途,但却不能担保它肯定能在正确的 类中得以克隆,这时又该怎么办呢?更有可能的一种情况是,假如我们想让别名发挥积极的作用——禁止不 必要的对象复制——但却不希望看到由此造成的副作用,那么又该如何处理呢? 一个办法是创建“不变对象”,令其从属于只读类。可定义一个特殊的类,使其中没有任何方法能造成对象 内部状态的改变。在这样的一个类中,别名处理是没有问题的。因为我们只能读取内部状态,所以当多处代 码都读取相同的对象时,不会出现任何副作用。 作为“不变对象”一个简单例子,Java 的标准库包含了“封装器”(w apper )类,可用于所有基本数据类 r 型。大家可能已发现了这一点,如果想在一个象 Vect or (只采用 O ect 句柄)这样的集合里保存一个 i nt bj 数值,可以将这个 i nt 封装到标准库的 I nt eger 类内部。如下所示: //: I m ut abl eI nt eger . j ava m // T he I nt eger cl as s cannot be changed i m t j ava. ut i l . * ; por publ i c cl as s I m ut abl eI nt eger { m publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Vect or v = new Vect or ( ) ; f or ( i nt i = 0; i < 10; i ++) v. addEl em ( new I nt eger ( i ) ) ; ent / / But how do you change t he i nt 369

// i ns i de t he I nt eger ? } } ///: ~ I nt eger 类(以及基本的“封装器”类)用简单的形式实现了“不变性”:它们没有提供可以修改对象的方 法。 若确实需要一个容纳了基本数据类型的对象,并想对基本数据类型进行修改,就必须亲自创建它们。幸运的 是,操作非常简单: //: M abl eI nt eger . j ava ut // A changeabl e w apper cl as s r i m t j ava. ut i l . * ; por cl as s I nt Val ue { i nt n; I nt Val ue( i nt x) { n = x; } publ i c St r i ng t oSt r i ng( ) { r et ur n I nt eger . t oSt r i ng( n) ; } } publ i c cl as s M abl eI nt eger { ut publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Vect or v = new Vect or ( ) ; f or ( i nt i = 0; i < 10; i ++) v. addEl em ( new I nt Val ue( i ) ) ; ent Sys t em out . pr i nt l n( v) ; . f or ( i nt i = 0; i < v. s i ze( ) ; i ++) ( ( I nt Val ue) v. el em A ( i ) ) . n++; ent t Sys t em out . pr i nt l n( v) ; . } } ///: ~ 注意 n 在这里简化了我们的编码。 若默认的初始化为零已经足够(便不需要构建器),而且不用考虑把它打印出来(便不需要 t oSt r i ng),那 么 I nt Val ue 甚至还能更加简单。如下所示: cl as s I nt Val ue { i nt n; } 将元素取出来,再对其进行造型,这多少显得有些笨拙,但那是 Vect or 的问题,不是 I nt Val ue 的错。

1 2 . 4 . 1 创建只读类
完全可以创建自己的只读类,下面是个简单的例子: //: I m ut abl e1. j ava m // O ect s t hat cannot be m f i ed bj odi // ar e i m une t o al i as i ng. m publ i c cl as s I m ut abl e1 { m pr i vat e i nt dat a; publ i c I m ut abl e1( i nt i ni t Val ) { m dat a = i ni t Val ; } publ i c i nt r ead( ) { r et ur n dat a; } 370

publ i c bool ean nonzer o( ) { r et ur n dat a ! = 0; } publ i c I m ut abl e1 quadr upl e ) { m ( r et ur n new I m ut abl e1( dat a * 4) ; m } s t at i c voi d f ( I m ut abl e1 i 1) { m I m ut abl e1 quad = i 1. quadr upl e( ) ; m Sys t em out . pr i nt l n( " i 1 = " + i 1. r ead( ) ) ; . Sys t em out . pr i nt l n( " quad = " + quad. r ead( ) ) ; . } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai I m ut abl e1 x = new I m ut abl e1( 47) ; m m Sys t em out . pr i nt l n( " x = " + x. r ead( ) ) ; . f ( x) ; Sys t em out . pr i nt l n( " x = " + x. r ead( ) ) ; . } } ///: ~ 所有数据都设为 pr i vat e,可以看到没有任何 publ i c方法对数据作出修改。事实上,确实需要修改一个对象 的方法是 quadr upl e( ) ,但它的作用是新建一个 I m ut abl e1 对象,初始对象则是原封未动的。 m 方法 f ( )需要取得一个 I m ut abl e1 对象,并对其采取不同的操作,而 m n( ) 的输出显示出没有对 x 作任何修 m ai 改。因此,x 对象可别名处理许多次,不会造成任何伤害,因为根据 I m ut abl e1 类的设计,它能保证对象不 m 被改动。

1 2 . 4 . 2 “一成不变”的弊端
从表面看,不变类的建立似乎是一个好方案。但是,一旦真的需要那种新类型的一个修改的对象,就必须辛 苦地进行新对象的创建工作,同时还有可能涉及更频繁的垃圾收集。对有些类来说,这个问题并不是很大。 但对其他类来说(比如 St r i ng类),这一方案的代价显得太高了。 为解决这个问题,我们可以创建一个“同志”类,并使其能够修改。以后只要涉及大量的修改工作,就可换 为使用能修改的同志类。完事以后,再切换回不可变的类。 因此,上例可改成下面这个样子: //: I m ut abl e2. j ava m // A com pani on cl as s f or m ng changes aki // t o i m ut abl e obj ect s . m cl as s M abl e { ut pr i vat e i nt dat a; publ i c M abl e( i nt i ni t Val ) { ut dat a = i ni t Val ; } publ i c M abl e add( i nt x) { ut dat a += x; r et ur n t hi s ; } publ i c M abl e m t i pl y( i nt x) { ut ul dat a * = x; r et ur n t hi s ; } publ i c I m ut abl e2 m m akeI m ut abl e2( ) { m r et ur n new I m ut abl e2( dat a) ; m } } 371

publ i c cl as s I m ut abl e2 { m pr i vat e i nt dat a; publ i c I m ut abl e2( i nt i ni t Val ) { m dat a = i ni t Val ; } publ i c i nt r ead( ) { r et ur n dat a; } publ i c bool ean nonzer o( ) { r et ur n dat a ! = 0; } publ i c I m ut abl e2 add( i nt x) { m r et ur n new I m ut abl e2( dat a + x) ; m } publ i c I m ut ab e2 m t i pl y( i nt x) { m l ul r et ur n new I m ut abl e2( dat a * x) ; m } publ i c M abl e m ut akeM abl e( ) { ut r et ur n new M abl e( dat a) ; ut } publ i c s t at i c I m ut abl e2 m f y1( I m ut abl e2 y) { m odi m I m ut abl e2 val = y. add( 12) ; m val = val . m t i pl y( 3) ; ul val = val . add 11) ; ( val = val . m t i pl y( 2) ; ul r et ur n val ; } // Thi s pr oduces t he s am r es ul t : e publ i c s t at i c I m ut abl e2 m f y2( I m ut abl e2 y) { m odi m M abl e m = y. m ut akeM abl e( ) ; ut m add( 12) . m t i pl y( 3) . add( 11) . m t i pl y( 2) ; . ul ul r et ur n m m . akeI m ut abl e2( ) ; m } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai I m ut abl e2 i 2 = new I m ut abl e2( 47) ; m m I m ut abl e2 r 1 = m f y1( i 2) ; m odi I m ut abl e2 r 2 = m f y2( i 2) ; m odi Sys t em out . pr i nt l n( " i 2 = " + i 2. r ead( ) ) ; . Sys t em out . pr i nt l n( " r 1 = " + r 1. r ead( ) ) ; . Sys t em out . pr i nt l n( " r 2 = " + r 2. r ead( ) ) ; . } } ///: ~ 和往常一样,I m ut abl e2 包含的方法保留了对象不可变的特征,只要涉及修改,就创建新的对象。完成这些 m 操作的是 add( ) 和 m t i pl y( ) 方法。同志类叫作 M abl e,它也含有 add( ) 和 m t i pl y( )方法。但这些方法 ul ut ul 能够修改 M abl e对象,而不是新建一个。除此以外,M abl e的一个方法可用它的数据产生一个 ut ut I m ut abl e2 对象,反之亦然。 m 两个静态方法 m f y1( ) 和 m f y2( ) 揭示出获得同样结果的两种不同方法。在 m f y1( ) 中,所有工作都是 odi odi odi 在 I m ut abl e2 类中完成的,我们可看到在进程中创建了四个新的 I m ut abl e2 对象(而且每次重新分配了 m m val ,前一个对象就成为垃圾)。 在方法 m f y2( ) 中,可看到它的第一个行动是获取 I m ut abl e2 y,然后从中生成一个 M abl e(类似于前 odi m ut 面对 cl one( )的调用,但这一次创建了一个不同类型的对象)。随后,用 M abl e对象进行大量修改操作, ut 同时用不着新建许多对象。最后,它切换回 I m ut abl e2。在这里,我们只创建了两个新对象(M abl e和 m ut I m ut abl e2 的结果),而不是四个。 m 这一方法特别适合在下述场合应用: 372

( 1) 需要不可变的对象,而且 ( 2) 经常需要进行大量修改,或者 ( 3) 创建新的不变对象代价太高

1 2 . 4 . 3 不变字串
请观察下述代码: //: St r i nger . j ava publ i c cl as s St r i nger { s t at i c St r i ng upcas e( St r i ng s ) { r et ur n s . t oUpper C e( ) ; as } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai St r i ng q = new St r i ng( " how ) ; dy" Sys t em out . pr i nt l n( q) ; // how . dy St r i ng qq = upcas e( q) ; Sys t em out . pr i nt l n( qq) ; // HO D . WY Sys t em out . pr i nt l n( q) ; // how . dy } } ///: ~ q传递进入 upcas e( ) 时,它实际是 q的句柄的一个副本。该句柄连接的对象实际只在一个统一的物理位置 处。句柄四处传递的时候,它的句柄会得到复制。 若观察对 upcas e( ) 的定义,会发现传递进入的句柄有一个名字 s ,而且该名字只有在 upcas e( ) 执行期间才会 存在。upcas e( ) 完成后,本地句柄 s 便会消失,而 upcas e( ) 返回结果——还是原来那个字串,只是所有字符 都变成了大写。当然,它返回的实际是结果的一个句柄。但它返回的句柄最终是为一个新对象的,同时原来 的 q并未发生变化。所有这些是如何发生的呢? 1. 隐式常数 若使用下述语句: St r i ng s = " as df " ; St r i ng x = St r i nger . upcas e( s ) ; 那么真的希望 upcas e( ) 方法改变自变量或者参数吗?我们通常是不愿意的,因为作为提供给方法的一种信 息,自变量一般是拿给代码的读者看的,而不是让他们修改。这是一个相当重要的保证,因为它使代码更易 编写和理解。 为了在 C ++中实现这一保证,需要一个特殊关键字的帮助:cons t 。利用这个关键字,程序员可以保证一个句 柄(C ++叫“指针”或者“引用”)不会被用来修改原始的对象。但这样一来,C ++程序员需要用心记住在所 有地方都使用 cons t 。这显然易使人混淆,也不容易记住。 2. 覆盖" +" 和 St r i ngBuf f er 利用前面提到的技术,St r i ng类的对象被设计成“不可变”。若查阅联机文档中关于 St r i ng类的内容(本 章稍后还要总结它),就会发现类中能够修改 St r i ng的每个方法实际都创建和返回了一个崭新的 St r i ng对 象,新对象里包含了修改过的信息——原来的 St r i ng是原封未动的。因此,Java 里没有与 C ++的 cons t 对应 的特性可用来让编译器支持对象的不可变能力。若想获得这一能力,可以自行设置,就象 St r i ng那样。 由于 St r i ng对象是不可变的,所以能够根据情况对一个特定的 St r i ng进行多次别名处理。因为它是只读 的,所以一个句柄不可能会改变一些会影响其他句柄的东西。因此,只读对象可以很好地解决别名问题。 通过修改产生对象的一个崭新版本,似乎可以解决修改对象时的所有问题,就象 St r i ng那样。但对某些操作 来讲,这种方法的效率并不高。一个典型的例子便是为 St r i ng对象覆盖的运算符“+”。“覆盖”意味着在 与一个特定的类使用时,它的含义已发生了变化(用于 St r i ng的“+”和“+=”是 Java 中能被覆盖的唯一运 算符,Java 不允许程序员覆盖其他任何运算符——注释④)。

373

④:C ++允许程序员随意覆盖运算符。由于这通常是一个复杂的过程(参见《Thi nki ng i n C ++》,Pr ent i ce Hal l 于 1995 年出版),所以 Java 的设计者认定它是一种“糟糕”的特性,决定不在 Java 中采用。但具有 讽剌意味的是,运算符的覆盖在 Java 中要比在 C ++中容易得多。 针对 St r i ng对象使用时,“+”允许我们将不同的字串连接起来: St r i ng s = " abc" + f oo + " def " + I nt eger . t oSt r i ng( 47) ; 可以想象出它“可能”是如何工作的:字串" abc" 可以有一个方法 append( ) ,它新建了一个字串,其中包含 " abc" 以及 f oo的内容;这个新字串然后再创建另一个新字串,在其中添加" def " ;以此类推。 这一设想是行得通的,但它要求创建大量字串对象。尽管最终的目的只是获得包含了所有内容的一个新字 串,但中间却要用到大量字串对象,而且要不断地进行垃圾收集。我怀疑 Java 的设计者是否先试过种方法 (这是软件开发的一个教训——除非自己试试代码,并让某些东西运行起来,否则不可能真正了解系统)。 我还怀疑他们是否早就发现这样做获得的性能是不能接受的。 解决的方法是象前面介绍的那样制作一个可变的同志类。对字串来说,这个同志类叫作 St r i ngBuf f er ,编译 器可以自动创建一个 St r i ngBuf f er ,以便计算特定的表达式,特别是面向 St r i ng对象应用覆盖过的运算符+ 和+=时。下面这个例子可以解决这个问题: //: I m ut abl eSt r i ngs . j ava m // D ons t r at i ng St r i ngBuf f er em publ i c cl as s I m ut abl eSt r i ngs { m publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai St r i ng f oo = " f oo" ; St r i ng s = " abc" + f oo + " def " + I nt eger . t oSt r i ng( 47) ; Sys t em out . pr i nt l n( s ) ; . // The " equi val ent " us i ng St r i ngBuf f er : St r i ngBuf f er s b = new St r i ngBuf f er ( " abc" ) ; // C eat es St r i ng! r s b. append( f oo) ; s b. append( " def " ) ; // C eat es St r i ng! r s b append( I nt eger . t oSt r i ng( 47) ) ; . Sys t em out . pr i nt l n( s b) ; . } } ///: ~ 创建字串 s 时,编译器做的工作大致等价于后面使用 s b的代码——创建一个 St r i ngBuf f er ,并用 append( ) 将新字符直接加入 St r i ngBuf f er 对象(而不是每次都产生新对象)。尽管这样做更有效,但不值得每次都创 建象" abc" 和" def " 这样的引号字串,编译器会把它们都转换成 St r i ng对象。所以尽管 St r i ngBuf f er 提供了 更高的效率,但会产生比我们希望的多得多的对象。

1 2 . 4 . 4 S t r i ng 和 S t r i ngBuf f er 类
这里总结一下同时适用于 St r i ng和 St r i ngBuf f er 的方法,以便对它们相互间的沟通方式有一个印象。这些 表格并未把每个单独的方法都包括进去,而是包含了与本次讨论有重要关系的方法。那些已被覆盖的方法用 单独一行总结。 首先总结 St r i ng类的各种方法: 方法 自变量,覆盖 用途 构建器 已被覆盖:默认,St r i ng,St r i ngBuf f er ,char 数组,byt e 数组 创建 St r i ng对象 l engt h( ) 无 St r i ng中的字符数量 374

char A ( ) i nt I ndex 位于 St r i ng内某个位置的 char t get C s ( ) ,get Byt es 开始复制的起点和终点,要向其中复制内容的数组,对目标数组的一个索引 将 char har 或 byt e 复制到外部数组内部 t oC A r ay( ) 无 产生一个 char [ ] ,其中包含了 St r i ng内部的字符 har r equal s ( ) ,equal s I gnor eC e( ) 用于对比的一个 St r i ng 对两个字串的内容进行等价性检查 as com eTo( ) 用于对比的一个 St r i ng 结果为负、零或正,具体取决于 St r i ng和自变量的字典顺序。注意大 par 写和小写不是相等的! r egi onM ches ( ) 这个 St r i ng以及其他 St r i ng的位置偏移,以及要比较的区域长度。覆盖加入了“忽略大 at 小写”的特性 一个布尔结果,指出要对比的区域是否相同 s t ar t s W t h( ) 可能以它开头的 St r i ng。覆盖在自变量里加入了偏移 一个布尔结果,指出 St r i ng是否以那 i 个自变量开头 ends W t h( ) 可能是这个 St r i ng后缀的一个 St r i ng 一个布尔结果,指出自变量是不是一个后缀 i i ndexO ( ) , l as t I ndexO ( ) 已覆盖:char ,char 和起始索引,St r i ng,St r i ng和起始索引 若自变量未在这 f f 个 St r i ng里找到,则返回- 1;否则返回自变量开始处的位置索引。l as t I ndexO ( ) 可从终点开始回溯搜索 f s ubs t r i ng( ) 已覆盖:起始索引,起始索引和结束索引 返回一个新的 St r i ng对象,其中包含了指定的字符 子集 concat ( ) 想连结的 St r i ng 返回一个新 St r i ng对象,其中包含了原始 St r i ng的字符,并在后面加上由自变 量提供的字符 r el pace( ) 要查找的老字符,要用它替换的新字符 返回一个新 St r i ng对象,其中已完成了替换工作。若没 有找到相符的搜索项,就沿用老字串 t oLow C e( ) , t oU er as pper C e( ) 无 返回一个新 St r i ng对象,其中所有字符的大小写形式都进行了统一。若 as 不必修改,则沿用老字串 t r i m ) 无 返回一个新的 St r i ng对象,头尾空白均已删除。若毋需改动,则沿用老字串 ( val ueO ( ) 已覆盖:obj ect ,char [ ] ,char [ ]和偏移以及计数,bool ean,char ,i nt,l o f ng,f l o t ,d ub e a o l 返回一个 St r i ng,其中包含自变量的一个字符表现形式 I nt er n( ) 无 为每个独一无二的字符顺序都产生一个(而且只有一个)St r i ng句柄 可以看到,一旦有必要改变原来的内容,每个 St r i ng方法都小心地返回了一个新的 St r i ng对象。另外要注 意的一个问题是,若内容不需要改变,则方法只返回指向原来那个 St r i ng的一个句柄。这样做可以节省存储 空间和系统开销。 下面列出有关 St r i ngBuf f er (字串缓冲)类的方法: 方法 自变量,覆盖 用途 构建器 已覆盖:默认,要创建的缓冲区长度,要根据它创建的 St r i ng 新建一个 St r i ngBuf f er 对象 t oSt r i ng( ) 无 根据这个 St r i ngBuf f er 创建一个 St r i ng l engt h( ) 无 St r i ngBuf f er 中的字符数量 capaci t y( ) 无 返回目前分配的空间大小 ens ur eC apaci t y( ) 用于表示希望容量的一个整数 使 St r i ngBuf f er 容纳至少希望的空间大小 s et Lengt h( ) 用于指示缓冲区内字串新长度的一个整数 缩短或扩充前一个字符串。如果是扩充,则用 nul l 值填充空隙 char A ( ) 表示目标元素所在位置的一个整数 返回位于缓冲区指定位置处的 char t s et C A ( ) 代表目标元素位置的一个整数以及元素的一个新 char 值 修改指定位置处的值 har t get C s ( ) 复制的起点和终点,要在其中复制的数组以及目标数组的一个索引 将 char 复制到一个外部数 har 组。和 St r i ng不同,这里没有 get Byt es ( ) 可供使用 append( ) 已覆盖:O ect ,St r i ng,char [ ],特定偏移和长度的 char [ ],bool ean,char ,i nt ,l ong, bj f l oat ,doubl e 将自变量转换成一个字串,并将其追加到当前缓冲区的末尾。若有必要,同时增大缓冲区的 长度 i ns er t ( ) 已覆盖,第一个自变量代表开始插入的位置:O ect ,St r i ng,char [ ] ,bool ean,char ,i nt , bj l ong,f l oat ,doubl e 第二个自变量转换成一个字串,并插入当前缓冲区。插入位置在偏移区域的起点处。 若有必要,同时会增大缓冲区的长度 r ever s e( ) 无 反转缓冲内的字符顺序 375

最常用的一个方法是 append( ) 。在计算包含了+和+=运算符的 St r i ng表达式时,编译器便会用到这个方法。 i ns er t ( ) 方法采用类似的形式。这两个方法都能对缓冲区进行重要的操作,不需要另建新对象。

1 2 . 4 . 5 字串的特殊性
现在,大家已知道 St r i ng类并非仅仅是 Java 提供的另一个类。St r i ng里含有大量特殊的类。通过编译器和 特殊的覆盖或过载运算符+和+=,可将引号字符串转换成一个 St r i ng。在本章中,大家已见识了剩下的一种 特殊情况:用同志 St r i ngBuf f er 精心构造的“不可变”能力,以及编译器中出现的一些有趣现象。

12. 5 总结
由于 Java 中的所有东西都是句柄,而且由于每个对象都是在内存堆中创建的——只有不再需要的时候,才会 当作垃圾收集掉,所以对象的操作方式发生了变化,特别是在传递和返回对象的时候。举个例子来说,在 C 和C ++中,如果想在一个方法里初始化一些存储空间,可能需要请求用户将那片存储区域的地址传递进入方 法。否则就必须考虑由谁负责清除那片区域。因此,这些方法的接口和对它们的理解就显得要复杂一些。但 在 Java 中,根本不必关心由谁负责清除,也不必关心在需要一个对象的时候它是否仍然存在。因为系统会为 我们照料一切。我们的程序可在需要的时候创建一个对象。而且更进一步地,根本不必担心那个对象的传输 机制的细节:只需简单地传递句柄即可。有些时候,这种简化非常有价值,但另一些时候却显得有些多余。 可从两个方面认识这一机制的缺点: ( 1) 肯定要为额外的内存管理付出效率上的损失(尽管损失不大),而且对于运行所需的时间,总是存在一 丝不确定的因素(因为在内存不够时,垃圾收集器可能会被强制采取行动)。对大多数应用来说,优点显得 比缺点重要,而且部分对时间要求非常苛刻的段落可以用 nat i ve方法写成(参见附录 A )。 ( 2) 别名处理:有时会不慎获得指向同一个对象的两个句柄。只有在这两个句柄都假定指向一个“明确”的 对象时,才有可能产生问题。对这个问题,必须加以足够的重视。而且应该尽可能地“克隆”一个对象,以 防止另一个句柄被不希望的改动影响。除此以外,可考虑创建“不可变”对象,使它的操作能返回同种类型 或不同种类型的一个新对象,从而提高程序的执行效率。但千万不要改变原始对象,使对那个对象别名的其 他任何方面都感觉不出变化。 有些人认为 Java 的克隆是一个笨拙的家伙,所以他们实现了自己的克隆方案(注释⑤),永远杜绝调用 O ect . cl one( ) 方法,从而消除了实现 C oneabl e和捕获 C oneN Suppor t Except i on 违例的需要。这一做法 bj l l ot 是合理的,而且由于 cl one( )在 Java 标准库中很少得以支持,所以这显然也是一种“安全”的方法。只要不 调用 O ect . cl one ) ,就不必实现 C oneabl e 或者捕获违例,所以那看起来也是能够接受的。 bj ( l ⑤:D oug Lea特别重视这个问题,并把这个方法推荐给了我,他说只需为每个类都创建一个名为 dupl i cat e( ) 的函数即可。 Java 中一个有趣的关键字是 byval ue(按值),它属于那些“保留但未实现”的关键字之一。在理解了别名 和克隆问题以后,大家可以想象 byval ue最终有一天会在 Java 中用于实现一种自动化的本地副本。这样做可 以解决更多复杂的克隆问题,并使这种情况下的编写的代码变得更加简单和健壮。

12. 6 练习
( 1) 创建一个 m r i ng 类,在其中包含了一个 St r i ng对象,以便用在构建器中用构建器的自变量对其进行 ySt 初始化。添加一个 t oSt r i ng( ) 方法以及一个 concat enat e( ) 方法,令其将一个 St r i ng对象追加到我们的内部 字串。在 m r i ng 中实现 cl one( )。创建两个 s t at i c 方法,每个都取得一个 m r i ng x 句柄作为自己的自 ySt ySt 变量,并调用 x. concat enat e( " t es t " ) 。但在第二个方法中,请首先调用 cl one( ) 。测试这两个方法,观察它 们不同的结果。 ( 2) 创建一个名为 Bat t er y(电池)的类,在其中包含一个 i nt ,用它表示电池的编号(采用独一无二的标识 符的形式)。接下来,创建一个名为 T oy 的类,其中包含了一个 Bat t er y 数组以及一个 t oSt r i ng,用于打印 出所有电池。为 T oy 写一个 cl one( )方法,令其自动关闭所有 Bat t er y 对象。克隆 T oy 并打印出结果,完成 对它的测试。 ( 3) 修改 C heckC oneabl e. j ava,使所有 cl one( ) 方法都能捕获 C oneN Suppor t Except i on 违例,而不是把 l l ot 它直接传递给调用者。 376

( 4) 修改 C pet e. j ava,为 Thi ng2 和 Thi ng4 类添加更多的成员对象,看看自己是否能判断计时随复杂性变 om 化的规律——是一种简单的线性关系,还是看起来更加复杂。 ( 5) 从 Snake. j ava开始,创建 Snake 的一个深层复制版本。

377

第十三章 创建窗口和程序片
在 Java 1. 0 中,图形用户接口(G )库最初的设计目标是让程序员构建一个通用的 G ,使其在所有平台 UI UI 上都能正常显示。 但遗憾的是,这个目标并未达到。事实上,Java 1. 0 版的“抽象 W ndow 工具包”(A T )产生的是在各系 i s W 统看来都同样欠佳的图形用户接口。除此之外,它还限制我们只能使用四种字体,并且不能访问操作系统中 现有的高级 G 元素。同时,Jave1. 0 版的 A T 编程模型也不是面向对象的,极不成熟。这类情况在 Java1. 1 UI W 版的 A T 事件模型中得到了很好的改进,例如:更加清晰、面向对象的编程、遵循 Java Beans 的范例,以及 W 一个可轻松创建可视编程环境的编程组件模型。Java1. 2 为老的 Java 1. 0 A T 添加了 Java 基础类(A T), W W 这是一个被称为“Sw ng i ”的 G 的一部分。丰富的、易于使用和理解的 Java Beans 能经过拖放操作(像手 UI 工编程一样的好),创建出能使程序员满意的 G 。软件业的“3 次修订版”规则看来对于程序设计语言也是 UI 成立的(一个产品除非经过第 3 次修订,否则不会尽如人意)。 Java 的主要设计目的之一是建立程序片,也就是建立运行在 W 浏览器上的小应用程序。由于它们必须是安 EB 全的,所以程序片在运行时必须加以限制。无论怎样,它们都是支持客户端编程的强有力的工具,一个重要 的应用便是在 W eb上。 在一个程序片中编程会受到很多的限制,我们一般说它“在沙箱内”,这是由于 Java 运行时一直会有某个东 西——即 Java 运行期安全系统——在监视着我们。Jave 1. 1 为程序片提供了数字签名,所以可选出能信赖 的程序片去访问主机。不过,我们也能跳出沙箱的限制写出可靠的程序。在这种情况下,我们可访问操作系 统中的其他功能。在这本书中我们自始至终编写的都是可靠的程序,但它们成为了没有图形组件的控制台程 序。A T 也能用来为可靠的程序建立 G 接口。 W UI 在这一章中我们将先学习使用老的 A T 工具,我们会与许多支持和使用 A T 的代码程序样本相遇。尽管这有 W W 一些困难,但却是必须的,因为我们必须用老的 A T 来维护和阅读传统的 Java 代码。有时甚至需要我们编写 W A T 代码去支持不能从 Java1. 0 升级的环境。在本章第二部分,我们将学习 Java 1. 1 版中新的 A T 结构并会 W W 看到它的事件模型是如此的优秀(如果能掌握的话,那么在编制新的程序时就可使用这最新的工具。最后, 我们将学习新的能像类库一样加入到 Java 1. 1 版中的 JFC i ng组件,这意味着不需要升级到 Java 1. 2 便 /Sw 能使用这一类库。 大多数的例程都将展示程序片的建立,这并不仅仅是因为这非常的容易,更因为这是 A T 的主要作用。另 W 外,当用 A T 创建一个可靠的程序时,我们将看到处理程序的不同之处,以及怎样创建能在命令行和浏览器 W 中运行的程序。 请注意的是这不是为了描述类的所有程序的综合解释。这一章将带领我们从摘要开始。当我们查找更复杂的 内容时,请确定我们的信息浏览器通过查找类和方法来解决编程中的问题(如果我们正在使用一个开发环 境,信息浏览器也许是内建的;如果我们使用的是 SUN公司的 JD 则这时我们要使用 W 浏览器并在 Java 根 K EB 目录下面开始)。附录 F 列出了用于深入学习库知识的其他一些参考资料。

13. 1 为何要用 AW ? T
对于本章要学习的“老式”A T,它最严重的缺点就是它无论在面向对象设计方面,还是在 G 开发包设计方 W UI 面,都有不尽如人意的表现。它使我们回到了程序设计的黑暗年代(换成其他话就是“拙劣的”、“可怕 的”、“恶劣的”等等)。必须为执行每一个事件编写代码,包括在其他环境中利用“资源”即可轻松完成 的一些任务。 许多象这样的问题在 Java 1. 1 里都得到了缓解或排除,因为: ( 1) Java 1. 1 的新型 A T 是一个更好的编程模型,并向更好的库设计迈出了可喜的一步。而 Java Beans 则是 W 那个库的框架。 ( 2)“G 构建器”(可视编程环境)将适用于所有开发系统。在我们用图形化工具将组件置入窗体的时候, UI Java Beans 和新的 A T 使 G 构建器能帮我们自动完成代码。其它组件技术如 A i veX等也将以相同的形式 W UI ct 支持。 既然如此,为什么还要学习使用老的 A T 呢?原因很简单,因为它的存在是个事实。就目前来说,这个事实 W 对我们来说显得有些不利,它涉及到面向对象库设计的一个宗旨:一旦我们在库中公布一个组件,就再不能 去掉它。如去掉它,就会损害别人已存在的代码。另外,当我们学习 Java 和所有使用老 A T 的程序时,会发 W 现有许多原来的代码使用的都是老式 A T 。 W 378

A T 必须能与固有操作系统的 G 组件打交通,这意味着它需要执行一个程序片不可能做到的任务。一个不 W UI 被信任的程序片在操作系统中不能作出任何直接调用,否则它会对用户的机器做出不恰当的事情。一个不被 信任的程序片不能访问重要的功能。例如,“在屏幕上画一个窗口”的唯一方法是通过调用拥有特殊接口和 安全检查的标准 Java 库。Sun公司的原始模型创建的信任库将仅仅供给 W eb浏览器中的 Java 系统信任关系 自动授权器使用,自动授权器将控制怎样进入到库中去。 但当我们想增加操作系统中访问新组件的功能时该怎么办?等待 Sun 来决定我们的扩展被合并到标准的 Java 库中,但这不一定会解决我们的问题。Java 1. 1 版中的新模型是“信任代码”或“签名代码”,因此一个特 殊服务器将校验我们下载的、由规定的开发者使用的公共密钥加密系统的代码。这样我们就可知道代码从何 而来,那真的是 Bob的代码,还是由某人伪装成 Bob的代码。这并不能阻止 Bob犯错误或作某些恶意的事, 但能防止 Bob逃避匿名制造计算机病毒的责任。一个数字签名的程序片——“被信任的程序片”——在 Java 1. 1 版能进入我们的机器并直接控制它,正像一些其它的应用程序从信任关系自动授权机中得到“信任”并 安装在我们的机器上。 这是老 A T 的所有特点。老的 A T 代码将一直存在,新的 Java 编程者在从旧的书本中学习时将会遇到老的 W W A T 代码。同样,老的 A T 也是值得去学习的,例如在一个只有少量库的例程设计中。老的 A T 所包括的范 W W W 围在不考虑深度和枚举每一个程序和类,取而代之的是给了我们一个老 A T 设计的概貌。 W

13. 2 基本程序片
库通常按照它们的功能来进行组合。一些库,例如使用过的,便中断搁置起来。标准的 Java 库字符串和矢量 类就是这样的一个例子。其他的库被特殊地设计,例如构建块去建立其它的库。库中的某些类是应用程序的 框架,其目的是协助我们构建应用程序,在提供类或类集的情况下产生每个特定应用程序的基本活动状况。 然后,为我们定制活动状况,必须继承应用程序类并且废弃程序的权益。应用程序框架的默认控制结构将在 特定的时间调用我们废弃的程序。应用程序的框架是“分离、改变和中止事件”的好例子,因为它总是努力 去尝试集中在被废弃的所有特殊程序段。 程序片利用应用程序框架来建立。我们从类中继承程序片,并且废弃特定的程序。大多数时间我们必须考虑 一些不得不运行的使程序片在 W 页面上建立和使用的重要方法。这些方法是: EB 方法 作用 i ni t ( ) 程序片第一次被创建,初次运行初始化程序片时调用 s t ar t ( ) 每当程序片进入 W eb浏览器中,并且允许程序片启动它的常规操作时调用(特殊的程序片被 s t op( ) 关闭);同样在 i ni t ( ) 后调用 pai nt ( ) 基础类 C ponent 的一部分(继承结构中上溯三级)。作为 updat e( ) 的一部分调用,以便对程序片 om 的画布进行特殊的描绘 s t op( ) 每次程序片从 W eb浏览器的视线中离开时调用,使程序片能关闭代价高昂的操作;同样在调用 des t r oy( ) 前调用 des t r oy( ) 程序片不再需要,将它从页面中卸载时调用,以执行资源的最后清除工作 现在来看一看 pai nt ( ) 方法。一旦 C ponent (目前是程序片)决定自己需要更新,就会调用这个方法——可 om 能是由于它再次回转屏幕,首次在屏幕上显示,或者是由于其他窗口临时覆盖了你的 W eb浏览器。此时程序 片会调用它的 updat e( ) 方法(在基础类 C ponent 中定义),该方法会恢复一切该恢复的东西,而调用 om pai nt ( )正是这个过程的一部分。没必要对 pai nt ( ) 进行过载处理,但构建一个简单的程序片无疑是方便的方 法,所以我们首先从 pai nt ( )方法开始。 updat e( ) 调用 pai nt ( ) 时,会向其传递指向 G aphi cs 对象的一个句柄,那个对象代表准备在上面描绘(作 r 图)的表面。这是非常重要的,因为我们受到项目组件的外观的限制,因此不能画到区域外,这可是一件好 事,否则我们就会画到线外去。在程序片的例子中,程序片的外观就是这界定的区域。 图形对象同样有一系列我们可对其进行的操作。这些操作都与在画布上作图有关。所以其中的大部分都要涉 及图像、几何菜状、圆弧等等的描绘(注意如果有兴趣,可在 Java 文档中找到更详细的说明)。有些方法允 许我们画出字符,而其中最常用的就是 dr aw r i ng( )。对于它,需指出自己想描绘的 St r i ng(字串),并指 St 定它在程序片作图区域的起点。这个位置用像素表示,所以它在不同的机器上看起来是不同的,但至少是可 以移植的。 根据这些信息即可创建一个简单的程序片: 379

//: A et 1. j ava ppl // Ver y s i m e appl et pl package c13; i m t j ava. aw . *; por t i m t j ava. appl et . * ; por publ i c cl as s A et 1 ext ends A et { ppl ppl publ i c voi d pai nt ( G aphi cs g) { r g. dr aw r i ng( " Fi r s t appl et " , 10, 10) ; St } } ///: ~ 注意这个程序片不需要有一个 m n( ) 。所有内容都封装到应用程序框架中;我们将所有启动代码都放在 ai i ni t ( )里。 必须将这个程序放到一个 W eb页中才能运行,而只能在支持 Java 的 W eb浏览器中才能看到此页。为了将一个 程序片置入 W eb页,需要在那个 W eb页的代码中设置一个特殊的标记(注释①),以指示网页装载和运行程 序片。这就是 appl et 标记,它在 A et 1 中的样子如下: ppl ①:本书假定读者已掌握了 HTM 的基本知识。这些知识不难学习,有许多书籍和网上资源都可以提供帮助。 L 其中,code 值指定了. cl as s 文件的名字,程序片就驻留在那个文件中。w dt h 和 hei ght 指定这个程序片的 i 初始尺寸(如前所述,以像素为单位)。还可将另一些东西放入 appl et 标记:用于在因特网上寻找其 他. cl as s 文件的位置(codebas e)、对齐和排列信息(al i gn)、使程序片相互间能够通信的一个特殊标识 符(nam e)以及用于提供程序片能接收的信息的参数。参数采取下述形式: nam 可根据需要设置任意多个这样的参数。 在简单的程序片中,我们要做的唯一事情是按上述形式在 W eb页中设置一个程序片标记(appl et ),令其装 载和运行程序片。

1 3 . 2 . 1 程序片的测试
我们可在不必建立网络连接的前提下进行一次简单的测试,方法是启动我们的 W eb浏览器,然后打开包含了 程序片标签的 HTM 文件(Sun公司的 JD 同样包括一个称为“程序片观察器”的工具,它能挑出 ht m 文件 L K l 的标记,并运行这个程序片,不必显示周围的 HTM 文本——注释②)。ht m 文件载入后,浏览器 L l 会发现程序片的标签,并查找由 code 值指定的. cl as s 文件。当然,它会先在 C SSPA LA TH(类路径)中寻找, 如果在 C SSPA LA TH下找不到类文件,就在 W 浏览器状态栏给出一个错误信息,告知不能找到. cl as s 文件。 EB ②;由于程序片观察器会忽略除 A PPLET 标记之外的任何东西,所以可将那些标记作为注释置入 Java 源码: // yA i 这样就可直接执行“appl et vi ew M ppl et . j ava er yA ”,不必再创建小的 HTM 文件来完成测试。 L 若想在 W eb站点上试验,还会碰到另一些麻烦。首先,我们必须有一个 W eb站点,这对大多数人来说都意味 着位于远程地点的一家服务提供商(I SP)。然后必须通过某种途径将 HTM 文件和. cl as s 文件从自己的站点 L 移至 I SP 机器上正确的目录(W W目录)。这一般是通过采用“文件传输协议”(FT P)的程序来做成的,网 W 上可找到许多这样的免费程序。所以我们要做的全部事情似乎就是用 FT P 协议将文件移至 I SP 的机器,然后 用自己的浏览器连接网站和 HTM 文件;假如程序片正确装载和执行,就表明大功告成。但真是这样吗? L 380

但这儿我们可能会受到愚弄。假如 W eb浏览器在服务器上找不到. cl as s 文件,就会在你的本地机器上搜寻 C SSPA LA TH。所以程序片或许根本不能从服务器上正确地装载,但在你看来却是一切正常的,因为浏览器在你 的机器上找到了它需要的东西。但在其他人访问时,他们的浏览器就无法找到那些类文件。所以在测试时, 必须确定已从自己的机器删除了相关的. cl as s 文件,以确保测试结果的真实。 我自己就遇到过这样的一个问题。当时是将程序片置入一个 package(包)中。上载了 HTM 文件和程序片 L 后,由于包名的问题,程序片的服务器路径似乎陷入了混乱。但是,我的浏览器在本地类路径(C SSPA LA TH) 中找到了它。这样一来,我就成了能够成功装载程序片的唯一一个人。后来我花了一些时间才发现原来是 package语句有误。一般地,应该将 package 语句置于程序片的外部。

1 3 . 2 . 2 一个更图形化的例子
这个程序不会太令人紧张,所以让我们试着增加一些有趣的图形组件。 //: A et 2. j ava ppl / / Easy gr aphi cs i m t j ava. aw . *; por t i m t j ava. appl et . * ; por publ i c cl as s A et 2 ext ends A et { ppl ppl publ i c voi d pai nt ( G aphi cs g) { r g. dr aw r i ng( " Second appl et " , 10, 15) ; St g. dr aw Rect ( 0, 0, 100, 20, t r ue) ; 3D } } ///: ~ 这个程序用一个方框将字符串包围起来。当然,所有数字都是“硬编码”的(指数字固定于程序内部),并 以像素为基础。所以在一些机器上,框会正好将字串围住;而在另一些机器上,也许根本看不见这个框,因 为不同机器安装的字体也会有所区别。 对 G aphi c 类而言,可在帮助文档中找到另一些有趣的内容。大多数涉及图形的活动都是很有趣的,所有我 r 将更多的试验留给读者自己去进行。

1 3 . 2 . 3 框架方法的演示
观看框架方法的实际运作是相当有趣的(这个例子只使用 i ni t ( ),s t ar t ( )和 s t op( ),因为 pai nt ( ) 和 des t r oy( ) 非常简单,很容易就能掌握)。下面的程序片将跟踪这些方法调用的次数,并用 pai nt ( ) 将其显示 出来: //: A et 3. j ava ppl // Show i ni t ( ) , s t ar t ( ) and s t op( ) act i vi t i es s i m t j ava. aw . *; por t i m t j ava. appl et . * ; por publ i c cl as s A et 3 ext ends A et { ppl ppl St r i ng s ; i nt i ni t s = 0; i nt s t ar t s = 0; i nt s t ops = 0; publ i c voi d i ni t ( ) { i ni t s ++; } publ i c voi d s t ar t ( ) { s t ar t s ++; } publ i c voi d s t op( ) { s t ops ++; } publ i c voi d pai nt ( G aphi cs g) { r s = " i ni t s : " + i ni t s + " , s t ar t s : " + s t ar t s + 381

" , s t ops : " + s t ops ; g. dr aw r i ng( s , 10, 10) ; St } } ///: ~ 正常情况下,当我们过载一个方法时,需检查自己是否需要调用方法的基础类版本,这是十分重要的。例 如,使用 i ni t ( ) 时可能需要调用 s uper . i ni t ( )。然而,A et 文档特别指出 i ni t ( )、s t ar t ( )和 st op( ) 在 ppl A et 中没有用处,所以这里不需要调用它们。 ppl 试验这个程序片时,会发现假如最小化 W 浏览器,或者用另一个窗口将其覆盖,那么就不能再调用 s t op( ) EB 和 s t ar t ( ) (这一行为会随着不同的实现方案变化;可考虑将 W eb浏览器的行为同程序片观察器的行为对照 一下)。调用唯一发生的场合是在我们转移到一个不同的 W eb页,然后返回包含了程序片的那个页时。

13. 3 制作按钮
制作一个按钮非常简单:只需要调用 But t on 构建器,并指定想在按钮上出现的标签就行了(如果不想要标 签,亦可使用默认构建器,但那种情况极少出现)。可参照后面的程序为按钮创建一个句柄,以便以后能够 引用它。 But t on是一个组件,象它自己的小窗口一样,会在更新时得以重绘。这意味着我们不必明确描绘一个按钮或 者其他任意种类的控件;只需将它们纳入窗体,以后的描绘工作会由它们自行负责。所以为了将一个按钮置 入窗体,需要过载 i ni t ( ) 方法,而不是过载 pai nt ( ): //: But t on1. j ava // Put t i ng but t ons on an appl et i m t j ava. aw . *; por t i m t j ava. appl et . * ; por publ i c cl as s But t on1 ext ends A et { ppl But t on b1 = new But t on( " But t on 1" ) , b2 = new But t on( " But t on 2" ) ; publ i c voi d i ni t ( ) { add( b1) ; add( b2) ; } } ///: ~ 但这还不足以创建 But t on(或其他任何控件)。必须同时调用 A et add( )方法,令按钮放置在程序片的窗 ppl 体中。这看起来似乎比实际简单得多,因为对 add( )的调用实际会(间接地)决定将控件放在窗体的什么地 方。对窗体布局的控件马上就要讲到。

13. 4 捕获事件
大家可注意到假如编译和运行上面的程序片,按下按钮后不会发生任何事情。必须进入程序片内部,编写用 于决定要发生什么事情的代码。对于由事件驱动的程序设计,它的基本目标就是用代码捕获发生的事件,并 由代码对那些事件作出响应。事实上,G 的大部分内容都是围绕这种事件驱动的程序设计展开的。 UI 经过本书前面的学习,大家应该有了面向对象程序设计的一些基础,此时可能会想到应当有一些面向对象的 方法来专门控制事件。例如,也许不得不继承每个按钮,并过载一些“按钮按下”方法(尽管这显得非常麻 烦有有限)。大家也可能认为存在一些主控“事件”类,其中为希望响应的每个事件都包含了一个方法。 在对象以前,事件控制的典型方式是 s w t ch 语句。每个事件都对应一个独一无二的整数编号;而且在主事件 i 控制方法中,需要专门为那个值写一个 s w t ch。 i Java 1. 0 的 A T 没有采用任何面向对象的手段。此外,它也没有使用 s w t ch 语句,没有打算依靠那些分配 W i 给事件的数字。相反,我们必须创建 i f 语句的一个嵌套系列。通过 i f 语句,我们需要尝试做的事情是侦测 到作为事件“目标”的对象。换言之,那是我们关心的全部内容——假如某个按钮是一个事件的目标,那么 382

它肯定是一次鼠标点击,并要基于那个假设继续下去。但是,事件里也可能包含了其他信息。例如,假如想 调查一次鼠标点击的像素位置,以便画一条引向那个位置的线,那么 Event 对象里就会包含那个位置的信息 (也要注意 Java 1. 0 的组件只能产生有限种类的事件,而 Java 1. 1 和 Sw ng/JFC组件则可产生完整的一系 i 列事件)。 Java 1. 0 版的 A T 方法串联的条件语句中存在 act i on( ) 方法的调用。虽然整个 Java 1. 0 版的事件模型不兼 W 容 Java 1. 1 版,但它在还不支持 Java1. 1 版的机器和运行简单的程序片的系统中更广泛地使用,忠告您使用 它会变得非常的舒适,包括对下面使用的 act i on( ) 程序方法而言。 act i on( ) 拥有两个自变量:第一个是事件的类型,包括所有的触发调用 act i on( ) 的事件的有关信息。例如鼠 标单击、普通按键按下或释放、特殊按键按下或释放、鼠标移动或者拖动、事件组件得到或丢失焦点,等 等。第二个自变量通常是我们忽略的事件目标。第二个自变量封装在事件目标中,所以它像一个自变量一样 的冗长。 需调用 act i on( ) 时情况非常有限:将控件置入窗体时,一些类型的控件(按钮、复选框、下拉列表单、菜 单)会发生一种“标准行动”,从而随相应的 Event 对象发起对 act i on( ) 的调用。比如对按钮来说,一旦按 钮被按下,而且没有再多按一次,就会调用它的 act i on( ) 方法。这种行为通常正是我们所希望的,因为这正 是我们对一个按钮正常观感。但正如本章后面要讲到的那样,还可通过 handl eEvent ( ) 方法来处理其他许多 类型的事件。 前面的例程可进行一些扩展,以便象下面这样控制按钮的点击: //: But t on2. j ava // C ur i ng but t on pr es s es apt i m t j ava. aw . *; por t i m t j ava. appl et . * ; por publ i c cl as s But t on2 ext ends A et { ppl But t on b1 = new But t on( " But t on 1" ) , b2 = new But t on( " But t on 2" ) ; publ i c voi d i ni t ( ) { add( b1) ; add( b2) ; } publ i c bool ean act i on( Event evt , O ect ar g) { bj i f ( evt . t ar get . equal s ( b1) ) get A et C ext ( ) . s how at us ( " But t on 1" ) ; ppl ont St el s e i f ( evt . t ar get . equal s ( b2) ) get A et C ext ( ) . s how at us ( " But t on 2" ) ; ppl ont St // Let t he bas e cl as s handl e i t : el s e r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; // W ve handl ed i t her e e' } } ///: ~ 为了解目标是什么,需要向 Event 对象询问它的 t ar get (目标)成员是什么,然后用 equal s ( ) 方法检查它是 否与自己感兴趣的目标对象句柄相符。为所有感兴趣的对象写好句柄后,必须在末尾的 el s e 语句中调用 s uper . act i on( evt , ar g) 方法。我们在第 7 章已经说过(有关多形性的那一章),此时调用的是我们过载过 的方法,而非它的基础类版本。然而,基础类版本也针对我们不感兴趣的所有情况提供了相应的控制代码。 除非明确进行,否则它们是不会得到调用的。返回值指出我们是否已经处理了它,所以假如确实与一个事件 相符,就应返回 t r ue;否则就返回由基础类 event ( )返回的东西。 对这个例子来说,最简单的行动就是打印出到底是什么按钮被按下。一些系统允许你弹出一个小消息窗口, 但 Java 程序片却防碍窗口的弹出。不过我们可以用调用 A et 方法的 get A et C ext ( ) 来访问浏览器, ppl ppl ont 然后用 s how at us ( ) 在浏览器窗口底部的状态栏上显示一条信息(注释③)。还可用同样的方法打印出对事 St 383

件的一段完整说明文字,方法是调用 get A et C ppl onext ( ) . s how at us ( evt + " " ) 。空字串会强制编译器将 St evt 转换成一个字符串。这些报告对于测试和调试特别有用,因为浏览器可能会覆盖我们的消息。 ③:Show at us ( )也属于 A et 的一个方法,所以可直接调用它,不必调用 get A et C ext ( ) 。 St ppl ppl ont 尽管看起来似乎很奇怪,但我们确实也能通过 event ( )中的第二个参数将一个事件与按钮上的文字相配。采 用这种方法,上面的例子就变成了: //: But t on3. j ava // M chi ng event s on but t on t ext at i m t j ava. aw . *; por t i m t j ava. appl et . * ; por publ i c cl as s But t on3 ext ends A et { ppl But t on b1 = new But t on( " But t on 1" ) , b2 = new But t on( " But t on 2" ) ; publ i c voi d i ni t ( ) { add( b1) ; add( b2) ; } publ i c bool ean act i on ( Event evt , O ect ar g) { bj i f ( ar g. equal s ( " But t on 1" ) ) get A et C ext ( ) . s how at us ( " But t on 1" ) ; ppl ont St el s e i f ( ar g. equal s ( " But t on 2" ) ) get A et C ext ( ) . s how at us ( " But t on 2" ) ; ppl ont St // Let t he bas e cl as s handl e i t : el s e r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; // W ve handl ed i t her e e' } } ///: ~ 很难确切知道 equal s ( ) 方法在这儿要做什么。这种方法有一个很大的问题,就是开始使用这个新技术的 Java 程序员至少需要花费一个受挫折的时期来在比较按钮上的文字时发现他们要么大写了要么写错了(我就有这 种经验)。同样,如果我们改变了按钮上的文字,程序代码将不再工作(但我们不会得到任何编译时和运行 时的信息)。所以如果可能,我们就得避免使用这种方法。

13. 5 文本字段
“文本字段”是允许用户输入和编辑文字的一种线性区域。文本字段从文本组件那里继承了让我们选择文 字、让我们像得到字符串一样得到选择的文字,得到或设置文字,设置文本字段是否可编辑以及连同我们从 在线参考书中找到的相关方法。下面的例子将证明文本字段的其它功能;我们能注意到方法名是显而易见 的: //: Text Fi el d1. j ava // Us i ng t he t ext f i el d cont r ol i m t j ava. aw . *; por t i m t j ava. appl et . * ; por publ i c cl as s Text Fi el d1 ext ends A et { ppl But t on b1 = new But t on( " G Text " ) , et 384

b2 = new But t on( " Set Text " ) ; Text Fi el d t = new Text Fi el d( " St ar t i ng t ext " , 30) ; St r i ng s = new St r i ng( ) ; publ i c voi d i ni t ( ) { add( b1) ; add( b2) ; add( t ) ; } publ i c bool ean act i on ( Event evt , O ect ar g) { bj i f ( evt . t ar get . equal s ( b1) ) { get A et C ext ( ) . s how at us ( t . get Text ( ) ) ; ppl ont St s = t . get Sel ect edText ( ) ; i f ( s . l engt h( ) == 0) s = t . get Text ( ) ; t . s et Edi t abl e( t r ue) ; } el s e i f ( evt . t ar get . equal s ( b2) ) { t . s et Text ( " I ns er t ed by But t on 2: " + s ) ; t . s et Edi t abl e( f al s e) ; } // Let t he bas e cl as s handl e i t : el s e r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; // W ve handl ed i t her e e' } } ///: ~ 有几种方法均可构建一个文本字段;其中之一是提供一个初始字符串,并设置字符域的大小。 按下按钮 1 是得到我们用鼠标选择的文字就是得到字段内所有的文字并转换成字符串 S。它也允许字段被编 辑。按下按钮 2 放一条信息和字符串 s 到 Text f i el ds ,并且阻止字段被编辑(尽管我们能够一直选择文 字)。文字的可编辑性是通过 s et Edi t abl e( ) 的真假值来控制的。

13. 6 文本区域
“文本区域”很像文字字段,只是它拥有更多的行以及一些引人注目的更多的功能。另外你能在给定位置对 一个文本字段追加、插入或者修改文字。这看起来对文本字段有用的功能相当不错,所以设法发现它设计的 特性会产生一些困惑。我们可以认为如果我们处处需要“文本区域”的功能,那么可以简单地使用一个线型 文字区域在我们将另外使用文本字段的地方。在 Java 1. 0 版中,当它们不是固定的时候我们也得到了一个文 本区域的垂直和水平方向的滚动条。在 Java 1. 1 版中,对高级构建器的修改允许我们选择哪个滚动条是当前 的。下面的例子演示的仅仅是在 Java1. 0 版的状况下滚动条一直打开。在下一章里我们将看到一个证明 Java 1. 1 版中的文字区域的例程。 //: Text A ea1. j ava r // Us i ng t he t ext ar ea cont r ol i m t j ava. aw . *; por t i m t j ava. appl et . *; por publ i c cl as s Text A ea1 ext ends A et { r ppl But t on b1 = new But t on( " Text A ea 1" ) ; r But t on b2 = new But t on( " Text A ea 2" ) ; r But t on b3 = new But t on( " Repl ace Text " ) ; But t on b4 = new But t on( " I ns er t Text " ) ; Text A ea t 1 = new Text A ea( " t 1" , 1, 30) ; r r 385

Text A ea t 2 = new Text A ea( " t 2" , 4, 30) ; r r publ i c voi d i ni t ( ) { add( b1) ; add( t 1) ; add( b2) ; add( t 2) ; add( b3) ; add( b4) ; } publ i c bool ean act i on ( Event evt , O ect ar g) { bj i f ( evt . t ar get . equal s ( b1) ) get A et C ext ( ) . s how at us ( t 1. get Text ( ) ) ; ppl ont St el s e i f ( evt . t ar get . equal s ( b2) ) { t 2. s et Text ( " I ns er t ed by But t on 2" ) ; t 2. appendText ( " : " + t 1. get Text ( ) ) ; get A et C ext ( ) . s how at us ( t 2. get Text ( ) ) ; ppl ont St } el s e i f ( evt . t ar get . equal s ( b3) ) { St r i ng s = " Repl acem ent " ; t 2. r epl aceText ( s , 3, 3 + s . l engt h( ) ) ; } el s e i f ( evt . t ar get . equal s ( b4) ) t 2. i ns er t Text ( " I ns er t ed " , 10) ; // Let t he bas e cl as s handl e i t : el s e r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; // W ve handl ed i t her e e' } } ///: ~ 程序中有几个不同的“文本区域”构建器,这其中的一个在此处显示了一个初始字符串和行号和列号。不同 的按钮显示得到、追加、修改和插入文字。

13. 7 标签
标签准确地运作:安放一个标签到窗体上。这对没有标签的 Text Fi el ds 和 Text ar eas 来说非常的重要,如 果我们简单地想安放文字的信息在窗体上也能同样的使用。我们能像本章中第一个例程中演示的那样,使用 dr aw r i ng( )里边的 pai nt ( )在确定的位置去安置一个文字。当我们使用的标签允许我们通过布局管理加入 St 其它的文字组件。(在这章的后面我们将进入讨论。) 使用构建器我们能创建一条包括初始化文字的标签(这是我们典型的作法),一个标签包括一行 C TER(中 EN 间)、LEFT (左)和 RI G 右)(静态的结果取整定义在类标签里)。如果我们忘记了可以用 get Text ( ) 和 HT( get al i gnm ( ) 读取值,我们同样可以用 s et Text ( ) 和 s et A i gnm ( ) 来改变和调整。下面的例子将演示标 ent l ent 签的特点: //: Label 1. j ava // Us i ng l abel s i m t j ava. aw . * ; por t i m t j ava. appl et . * ; por publ i c cl as s Label 1 ext ends A et { ppl Text Fi el d t 1 = new Text Fi el d( " t 1" , 10) ; Label l abl 1 = new Label ( " Text Fi el d t 1" ) ; Label l abl 2 = new Label ( "

" ); 386

Label l abl 3 = new Label ( " ", Label . RI G ; HT) But t on b1 = new But t on( " Tes t 1" ) ; But t on b2 = new But t on( " Tes t 2" ) ; publ i c voi d i ni t ( ) { add( l abl 1) ; add( t 1) ; add( b1) ; add( l abl 2) ; add( b2) ; add( l abl 3) ; } publ i c bool ean act i on ( Event evt , O ect ar g) { bj i f ( evt . t ar get . equal s ( b1) ) l abl 2. s et T ext ( " T ext s et i nt o Label " ) ; el s e i f ( evt . t ar get . equal s ( b2) ) { i f ( l abl 3. get Text ( ) . t r i m ) . l engt h( ) == 0) ( l abl 3. s et Text ( " l abl 3" ) ; i f ( l abl 3. get A i gnm ( ) == Label . LEFT) l ent l abl 3. s et A i gnm ( Label . C TER) ; l ent EN el s e i f ( l abl 3. get A i gnm ( ) ==Label . C TER) l ent EN l abl 3. s et A i gnm ( Label . RI G ; l ent HT) el s e i f ( l abl 3. get A i gnm ( ) == Label . RI G l ent HT) l abl 3. s et A i gnm ( Label . LEFT) ; l ent } el s e r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; } } ///: ~ 首先是标签的最典型的用途:标记一个文本字段或文本区域。在例程的第二部分,当我们按下“t es t 1”按 钮通过 s et Text ( ) 将一串空的空格插入到的字段里。因为空的空格数不等于同样的字符数(在一个等比例间 隔的字库里),当插入文字到标签里时我们会看到文字将被省略掉。在例子的第三部分保留的空的空格在我 们第一次按下“t es t 2”会发现标签是空的(t r i m )删除了每个字符串结尾部分的空格)并且在开头的左列 ( 插入了一个短的标签。在工作的其余时间中我们按下按钮进行调整,因此就能看到效果。 我们可能会认为我们可以创建一个空的标签,然后用 s et Text ( ) 安放文字在里面。然而我们不能在一个空标 签内加入文字-这大概是因为空标签没有宽度-所以创建一个没有文字的空标签是没有用处的。在上面的例 子里,“bl ank”标签里充满空的空格,所以它足够容纳后面加入的文字。 同样的,s et A i gnm ( ) 在我们用构建器创建的典型的文字标签上没有作用。这个标签的宽度就是文字的宽 l ent 度,所以不能对它进行任何的调整。但是,如果我们启动一个长标签,然后把它变成短的,我们就可以看到 调整的效果。 这些导致事件连同它们最小化的尺寸被挤压的状况被程序片使用的默认布局管理器所发现。有关布局管理器 的部分包含在本章的后面。

13. 8 复选框
复选框提供一个制造单一选择开关的方法;它包括一个小框和一个标签。典型的复选框有一个小的“X”(或 者它设置的其它类型)或是空的,这依靠项目是否被选择来决定的。 我们会使用构建器正常地创建一个复选框,使用它的标签来充当它的自变量。如果我们在创建复选框后想读 出或改变它,我们能够获取和设置它的状态,同样也能获取和设置它的标签。注意,复选框的大写是与其它 的控制相矛盾的。 无论何时一个复选框都可以设置和清除一个事件指令,我们可以捕捉同样的方法做一个按钮。在下面的例子 里使用一个文字区域枚举所有被选中的复选框: //: C heckBox1. j ava 387

// Us i ng check boxes i m t j ava. aw . *; por t i m t j ava. appl et . * ; por publ i c cl as s C heckBox1 ext ends A et { ppl Text A ea t = new Text A ea( 6, 20) ; r r C heckbox cb1 = new C heckbox( " C heck Box 1" ) ; C heckbox cb2 = new C heckbox( " C heck Box 2" ) ; C heckbox cb3 = new C heckbox( " C heck Box 3" ) ; publ i c voi d i ni t ( ) { add( t ) ; add( cb1) ; add( cb2) ; add( cb3) ; } publ i c bool ean act i on ( Event evt , O ect ar g) { bj i f ( evt . t ar get . equal s ( cb1) ) t r ace( " 1" , cb1. get St at e( ) ) ; el s e i f ( evt . t ar get . equal s ( cb2) ) t r ace( " 2" , cb2. get St at e( ) ) ; el s e i f ( evt . t ar get . equal s ( cb3) ) t r ace( " 3" , cb3. get St at e( ) ) ; el s e r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; } voi d t r ace( St r i ng b, bool ean s t at e) { i f ( s t at e) t . appendText ( " Box " + b + " Set \n" ) ; el s e t . appendText ( " Box " + b + " C ear ed\n" ) ; l } } ///: ~ t r ace( )方法将选中的复选框名和当前状态用 appendText ( )发送到文字区域中去,所以我们看到一个累积的 被选中的复选框和它们的状态的列表。

13. 9 单选钮
单选钮在 G 程序设计中的概念来自于老式的电子管汽车收音机的机械按钮:当我们按下一个按钮时,其它 UI 的按钮就会弹起。因此它允许我们强制从众多选择中作出单一选择。 A T 没有单独的描述单选钮的类;取而代之的是复用复选框。然而将复选框放在单选钮组中(并且修改它的 W 外形使它看起来不同于一般的复选框)我们必须使用一个特殊的构建器象一个自变量一样的作用在 checkboxG oup 对象上。(我们同样能在创建复选框后调用 s et C r heckboxG oup( ) 方法。) r 一个复选框组没有构建器的自变量;它存在的唯一理由就是聚集一些复选框到单选钮组里。一个复选框对象 必须在我们试图显示单选钮组之前将它的状态设置成 t r ue,否则在运行时我们就会得到一个异常。如果我们 设置超过一个的单选钮为 t r ue,只有最后的一个能被设置成真。 这里有个简单的使用单选钮的例子。注意我们可以像其它的组件一样捕捉单选钮的事件: //: Radi oBut t on1. j ava // Us i ng r adi o but t ons i m t j ava. aw . *; por t i m t j ava. appl et . * ; por publ i c cl as s Radi oBut t on1 ext ends A et { ppl Text Fi el d t = 388

new Text Fi e d( " Radi o but t on 2" , 30) ; l C heckboxG oup g = new C r heckboxG oup( ) ; r C heckbox cb1 = new C heckbox( " one" , g, f al s e) , cb2 = new C heckbox( " t w , g, t r ue) , o" cb3 = new C heckbox( " t hr ee" , g, f al s e) ; publ i c voi d i ni t ( ) { t . s et Edi t abl e( f al s e) ; add( t ) ; add( cb1) ; add( cb2) ; add( cb3) ; } publ i c bool ean act i on ( Event evt , O ect ar g) { bj i f ( evt . t ar get . equal s ( cb1) ) t . s et Text ( " Radi o but t on 1" ) ; el s e i f ( evt . t ar get . equal s ( cb2) ) t . s et Text ( " Radi o but t on 2" ) ; el s e i f ( evt . t ar ge . equal s ( cb3) ) t t . s et Text ( " Radi o but t on 3" ) ; el s e r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; } } ///: ~ 显示的状态是一个文字字段在被使用。这个字段被设置为不可编辑的,因为它只是用来显示数据而不是收 集。这演示了一个使用标签的可取之道。注意字段内的文字是由最早选择的单选钮“Radi o but t on 2”初始 化的。 我们可以在窗体中拥有相当多的复选框组。

1 3 . 1 0 下拉列表
下拉列表像一个单选钮组,它是强制用户从一组可实现的选择中选择一个对象的方法。而且,它是一个实现 这点的相当简洁的方法,也最易改变选择而不至使用户感到吃力(我们可以动态地改变单选钮,但那种方法 显然不方便)。Java 的选择框不像 W ndow 中的组合框可以让我从列表中选择或输入自己的选择。在一个选 i s 择框中你只能从列表中选择仅仅一个项目。在下面的例子里,选择框从一个确定输入的数字开始,然后当按 下一个按钮时,新输入的数字增加到框里。你将可以看到选择框的一些有趣的状态: //: C ce1. j ava hoi / / Us i ng dr op dow l i s t s n i m t j ava. aw . *; por t i m t j ava. appl et . * ; por publ i c cl as s C ce1 ext ends A et { hoi ppl St r i ng[ ] des cr i pt i on = { " Ebul l i ent " , " O us e" , bt " Recal ci t r ant " , " Br i l l i ant " , " Som cent " , nes " Ti m ous " , " Fl or i d" , " Put r es cent " } ; or Text Fi el d t = new T ext Fi el d( 30) ; C ce c = new C ce( ) ; hoi hoi But t on b = new But t on( " A i t em " ) ; dd s i nt count = 0; publ i c voi d i ni t ( ) { t . s et Edi t abl e( f al s e) ; f or ( i nt i = 0; i < 4; i ++) 389

c. addI t em des cr i pt i on[ count ++] ) ; ( add( t ) ; add( c) ; add( b) ; } publ i c bool ean act i on ( Event evt , O ect ar g) { bj i f ( evt . t ar get . equal s ( c) ) t . s et Text ( " i ndex: " + c. get Sel ect edI ndex( ) + " " + ( St r i ng) ar g) ; el s e i f ( evt . t ar get . equal s ( b) ) { i f ( count < des cr i pt i on. l engt h) c. addI t em des cr i pt i on[ count ++] ) ; ( } el s e r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; } } ///: ~ 文本字字段中显示的“s el ect ed i ndex, " 也就是当前选择的项目的序列号,在事件中选择的字符串就像 act i on( ) 的第二个自变量的字串符描述的一样好。 运行这个程序片时,请注意对 C ce 框大小的判断:在 w ndow 里,这个大小是在我们拉下列表时确定的。 hoi i s 这意味着如果我们拉下列表,然后增加更多的项目到列表中,这项目将在那,但这个下拉列表不再接受(我 们可以通过项目来滚动观察——注释④)。然而,如果我们在第一次拉下下拉列表前将所的项目装入下拉列 表,它的大小就会合适。当然,用户在使用时希望看到整个的列表,所以会在下拉列表的状态里对增加项目 到选择框里加以特殊的限定。 ④:这一行为显然是一种错误,会 Java 以后的版本里解决。

13. 11 列表框
列表框与选择框有完全的不同,而不仅仅是当我们在激活选择框时的显示不同,列表框固定在屏幕的指定位 置不会改变。另外,一个列表框允许多个选择:如果我们单击在超过一个的项目上,未选择的则表现为高亮 度,我们可以选择象我们想要的一样的多。如果我们想察看项目列表,我们可以调用 get Sel ect edI t em )来 ( 产生一个被选择的项目列表。要想从一个组里删除一个项目,我们必须再一次的单击它。列表框,当然这里 有一个问题就是它默认的动作是双击而不是单击。单击从组中增加或删除项目,双击调用 act i on( ) 。解决这 个问题的方法是象下面的程序假设的一样重新培训我们的用户。 //: Li s t 1. j ava // Us i ng l i s t s w t h act i on( ) i i m t j ava. aw . *; por t i m t j ava. appl et . * ; por publ i c cl as s Li s t 1 ext ends A et { ppl St r i ng[ ] f l avor s = { " C hocol at e" , " St r aw r y" , ber " Vani l l a Fudge Sw r l " , " M nt C p" , i i hi "M ocha A m l ond Fudge" , " Rum Rai s i n" , " Pr al i ne C eam , " M Pi e" } ; r " ud // Show 6 i t em , al l ow m t i pl e s el ect i on: s ul Li s t l s t = new Li s t ( 6, t r ue) ; Text A ea t = new Text A ea( f l avor s . l engt h, 30) ; r r But t on b = new But t on( " t es t " ) ; i nt count = 0; 390

publ i c voi d i ni t ( ) { t . s et Edi t abl e( f al s e) ; f or ( i nt i = 0; i < 4; i ++) l s t . addI t em f l avor s [ count ++] ) ; ( add( t ) ; add( l s t ) ; add( b) ; } publ i c bool ean act i on ( Event evt , O ect ar g) { bj i f ( evt . t ar get . equal s ( l s t ) ) { t . s et Text ( " " ) ; St r i ng[ ] i t em = l s t . get Sel ect edI t em ( ) ; s s f or ( i nt i = 0; i < i t em . l engt h; i ++) s t . appendText ( i t em [ i ] + " \n" ) ; s } el s e i f ( evt . t ar get . equal s ( b) ) { i f ( count < f l avor s . l engt h) l s t . addI t em f l avor s [ count ++] , 0) ; ( } el s e r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; } } ///: ~ 按下按钮时,按钮增加项目到列表的顶部(因为 addI t em ) 的第二个自变量为零)。增加项目到列表框比到 ( 选择框更加的合理,因为用户期望去滚动一个列表框(因为这个原因,它有内建的滚动条)但用户并不愿意 像在前面的例子里不得不去计算怎样才能滚动到要要的那个项目。 然而,调用 act i on( ) 的唯一方法就是通过双击。如果我们想监视用户在我们的列表中的所作所为(尤其是单 击),我们必须提供一个可供选择的方法。

1 3 . 1 1 . 1 handl eE v ent ( )
到目前为止,我们已使用了 act i on( ) ,现有另一种方法 handl eEvent ( ) 可对每一事件进行尝试。当一个事件 发生时,它总是针对单独事件或发生在单独的事件对象上。该对象的 handl eEvent ( ) 方法是自动调用的,并 且是被 handl eEvent ( ) 创建并传递到 handl eEvent ( ) 里。默认的 handl eEvent ( ) (handl eEvent ( ) 定义在组件 里,基础类的所有控件都在 A T 里)将像我们以前一样调用 act i on( ) 或其它同样的方法去指明鼠标的活动、 W 键盘活动或者指明移动的焦点。我们将会在本章的后面部分看到。 如果其它的方法-特别是 act i on( ) -不能满足我们的需要怎么办呢?至于列表框,例如,如果我想捕捉鼠标 单击,但 act i on( ) 只响应双击怎么办呢?这个解答是过载 handl eEvent ( ) ,毕竟它是从程序片中得到的,因 此可以过载任何非确定的方法。当我们为程序片过载 handl eEvent ( ) 时,我们会得到所有的事件在它们发送 出去之前,所以我们不能假设“这里有我的按钮可做的事件,所以我们可以假设按钮被按下了”从它被 act i on( ) 设为真值。在 handl eEvent ( ) 中按钮拥有焦点且某人对它进行分配都是可能的。不论它合理与否, 我们可测试这些事件并遵照 handl e Event ( ) 来进行操作。 为了修改列表样本,使它会响应鼠标的单击,在 act i on( ) 中按钮测试将被过载,但代码会处理的列表将像下 面的例子被移进 handl eEvent ( ) 中去: //: Li s t 2. j ava // Us i ng l i s t s w t h handl eEvent ( ) i i m t j ava. aw . *; por t i m t j ava. appl et . * ; por publ i c cl as s Li s t 2 ext ends A et { ppl 391

St r i ng[ ] f l avor s = { " C hocol at e" , " St r aw r y" , ber " Vani l l a Fudge Sw r l " , " M nt C p" , i i hi "M ocha A m l ond Fudge" , " Rum Rai s i n" , " Pr al i ne C eam , " M Pi e" } ; r " ud // Show 6 i t em , al l ow m t i pl e s el ect i on: s ul Li s t l s t = new Li s t ( 6, t r ue) ; Text A ea t = new Text A ea( f l avor s . l engt h, 30) ; r r But t on b = new But t on( " t es t " ) ; i nt count = 0; publ i c voi d i ni t ( ) { t . s et Edi t abl e( f al s e) ; f or ( i nt i = 0; i < 4; i ++) l s t . addI t em f l avor s [ count ++] ) ; ( add( t ) ; add( l s t ) ; add( b) ; } publ i c bool ean handl eEvent ( Event evt ) { i f ( evt . i d == Event . LI ST_SELEC || T evt . i d == Event . LI ST_D ESELEC { T) i f ( evt . t ar get . equal s ( l s t ) ) { t . s et Text ( " " ) ; St r i ng[ ] i t em = l s t . get Sel ect edI t em ( ) ; s s f or ( i nt i = 0; i < i t em . l engt h; i ++) s t . appendText ( i t em [ i ] + " \n" ) ; s } el s e r et ur n s uper . handl eEvent ( evt ) ; } el s e r et ur n s uper . handl eEvent ( evt ) ; r et ur n t r ue; } publ i c bool ean act i on( Event evt , O ect ar g) { bj i f ( evt . t ar get . equal s ( b) ) { i f ( count < f l avor s . l engt h) l s t . addI t em f l avor s [ count ++] , 0) ; ( } el s e r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; } } ///: ~ 这个例子同前面的例子相同除了增加了 handl eEvent ( ) 外简直一模一样。在程序中做了试验来验证是否列表 框的选择和非选择存在。现在请记住,handl eEvent ( ) 被程序片所过载,所以它能在窗体中任何存在,并且被 其它的列表当成事件来处理。因此我们同样必须通过试验来观察目标。(虽然在这个例子中,程序片中只有 一个列表框所以我们能假设所有的列表框事件必须服务于列表框。这是一个不好的习惯,一旦其它的列表框 加入,它就会变成程序中的一个缺陷。)如果列表框匹配一个我们感兴趣的列表框,像前面的一样的代码将 按上面的策略来运行。注意 handl eEvent ( ) 的窗体与 act i on( ) 的相同:如果我们处理一个单独的事件,将返 回真值,但如果我们对其它的一些事件不感兴趣,通过 handl eEvent ( ) 我们必须返回 s uper . handl eEvent ( ) 值。这便是程序的核心,如果我们不那样做,其它的任何一个事件处理代码也不会被调用。例如,试注解在 392

上面的代码中返回 s uper . handl eEvent ( evt ) 的值。我们将发现 act i on( ) 没有被调用,当然那不是我们想得到 的。对 act i on( ) 和 handl Event ( ) 而言,最重要的是跟着上面例子中的格式,并且当我们自己不处理事件时一 直返回基础类的方法版本信息。(在例子中我们将返回真值)。(幸运的是,这些类型的错误的仅属于 Java 1. 0 版,在本章后面将看到的新设计的 Java 1. 1 消除了这些类型的错误。) 在 w ndow 里,如果我们按下 s hi f t 键,列表框自动允许我们做多个选择。这非常的棒,因为它允许用户做 i s 单个或多个的选择而不是编程期间固定的。我们可能会认为我们变得更加的精明,并且当一个鼠标单击被 evt . s hi f t dow )产生时如果 s hi f t 键是按下的将执行我们自己的试验程序。A T 的设计妨碍了我们-我们不 n( W 得不去了解哪个项目被鼠标点击时是否按下了 s hi f t 键,所以我们能取消其余部分所有的选择并且只选择那 一个。不管怎样,我们是不可能在 Java 1. 0 版中做出来的。(Java 1. 1 将所有的鼠标、键盘、焦点事件传 送到列表中,所以我们能够完成它。)

13. 12 布局的控制
在 Java 里该方法是安一个组件到一个窗体中去,它不同我们使用过的其它 G 系统。首先,它是全代码的; UI 没有控制安放组件的“资源”。其次,该方法的组件被安放到一个被“布局管理器”控制的窗体中,由“布 局管理器”根据我们 add( ) 它们的决定来安放组件。大小,形状,组件位置与其它系统的布局管理器显著的 不同。另外,布局管理器使我们的程序片或应用程序适合窗口的大小,所以,如果窗口的尺寸改变(例如, 在 HTM 页面的程序片指定的规格),组件的大小,形状和位置都会改变。 L 程序片和帧类都是来源于包含和显示组件的容器。(这个容器也是一个组件,所以它也能响应事件。)在容 器中,调用 s et Layout ( )方法允许我选择不同的布局管理器。 在这节里我们将探索不同的布局管理器,并安放按钮在它们之上。这里没有捕捉按钮的事件,正好可以演示 如何布置这些按钮。

1 3 . 1 2 . 1 F l owL ay out
到目前为止,所有的程序片都被建立,看起来使用一些不可思议的内部逻辑来布置它们的组件。那是因为程 序使用一个默认的方式:Fl ow Layout 。这个简单的“Fl ow ”的组件安装在窗体中,从左到右,直到顶部的空 格全部再移去一行,并继续循环这些组件。 这里有一个例子明确地(当然也是多余地)设置一个程序片的布局管理器去 Fl ow Layout ,然后在窗体中安放 按钮。我们将注意到 Fl ow Layout 组件使用它们本来的大小。例如一个按钮将会变得和它的字串符一样的大 小。 //: Fl ow Layout 1. j ava // D ons t r at i ng t he Fl ow em Layout i m t j ava. aw . *; por t i m t j ava. appl et . * ; por publ i c cl as s Fl ow Layout 1 ext ends A et { ppl publ i c voi d i ni t ( ) { s et Layout ( new Fl ow Layout ( ) ) ; f or ( i nt i = 0; i < 20; i ++) add( new But t on( " But t on " + i ) ) ; } } ///: ~ 所有组件将在 Fl ow Layout 中被压缩为它们的最小尺寸,所以我们可能会得到一些奇怪的状态。例如,一个标 签会合适它自已的字符串的尺寸,所以它会右对齐产生一个不变的显示。

1 3 . 1 2 . 2 Bor der L ay out
布局管理器有四边和中间区域的概念。当我们增加一些事物到使用 Bor der Layout 的面板上时我们必须使用 add( ) 方法将一个字符串对象作为它的第一个自变量,并且字符串必须指定(正确的大写)“N t h” or (上),“Sout h”(下),“w t ”(左),“Eas t ”(右)或者“C er ”。如果我们拼写错误或没有大 es ent 写,就会得到一个编译时的错误,并且程序片不会像你所期望的那样运行。幸运的是,我们会很快发现在 393

Java 1. 1 中有了更多改进。 这是一个简单的程序例子: //: Bor der Layout 1. j ava // D ons t r at i ng t he Bor der Layout em i m t j ava. aw . *; por t i m t j ava. appl et . * ; por publ i c cl as s Bor der Layout 1 ext ends A et { ppl publ i c voi d i ni t ( ) { i nt i = 0; s et Layout ( new Bor der Layout ( ) ) ; add( " N t h" , new But t on( " But t on " + i ++) ) ; or add( " Sout h" , new But t on( " But t on " + i ++) ) ; add( " Eas t " , new But t on( " But t on " + i ++) ) ; add( " W t " , new But t on( " But t on " + i ++) ) ; es add( " C er " , new But t on( " But t on " + i ++) ) ; ent } } ///: ~ 除了“C er ”的每一个位置,当元素在其它空间内扩大到最大时,我们会把它压缩到适合空间的最小尺 ent 寸。但是,“C er ”扩大后只会占据中心位置。 ent Bor der Layout 是应用程序和对话框的默认布局管理器。

1 3 . 1 2 . 3 Gr i dL ay out
G i dLayout 允许我们建立一个组件表。添加那些组件时,它们会按从左到右、从上到下的顺序在网格中排 r 列。在构建器里,需要指定自己希望的行、列数,它们将按正比例展开。 //: G i dLayout 1. j ava r // D ons t r at i ng t he G i dLayout em r i m t j ava. aw . *; por t i m t j ava. appl et . * ; por publ i c cl as s G i dLayout 1 ext ends A et { r ppl publ i c voi d i ni t ( ) { s et Layout ( new G i dLayout ( 7, 3) ) ; r f or ( i nt i = 0; i < 20; i ++) add( new But t on( " But t on " + i ) ) ; } } ///: ~ 在这个例子里共有 21 个空位,但却只有 20 个按钮,最后的一个位置作留空处理;注意对 G i dLayout 来说, r 并不存在什么“均衡”处理。

1 3 . 1 2 . 4 Car dL ay out
C dLayout 允许我们在更复杂的拥有真正的文件夹卡片与一条边相遇的环境里创建大致相同于“卡片式对话 ar 框”的布局,我们必须压下一个卡片使不同的对话框带到前面来。在 A T 里不是这样的:C dLayout 是简单 W ar 的空的空格,我们可以自由地把新卡片带到前面来。(JFC i ng库包括卡片式的窗格看起来非常的棒,且 /Sw 可以我们处理所有的细节。) 1. 联合布局(C bi ni ng l a om yout s ) 394

下面的例子联合了更多的布局类型,在最初只有一个布局管理器被程序片或应用程序操作看起来相当的困 难。这是事实,但如果我们创建更多的面板对象,每个面板都能拥有一个布局管理器,并且像被集成到程序 片或应用程序中一样使用程序片或应用程序的布局管理器。这就象下面程序中的一样给了我们更多的灵活 性: //: C dLayout 1. j ava ar // D ons t r at i ng t he C dLayout em ar i m t j ava. aw . *; por t i m t j ava. appl et . A et ; por ppl cl as s But t onPanel ext ends Panel { But t onPanel ( St r i ng i d) { s et Layout ( new Bor der Layout ( ) ) ; add( " C er " , new But t on( i d) ) ; ent } } publ i c cl as s C dLayout 1 ext ends A et { ar ppl But t on f i r s t = new But t on( " Fi r s t " ) , s econd = new But t on( " Second" ) , t hi r d = new But t on( " Thi r d" ) ; Panel car ds = new Panel ( ) ; C dLayout cl = new C dLayout ( ) ; ar ar publ i c voi d i ni t ( ) { s et Layout ( new Bor der Layout ( ) ) ; Panel p = new Panel ( ) ; p. s et Layout ( new Fl ow Layout ( ) ) ; p. add( f i r s t ) ; p. add( s econd) ; p. add( t hi r d) ; add( " N t h" , p) ; or car ds . s et Layout ( cl ) ; car ds . add( " Fi r s t car d" , new But t onPanel ( " The f i r s t one" ) ) ; car ds . add( " Second car d" , new But t onPanel ( " The s econd one" ) ) ; car ds . add( " Thi r d car d" , new But t onPanel ( " The t hi r d one" ) ) ; add( " C er " , car ds ) ; ent } publ i c bool ean act i on( Event evt , O ect ar g) { bj i f ( evt . t ar get . equal s ( f i r s t ) ) { cl . f i r s t ( car ds ) ; } el s e i f ( evt . t ar get . equal s ( s econd) ) { cl . f i r s t ( car ds ) ; cl . next ( car ds ) ; } el s e i f ( evt . t ar get . equal s ( t hi r d) ) { cl . l as t ( car ds ) ; } 395

el s e r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; } } ///: ~ 这个例子首先会创建一种新类型的面板:Bot t onPanel (按钮面板)。它包括一个单独的按钮,安放在 Bor der Layout 的中央,那意味着它将充满整个的面板。按钮上的标签将让我们知道我们在 C dLayout 上的 ar 那个面板上。 在程序片里,面板卡片上将存放卡片和布局管理器 C 因为 C dLayout 必须组成类,因为当我们需要处理卡 L ar 片时我们需要访问这些句柄。 这个程序片变成使用 Bor der Layout 来取代它的默认 Fl ow Layout ,创建面板来容纳三个按钮(使用 Fl ow Layout ),并且这个面板安置在程序片末尾的“N t h”。卡片面板增加到程序片的“C er ”里,有效 or ent 地占据面板的其余地方。 当我们增加 Bot t onPanel s ( 或者任何其它我们想要的组件)到卡片面板时,add( ) 方法的第一个自变量不是 “N t h”,“Sout h”等等。相反的是,它是一个描述卡片的字符串。如果我们想轻击那张卡片使用字符 or 串,我们就可以使用,虽然这字符串不会显示在卡片的任何地方。使用的方法不是使用 act i on( ) ;代之使用 f i r s t ( )、next ( ) 和 l as t ( ) 等方法。请查看我们有关其它方法的文件。 在 Java 中,使用的一些卡片式面板结构十分的重要,因为(我们将在后面看到)在程序片编程中使用的弹出 式对话框是十分令人沮丧的。对于 Java 1. 0 版的程序片而言,C dLayout 是唯一有效的取得很多不同的 ar “弹出式”的窗体。

1 3 . 1 2 . 5 Gr i dBagL ay out
很早以前,人们相信所有的恒星、行星、太阳及月亮都围绕地球公转。这是直观的观察。但后来天文学家变 得更加的精明,他们开始跟踪个别星体的移动,它们中的一些似乎有时在轨道上缓慢运行。因为天文学家知 道所有的天体都围绕地球公转,天文学家花费了大量的时间来讨论相关的方程式和理论去解释天体对象的运 行。当我们试图用 G i dBagLayout 来工作时,我们可以想像自己为一个早期的天文学家。基础的条例是(公 r 告:有趣的是设计者居然在太阳上(这可能是在天体图中标错了位置所致,译者注) )所有的天体都将遵守规 则来运行。哥白尼日新说(又一次不顾嘲讽,发现太阳系内的所有的行星围绕太阳公转。)是使用网络图来 判断布局,这种方法使得程序员的工作变得简单。直到这些增加到 Java 里,我们忍耐(持续的冷嘲热讽)西 班牙的 G i dBagLayout 和 G i dBagC t r ai nt s 狂热宗教。我们建议废止 G i dBagLayout 。取代它的是,使用 r r ons r 其它的布局管理器和特殊的在单个程序里联合几个面板使用不同的布局管理器的技术。我们的程序片看起来 不会有什么不同;至少不足以调整 G i dBagLayout 限制的麻烦。对我而言,通过一个例子来讨论它实在是令 r 人头痛(并且我不鼓励这种库设计)。相反,我建议您从阅读 C nel l 和 Hor s t m 撰写的《核心 Java》 or ann (第二版,Pr ent i ce- Hal l 出版社,1997 年)开始。 在这范围内还有其它的:在 JFC i ng库里有一个新的使用 Sm l t al k 的受人欢迎的“Spr i ng and St r ut s ” /Sw al 布局管理器并且它能显著地减少 G i dBagLayout 的需要。 r

13. 13 act i on 的替代品
正如早先指出的那样,act i on( ) 并不是我们对所有事进行分类后自动为 handl eEvent ( ) 调用的唯一方法。有 三个其它的被调用的方法集,如果我们想捕捉某些类型的事件(键盘、鼠标和焦点事件),因此我们不得不 过载规定的方法。这些方法是定义在基础类组件里,所以他们几乎在所有我们可能安放在窗体中的组件中都 是有用的。然而,我们也注意到这种方法在 Java 1. 1 版中是不被支持的,同样尽管我们可能注意到继承代码 利用了这种方法,我们将会使用 Java 1. 1 版的方法来代替(本章后面有详细介绍)。 组件方法 何时调用 act i on( Event evt , O ect w ) 当典型的事件针对组件发生(例如,当按下一个按钮或下拉列表项目被选 bj hat 中)时调用 keyD n( Event evt , i nt key) 当按键被按下,组件拥有焦点时调用。第二个自变量是按下的键并且是冗余 ow 的是从 evt . key 处复制来的 396

keyup( Event evt , i nt key) 当按键被释放,组件拥有焦点时调用 l os t Focus ( Event evt , O ect w ) 焦点从目标处移开时调用。通常,w 是从 evt . ar g里冗余复制的 bj hat hat got Focus ( Event evt , O ect w ) 焦点移动到目标时调用 bj hat m eD n( Event evt , i nt x ,i nt y) 一个鼠标按下存在于组件之上,在 X,Y 座标处时调用 ous ow m eUp( Event evt , i nt x, i nt y) 一个鼠标升起存在于组件之上时调用 ous m eM ous ove( Event evt , i nt x, i nt y) 当鼠标在组件上移动时调用 m eD ag( Event evt , i nt x, i nt y) 鼠标在一次 m eD n 事件发生后拖动。所有拖动事件都会报告给 ous r ous ow 内部发生了 m eD n 事件的那个组件,直到遇到一次 m eUp为止 ous ow ous m eEnt er ( Event evt , i nt x, i nt y) 鼠标从前不在组件上方,但目前在 ous m eExi t ( Event evt , i nt x, i nt y) 鼠标曾经位于组件上方,但目前不在 ous 当我们处理特殊情况时——一个鼠标事件,例如,它恰好是我们想得到的鼠标事件存在的座标,我们将看到 每个程序接收一个事件连同一些我们所需要的信息。有趣的是,当组件的 handl eEvent ( ) 调用这些方法时 (典型的事例),附加的自变量总是多余的因为它们包含在事件对象里。事实上,如果我们观察 com ponent . handl eEvent ( ) 的源代码,我们能发现它显然将增加的自变量抽出事件对象(这可能是考虑到在一 些语言中无效率的编码,但请记住 Java 的焦点是安全的,不必担心。)试验对我们表明这些事件事实上在被 调用并且作为一个有趣的尝试是值得创建一个过载每个方法的程序片,(act i on( ) 的过载在本章的其它地 方)当事件发生时显示它们的相关数据。 这个例子同样向我们展示了怎样制造自己的按钮对象,因为它是作为目标的所有事件权益来使用。我可能会 首先(也是必须的)假设制造一个新的按钮,我们从按钮处继承。但它并不能运行。取而代之的是,我们从 画布组件处(一个非常普通组件)继承,并在其上不使用 pai nt ( )方法画出一个按钮。正如我们所看到的, 自从一些代码混入到画按钮中去,按钮根本就不运行,这实在是太糟糕了。(如果您不相信我,试图在例子 中为画布组件交换按钮,请记住调用称为 s uper 的基础类构建器。我们会看到按钮不会被画出,事件也不会 被处理。) m yBut t on 类是明确说明的:它只和一个自动事件(A oEvent )“父窗口”一起运行(父窗口不是一个基础 ut 类,它是按钮创建和存在的窗口。)。通过这个知识,m yBut t on 可能进入到父窗口并且处理它的文字字段, 必然就能将状态信息写入到父窗口的字段里。当然这是一种非常有限的解决方法,m yBut t on 仅能在连结 A oEvent 时被使用。这种代码有时称为“高度结合”。但是,制造 m ut yBut t on 更需要很多的不是为例子(和 可能为我们将写的一些程序片)担保的努力。再者,请注意下面的代码使用了 Java 1. 1 版不支持的 A 。 PI //: A oEvent . j ava ut // A t er nat i ves t o act i on( ) l i m t j ava. aw . *; por t i m t j ava. appl et . * ; por i m t j ava. ut i l . *; por cl as s M yBut t on ext ends C anvas { A oEvent par ent ; ut C or col or ; ol St r i ng l abel ; M yBut t on( A oEvent par ent , ut C or col or , St r i ng l abel ) { ol t hi s . l abel = l abel ; t hi s . par ent = par ent ; t hi s . col or = col or ; } publ i c voi d pai nt ( G aphi cs g) { r g. s et C or ( col or ) ; ol i nt r nd = 30; g. f i l l RoundRect ( 0, 0, s i ze( ) . w dt h, i s i ze( ) . hei ght , r nd, r nd) ; g. s et C or ( C or . bl ack) ; ol ol 397

g. dr aw RoundRect ( 0, 0, s i ze( ) . w dt h, i s i ze( ) . hei ght , r nd, r nd) ; Font M r i cs f m = g. get Font M r i cs ( ) ; et et i nt w dt h = f m s t r i ngW dt h( l abel ) ; i . i i nt hei ght = f m get Hei ght ( ) ; . i nt as cent = f m get A cent ( ) ; . s i nt l eadi ng = f m get Leadi ng( ) ; . i nt hor i zM gi n = ( s i ze( ) . w dt h - w dt h) /2; ar i i i nt ver M gi n = ( s i ze( ) . hei ght - hei ght ) /2; ar g. s et C or ( C or . w t e) ; ol ol hi g. dr aw r i ng( l abel , hor i zM gi n, St ar ver M gi n + as cent + l eadi ng) ; ar } publ i c bool ean keyD n( Event evt , i nt key) { ow Text Fi el d t = ( Text Fi el d) par ent . h. get ( " keyD n" ) ; ow t . s et Text ( evt . t oSt r i ng( ) ) ; r et ur n t r ue; } publ i c bool ean keyUp( Event evt , i nt key) { Text Fi el d t = ( Text Fi el d) par ent . h. get ( " keyUp" ) ; t . s et Text ( evt . t oSt r i ng( ) ) ; r et ur n t r ue; } publ i c bool ean l os t Focus ( Event evt , O ect w { bj ) Text Fi el d t = ( Text Fi el d) par ent . h. get ( " l os t Focus " ) ; t . s et Text ( evt . t oSt r i ng( ) ) ; r et ur n t r ue; } publ i c bool ean got Focus ( Event evt , O ect w { bj ) Text Fi el d t = ( Text Fi el d) par ent . h. get ( " got Focus " ) ; t . s et Text ( evt . t oSt r i ng( ) ) ; r et ur n t r ue; } publ i c bool ean m eD n( Event evt , i nt x, i nt y) { ous ow Text Fi el d t = ( Text Fi el d) par ent . h. get ( " m eD n" ) ; ous ow t . s et Text ( evt . t oSt r i ng( ) ) ; r et ur n t r ue; } publ i c bool ean m eD ag( Event evt , i nt x, i nt y) { ous r Text Fi el d t = ( Text Fi el d) par ent . h. get ( " m eD ag" ) ; ous r t . s et Text ( evt . t oSt r i ng( ) ) ; r et ur n t r ue; } publ i c bool ean 398

m eEnt er ( Event evt , i nt x, i nt y) { ous Text Fi el d t = ( Text Fi el d) par ent . h. get ( " m eEnt er " ) ; ous t . s et Text ( evt . t oSt r i ng( ) ) ; r et ur n t r ue; } publ i c bool ean m eExi t ( Event evt , i nt x, i nt y) { ous Text Fi el d t = ( Text Fi el d) par ent . h. get ( " m eExi t " ) ; ous t . s et Text ( evt . t oSt r i ng( ) ) ; r et ur n t r ue; } publ i c bool ean m eM ous ove( Event evt , i nt x, i nt y) { Text Fi el d t = ( Text Fi el d) par ent . h. get ( " m eM ous ove" ) ; t . s et Text ( evt . t oSt r i ng( ) ) ; r et ur n t r ue; } publ i c bool ean m eUp( Event evt , i nt x, i nt y) { ous Text Fi el d t = ( Text Fi el d) par ent . h. get ( " m eUp" ) ; ous t . s et Text ( evt . t oSt r i ng( ) ) ; r et ur n t r ue; } } publ i c cl as s A oEvent ext ends A et { ut ppl Has ht abl e h = new Has ht abl e( ) ; St r i ng[ ] event = { " keyD n" , " keyUp" , " l os t Focus " , ow " got Focus " , " m eD n" , " m eUp" , ous ow ous " m eM ous ove" , " m eD ag" , " m eEnt er " , ous r ous " m eExi t " ous }; M yBut t on b1 = new M yBut t on( t hi s , C or . bl ue, " t es t 1" ) , ol b2 = new M yBut t on( t hi s , C or . r ed, " t es t 2" ) ; ol publ i c voi d i ni t ( ) { s et Layout ( new G i dLayout ( event . l engt h+1, 2) ) ; r f or ( i nt i = 0; i < event . l engt h; i ++) { Text Fi el d t = new Text Fi el d( ) ; t . s et Edi t abl e( f al s e) ; add( new Label ( event [ i ] , Label . C TER) ) ; EN add( t ) ; h. put ( event [ i ] , t ) ; } add( b1) ; add( b2) ; }

399

} ///: ~ 我们可以看到构建器使用利用自变量同名的方法,所以自变量被赋值,并且使用 t hi s 来区分: t hi s . l abel = l abel ; pai nt ( )方法由简单的开始:它用按钮的颜色填充了一个“圆角矩形”,然后画了一个黑线围绕它。请注意 s i z e( )的使用决定了组件的宽度和长度(当然,是像素)。这之后,pai nt ( ) 看起来非常的复杂,因为有大量 的预测去计算出怎样利用“f ont m r i cs”集中按钮的标签到按钮里。我们能得到一个相当好的关于继续关 et 注方法调用的主意,它将程序中那些相当平凡的代码挑出,当我们想集中一个标签到一些组件里时,我们正 好可以对它进行剪切和粘贴。 您直到注意到 A oEvent 类才能正确地理解 keyD n( ) , keyUp( )及其它方法的运行。这包含一个 Has ht abl e ut ow (译者注:散列表)去控制字符串来描述关于事件处理的事件和 Text Fi el d类型。当然,这些能被静态的创 建而不是放入 Has ht abl e 但我认为您会同意它是更容易使用和改变的。特别是,如果我们需要在 A oEvent ut 中增加或删除一个新的事件类型,我们只需要简单地在事件列队中增加或删除一个字符串——所有的工作都 自动地完成了。 我们查出在 keyD n( ) ,keyup( ) 及其它方法中的字符串的位置回到 m ow yBut t on 中。这些方法中的任何一个都 用父句柄试图回到父窗口。父类是一个 A oEvent ,它包含 Has ht abl e h 和 get ( ) 方法,当拥有特定的字符串 ut 时,将对一个我们知道的 Text Fi el d对象产生一个句柄(因此它被选派到那)。然后事件对象修改显示在 Text Fi el d中的字符串陈述。从我们可以真正注意到举出的例子在我们的程序中运行事件时以来,可以发现 这个例子运行起来颇为有趣的。

13. 14 程序片的局限
出于安全缘故,程序片十分受到限制,并且有很多的事我们都不能做。您一般会问:程序片看起来能做什 么,传闻它又能做什么:扩展浏览器中 W 页的功能。自从作为一个网上冲浪者,我们从未真正想了解是否 EB 一个 W 页来自友好的或者不友好的站点,我们想要一些可以安全地行动的代码。所以我们可能会注意到大 EB 量的限制: ( 1) 一个程序片不能接触到本地的磁盘。这意味着不能在本地磁盘上写和读,我们不想一个程序片通过 W EB 页面阅读和传送重要的信息。写是被禁止的,当然,因为那将会引起病毒的侵入。当数字签名生效时,这些 限制会被解除。 ( 2) 程序片不能拥有菜单。(注意:这是规定在 Sw ng中的)这可能会减少关于安全和关于程序简化的麻 i 烦。我们可能会接到有关程序片协调利益以作为 W 页面的一部分的通知;而我们通常不去注意程序片的范 EB 围。这儿没有帧和标题条从菜单处弹出,出现的帧和标题条是属于 W 浏览器的。也许将来设计能被改变成 EB 允许我们将浏览器菜单和程序片菜单相结合起来——程序片可以影响它的环境将导致太危及整个系统的安全 并使程序片过于的复杂。 ( 3) 对话框是不被信任的。在 Java 中,对话框存在一些令人难解的地方。首先,它们不能正确地拒绝程序 片,这实在是令人沮丧。如果我们从程序片弹出一个对话框,我们会在对话框上看到一个附上的消息框“不 被信任的程序片”。这是因为在理论上,它有可能欺骗用户去考虑他们在通过 W 同一个老顾客的本地应用 EB 程序交易并且让他们输入他们的信用卡号。在看到 A T 开发的那种 G 后,我们可能会难过地相信任何人都 W UI 会被那种方法所愚弄。但程序片是一直附着在一个 W eb页面上的,并可以在浏览器中看到,而对话框没有这 种依附关系,所以理论上是可能的。因此,我们很少会见到一个使用对话框的程序片。 在较新的浏览器中,对受到信任的程序片来说,许多限制都被放宽了(受信任程序片由一个信任源认证)。 涉及程序片的开发时,还有另一些问题需要考虑: ■程序片不停地从一个适合不同类的单独的服务器上下载。我们的浏览器能够缓存程序片,但这没有保证。 在 Java 1. 1 版中的一个改进是 JA R(Java A Rchi ve )文件,它允许将所有的程序片组件(包括其它的类文 件、图像、声音)一起打包到一个的能被单个服务器处理下载的压缩文件。“数字签字”(能校验类创建 器)可有效地加入每个单独的 JA 文件。 R ■因为安全方面的缘故,我们做某些工作更加困难,例如访问数据库和发送电子邮件。另外,安全限制规则 使访问多个主机变得非常的困难,因为每一件事都必须通过 W 服务器路由,形成一个性能瓶颈,并且单一 EB 环节的出错都会导致整个处理的停止。 ■浏览器里的程序片不会拥有同样的本地应用程序运行的控件类型。例如,自从用户可以开关页面以来,在 程序片中不会拥有一个形式上的对话框。当用户对一个 W 页面进行改变或退出浏览器时,对我们的程序片 EB 而言简直是一场灾难——这时没有办法保存状态,所以如果我们在处理和操作中时,信息会被丢失。另外, 当我们离开一个 W 页面时,不同的浏览器会对我们的程序片做不同的操作,因此结果本来就是不确定的。 EB 400

1 3 . 1 4 . 1 程序片的优点
如果能容忍那些限制,那么程序片的一些优点也是非常突出的,尤其是在我们构建客户/服务器应用或者其 它网络应用时: ■没有安装方面的争议。程序片拥有真正的平台独立性(包括容易地播放声音文件等能力)所以我们不需要 针对不同的平台修改代码也不需要任何人根据安装运行任何的“t w eaki ng”。事实上,安装每次自动地将 W 页连同程序片一起,因此安静、自动地更新。在传统的客户机/ 服务器系统中,建立和安装一个新版本的 EB 客户端软件简直就是一场恶梦。 ■因为安全的原因创建在核心 Java 语言和程序片结构中,我们不必担心坏的代码而导致毁坏某人的系统。这 样,连同前面的优点,可使用 Java(可从 JavaScr i pt 和 VBScr i pt 中选择客户端的 W 编程工具)为所谓的 EB I nt r ant (在公司内部使用而不向 I nt er net 转移的企业内部网络)客户机/服务器开发应用程序。 ■由于程序片是自动同 HTM 集成的,所以我们有一个内建的独立平台文件系统去支持程序片。这是一个很有 L 趣的方法,因为我们惯于拥有程序文件的一部分而不是相反的拥有文件系统。

13. 15 视窗化应用
出于安全的缘故,我们会看到在程序片我们的行为非常的受到限制。我们真实地感到,程序片是被临时地加 入在 W 浏览器中的,因此,它的功能连同它的相关知识,控件都必须加以限制。但是,我们希望 Java 能制 EB 造一个开窗口的程序去运行一些事物,否则宁愿安放在一个 W 页面上,并且也许我们希望它可以运行一些 EB 可靠的应用程序,以及夸张的实时便携性。在这本书前面的章节中我们制造了一些命令行应用程序,但在一 些操作环境中(例如:M nt os h)没有命令行。所以我们有很多的理由去利用 Java 创建一个设置窗口,非 aci 程序片的程序。这当然是一个十分合理的要求。 一个 Java 设置窗口应用程序可以拥有菜单和对话框(这对一个程序片来说是不可能的和很困难的),可是如 果我们使用一个老版本的 Java,我们将会牺牲本地操作系统环境的外观和感受。JFC i ng库允许我们制造 /Sw 一个保持原来操作系统环境的外观和感受的应用程序。如果我们想建立一个设置窗口应用程序,它会合理地 运作,同样,如果我们可以使用最新版本的 Java 并且集合所有的工具,我们就可以发布不会使用户困惑的应 用程序。如果因为一些原因,我们被迫使用老版本的 Java,请在毁坏以建立重要的设置窗口的应用程序前仔 细地考虑。

1 3 . 1 5 . 1 菜单
直接在程序片中安放一个菜单是不可能的(Java 1. 0, Java1. 1 和 Sw ng库不允许),因为它们是针对应用程 i 序的。继续,如果您不相信我并且确定在程序片中可以合理地拥有菜单,那么您可以去试验一下。程序片中 没有 s et M enuBar ( )方法,而这种方法是附在菜单中的(我们会看到它可以合理地在程序片产生一个帧,并且 帧包含菜单)。 有四种不同类型的 M enuC ponent (菜单组件),所有的菜单组件起源于抽象类:菜单条(我们可以在一个 om 事件帧里拥有一个菜单条),菜单去支配一个单独的下拉菜单或者子菜单、菜单项来说明菜单里一个单个的 元素,以及起源于 M enuI t em 产生检查标志(checkm k)去显示菜单项是否被选择的 C , ar heckBoxM enuI t em 。 不同的系统使用不同的资源,对 Java 和 A T 而言,我们必须在源代码中手工汇编所有的菜单。 W //: M enu1. j ava // M enus w k onl y w t h Fr am . or i es // Show s ubm s enus , checkbox m enu i t em s // and s w appi ng m enus . i m t j ava. aw . *; por t publ i c cl as s M enu1 ext ends Fr am { e St r i ng[ ] f l avor s = { " C hocol at e" , " St r aw r y" , ber " Vani l l a Fudge Sw r l " , " M nt C p" , i i hi "M ocha A m l ond Fudge" , " Rum Rai s i n" , " Pr al i ne C eam , " M Pi e" } ; r " ud Text Fi el d t = new Text Fi el d( " N f l avor " , 30) ; o M enuBar m = new M b1 enuBar ( ) ; M enu f = new M enu( " Fi l e" ) ; 401

M enu m = new M enu( " Fl avor s " ) ; M enu s = new M enu(" Saf et y" ) ; // A t er nat i ve appr oach: l C heckboxM enuI t em ] s af et y = { [ new C heckboxM enuI t em " G d" ) , ( uar new C heckboxM enuI t em " Hi de" ) ( }; M enuI t em ] f i l e = { [ new M enuI t em " O ( pen" ) , new M enuI t em " Exi t " ) ( }; // A s econd m enu bar t o s w t o: ap M enuBar m = new M b2 enuBar ( ) ; M enu f ooBar = new M enu( " f ooBar " ) ; M enuI t em ] ot her = { [ new M enuI t em " Foo" ) , ( new M enuI t em " Bar " ) , ( new M enuI t em " Baz" ) , ( }; But t on b = new But t on( " Sw M ap enus " ) ; publ i c M enu1( ) { f or ( i nt i = 0; i < f l avor s . l engt h; i ++) { m add( new M . enuI t em f l avor s [ i ] ) ) ; ( // A s epar at or s at i nt er val s : dd i f ( ( i +1) % 3 == 0) m addSepar at or ( ) ; . } f or ( i nt i = 0; i < s af et y. l engt h; i ++) s . add( s af et y[ i ] ) ; f . add( s ) ; f or ( i nt i = 0; i < f i l e. l engt h; i ++) f . add( f i l e[ i ] ) ; m add( f ) ; b1. m add( m ; b1. ) s et M enuBar ( m ; b1) t . s et Edi t abl e( f al s e) ; add( " C er " , t ) ; ent // Set up t he s ys t em f or s w appi ng m enus : add( " N t h" , b) ; or f or ( i nt i = 0; i < ot her . l engt h; i ++) f ooBar . add( ot her [ i ] ) ; m add( f ooBar ) ; b2. } publ i c bool ean handl eEvent ( Event evt ) { i f ( evt . i d == Event . W N O _D I D W ESTRO Y) Sys t em exi t ( 0) ; . el s e r et ur n s uper . handl eEvent ( evt ) ; r et ur n t r ue; } publ i c bool ean act i on( Event evt , O ect ar g) { bj i f ( evt . t ar get . equal s ( b) ) { 402

M enuBar m = get M enuBar ( ) ; i f ( m == m b1) s et M enuBar ( m ; b2) el s e i f ( m == m b2) s et M enuBar ( m ; b1) } el s e i f ( evt . t ar get i ns t anceof M enuI t em { ) i f ( ar g. equal s ( " O pen" ) ) { St r i ng s = t . get Text ( ) ; bool ean chos en = f al s e; f or ( i nt i = 0; i < f l avor s . l engt h; i ++) i f ( s . equal s ( f l avor s [ i ] ) ) chos en = t r ue; i f ( ! chos en) t . s et Text ( " C hoos e a f l avor f i r s t ! " ) ; el s e t . s et Text ( " O peni ng " + s +" . M m m ! " ) ; m, m } el s e i f ( evt . t ar get . equal s ( f i l e[ 1] ) ) Sys t em exi t ( 0) ; . // C heckboxM enuI t em cannot us e St r i ng s // m chi ng; you m t m ch t he t ar get : at us at el s e i f ( evt . t ar get . equal s ( s af et y[ 0] ) ) t . s et Text ( " G d t he I ce C eam " + uar r ! " G di ng i s " + s af et y[ 0] . get St at e( ) ) ; uar el s e i f ( evt . t ar get . equal s ( s af et y[ 1] ) ) t . s et Text ( " Hi de t he I ce C eam " + r ! " I s i t col d? " + s af et y[ 1] . get St at e( ) ) ; el s e t . s et Text ( ar g. t oSt r i ng( ) ) ; } el s e r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai M enu1 f = new M enu1( ) ; f . r es i ze( 300, 200) ; f . s how ) ; ( } } ///: ~ 在这个程序中,我避免了为每个菜单编写典型的冗长的 add( ) 列表调用,因为那看起来像许多的无用的标 志。取而代之的是,我安放菜单项到数组中,然后在一个 f or 的循环中通过每个数组调用 add( ) 简单地跳 过。这样的话,增加和减少菜单项变得没那么讨厌了。 作为一个可选择的方法(我发现这很难令我满意,因为它需要更多的分配)C heckboxM enuI t em 在数组的句 s 柄中被创建是被称为安全创建;这对数组文件和其它的文件而言是真正的安全。 程序中创建了不是一个而是二个的菜单条来证明菜单条在程序运行时能被交换激活。我们可以看到菜单条怎 样组成菜单,每个菜单怎样组成菜单项(M enuI t em ),chenkboxM s enuI t em 或者其它的菜单(产生子菜 s 单)。当菜单组合后,可以用 s et M enuBar ( ) 方法安装到现在的程序中。值得注意的是当按钮被压下时,它将 检查当前的菜单安装使用 get M enuBar ( ) ,然后安放其它的菜单条在它的位置上。 当测试是“open”(即开始)时,注意拼写和大写,如果开始时没有对象,Java 发出 no er r or (没有错误) 的信号。这种字符串比较是一个明显的程序设计错误源。 校验和非校验的菜单项自动地运行,与之相关的 C heckBoxM enuI t em 着实令人吃惊,这是因为一些原因它们 s 不允许字符串匹配。(这似乎是自相矛盾的,尽管字符串匹配并不是一种很好的办法。)因此,我们可以匹 403

配一个目标对象而不是它们的标签。当演示时,get St at e( ) 方法用来显示状态。我们同样可以用 s et St at e( ) 改变 C heckboxM enuI t em 的状态。 我们可能会认为一个菜单可以合理地置入超过一个的菜单条中。这看似合理,因为所有我们忽略的菜单条的 add( ) 方法都是一个句柄。然而,如果我们试图这样做,这个结果将会变得非常的别扭,而远非我们所希望得 到的结果。(很难知道这是一个编程中的错误或者说是他们试图使它以这种方法去运行所产生的。)这个例 子同样向我们展示了为什么我们需要建立一个应用程序以替代程序片。(这是因为应用程序能支持菜单,而 程序片是不能直接使用菜单的。)我们从帧处继承代替从程序片处继承。另外,我们为类建一个构建器以取 代 i ni t ( ) 安装事件。最后,我们创建一个 m n( ) 方法并且在我们建的新型对象里,调整它的大小,然后调用 ai s how )。它与程序片只在很小的地方有不同之处,然而这时它已经是一个独立的设置窗口应用程序并且我们 ( 可以使用菜单。

1 3 . 1 5 . 2 对话框
对话框是一个从其它窗口弹出的窗口。它的目的是处理一些特殊的争议和它们的细节而不使原来的窗口陷入 混乱之中。对话框大量在设置窗口的编程环境中使用,但就像前面提到的一样,鲜于在程序片中使用。 我们需要从对话类处继承以创建其它类型的窗口、像帧一样的对话框。和窗框不同,对话框不能拥有菜单条 也不能改变光标,但除此之外它们十分的相似。一个对话框拥有布局管理器(默认的是 Bor der Layout 布局管 理器)和过载 act i on( ) 等等,或用 handl eEvent ( ) 去处理事件。我们会注意到 handl eEvent ( ) 的一个重要差 异:当 W N O _D I D W ESTO 事件发生时,我们并不希望关闭正在运行的应用程序! RY 相反,我们可以使用对话窗口通过调用 di s pace( ) 释放资源。在下面的例子中,对话框是由定义在那儿作为 类的 ToeBut t on 的特殊按钮组成的网格构成的(利用 G i dLayout 布局管理器)。ToeBut t on 按钮围绕它自已 r 画了一个帧,并且依赖它的状态:在空的中的“X”或者“O”。它从空白开始,然后依靠使用者的选择, 转换成“X”或“O”。但是,当我们单击在按钮上时,它会在“X”和“O”之间来回交换。(这产生了 一种类似填字游戏的感觉,当然比它更令人讨厌。)另外,这个对话框可以被设置为在主应用程序窗口中为 很多的行和列变更号码。 //: ToeTes t . j ava // D ons t r at i on of di al og boxes em // and cr eat i ng your ow com n ponent s i m t j ava. aw . *; por t cl as s ToeBut t on ext ends C anvas { i nt s t at e = ToeD al og. BLA K; i N ToeD al og par ent ; i ToeBut t on( ToeD al og par ent ) { i t hi s . par ent = par ent ; } publ i c voi d pai nt ( G aphi cs g) { r i nt x1 = 0; i nt y1 = 0; i nt x2 = s i ze( ) . w dt h - 1; i i nt y2 = s i ze( ) . hei ght - 1; g. dr aw Rect ( x1, y1, x2, y2) ; x1 = x2/4; y1 = y2/4; i nt w de = x2/2; i i nt hi gh = y2/2; i f ( s t at e == ToeD al og. XX) { i g. dr aw ne( x1, y1, x1 + w de, y1 + hi gh) ; Li i g. dr aw ne( x1, y1 + hi gh, x1 + w de, y1) ; Li i } i f ( s t at e == ToeD al og. O ) { i O g. dr aw val ( x1, y1, x1+w de/2, y1+hi gh/2) ; O i 404

} } publ i c bool ean m eD n( Event evt , i nt x, i nt y) { ous ow i f ( s t at e == ToeD al og. BLA K) { i N s t at e = par ent . t ur n; par ent . t ur n= ( par ent . t ur n == ToeD al og. XX ? i ToeD al og. O : ToeD al og. XX) ; i O i } el s e s t at e = ( s t at e == ToeD al og. XX ? i ToeD al og. O : ToeD al og. XX) ; i O i r epai nt ( ) ; r et ur n t r ue; } } cl as s ToeD al og ext ends D al og { i i // w = num ber of cel l s w de i // h = num ber of cel l s hi gh s t at i c f i nal i nt BLA K = 0; N s t at i c f i nal i nt XX = 1; s t at i c f i nal i nt O = 2; O i nt t ur n = XX; // St ar t w t h x' s t ur n i publ i c ToeD al o Fr am par ent , i nt w i nt h) { i g( e , s uper ( par ent , " The gam i t s el f " , f al s e) ; e s et Layout ( new G i dLayout ( w h) ) ; r , f or ( i nt i = 0; i < w * h; i ++) add( new ToeBut t on( t hi s ) ) ; r es i ze( w * 50, h * 50) ; } publ i c bool ean handl eEvent ( Event evt ) { i f ( evt . i d == Event . W N O _D I D W ESTRO Y) di s pos e( ) ; el s e r et ur n s uper . handl eEvent ( evt ) ; r et ur n t r ue; } } publ i c cl as s ToeTes t ext ends Fr am { e Text Fi el d r ow = new Text Fi el d( " 3" ) ; s Text Fi el d col s = new Text Fi el d( " 3" ) ; publ i c ToeTes t ( ) { s et Ti t l e( " Toe Tes t " ) ; Panel p = new Panel ( ) ; p. s et Layout ( new G i dLayout ( 2, 2) ) ; r p. add( new Label ( " Row " , Label . C TER) ) ; s EN p. add( r ow ) ; s p. add( new Label ( " C um " , Label . C TER) ) ; ol ns EN p. add( col s ) ; add( " N t h" , p) ; or 405

add( " Sout h" , new But t on( " go" ) ) ; } publ i c bool ean handl eEvent ( Event evt ) { i f ( evt . i d == Event . W N O _D I D W ESTRO Y) Sys t em exi t ( 0) ; . el s e r et ur n s uper . handl eEvent ( evt ) ; r et ur n t r ue; } publ i c bool ean act i on( Event evt , O ect ar g) { bj i f (ar g. equal s ( " go" ) ) { D al og d = new ToeD al og( i i t hi s , I nt eger . par s eI nt ( r ow . get Text ( ) ) , s I nt eger . par s eI nt ( col s . get Text ( ) ) ) ; d. s how ) ; ( } el s e r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Fr am f = new ToeTes t ( ) ; e f . r es i ze( 200, 100) ; f . s how ) ; ( } } ///: ~ ToeBut t on 类保留了一个句柄到它 ToeD al og型的父类中。正如前面所述,ToeBut t on 和 ToeD a og高度的结 i i l 合因为一个 ToeBut t on 只能被一个 ToeD al og所使用,但它却解决了一系列的问题,事实上这实在不是一个 i 糟糕的解决方案因为没有另外的可以记录用户选择的对话类。当然我们可以使用其它的制造 ToeD al og. t ur n i (ToeBut t on 的静态的一部分)方法。这种方法消除了它们的紧密联系,但却阻止了我们一次拥有多个 ToeD al og(无论如何,至少有一个正常地运行)。 i pai nt ( )是一种与图形有关的方法:它围绕按钮画出矩形并画出“X”或“O”。这完全是冗长的计算,但却 十分的直观。 一个鼠标单击被过载的 m eD n( )方法所俘获,最要紧的是检查是否有事件写在按钮上。如果没有,父窗 ous ow 口会被询问以找出谁选择了它并用来确定按钮的状态。值得注意的是按钮随后交回到父类中并且改变它的选 择。如果按钮已经显示这为“X”和“O”,那么它们会被改变状态。我们能注意到本书第三章中描述的在 这些计算中方便的使用的三个一组的 I f - el s e。当一个按钮的状态改变后,按钮会被重画。 ToeD al og的构建器十分的简单:它像我们所需要的一样增加一些按钮到 G i dLayout 布局管理器中,然后调 i r 整每个按钮每边大小为 50 个像素(如果我们不调整窗口,那么它就不会显示出来)。注意 handl eEvent ( ) 正 好为 W N O _D I D W ESTRO 调用 di s pos e( ) ,因此整个应用程序不会被关闭。 Y T oeT es t 设置整个应用程序以创建 Text Fi el d(为输入按钮网格的行和列)和“go”按钮。我们会领会 act i on( ) 在这个程序中使用不太令人满意的“字符串匹配”技术来测试按钮的按下(请确定我们拼写和大写 都是正确的!)。当按钮按下时,Text Fi el d中的数据将被取出,并且,因为它们在字符串结构中,所以需 要利用静态的 I nt eger . par es I nt ( ) 方法来转变成中断。一旦对话类被建立,我们就必须调用 s how )方法来显 ( 示和激活它。 我们会注意到 ToeD al og对象赋值给一个对话句柄 d i 。这是一个上溯造型的例子,尽管它没有真正地产生重 要的差异,因为所有的事件都是 s how ) 调用的。但是,如果我们想调用 ToeD al og中已经存在的一些方法, ( i 我们需要对 ToeD al og句柄赋值,就不会在一个上溯中丢失信息。 i 1. 文件对话类 406

在一些操作系统中拥有许多的特殊内建对话框去处理选择的事件,例如:字库,颜色,打印机以及类似的事 件。几乎所有的操作系统都支持打开和保存文件,但是,Java 的 Fi l eD al og包更容易使用。当然这会不再 i 检测所有使用的程序片,因为程序片在本地磁盘上既不能读也不能写文件。(这会在新的浏览器中交换程序 片的信任关系。) 下面的应用程序运用了两个文件对话类的窗体,一个是打开,一个是保存。大多数的代码到如今已为我们所 熟悉,而所有这些有趣的活动发生在两个不同按钮单击事件的 act i on( ) 方法中。 //: Fi l eD al ogTes t . j ava i // D ons t r at i on of Fi l e di al og boxes em i m t j ava. aw . *; por t publ i c cl as s Fi l eD al ogTes t ext ends Fr am { i e Text Fi el d f i l enam = new Text Fi el d( ) ; e Text Fi el d di r ect or y = new Text Fi el d( ) ; But t on open = new But t on( " O pen" ) ; But t on s ave = new But t on( " Save" ) ; publ i c Fi l eD al ogTes t ( ) { i s et Ti t l e( " Fi l e D al og Tes t " ) ; i Panel p = new Panel ( ) ; p. s et Layout ( new Fl ow Layout ( ) ) ; p. add( open) ; p. add( s ave) ; add( " Sout h" , p) ; di r ect or y. s et Edi t abl e( f al s e) ; f i l enam s et Edi t abl e( f al s e) ; e. p = new Panel ( ) ; p. s et Layout ( new G i dLayout ( 2, 1) ) ; r p. add( f i l enam ; e) p. add( di r ect or y) ; add( " N t h" , p) ; or } publ i c bool ean hand eEvent ( Event evt ) { l i f ( evt . i d == Event . W N O _D I D W ESTRO Y) Sys t em exi t ( 0) ; . el s e r et ur n s uper . handl eEvent ( evt ) ; r et ur n t r ue; } publ i c bool ean act i on( Event evt , O ect ar g) { bj i f ( evt . t ar get . equal s ( open) ) { // Tw ar gum s , def aul t s t o open f i l e: o ent Fi l eD al og d = new Fi l eD al og( t hi s , i i "W hat f i l e do you w ant t o open?" ) ; d. s et Fi l e( " *. j ava" ) ; // Fi l enam f i l t er e d. s et D r ect or y( " . " ) ; // C r ent di r ect or y i ur d. s how ) ; ( St r i ng openFi l e; i f ( ( openFi l e = d. get Fi l e( ) ) ! = nul l ) { f i l enam s et Text ( openFi l e) ; e. di r ect or y. s et Text ( d. get D r ect or y( ) ) ; i } el s e { f i l enam s et Text ( " You pr es s ed cancel " ) ; e. 407

di r ect or y. s et Text ( " " ) ; } } el s e i f ( evt . t ar get . equal s ( s ave) ) { Fi l eD al og d = new Fi l eD al og( t hi s , i i "W hat f i l e do you w ant t o s ave?" , Fi l eD al og. SA ; i VE) d. s et Fi l e( " *. j ava" ) ; d. s et D r ect or y( " . " ) ; i d. s how ) ; ( St r i ng s aveFi l e; i f ( ( s aveFi l e = d. get Fi l e( ) ) ! = nul l ) { f i l enam s et Text ( s aveFi l e) ; e. di r ect or y. s et Text ( d. get D r ect or y( ) ) ; i } el s e { f i l enam s et Text ( " You pr es s ed cancel " ) ; e. di r ect or y. s et Text ( " " ) ; } } el s e r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Fr am f = new Fi l eD al ogTes t ( ) ; e i f . r es i ze( 250, 110) ; f . s how ) ; ( } } ///: ~ 对一个“打开文件”对话框,我们使用构建器设置两个自变量;首先是父窗口句柄,其次是 Fi l eD al og标题 i 条的标题。s et Fi l e( ) 方法提供一个初始文件名--也许本地操作系统支持通配符,因此在这个例子中所有 的. j ava文件最开头会被显示出来。s et D r ect or y( ) 方法选择文件决定开始的目录(一般而言,操作系统允 i 许用户改变目录)。 s how )命令直到对话类关闭才返回。Fi l eD al og对象一直存在,因此我们可以从它那里读取数据。如果我们 ( i 调用 get Fi l e( ) 并且它返回空,这意味着用户退出了对话类。文件名和调用 get D r ect or y( ) 方法的结果都显 i 示在 Text Fi el ds 里。 按钮的保存工作使用同样的方法,除了因为 Fi l eD al og而使用不同的构建器。这个构建器设置了三个自变量 i 并且第三的一个自变量必须为 Fi l eD al og. SA 或 Fi l eD al og. O 。 i VE i PEN

13. 16 新型 AW T
在 Java 1. 1 中一个显著的改变就是完善了新 A T 的创新。大多数的改变围绕在 Java 1. 1 中使用的新事件模 W 型:老的事件模型是糟糕的、笨拙的、非面向对象的,而新的事件模型可能是我所见过的最优秀的。难以理 解一个如此糟糕的(老的 A T )和一个如此优秀的(新的事件模型)程序语言居然出自同一个集团之手。新 W 的考虑事件的方法看来中止了,因此争议不再变成障碍,从而轻易进入我们的意识里;相反,它是一个帮助 我们设计系统的工具。它同样是 Java Beans 的精华,我们会在本章后面部分进入讲述。 新的方法设计对象做为“事件源”和“事件接收器”以代替老 A T 的非面向对象串联的条件语句。正象我们 W 将看到的内部类的用途是集成面向对象的原始状态的新事件。另外,事件现在被描绘为在一个类体系以取代 单一的类并且我们可以创建自己的事件类型。 我们同样会发现,如果我们采用老的 A T 编程,Java 1. 1 版会产生一些看起来不合理的名字转换。例如, W s et s i ze( ) 改成 r es i ze( ) 。当我们学习 Java Beans 时这会变得更加的合理,因为 Beans 使用一个独特的命名 协议。名字必须被修改以在 Beans 中产生新的标准 A T 组件。 W 408

剪贴板操作在 Java 1. 1 版中也得到支持,尽管拖放操作“将在新版本中被支持”。我们可能访问桌面色彩组 织,所以我们的 Java 可以同其余桌面保持一致。可以利用弹出式菜单,并且为图像和图形作了改进。也同样 支持鼠标操作。还有简单的为打印的 A 以及简单地支持滚动。 PI

1 3 . 1 6 . 1 新的事件模型
在新的事件模型的组件可以开始一个事件。每种类型的事件被一个个别的类所描绘。当事件开始后,它受理 一个或更多事件指明“接收器”。因此,事件源和处理事件的地址可以被分离。 每个事件接收器都是执行特定的接收器类型接口的类对象。因此作为一个程序开发者,我们所要做的是创建 接收器对象并且在被激活事件的组件中进行注册。event - f i r i ng组件调用一个 addXXXLi s t ener ( )方法来完成 注册,以描述 XXX事件类型接受。我们可以容易地了解到以 addLi s t ened 名的方法通知我们任何的事件类型 都可以被处理,如果我们试图接收事件我们会发现编译时我们的错误。Java Beans 同样使用这种 addLi s t ener 名的方法去判断那一个程序可以运行。 我们所有的事件逻辑将装入到一个接收器类中。当我们创建一个接收器类时唯一的一点限制是必须执行专用 的接口。我们可以创建一个全局接收器类,这种情况在内部类中有助于被很好地使用,不仅仅是因为它们提 供了一个理论上的接收器类组到它们服务的 UI 或业务逻辑类中,但因为(正像我们将会在本章后面看到的) 事实是一个内部类维持一个句柄到它的父对象,提供了一个很好的通过类和子系统边界的调用方法。 一个简单的例子将使这一切变得清晰明确。同时思考本章前部 But t on2. j ava例子与这个例子的差异。 //: But t on2N . j ava ew // C ur i ng but t on pr es s es apt i m t j ava. aw . *; por t i m t j ava. aw . event . *; // M t add t hi s por t us i m t j ava. appl et . * ; por publ i c cl as s But t on2N ext ends A et { ew ppl But t on b1 = new But t on( " But t on 1" ) , b2 = new But t on( " But t on 2" ) ; publ i c voi d i ni t ( ) { b1. addA i onLi s t ener ( new B1( ) ) ; ct b2. addA i onLi s t ener ( new B2( ) ) ; ct add( b1) ; add( b2) ; } cl as s B1 i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct get A et C ext ( ) . s how at us ( " But t on 1" ) ; ppl ont St } } cl as s B2 i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct get A et C ext ( ) . s how at us ( " But t on 2" ) ; ppl ont St } } /* The ol d w ay: publ i c bool ean act i on( Event evt , O ect ar g) { bj i f ( evt . t ar get . equal s ( b1) ) get A et C ext ( ) . s how at us ( " But t on 1" ) ; ppl ont St el s e i f ( evt . t ar get . equal s ( b2) ) get A et C ext ( ) . s how at us ( " But t on 2" ) ; ppl ont St // Let t he bas e cl as s handl e i t : el s e 409

r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; // W ve handl ed i t her e e' } */ } ///: ~ 我们可比较两种方法,老的代码在左面作为注解。在 i ni t ( ) 方法里,只有一个改变就是增加了下面的两行: b1. addA i onLi s t ener ( new B1( ) ) ; ct b2. addA i onLi s t ener ( new B2( ) ) ; ct 按钮按下时,addA i onLi s t ener ( ) 通知按钮对象被激活。B1 和 B2 类都是执行接口 A i onLi s t ener 的内部 ct ct 类。这个接口包括一个单一的方法 act i onPer f or m )(这意味着当事件激活时,这个动作将被执行)。注 ed( 意 act i onPr ef or m ) 方法不是一个普通事件,说得更恰当些是一个特殊类型的事件,A i onEvent 。如果我 ed( ct 们想提取特殊 A i onEvent 的信息,因此我们不需要故意去测试和下溯造型自变量。 ct 对编程者来说一个最好的事便是 act i onPer f or m )十分的简单易用。它是一个可以调用的方法。同老的 ed( act i on( ) 方法比较,老的方法我们必须指出发生了什么和适当的动作,同样,我们会担心调用基础类 act i on( ) 的版本并且返回一个值去指明是否被处理。在新的事件模型中,我们知道所有事件测试推理自动进 行,因此我们不必指出发生了什么;我们刚刚表示发生了什么,它就自动地完成了。如果我们还没有提出用 新的方法覆盖老的方法,我们会很快提出。

1 3 . 1 6 . 2 事件和接收者类型
所有 A T 组件都被改变成包含 addXXXLi s t ener ( ) 和 r em W oveXXXLi s t ener ( ) 方法,因此特定的接收器类型可从 每个组件中增加和删除。我们会注意到“XXX”在每个场合中同样表示自变量的方法,例如, addFooLi s t ener ( FooLi s t ener f l ) 。下面这张表格总结了通过提供 addXXXLi s t ener ( ) 和 r em oveXXXLi st ener ( ) 方法,从而支持那些特定事件的相关事件、接收器、方法以及组件。 事件,接收器接口及添加和删除方法 支持这个事件的组件 Event , l i s t ener i nt er f ace and Compon en t s s u ppor t i n g t h i s ev en t add - an d r emov e - met h ods Ac t i on E v e n t Act i on L i s t en er addAc t i on L i s t en er ( ) r emov eAc t i on L i s t en er ( ) B u t t o n, L i s t , T ex t F i el d, Men u I t em, a i t s d r i va i ve nd e t s i ncl udi ng Ch e c k box Me n u I t e m Me n u , and P o pu pMe n u ,

Adj u s t me n t E v e n t S c r o l l bar Adj u s t men t L i s t en er A hi ng you cr eat e t hat i m em s t he Adj us t abl e i nte f a e nyt pl ent r c addAdj u s t me n t L i s t e n e r ( ) r em eAdj us t m ov ent L i s t ener ( ) Compon en t E v en t Co mpo n e n t L i s t e n e r addCompon en t L i s t en er ( ) r em eCom ov ponent L i s t ener ( ) Co mpo n e n t and i t s der i vat i ves , i ncl udi ng B ut t on, Canv as , Check box , Choi ce, Cont ai ner , Panel , Appl et , S cr ol l Pane, W ndow Di al og, F i l eDi al og, F r am L abel , Li s t , Scr ol l bar , i , e, T e x t Ar e a, and T e x t F i e l d

Con t ai n e r E v e n t Co n t ai n e r and i t s der i vat i ves , i ncl udi ng P anel , Appl et , Co n t ai n e r L i s t e n e r S cr ol l P an e , Wi n do w, Di al o g, F i l e Di al og, and F r am e addCon t ai n er L i s t en er ( ) r em eCont ai ner L i s t ener ( ) ov F ocu s E v en t F ocu s L i s t en er addF o c u s L i s t e n e r ( ) r e mov e F oc u s L i s t e n e r ( ) K ey E v en t Co mpo n e n t and i t s der i vat i ves , i ncl udi ng B ut t on, Canv as , Check box , Choi ce, Cont ai ner , Panel , Appl et , S cr ol l Pane, W ndow Di al og, F i l eDi al og, F r am L abel , Li s t , Scr ol l bar , i , e T e x t Ar e a, and T e x t F i e l d Co mpo n e n t and i t s der i vat i ves , i ncl udi ng B ut t on, Canv as , 410

K ey L i s t en er addK e y L i s t e n e r ( ) r emov eK ey L i s t en er ( )

Check box , Choi ce, Cont ai ner , Panel , Appl et , S cr ol l Pane, W ndow Di al og, F i l eDi al og, F r am L abel , Li s t , Scr ol l bar , i , e, T e x t Ar e a, and T e x t F i e l d

Mou s eE v en t (f o b t h c i c a Co mpo n e n t and i t s der i vat i ves , i ncl udi ng B ut t on, Canv as , r o l ks nd m i on) ot Check box , Choi ce, Cont ai ner , Panel , Appl et , S cr ol l Pane, Mo u s e L i s t e n e r W ndow Di al og, F i l eDi al og, F r am L abel , Li s t , Scr ol l bar , i , e, addMou s eL i s t en er ( ) T e x t Ar e a, and T e x t F i e l d r e mov e Mou s e L i s t e n e r ( ) Mou s eE v en t [55] (f o b t h c i c r o l ks and m i on) ot Mou s e Mot i on L i s t e n e r addMou s eMot i on L i s t en er ( ) r em oveM eM i onL i s t ener ( ) ous ot Wi n dowE v e n t Wi n dowL i s t en er addWi n dowL i s t en er ( ) r emov eWi n dowL i s t en er ( ) I t e mE v e n t I t e mL i s t e n e r addI t e mL i s t e n e r ( ) r e mov e I t e mL i s t e n e r ( ) T ex t E v en t T ex t L i s t en er addT e x t L i s t e n e r ( ) r e mov e T e x t L i s t e n e r ( ) Co mpo n e n t and i t s der i vat i ves , i ncl udi ng B ut t on, Canv as , Check box , Choi ce, Cont ai ner , Panel , Appl et , S cr ol l Pane, W ndow Di al og, F i l eDi al og, F r am L abel , Li s t , Scr ol l bar , i , e, T ex t Ar ea, and T e x t F i e l d W ndow a i t s d r i va i ve i ncl ud ng Di al og, F i l eDi al og, a i nd e t s, i nd F r ame

Check box , Check box M enuI t em, Choi ce, L i s t , a d a yth n th t n n i g a i m em s t he I t emS el ect abl e i nt er f ace pl ent

A hi ng der i ved f r omT ex t Com nyt ponent , i nc ud ng T ex t Ar ea a l i nd T ex t F i el d

⑤:即使表面上如此,但实际上并没有 M eM i i onEvent (鼠标运动事件)。单击和运动都合成到 ous ot M eEvent 里,所以 M eEvent 在表格中的这种另类行为并非一个错误。 ous ous 可以看到,每种类型的组件只为特定类型的事件提供了支持。这有助于我们发现由每种组件支持的事件,如 下表所示: 组件类型 支持的事件

Compon en t Adj u s t abl e Appl e t B u t t on Can v as Ch e c k box

t y pe

E v e n t s s u ppo r t e d by t h i s c o mpo n e n t Adj u s t me n t E v e n t Cont ai ner Event , F ocus Event , KeyEvent , M eEvent , Com ous ponent Event Act i onE v ent , F ocus E v ent , Key E v ent , M ous eE v ent , Com ponent E v ent F oc u s E v e n t , K ey E v en t , I t e mE v e n t Mou s e E v e n t , Compon e n t E v e n t I t em v ent , F ocus E v ent , K ey E v ent , Mous eE v ent , Com E ponent E v ent

Ch eck box Men u I t em Ac t i o n E v e n t , Ch o i c e Co mpo n e n t Co n t ai n e r

I t em v ent , F ocus E v ent , K ey E v ent , Mous eE v ent , Com E ponent E v ent F oc u s E v e n t , K ey E v en t , Mou s e E v e n t , Compon e n t E v e n t Cont ai ner Event , F ocus Event , KeyEvent , M eEvent , Com ous ponent Event 411

Di al o g F i l eDi al og F r ame L abe l Li st Me n u Me n u I t e m P an e l P o pu pMe n u S c r o l l bar S cr ol l P an e T e x t Ar e a T e x t Co mpo n e n t T ex t F i el d Wi n do w

Cont ai ner E v ent , W ndowE v ent , F ocus E v ent , Key E v ent , M i ous eE v ent , Compon en t E v en t Cont ai ner E v ent , W ndowE v ent , F ocus E v ent , Key E v ent , M i ous eE v ent , Compon en t E v en t Cont ai ner E v ent , W ndowE v ent , F ocus E v ent , Key E v ent , M i ous eE v ent , Compon en t E v en t F oc u s E v e n t , Ac t i on E v e n t Ac t i on E v e n t Cont ai ner Event , F ocus Event , KeyEvent , M eEvent , Com ous ponent Event Act i on E v e n t Adj us t m Event , F ocus Event , KeyEvent , M eEvent , Com ent ous ponent Event Cont ai ner Event , F ocus Event , KeyEvent , M eEvent , Com ous ponent Event T ex t E v ent , F ocus E v ent , K ey E v ent , Mous eE v ent , Com ponent E v ent T e x t E v e n t , F ocus E v ent , Key E v ent , Mous eE v ent , Com ponent E v ent Act i onEvent , T ext Event , Focus Event , KeyEvent , M eEvent , C ponent Event ous om Cont ai ner E v ent , W ndowE v ent , F ocus E v ent , Key E v ent , M i ous eE v ent , Compon en t E v en t K ey E v en t , Mou s e E v e n t , Compon e n t E v e n t

Act i onEvent , Focus Event , KeyEvent , M eEvent , I t em ous Event , C ponent Event om

一旦知道了一个特定的组件支持哪些事件,就不必再去寻找任何东西来响应那个事件。只需简单地: ( 1) 取得事件类的名字,并删掉其中的“Event ”字样。在剩下的部分加入“Li s t ener ”字样。这就是在我们 的内部类里需要实现的接收器接口。 ( 2) 实现上面的接口,针对想要捕获的事件编写方法代码。例如,假设我们想捕获鼠标的移动,所以需要为 M eM i i onLi s t ener 接口的 m eM ous ot ous oved( )方法编写代(当然还必须实现其他一些方法,但这里有捷径可 循,马上就会讲到这个问题)。 ( 3) 为步骤 2 中的接收器类创建一个对象。随自己的组件和方法完成对它的注册,方法是在接收器的名字里 加入一个前缀“add ”。比如 addM eM i onLi s t ener ( )。 ous ot 下表是对接收器接口的一个总结: 接收器接口 接口中的方法 L i s t ener i nt er f ace Me t h ods i n i n t e r f ac e w/ adapt er Act i on L i s t en er act i onPer f or m Act i onE vent ) ed(

Adj us t m L i s t ener adj u s t me n t Val u e Ch an ge d( ent Adj u s t men t E v en t ) Com ponent L i s t ener com ponent Hi dden( Com ponent Event ) Com ponent Adapt er com ponent Shown( Com ponent Event ) com ponent M oved( Com ponent Event ) com ponent Res i z ed( C ponent Event ) om Cont ai ner L i s t ener com ponent Added( Cont ai ner Event ) Cont ai ner Adapt er com ponent Rem oved( C ai ner Event ) ont F ocu s L i s t en er f oc u s Gai n e d( F oc u s E v e n t ) 412

F oc u s Adapt e r K ey L i s t en er K ey Adapt er Mo u s e L i s t e n e r Mou s e Adapt e r

f oc u s L os t ( F oc u s E v en t ) k e y P r e s s e d( K e y E v e n t ) k ey R el eas ed( K ey E v en t ) k ey T y ped( K ey E v en t ) mou s e Cl i c k e d( Mou s e E v e n t mou s e E n t e r e d( Mou s e E v e n t mou s e E x i t e d( Mou s e E v e n t ) mou s e P r e s s e d( Mou s e E v e n t mou s eR el eas ed( Mou s eE v en t ) ) ) )

M eM i onLi s t ener mou s e Dr agge d( Mou s e E v e n t ) ous ot M eM i onAdapt er mou s e Mov e d( Mou s e E v e n t ) ous ot Wi n dowL i s t en er Wi n do wAdapt e r wi n dowOpen ed( Wi n dowE v en t ) wi ndowCl os i ng( W ndowE v ent ) i wi n do wCl os ed( W ndowE v ent ) i wi ndowAct i vat ed( W ndowE vent ) i wi ndowDeact i vat ed( W ndowEvent ) i wi ndowI coni f i ed( W ndowE vent ) i wi ndowDei coni f i ed( W ndowEvent ) i i t em t at eChanged( I t em v ent ) S E t ex t Val ueChanged( T ex t E v ent )

I t e mL i s t e n e r T ex t L i s t en er

1. 用接收器适配器简化操作 在上面的表格中,我们可以注意到一些接收器接口只有唯一的一个方法。它们的执行是无轻重的,因为我们 仅当需要书写特殊方法时才会执行它们。然而,接收器接口拥有多个方法,使用起来却不太友好。例如,我 们必须一直运行某些事物,当我们创建一个应用程序时对帧提供一个 W ndow s t ener ,以便当我们得到 i Li w ndow l os i ng( )事件时可以调用 Sys t em exi t ( 0)以退出应用程序。但因为 W ndow s t ener 是一个接口,我 i C . i Li 们必须执行其它所有的方法即使它们不运行任何事件。这真令人讨厌。 为了解决这个问题,每个拥有超过一个方法的接收器接口都可拥有适配器,它们的名我们可以在上面的表格 中看到。每个适配器为每个接口方法提供默认的方法。(W ndow dapt er 的默认方法不是 w ndow l os i ng( ) , i A i C 而是 Sys t em exi t ( 0) 方法。)此外我们所要做的就是从适配器处继承并过载唯一的需要变更的方法。例如, . 典型的 W ndow s t ener 我们会像下面这样的使用。 i Li cl as s M i ndow s t ener ext ends W ndow dapt er { yW Li i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } } 适配器的全部宗旨就是使接收器的创建变得更加简便。 但所谓的“适配器”也有一个缺点,而且较难发觉。假定我们象上面那样写一个 W ndow dapt er : i A cl as s M i ndow s t ener ext ends W ndow dapt er { yW Li i A publ i c voi d W ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } } 表面上一切正常,但实际没有任何效果。每个事件的编译和运行都很正常——只是关闭窗口不会退出程序。 您注意到问题在哪里吗?在方法的名字里:是 W ndow l os i ng( ),而不是 w ndow l os i ng( )。大小写的一个简 i C i C 单失误就会造成一个崭新的方法。但是,这并非我们关闭窗口时调用的方法,所以当然没有任何效果。 413

1 3 . 1 6 . 3 用 J av a 1 . 1 AW 制作窗口和程序片 T
我们经常都需要创建一个类,使其既可作为一个窗口调用,亦可作为一个程序片调用。为做到这一点,只需 为程序片简单地加入一个 m n( ) 即可,令其在一个 Fr am (帧)里构建程序片的一个实例。作为一个简单的 ai e 示例,下面让我们来看看如何对 But t on2N . j ava作一番修改,使其能同时作为应用程序和程序片使用: ew //: But t on2N B. j ava ew // A appl i cat i on and an appl et n i m t j ava. aw . *; por t i m t j ava. aw . event . *; // M t add t hi s por t us i m t j ava. appl et . * ; por publ i c cl as s But t on2N B ext ends A pl et { ew p But t on b1 = new But t on( " But t on 1" ) , b2 = new But t on( " But t on 2" ) ; Text Fi el d t = new Text Fi el d( 20) ; publ i c voi d i ni t ( ) { b1. addA i onLi s t ener ( new B1( ) ) ; ct b2. addA i onLi s t ener ( new B2( ) ) ; ct add( b1) ; add( b2) ; add( t ) ; } cl as s B1 i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct t . s et Text ( " But t on 1" ) ; } } cl as s B2 i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct t . s et Text ( " But t on 2" ) ; } } // To cl os e t he appl i cat i on: s t at i c cl as s W ext ends W ndow dapt er { L i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } } // A m n( ) f or t he appl i cat i on: ai publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai But t on2N B appl et = new But t on2N B( ) ; ew ew Fr am aFr am = new Fr am " But t on2N B" ) ; e e e( ew aFr am addW ndow s t ener ( new W ) ) ; e. i Li L( aFr am add( appl et , Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 300, 200) ; e. appl et . i ni t ( ) ; appl et . s t ar t ( ) ; aFr am s et Vi s i bl e( t r ue) ; e. } } ///: ~ 414

内部类 W 和 m n( ) 方法是加入程序片的唯一两个元素,程序片剩余的部分则原封未动。事实上,我们通常 L ai 将 W 类和 m n( ) 方法做一结小的改进复制和粘贴到我们自己的程序片里(请记住创建内部类时通常需要一 L ai 个外部类来处理它,形成它静态地消除这个需要)。我们可以看到在 m n( ) 方法里,程序片明确地初始化和 ai 开始,因为在这个例子里浏览器不能为我们有效地运行它。当然,这不会提供全部的浏览器调用 s t op( )和 des t r oy( ) 的行为,但对大多数的情况而言它都是可接受的。如果它变成一个麻烦,我们可以: ( 1) 使程序片句柄为一个静态类(以代替局部可变的 m n( ) ),然后: ai ( 2) 在我们调用 Sys t em exi t ( ) 之前在 W ndow dapt er . w ndow l os i ng( )中调用 appl et . s t op( ) 和 . i A i C appl et . des t r oy( )。 注意最后一行: aFr am s et Vi s i bl e( t r ue) ; e. 这是 Java 1. 1 A T 的一个改变。s how )方法不再被支持,而 s et Vi s i bl e( t r ue)则取代了 s how ) 方法。当我 W ( ( 们在本章后面部分学习 Java Beans 时,这些表面上易于改变的方法将会变得更加的合理。 这个例子同样被使用 Text Fi el d修改而不是显示到控制台或浏览器状态行上。在开发程序时有一个限制条件 就是程序片和应用程序我们都必须根据它们的运行情况选择输入和输出结构。 这里展示了 Java 1. 1 A T 的其它小的新功能。我们不再需要去使用有错误倾向的利用字符串指定 W Bor der Layout 定位的方法。当我们增加一个元素到 Java 1. 1 版的 Bor der Layout 中时,我们可以这样写: aFr am add( appl et , Bor der Layout . C TER) ; e. EN 我们对位置规定一个 Bor der Layout 的常数,以使它能在编译时被检验(而不是对老的结构悄悄地做不合适的 事)。这是一个显著的改善,并且将在这本书的余下部分大量地使用。 2. 将窗口接收器变成匿名类 任何一个接收器类都可作为一个匿名类执行,但这一直有个意外,那就是我们可能需要在其它场合使用它们 的功能。但是,窗口接收器在这里仅作为关闭应用程序窗口来使用,因此我们可以安全地制造一个匿名类。 然后,m n( )中的下面这行代码: ai aFr am addW ndow s t ener ( new W ) ) ; e. i Li L( 会变成: aFr am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); 这有一个优点就是它不需要其它的类名。我们必须对自己判断是否它使代码变得易于理解或者更难。不过, 对本书余下部分而言,匿名内部类将通常被使用在窗口接收器中。 3. 将程序片封装到 JA R文件里 一个重要的 JA R应用就是完善程序片的装载。在 Java 1. 0 版中,人们倾向于试法将它们的代码填入到单个的 程序片类里,因此客户只需要单个的服务器就可适合下载程序片代码。但这不仅使结果凌乱,难以阅读(当 然维护也然)程序,但类文件一直不能压缩,因此下载从来没有快过。 JA R文件将我们所有的被压缩的类文件打包到一个单个儿的文件中,再被浏览器下载。现在我们不需要创建 一个糟糕的设计以最小化我们创建的类,并且用户将得到更快地下载速度。 仔细想想上面的例子,这个例子看起来像 But t on2N B,是一个单类,但事实上它包含三个内部类,因此共 ew 有四个。每当我们编译程序,我会用这行代码打包它到一个 JA R文件: j ar cf But t on2N B. j ar *. cl as s ew 这是假定只有一个类文件在当前目录中,其中之一来自 But t on2N B. j ava ew (否则我们会得到特别的打包)。 现在我们可以创建一个使用新文件标签来指定 JA R文件的 HTM 页,如下所示: L But t on2N B Exam e A et ew pl ppl 415

i 与 HTM 文件中的程序片标记有关的其他任何内容都保持不变。 L

1 3 . 1 6 . 4 再研究一下以前的例子
为注意到一些利用新事件模型的例子和为学习程序从老到新事件模型改变的方法,下面的例子回到在本章第 一部分利用事件模型来证明的一些争议。另外,每个程序包括程序片和应用程序现在都可以借助或不借助浏 览器来运行。 1. 文本字段 这个例子同 Text Fi el d1. j ava相似,但它增加了显然额外的行为: //: Text N . j ava ew // Text f i el ds w t h Java 1. 1 event s i i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. appl et . * ; por publ i c cl as s Text N ext ends A et { ew ppl But t on b1 = new But t on( " G Text " ) , et b2 = new But t on( " Set Text " ) ; Text Fi el d t 1 = new T ext Fi el d( 30) , t 2 = new Text Fi el d( 30) , t 3 = new Text Fi el d( 30) ; St r i ng s = new St r i ng( ) ; publ i c voi d i ni t ( ) { b1. addA i onLi s t ener ( new B1( ) ) ; ct b2. addA i onLi s t ener ( new B2( ) ) ; ct t 1. addText Li s t ener ( new T1( ) ) ; t 1. addA i onLi s t ener ( new T1A ) ) ; ct ( t 1. addKeyLi s t ener ( new T1K( ) ) ; add( b1) ; add( b2) ; add( t 1) ; add( t 2) ; add( t 3) ; } cl as s T1 i m em s Text Li s t ener { pl ent publ i c voi d t ext Val ueC hanged( Text Event e) { t 2. s et Text ( t 1. get Text ( ) ) ; } } cl as s T1A i m em s A i onLi s t ener { pl ent ct pr i vat e i nt count = 0; publ i c voi d act i onPer f or m A i onEvent e) { ed( ct t 3. s et Text ( " t 1 A i on Event " + count ++) ; ct 416

} } cl as s T1K ext ends KeyA dapt er { publ i c voi d keyTyped( KeyEvent e) { St r i ng t s = t 1. get Text ( ) ; i f ( e. get KeyC ( ) == har KeyEvent . VK_BA K_SPA E) { C C // Ens ur e i t ' s not em y: pt i f ( t s . l engt h( ) > 0) { t s = t s . s ubs t r i ng( 0, t s . l engt h( ) - 1) ; t 1. s et Text ( t s ) ; } } el s e t 1. s et Text ( t 1. get Text ( ) + C act er . t oUpper C e( har as e. get KeyC ( ) ) ) ; har t 1. s et C et Pos i t i on( ar t 1. get Text ( ) . l engt h( ) ) ; // St op r egul ar char act er f r om appear i ng: e. cons um ) ; e( } } cl as s B1 i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct s = t 1. get Sel ect edText ( ) ; i f ( s . l engt h( ) == 0) s = t 1. get Text ( ) ; t 1. s et Edi t abl e( t r ue) ; } } cl as s B2 i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct t 1. s et Text ( " I ns er t ed by But t on 2: " + s ) ; t 1. s et Edi t abl e( f al s e) ; } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai T ext N appl et = new T ext N ( ) ; ew ew Fr am aFr am = new Fr am " Text N " ) ; e e e( ew aFr am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); aFr am add( appl et , Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 300, 200) ; e. appl et . i ni t ( ) ; appl et . s t ar t ( ) ; aFr am s et Vi s i bl e( t r ue) ; e. } 417

} ///: ~ 当 Text Fi el d t 1 的动作接收器被激活时,Text Fi el d t 3 就是一个需要报告的场所。我们注意到仅当我们按 下“ent er ”键时,动作接收器才会为“Text Fi el d”所激活。 Text Fi el d t 1 附有几个接收器。T 1 接收器从 t 1 复制所有文字到 t 2,强制所有字符串转换成大写。我们会发 现这两个工作同是进行的,并且如果我们增加 T 1K 接收器后我们再增加 T 1 接收器,它就不那么重要:在文字 字段内的所有的字符串将一直被强制变为大写。这看起来键盘事件一直在文字组件事件前被激活,并且如果 我们需要保留 t 2 的字符串原来输入时的样子,我们就必须做一些特别的工作。 T 1K 有着其它的一些有趣的活动。我们必须测试 backs pace(因为我们现在控制着每一个事件)并执行删除。 car et 必须被明确地设置到字段的结尾;否则它不会像我们希望的运行。最后,为了防止原来的字符串被默 认的机制所处理,事件必须利用为事件对象而存在的 cons um ) 方法所“耗尽”。这会通知系统停止激活其 e( 余特殊事件的事件处理器。 这个例子同样无声地证明了设计内部类的带来的诸多优点。注意下面的内部类: cl as s T1 i m em s Text L i s t ener { pl ent publ i c voi d t ext Val ueC hanged( Text Event e) { t 2. s et Text ( t 1. get Text ( ) ) ; } } t 1 和 t 2 不属于 T 1 的一部分,并且到目前为止它们都是很容易理解的,没有任何的特殊限制。这是因为一个 内部类的对象能自动地捕捉一个句柄到外部的创建它的对象那里,因此我们可以处理封装类对象的方法和内 容。正像我们看到的,这十分方便(注释⑥)。 ⑥:它也解决了“回调”的问题,不必为 Java 加入任何令人恼火的“方法指针”特性。 2. 文本区域 Java 1. 1 版中 Text A ea最重要的改变就滚动条。对于 Text A ea 的构建器而言,我们可以立即控制 r r Text A ea 是否会拥有滚动条:水平的,垂直的,两者都有或者都没有。这个例子更正了前面 Java 1. 0 版 r Text A ea1. j ava程序片,演示了 Java 1. 1 版的滚动条构建器: r //: Text A eaN . j ava r ew // C r ol l i ng s cr ol l bar s w t h t he T ext A ea ont i r // com ponent i n Java 1. 1 i m t j ava. aw . * ; por t i m t j ava. aw . event . * ; por t i m t j ava. appl et . * ; por publ i c cl as s Text A eaN ext ends A et { r ew ppl But t on b1 = new But t on( " Text A ea 1" ) ; r But t on b2 = new But t on( " Text A ea 2" ) ; r But t on b3 = new But t on( " Repl ace Text " ) ; But t on b4 = new But t on( " I ns er t Text " ) ; Text A ea t 1 = new Text A ea( " t 1" , 1, 30) ; r r Text A ea t 2 = new Text A ea( " t 2" , 4, 30) ; r r Text A ea t 3 = new Text A ea( " t 3" , 1, 30, r r Text A ea. SC LLBA r RO RS_N N ; O E) Text A ea t 4 = new Text A ea( " t 4" , 10, 10, r r Text A ea. SC LLBA r RO RS_VERTI C L_O LY) ; A N Text A ea t 5 = new Text A ea( " t 5" , 4, 30, r r Text A ea. SC LLBA r RO RS_HO ZO TA N ; RI N L_O LY) Text A ea t 6 = new Text A ea( " t 6" , 10, 10, r r 418

Text A ea. SC LLBA r RO RS_BO ; TH) publ i c voi d i ni t ( ) { b1. addA i onLi s t ener ( new B1L( ) ) ; ct add( b1) ; add( t 1) ; b2. addA i onLi s t ener ( new B2L( ) ) ; ct add( b2) ; add( t 2) ; b3. addA i onLi s t ener ( new B3L( ) ) ; ct add( b3) ; b4. addA i onLi s t ener ( new B4L( ) ) ; ct add( b4) ; add( t 3) ; add( t 4) ; add( t 5) ; add( t 6) ; } cl as s B1L i m em s A i onLi s t ener { pl ent ct publ i c voi d act i o nPer f or m A i onEvent e) { ed( ct t 5. append( t 1. get Text ( ) + " \n" ) ; } } cl as s B2L i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct t 2. s et Text ( " I ns er t ed by But t on 2" ) ; t 2. append( " : " + t 1. get Text ( ) ) ; t 5. ap pend( t 2. get Text ( ) + " \n" ) ; } } cl as s B3L i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct St r i ng s = " Repl acem ent " ; t 2. r epl aceRange( s , 3, 3 + s . l engt h( ) ) ; } } cl as s B4L i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct t 2. i ns er t ( " I ns er t ed " , 10) ; } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Text A eaN appl et = new Text A eaN ( ) ; r ew r ew Fr am aFr am = new Fr am " Text A eaN " ) ; e e e( r ew aFr am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); aFr am add( appl et , Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 300, 725) ; e. appl et . i ni t ( ) ; appl et . s t ar t ( ) ; aFr am s et Vi s i bl e( t r ue) ; e. } 419

} ///: ~ 我们发现只能在构造 Text A ea 时能够控制滚动条。同样,即使 T E A 没有滚动条,我们滚动光标也将被制止 r R (可通过运行这个例子中验证这种行为)。 3. 复选框和单选钮 正如早先指出的那样,复选框和单选钮都是同一个类建立的。单选钮和复选框略有不同,它是复选框安置到 C heckboxG oup 中构成的。在其中任一种情况下,有趣的 I t em r Event 事件为我们创建一个 I t em s t ener 项目 Li 接收器。 当处理一组复选框或者单选钮时,我们有一个不错的选择。我们可以创建一个新的内部类去为每个复选框处 理事件,或者创建一个内部类判断哪个复选框被单击并注册一个内部类单独的对象为每个复选对象。下面的 例子演示了两种方法: //: Radi oC heckN . j ava ew // Radi o but t ons and C heck Boxes i n Java 1. 1 i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. appl et . * ; por publ i c cl as s Radi oC heckN ext ends A et { ew ppl Text Fi el d t = new Text Fi el d( 30) ; C heckbox[ ] cb = { new C heckbox( " C heck Box 1" ) , new C heckbox( " C heck Box 2" ) , new C heckbox( " C heck Box 3" ) } ; C heckboxG oup g = new C r heckboxG oup( ) ; r C heckbox cb4 = new C heckbox( " f our " , g, f al s e) , cb5 = new C heckbox( " f i ve" , g, t r ue) , cb6 = new C heckbox( " s i x" , g, f al s e) ; publ i c voi d i ni t ( ) { t . s et Edi t abl e( f al s e) ; add( t ) ; I LC heck i l = new I LC heck( ) ; f or ( i nt i = 0; i < cb. l engt h; i ++) { cb[ i ] . addI t em s t ener ( i l ) ; Li add( cb[ i ]) ; } cb4. addI t em s t ener ( new I L4( ) ) ; Li cb5. addI t em s t ener ( new I L5( ) ) ; Li cb6. addI t em s t ener ( new I L6( ) ) ; Li add( cb4) ; add( cb5) ; add( cb6) ; } // C hecki ng t he s our ce: cl as s I LC heck i m em s I t em s t ener { pl ent Li publ i c voi d i t em at eC St hanged( I t em Event e) { f or ( i nt i = 0; i < cb. l engt h; i ++) { i f ( e. get Sour ce( ) . equal s ( cb[ i ] ) ) { t . s et Text ( " C heck box " + ( i + 1) ) ; r et ur n; } } 420

} } // vs . an i ndi vi dual cl as s f or each i t em : cl as s I L4 i m em s I t em s t ener { pl ent Li publ i c voi d i t em at eC St hanged( I t em Event e) { t . s et Text ( " Radi o but t on f our " ) ; } } cl as s I L5 i m em s I t em s t ener { pl ent Li publ i c voi d i t em at eC St hanged( I t em Event e) { t . s et Text ( " Radi o but t on f i ve" ) ; } } cl as s I L6 i m em s I t em s t ener { pl ent Li publ i c voi d i t em at eC St hanged( I t em Event e) { t . s et Text ( " Radi o but t on s i x" ) ; } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Radi oC heckN appl et = new Radi oC ew heckN ( ) ; ew Fr am aFr am = new Fr am " Radi oC e e e( heckN " ) ; ew aFr am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); aFr am add( appl et , Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 300, 200) ; e. appl et . i ni t ( ) ; appl et . s t ar t ( ) ; aFr am s et Vi s i bl e( t r ue) ; e. } } ///: ~ I LC heck 拥有当我们增加或者减少复选框时自动调整的优点。当然,我们对单选钮使用这种方法也同样的 好。但是,它仅当我们的逻辑足以普遍的支持这种方法时才会被使用。如果声明一个确定的信号——我们将 重复利用独立的接收器类,否则我们将结束一串条件语句。 4. 下拉列表 下拉列表在 Java 1. 1 版中当一个选择被改变时同样使用 I t em s t ener 去告知我们: Li //: C ceN . j ava hoi ew / / D op dow l i s t s w t h Java 1. 1 r n i i m t j ava. aw . *; por t i m or t j ava. aw . event . *; p t i m t j ava. appl et . * ; por publ i c cl as s C ceN ext ends A et { hoi ew ppl St r i ng[ ] des cr i pt i on = { " Ebul l i ent " , " O us e" , bt " Recal ci t r ant " , " Br i l l i ant " , " Som cent " , nes " Ti m ous " , " Fl or i d" , " Put r es cent " } ; or 421

Text Fi el d t = new Text Fi el d( 100) ; C ce c = new C ce( ) ; hoi hoi But t on b = new But t on( " A i t em " ) ; dd s i nt count = 0; publ i c voi d i ni t ( ) { t . s et Edi t abl e( f al s e) ; f or ( i nt i = 0; i < 4; i ++) c. addI t em des cr i pt i on[ count ++] ) ; ( add( t ) ; add( c) ; add( b) ; c. addI t em s t ener ( new C ) ) ; Li L( b. addA i onLi s t ener ( new BL( ) ) ; ct } cl as s C i m em s I t em s t ener { L pl ent Li publ i c voi d i t em at eC St hanged( I t em Event e) { t . s et Text ( " i ndex: " + c. get Sel ect edI ndex( ) + " " + e. t oSt r i ng( ) ) ; } } cl as s BL i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct i f ( count < des cr i pt i on. l engt h) c. addI t em des cr i pt i on[ count ++] ) ; ( } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C ceN appl et = new C ceN ( ) ; hoi ew hoi ew Fr am aFr am = new Fr am " C ceN " ) ; e e e( hoi ew aFr am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); aFr am add( appl et , Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 750, 100) ; e. appl et . i ni t ( ) ; appl et . s t ar t ( ) ; aFr am s et Vi s i bl e( t r ue) ; e. } } ///: ~ 这个程序中没什么特别新颖的东西(除了 Java 1. 1 版的 UI 类里少数几个值得关注的缺陷)。 5. 列表 我们消除了 Java 1. 0 中 Li s t 设计的一个缺陷,就是 Li s t 不能像我们希望的那样工作:它会与单击在一个列 表元素上发生冲突。 //: Li s t N . j ava ew // Java 1. 1 Li s t s ar e eas i er t o us e i m t j ava. aw . *; por t 422

i m t j ava. aw . event . * ; por t i m t j ava. appl et . * ; por publ i c cl as s Li st N ext ends A et { ew ppl St r i ng[ ] f l avor s = { " C hocol at e" , " St r aw r y" , ber " Vani l l a Fudge Sw r l " , " M nt C p" , i i hi "M ocha A m l ond Fudge" , " Rum Rai s i n" , " Pr al i ne C eam , " M Pi e" } ; r " ud // Show 6 i t em , al l ow m t i pl e s el ect i on: s ul Li s t l s t = new Li s t ( 6, t r ue) ; Text A ea t = new Text A ea( f l avor s . l engt h, 30) ; r r But t on b = new But t on( " t es t " ) ; i nt count = 0; publ i c voi d i ni t ( ) { t . s et Edi t abl e( f al s e) ; f or ( i nt i = 0; i < 4; i ++) l s t . addI t em f l avor s [ count ++] ) ; ( add( t ) ; add( l s t ) ; add( b) ; l s t . addI t em s t ener ( new LL( ) ) ; Li b. addA i onLi s t ener ( new BL( ) ) ; ct } cl as s LL i m em s I t em s t ener { pl ent Li publ i c voi d i t em at eC St hanged( I t em Event e) { t . s et Text ( " " ) ; St r i ng[ ] i t em = l s t . get Sel ect edI t em ( ) ; s s f or ( i nt i = 0; i < i t em . l engt h; i ++) s t . append( i t em [ i ] + " \n" ) ; s } } cl as s BL i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct i f ( count < f l avor s . l engt h) l s t . addI t em f l avor s [ count ++] , 0) ; ( } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Li s t N appl et = new Li s t N ( ) ; ew ew Fr am aFr am = new Fr am " Li s t N " ) ; e e e( ew aFr am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }) ; aFr am add( appl et , Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 300, 200) ; e. appl et . i ni t ( ) ; appl et . s t ar t ( ) ; aFr am s et Vi s i bl e( t r ue) ; e. } 423

} ///: ~ 我们可以注意到在列表项中无需特别的逻辑需要去支持一个单击动作。我们正好像我们在其它地方所做的那 样附加上一个接收器。 6. 菜单 为菜单处理事件看起来受益于 Java 1. 1 版的事件模型,但 Java 生成菜单的方法常常麻烦并且需要一些手工 编写代码。生成菜单的正确方法看起来像资源而不是一些代码。请牢牢记住编程工具会广泛地为我们处理创 建的菜单,因此这可以减少我们的痛苦(只要它们会同样处理维护任务!)。另外,我们将发现菜单不支持 并且将导致混乱的事件:菜单项使用 A i onLi s t ener s (动作接收器),但复选框菜单项使用 I t em st ener s ct Li (项目接收器)。菜单对象同样能支持 A i onLi s t ener s(动作接收器),但通常不那么有用。一般来说, ct 我们会附加接收器到每个菜单项或复选框菜单项,但下面的例子(对先前例子的修改)演示了一个联合捕捉 多个菜单组件到一个单独的接收器类的方法。正像我们将看到的,它或许不值得为这而激烈地争论。 //: M enuN . j ava ew // M enus i n Java 1. 1 i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t publ i c cl as s M enuN ext ends Fr am { ew e St r i ng[ ] f l avor s = { " C hocol at e" , " St r aw r y" , ber " Vani l l a Fudge Sw r l " , " M nt C p" , i i hi "M ocha A m l ond Fudge" , " Rum Rai s i n" , " Pr al i ne C eam , " M Pi e" } ; r " ud Text Fi el d t = new Text Fi el d( " N f l avor " , 30) ; o M enuBar m = new M b1 enuBar ( ) ; M enu f = new M enu( " Fi l e" ) ; M enu m = new M enu( " Fl avor s " ) ; M enu s = new M enu( " Saf et y" ) ; // A t er nat i ve appr oach: l C heckboxM enuI t em ] s af et y = { [ new C heckboxM enuI t em " G d" ) , ( uar new C heckboxM enuI t em " Hi de" ) ( }; M enuI t em ] f i l e = { [ // N m o enu s hor t cut : new M enuI t em " O ( pen" ) , // A ng a m ddi enu s hor t cut i s ver y s i m e: pl new M enuI t em " Exi t " , ( new M enuShor t cut ( KeyEvent . VK_E) ) }; // A s econd m enu bar t o s w t o: ap M enuBar m = new M b2 enuBar ( ) ; M enu f ooBar = new M enu( " f ooBar " ) ; M enuI t em ] ot her = { [ new M enuI t em " Foo" ) , ( new M enuI t em " Bar " ) , ( new M enuI t em " Baz" ) , ( }; // I ni t i al i zat i on code: { M m = new M ) ; L l L( 424

C I L cm l = new C I L( ) ; M i M s af et y[ 0] . s et A i onC m ct om and( " G d" ) ; uar s af et y[ 0] . addI t em s t ener ( cm l ) ; Li i s af et y[ 1] . s et A i onC m ct om and( " Hi de" ) ; s af et y[ 1] . addI t em s t ener ( cm l ) ; Li i f i l e[ 0] . s et A i onC m ct om and( " O pen" ) ; f i l e[ 0] . addA i onLi s t ener ( m ) ; ct l f i l e[ 1] . s et A i onC m ct om and( " Exi t " ) ; f i l e[ 1] . addA i onLi s t ener ( m ) ; ct l ot her [ 0] . addA i onLi s t ener ( new FooL( ) ) ; ct ot her [ 1] . addA i onLi s t ener ( new Bar L( ) ) ; ct ot her [ 2] . addA i onLi s t ener ( new BazL( ) ) ; ct } But t on b = new But t on( " Sw M ap enus " ) ; publ i c M enuN ( ) { ew FL f l = new FL( ) ; f or ( i nt i = 0; i < f l avor s . l engt h; i ++) { M enuI t em m = new M i enuI t em f l avor s [ i ] ) ; ( m . addA i onLi s t ener ( f l ) ; i ct m add( m ) ; . i // A s epar at or s at i nt er val s : dd i f ( ( i +1) % 3 == 0) m addSepar at or ( ) ; . } f or ( i nt i = 0; i < s af et y. l engt h; i ++) s . add( s af et y[ i ] ) ; f . add( s ) ; f or ( i nt i = 0; i < f i l e. l engt h; i ++) f . add( f i l e[ i ] ) ; m add( f ) ; b1. m add( m ; b1. ) s et M enuBar ( m ; b1) t . s et Edi t abl e( f al s e) ; add( t , Bor der Layout . C TER) ; EN // Set up t he s ys t em f or s w appi ng m enus : b. addA i onLi s t ener ( new BL( ) ) ; ct add( b, Bor der Layout . N RTH) ; O f or ( i nt i = 0; i < ot her . l engt h; i ++) f ooBar . add( ot her [ i ] ) ; m add( f ooBar ) ; b2. } cl as s BL i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct M enuBar m = get M enuBar ( ) ; i f ( m == m b1) s et M enuBar ( m ; b2) el s e i f ( m == m b2) s et M enuBar ( m ; b1) } } cl as s M i m em s A i onLi s t ener { L pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct M enuI t em t ar get = ( M enuI t em e. get Sour ce( ) ; ) St r i ng act i onC m om and = 425

t ar get . get A i onC m ct om and( ) ; i f ( act i onC m om and. equal s ( " O pen" ) ) { St r i ng s = t . get Text ( ) ; bool ean chos en = f al s e; f or ( i nt i = 0; i < f l avor s . l engt h; i ++) i f ( s . equal s ( f l avor s [ i ] ) ) chos en = t r ue; i f ( ! chos en) t . s et Text ( " C hoos e a f l avor f i r s t ! " ) ; el s e t . s et Text ( " O peni ng " + s +" . M m m ! " ) ; m, m } el s e i f ( act i onC m om and. equal s ( " Exi t " ) ) { di s pat chEvent ( new W ndow i Event ( M enuN . t hi s , ew W ndow i Event . WN O _C SI N ) ) ; I D W LO G } } } cl as s FL i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct M enuI t em t ar get = ( M enuI t em e. get Sour ce( ) ; ) t . s et Text ( t ar get . get Label ( ) ) ; } } // A t er nat i vel y, you can cr eat e a di f f er ent l // cl as s f or each di f f er ent M enuI t em Then you . // D t have t o f i gur e out w ch one i t i s : on' hi cl as s FooL i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct t . s et Text ( " Foo s el ect ed" ) ; } } cl as s Bar L i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct t . s et Text ( " Bar s el ect ed" ) ; } } cl as s BazL i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct t . s et Text ( " Baz s el ect ed" ) ; } } cl as s C I L i m em s I t em s t ener { M pl ent Li publ i c voi d i t em at eC St hanged( I t em Event e) { C heckboxM enuI t em t ar get = (C heckboxM enuI t em e. get Sour ce( ) ; ) St r i ng act i onC m om and = t ar get . get A i onC m ct om and( ) ; i f ( act i onC m om and. equal s ( " G d" ) ) uar t . s et Text ( " G d t he I ce C eam " + uar r ! " G di ng i s " + t ar get . get St at e( ) ) ; uar el s e i f ( act i onC m om and. equal s ( " Hi de" ) ) t . s et Text ( " Hi de t he I ce C eam " + r ! 426

" I s i t col d? " + t ar get . get St at e( ) ) ; } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai M enuN f = new M ew enuN ( ) ; ew f . addW ndow s t ener ( i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); f . s et Si ze( 300, 200) ; f . s et Vi s i bl e( t r ue) ; } } ///: ~ 在我们开始初始化节(由注解“I ni t i al i zat i on code: ”后的右大括号指明)的前面部分的代码同先前 (Java 1. 0 版)版本相同。这里我们可以注意到项目接收器和动作接收器被附加在不同的菜单组件上。 Java 1. 1 支持“菜单快捷键”,因此我们可以选择一个菜单项目利用键盘替代鼠标。这十分的简单;我们只 要使用过载菜单项构建器设置第二个自变量为一个 M enuShor t cut (菜单快捷键事件)对象即可。菜单快捷键 构建器设置重要的方法,当它按下时不可思议地显示在菜单项上。上面的例子增加了 C r ol - E 到“Exi t ” ont 菜单项中。 我们同样会注意 s et A i onC m ct om and( ) 的使用。这看似一点陌生因为在各种情况下“act i on com and m ”完全同 菜单组件上的标签一样。为什么不正好使用标签代替可选择的字符串呢?这个难题是国际化的。如果我们重 新用其它语言写这个程序,我们只需要改变菜单中的标签,并不审查代码中可能包含新错误的所有逻辑。因 此使这对检查文字字符串联合菜单组件的代码而言变得简单容易,当菜单标签能改变时“动作指令”可以不 作任何的改变。所有这些代码同“动作指令”一同工作,因此它不会受改变菜单标签的影响。注意在这个程 序中,不是所有的菜单组件都被它们的动作指令所审查,因此这些组件都没有它们的动作指令集。 大多数的构建器同前面的一样,将几个调用的异常增加到接收器中。大量的工作发生在接收器里。在前面例 子的 BL 中,菜单交替发生。在 M 中,“寻找 r i ng”方法被作为动作事件(A i onEvent )的资源并对它进 L ct 行造型送入菜单项,然后得到动作指令字符串,再通过它去贯穿串联组,当然条件是对它进行声明。这些大 多数同前面的一样,但请注意如果“Exi t ”被选中,通过进入封装类对象的句柄(M enuN . t hi s )并创建一 ew 个 W N O _C SI N I D W LO G事件,一个新的窗口事件就被创建了。新的事件被分配到封装类对象的 di s pat chEvent ( ) 方法,然后结束调用 w ndow C os i ng( ) 内部帧的窗口接收器(这个接收器作为一个内部类被创建在 m n( ) i s l ai 里),似乎这是“正常”产生消息的方法。通过这种机制,我们可以在任何情况下迅速处理任何的信息,因 此,它非常的强大。 FL 接收器是很简单尽管它能处理特殊菜单的所有不同的特色。如果我们的逻辑十分的简单明了,这种方法对 我们就很有用处,但通常,我们使用这种方法时需要与 FooL ,Bar L 和 BazL 一道使用,它们每个都附加到一 个单独的菜单组件上,因此必然无需测试逻辑,并且使我们正确地辨识出谁调用了接收器。这种方法产生了 大量的类,内部代码趋向于变得小巧和处理起来简单、安全。 7. 对话框 在这个例子里直接重写了早期的 ToeTes t . j ava程序。在这个新的版本里,任何事件都被安放进一个内部类 中。虽然这完全消除了需要记录产生的任何类的麻烦,作为 ToeTes t . j ava的一个例子,它能使内部类的概念 变得不那遥远。在这点,内嵌类被嵌套达四层之深!我们需要的这种设计决定了内部类的优点是否值得增加 更加复杂的事物。另外,当我们创建一个非静态的内部类时,我们将捆绑非静态类到它周围的类上。有时, 单独的类可以更容易地被复用。 //: ToeTes t N . j ava ew // D ons t r at i on of di al og boxes em // and cr eat i ng your ow com n ponent s i m t j ava. aw . *; por t 427

i m t j ava. aw . event . * ; por t publ i c cl as s ToeTes t N ext ends Fr am { ew e Text Fi el d r ow = new Text Fi el d( " 3" ) ; s Text Fi el d col s = new Text Fi el d( " 3" ) ; publ i c ToeTes t N ( ) { ew s et Ti t l e( " Toe Tes t " ) ; Panel p = new Panel ( ) ; p. s et Layout ( new G i dLayout ( 2, 2) ) ; r p. add( new Label ( " Row " , Label . C TER) ) ; s EN p. add( r ow ) ; s p. add( new Label ( " C um " , Label . C TER) ) ; ol ns EN p. add( col s ) ; add( p, Bor der Layout . N RTH) ; O But t on b = new But t on( " go" ) ; b. addA i onLi s t ener ( new BL( ) ) ; ct add( b, Bor der Layout . SO UTH) ; } s t at i c f i nal i nt BLA K = 0; N s t at i c f i nal i nt XX = 1; s t at i c f i nal i nt O = 2; O cl as s ToeD al og ext ends D al og { i i // w = num ber of cel l s w de i // h = num ber of cel l s hi gh i nt t ur n = XX; // St ar t w t h x' s t ur n i publ i c ToeD al og( i nt w i nt h) { i , s uper ( ToeTes t N . t hi s , ew " The gam i t s el f " , f al s e) ; e s et Layout ( new G i dLayout ( w h) ) ; r , f or ( i nt i = 0; i < w * h; i ++) add( new ToeBut t on( ) ) ; s et Si ze( w * 50, h * 50) ; addW ndow s t ener ( new W ndow dapt er ( ) { i Li i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { di s pos e( ) ; } }); } cl as s ToeBut t on ext ends C anvas { i nt s t at e = BLA K; N ToeBut t on( ) { addM eLi s t ener ( new M ) ) ; ous L( } publ i c voi d pai nt ( G aphi cs g) { r i nt x1 = 0; i nt y1 = 0; i nt x2 = get Si z e( ). w dt h - 1; i i nt y2 = get Si z e( ) . hei ght - 1; g. dr aw Rect ( x1, y1, x2, y2) ; x1 = x2/4; y1 = y2/4; i nt w de = x2/2; i 428

i nt hi gh = y2/2; i f ( s t at e == XX) { g. dr aw ne( x1, y1, Li x1 + w de, y1 + hi gh) ; i g. dr aw ne( x1, y1 + hi gh, Li x1 + w de, y1) ; i } i f ( s t at e == O ) { O g. dr aw val ( x1, y1, O x1 + w de/2, y1 + hi gh/2) ; i } } cl as s M ext ends M eA L ous dapt er { publ i c voi d m ePr es s ed( M eEvent e) { ous ous i f ( s t at e == BLA K) { N s t at e = t ur n; t ur n = ( t ur n == XX ? O : XX) ; O } el s e s t at e = ( s t at e == XX ? O : XX) ; O r epai nt ( ) ; } } } } cl as s BL i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct D al og d = new ToeD al og( i i I nt eger . par s eI nt ( r ow . get Text ( ) ) , s I nt eger . par s eI nt ( col s . get Text ( ) ) ) ; d. s how ) ; ( } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Fr am f = new ToeTes t N ( ) ; e ew f . addW ndow s t ener ( i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); f . s et Si ze( 200, 100) ; f . s et Vi s i bl e( t r ue) ; } } ///: ~ 由于“静态”的东西只能位于类的外部一级,所以内部类不可能拥有静态数据或者静态内部类。 8. 文件对话框 这个例子是直接用新事件模型对 Fi l eD al ogTes t . j ava修改而来。 i //: Fi l eD al ogN . j ava i ew 429

// D ons t r at i on of Fi l e di al og boxes em i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t publ i c cl as s Fi l eD al ogN ext ends Fr am { i ew e Text Fi el d f i l enam = new Text Fi el d( ) ; e Text Fi el d di r ect or y = new Text Fi el d( ) ; But t on open = new But t on( " O pen" ) ; But t on s ave = new But t on( " Save" ) ; publ i c Fi l eD al ogN ( ) { i ew s et Ti t l e( " Fi l e D al og Tes t " ) ; i Panel p = new Panel ( ) ; p. s et Layout ( new Fl ow Layout ( ) ) ; open. addA i onLi s t ener ( new O ct penL( ) ) ; p. add( open) ; s ave. addA i onLi s t ener ( new SaveL( ) ) ; ct p. add( s ave) ; add( p, Bor der Layout . SO UTH) ; di r ect or y. s et Edi t abl e( f al s e) ; f i l enam s et Edi t abl e( f al s e) ; e. p = new Panel ( ) ; p. s et Layout ( new G i dLayout ( 2, 1) ) ; r p. add( f i l enam ; e) p. add( di r ect or y) ; add( p, Bor der Layout . N RTH) ; O } cl as s O penL i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct // Tw ar gum s , def aul t s t o open f i l e: o ent Fi l eD al o d = new Fi l eD al og( i g i Fi l eD al ogN . t hi s , i ew "W hat f i l e do you w ant t o open?" ) ; d. s et Fi l e( " *. j ava" ) ; d. s et D r ect or y( " . " ) ; // C r ent di r ect or y i ur d. s how ) ; ( St r i ng your Fi l e = " *. *" ; i f ( ( your Fi l e = d. get Fi l e( ) ) ! = nul l ) { f i l enam s et Text ( your Fi l e) ; e. di r ect or y. s et Text ( d. get D r ect or y( ) ) ; i } el s e { f i l enam s et Text ( " You pr es s ed cancel " ) ; e. di r ect or y. s et Text ( " " ) ; } } } cl as s SaveL i m em s A i onLi s t ener { pl ent ct publ i c voi d a i onPer f or m A i onEvent e) { ct ed( ct Fi l eD al og d = new Fi l eD al og( i i Fi l eD al ogN . t hi s , i ew "W hat f i l e do you w ant t o s ave?" , Fi l eD al og. SA ; i VE) d. s et Fi l e( " *. j ava" ) ; 430

d. s et D r ect or y( " . " ) ; i d. s how ) ; ( St r i ng s aveFi l e ; i f ( ( s aveFi l e = d. get Fi l e( ) ) ! = nul l ) { f i l enam s et Text ( s aveFi l e) ; e. di r ect or y. s et Text ( d. get D r ect or y( ) ) ; i } el s e { f i l enam s et Text ( " You pr es s ed cancel " ) ; e. di r ect or y. s et Text ( " " ) ; } } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Fr am f = new Fi l eD al ogN ( ) ; e i ew f . addW ndow s t ener ( i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); f . s et Si ze( 250, 110) ; f . s et Vi s i bl e( t r ue) ; } } ///: ~ 如果所有的改变是这样的容易那将有多棒,但至少它们已足够容易,并且我们的代码已受益于这改进的可读 性上。

1 3 . 1 6 . 5 动态绑定事件
新 A T 事件模型给我们带来的一个好处就是灵活性。在老的模型中我们被迫为我们的程序动作艰难地编写代 W 码。但新的模型我们可以用单一方法调用增加和删除事件动作。下面的例子证明了这一点: //: D ynam cEvent s . j ava i // The new Java 1. 1 event m odel al l ow you t o s // change event behavi or dynam cal l y. A s o i l // dem t r at es m t i pl e act i ons f or an event . ons ul i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. ut i l . * ; por publ i c cl as s D ynam cEvent s ext ends Fr am { i e Vect or v = new Vect or ( ) ; i nt i = 0; But t on b1 = new But t on( " But t on 1" ) , b2 = new But t on( " But t on 2" ) ; publ i c D ynam cEvent s ( ) { i s et Layout ( new Fl ow Layout ( ) ) ; b1. addA i onLi s t ener ( new B( ) ) ; ct b1. addA i onLi s t ener ( new B1( ) ) ; ct b2. addA i onLi s t ener ( new B( ) ) ; ct b2. addA i onLi s t ener ( new B2( ) ) ; ct 431

add( b1) ; add( b2) ; } cl as s B i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct Sys t em out . pr i nt l n( " A but t on w pr es s ed" ) ; . as } } cl as s C ount Li s t ener i m em s A i onLi s t ener { pl ent ct i nt i ndex; publ i c C ount Li s t ener ( i nt i ) { i ndex = i ; } publ i c voi d act i onPer f or m A i onEvent e) { ed( ct Sys t em out . pr i nt l n( . "C ount ed Li s t ener " + i ndex) ; } } cl as s B1 i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct Sys t em out . pr i nt l n( " But t on 1 pr es s ed" ) ; . A i onLi s t ener a = new C ct ount Li s t ener ( i ++) ; v. addEl em ( a) ; ent b2. addA i onLi s t ener ( a) ; ct } } cl as s B2 i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct Sys t em out . pr i nt l n( " But t on 2 pr es s ed" ) ; . i nt end = v. s i ze( ) - 1; i f ( end >= 0) { b2. r em oveA i onLi s t ener ( ct ( A i onLi s t ener ) v. el em A ( end) ) ; ct ent t v. r em oveEl em A ( end) ; ent t } } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Fr am f = new D e ynam cEvent s ( ) ; i f . addW ndow s t ener ( i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); f . s et Si ze( 300, 200) ; f . s how ) ; ( } } ///: ~ 这个例子采取的新手法包括: ( 1) 在每个按钮上附着不少于一个的接收器。通常,组件把事件作为多造型处理,这意味着我们可以为单个 事件注册许多接收器。当在特殊的组件中一个事件作为单一造型被处理时,我们会得到 TooM anyLi s t ener s Except i on(即太多接收器异常)。 432

( 2) 程序执行期间,接收器动态地被从按钮 B2 中增加和删除。增加用我们前面见到过的方法完成,但每个组 件同样有一个 r em oveXXXLi s t ener ( ) (删除 XXX接收器)方法来删除各种类型的接收器。 这种灵活性为我们的编程提供了更强大的能力。 我们注意到事件接收器不能保证在命令他们被增加时可被调用(虽然事实上大部分的执行工作都是用这种方 法完成的)。

1 3 . 1 6 . 6 将事务逻辑与 UI 逻辑区分开
一般而言,我们需要设计我们的类如此以至于每一类做“一件事”。当涉及用户接口代码时就更显得尤为重 要,因为它很容易地封装“您要做什么”和“怎样显示它”。这种有效的配合防止了代码的重复使用。更不 用说它令人满意的从 G 中区分出我们的“事物逻辑”。使用这种方法,我们可以不仅仅更容易地重复使用 UI 事物逻辑,它同样可以更容易地重复使用 G 。 UI 其它的争议是“动作对象”存在的完成分离机器的多层次系统。动作主要的定位规则允许所有新事件修改后 立刻生效,并且这是如此一个引人注目的设置系统的方法。但是这些动作对象可以被在一些不同的应用程序 使用并且因此不会被一些特殊的显示模式所约束。它们会合理地执行动作操作并且没有多余的事件。 下面的例子演示了从 G 代码中多么地轻松的区分事物逻辑: UI //: Separ at i on. j ava // Separ at i ng G l ogi c and bus i nes s obj ect s UI i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. appl et . * ; por cl as s Bus i nes s Logi c { pr i vat e i nt m f i er ; odi Bus i nes s Logi c( i nt m od) { m f i er = m odi od; } publ i c voi d s et M f i er ( i nt m odi od) { m f i er = m odi od; } publ i c i nt get M f i er ( ) { odi r et ur n m f i er ; odi } // Som bus i nes s oper at i ons : e publ i c i nt cal cul at i on1( i nt ar g) { r et ur n ar g * m f i er ; odi } publ i c i nt cal cul at i on2( i nt ar g) { r et ur n ar g + m f i er ; odi } } publ i c cl as s Separ at i on ext ends A et { ppl Text Fi el d t = new T ext Fi el d( 20) , m = new Text Fi el d( 20) ; od Bus i nes s Logi c bl = new Bus i nes s Logi c( 2) ; But t on cal c1 = new But t on( " C cul at i on 1" ) , al cal c2 = new But t on( " C cul at i on 2" ) ; al publ i c voi d i ni t ( ) { 433

add( t ) ; cal c1. addA i onLi s t ener ( new C c1L( ) ) ; ct al cal c2. addA i onLi s t ener ( new C c2L( ) ) ; ct al add( cal c1) ; add( cal c2) ; m addText Li s t ener ( new M od. odL( ) ) ; add( new Label ( " M f i er : " ) ) ; odi add( m ; od) } s t at i c i nt get Val ue( Text Fi el d t f ) { try { r et ur n I nt eger . par s eI nt ( t f . get Text ( ) ) ; } cat ch( N ber For m Except i on e) { um at r et ur n 0; } } cl as s C c1L i m em s A i onLi s t ener { al pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct t . s et Text ( I nt eger . t oSt r i ng( bl . cal cul at i on1( get Val ue( t ) ) ) ) ; } } cl as s C c2L i m em s A i onLi s t ener { al pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct t . s et Text ( I nt eger . t oSt r i ng( bl . cal cul at i on2( get Val ue( t ) ) ) ) ; } } cl as s M odL i m em s Text Li s t ener { pl ent publ i c voi d t ext Val ueC hanged( Text Event e) { bl . s et M f i er ( get Val ue( m ) ; odi od) } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Separ at i on appl et = new Separ at i on( ) ; Fr am aFr am = new Fr am " Separ at i on" ) ; e e e( aFr am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); aFr am add( appl et , Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 200, 200) ; e. appl et . i ni t ( ) ; appl et . s t ar t ( ) ; aFr am s et Vi s i bl e( t r ue) ; e. } } ///: ~ 可以看到,事物逻辑是一个直接完成它的操作而不需要提示并且可以在 G 环境下使用的类。它正适合它的 UI 工作。区分动作记录了所有 UI 的详细资料,并且它只通过它的公共接口与事物逻辑交流。所有的操作围绕中 心通过 UI 和事物逻辑对象来回获取信息。因此区分,轮流做它的工作。因为区分中只知道它同事物逻辑对象 434

对话(也就是说,它没有高度的结合),它可以被强迫同其它类型的对象对话而没有更多的烦恼。 思考从事物逻辑中区分 UI 的条件,同样思考当我们调整传统的 Java 代码使它运行时,怎样使它更易存活。

1 3 . 1 6 . 7 推荐编码方法
内部类是新的事件模型,并且事实上旧的事件模型连同新库的特征都被它好的支持,依赖老式的编程方法无 疑增加了一个新的混乱的因素。现在有更多不同的方法为我们编写讨厌的代码。凑巧的是,这种代码显现在 本书中和程序样本中,并且甚至在文件和程序样本中同 SUN公司区别开来。在这一节中,我们将看到一些关 于我们会和不会运行新 A T 的争执,并由向我们展示除了可以原谅的情况,我们可以随时使用接收器类去解 W 决我们的事件处理需要来结束。因为这种方法同样是最简单和最清晰的方法,它将会对我们学习它构成有效 的帮助。 在看到任何事以前,我们知道尽管 Java 1. 1 向后兼容 Java 1. 0(也就是说,我们可以在 1. 1 中编译和运行 1. 0 的程序),但我们并不能在同一个程序里混合事件模型。换言之,当我们试图集成老的代码到一个新的 程序中时,我们不能使用老式的 act i on( ) 方法在同一个程序中,因此我们必须决定是否对新程序使用老的, 难以维护的方法或者升级老的代码。这不会有太多的竞争因为新的方法对老的方法而言是如此的优秀。 1. 准则:运行它的好方法 为了给我们一些事物来进行比较,这儿有一个程序例子演示向我们推荐的方法。到现在它会变得相当的熟悉 和舒适。 //: G oodI dea. j ava // T he bes t w t o des i gn cl as s es us i ng t he new ay // Java 1. 1 event m odel : us e an i nner cl as s f or // each di f f er ent event . Thi s m m zes axi i // f l exi bi l i t y and m odul ar i t y. i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. ut i l . * ; por publ i c cl as s G oodI dea ext ends Fr am { e But t on b1 = new But t on( " But t on 1" ) , b2 = new But t on( " But t on 2" ) ; publ i c G oodI dea( ) { s et Layout ( new Fl ow Layout ( ) ) ; b1. addA i onLi s t ener ( new B1L( ) ) ; ct b2. addA i onLi s t ener ( new B2L( ) ) ; ct add( b1) ; add b2) ; ( } publ i c cl as s B1L i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct Sys t em out . pr i nt l n( " But t on 1 pr es s ed" ) ; . } } publ i c cl as s B2L i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct Sys t em out . pr i nt l n( " But t on 2 pr es s ed" ) ; . } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Fr am f = new G e oodI dea( ) ; f . addW ndow s t ener ( i Li 435

new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em out . pr i nt l n( " W ndow C os i ng" ) ; . i l Sys t em exi t ( 0) ; . } }); f . s et Si ze( 300, 200) ; f . s et Vi s i bl e( t r ue) ; } } ///: ~ 这是颇有点微不足道的:每个按钮有它自己的印出一些事物到控制台的接收器。但请注意在整个程序中这不 是一个条件语句,或者是一些表示“我想要知道怎样使事件发生”的语句。每块代码都与运行有关,而不是 类型检验。也就是说,这是最好的编写我们的代码的方法;不仅仅是它更易使我们理解概念,至少是使我们 更易阅读和维护。剪切和粘贴到新的程序是同样如此的容易。

2. 将主类作为接收器实现 第一个坏主意是一个通常的和推荐的方法。这使得主类(有代表性的是程序片或帧,但它能变成一些类)执 行各种不同的接收器。下面是一个例子: //: BadI dea1. j ava // Som l i t er at ur e r ecom ends t hi s appr oach, e m // but i t ' s m s s i ng t he poi nt of t he new event i // m odel i n Java 1. 1 i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. ut i l . * ; por publ i c cl as s BadI dea1 ext ends Fr am e i m em s A i onLi s t ener , W ndow s t ener { pl ent ct i Li But t on b1 = new But t on( " But t on 1" ) , b2 = new But t on( " But t on 2" ) ; publ i c BadI dea1( ) { s et Layout ( new Fl ow Layout ( ) ) ; addW ndow s t ener ( t hi s ) ; i Li b1. addA i onLi s t ener ( t hi s ) ; ct b2. addA i onLi s t ener ( t hi s ) ; ct add( b1) ; add( b2) ; } publ i c voi d act i onPer f or m A i onEvent e) { ed( ct O ect s our ce = e. get Sour ce( ) ; bj i f ( s our ce == b1) Sys t em out . pr i nt l n( " But t on 1 pr es s ed" ) ; . el s e i f ( s our ce == b2) Sys t em out . pr i nt l n( " But t on 2 pr es s ed" ) ; . el s e Sys t em out . pr i nt l n( " Som hi ng el s e" ) ; . et } publ i c voi d w ndow l os i ng( W ndow i C i Event e) { 436

Sys t em out . pr i nt l n( " W ndow C os i ng" ) ; . i l Sys t em exi t ( 0) ; . } publ publ publ publ publ publ ic ic ic ic ic ic voi d voi d voi d voi d voi d voi d w ndow l os ed( W ndow i C i Event e) { } w ndow ei coni f i ed( W ndow i D i Event e) { } w ndow coni f i ed( W ndow i I i Event e) { } w ndow ct i vat ed( W ndow i A i Event e) { } w ndow eact i vat ed( W ndow i D i Event e) { } w ndow pened( W ndow i O i Event e) { }

publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Fr am f = new BadI dea1( ) ; e f . s et Si ze( 300, 200) ; f . s et Vi s i bl e( t r ue) ; } } ///: ~ 这样做的用途显示在下述三行里: addW ndow s t ener ( t hi s ) ; i Li b1. addA i onLi s t ener ( t hi s ) ; ct b2. addA i onLi s t ener ( t hi s ) ; ct 因为 Badi dea1 执行动作接收器和窗中接收器,这些程序行当然可以接受,并且如果我们一直坚持设法使少量 的类去减少服务器检索期间的程序片载入的作法,它看起来变成一个不错的主意。但是: ( 1) Java 1. 1 版支持 JA R文件,因此所有我们的文件可以被放置到一个单一的压缩的 JA 文件中,只需要一 R 次服务器检索。我们不再需要为 I nt er net 效率而减少类的数量。 ( 2) 上面的代码的组件更加的少,因此它难以抓住和粘贴。注意我们必须不仅要执行各种各样的接口为我们 的主类,但在 act i onPer f or m ) 方法中,我们利用一串条件语句测试哪个动作被完成了。不仅仅是这个状 ed( 态倒退,远离接收器模型,除此之外,我们不能简单地重复使用 act i onPer f or m ) 方法因为它是指定为这 ed( 个特殊的应用程序使用的。将这个程序例子与 G oodI dea. j ava 进行比较,我们可以正好捕捉一个接收器类并 粘贴它和最小的焦急到任何地方。另外我们可以为一个单独的事件注册多个接收器类,允许甚至更多的模块 在每个接收器类在每个接收器中运行。 3. 方法的混合 第二个 bad i dea混合了两种方法:使用内嵌接收器类,但同样执行一个或更多的接收器接口以作为主类的一 部分。这种方法无需在书中和文件中进行解释,而且我可以臆测到 Java 开发者认为他们必须为不同的目的而 采取不同的方法。但我们却不必——在我们编程时,我们或许可能会倾向于使用内嵌接收器类。 //: BadI dea2. j ava // A i m ovem n pr ent over BadI dea1. j ava, s i nce i t // us es t he W ndow dapt er as an i nner cl as s i A / / i nst ead of i m em i ng al l t he m hods of pl ent et // W ndow s t ener , but s t i l l m s s es t he i Li i // val uabl e m odul ar i t y of i nner cl as s es i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. ut i l . * ; por publ i c cl as s BadI dea2 ext ends Fr am e i m em s A i onLi s t ener { pl ent ct But t on b1 = new But t on( " But t on 1" ) , b2 = new But t on( " But t on 2" ) ; 437

publ i c BadI dea2( ) { s et Layout ( new Fl ow Layout ( ) ) ; addW ndow s t ener ( new W ) ) ; i Li L( b1. addA i onLi s t ener ( t hi s ) ; ct b2. addA i onLi s t ener ( t hi s ) ; ct add( b1) ; add( b2) ; } publ i c voi d act i onPer f or m A i onEvent e) { ed( ct O ect s our ce = e. get Sour ce( ) ; bj i f ( s our ce == b1) Sys t em out . pr i nt l n( " But t on 1 pr es s ed" ) ; . el s e i f ( s our ce == b2) Sys t em out . pr i nt l n( " But t on 2 pr es s ed" ) ; . el s e Sys t em out . pr i nt l n(" Som hi ng el s e" ) ; . et } cl as s W ext ends W ndow dapt er { L i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em out . pr i nt l n( " W ndow C os i ng" ) ; . i l Sys t em exi t ( 0) ; . } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Fr am f = new BadI dea2( ) ; e f . s et Si ze( 300, 200) ; f . s et Vi s i bl e( t r ue) ; } } ///: ~ 因为 act i onPer f or m )动作完成方法同主类紧密地结合,所以难以复用代码。它的代码读起来同样是凌乱 ed( 和令人厌烦的,远远超过了内部类方法。不合理的是,我们不得不在 Java 1. 1 版中为事件使用那些老的思 路。 4. 继承一个组件 创建一个新类型的组件时,在运行事件的老方法中,我们会经常看到不同的地方发生了变化。这里有一个程 序例子来演示这种新的工作方法: //: G oodTechni que. j ava // Your f i r s t choi ce w hen over r i di ng com ponent s // s houl d be t o i ns t al l l i s t ener s . T he code i s // m uch s af er , m e m or odul ar and m nt ai nabl e. ai i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t cl as s D s pl ay { i publ i c s t at i c f i nal i nt EVEN = 0, C M N T = 1, T O PO EN M USE = 2, M USE_M VE = 3, O O O FO US = 4, KEY = 5, A TI O = 6, C C N LA = 7; ST publ i c St r i ng[ ] evnt ; 438

D s pl ay( ) { i evnt = new St r i ng[ LA ; ST] f or ( i nt i = 0; i < LA ST; i ++) evnt [ i ] = new St r i ng( ) ; } publ i c voi d s how G aphi cs g { ( r ) f or ( i nt i = 0; i < LA ST; i ++) g. dr aw r i ng( evnt [ i ] , 0, 10 * i + 10) ; St } } cl as s Enabl edPanel ext ends Panel { C or c; ol i nt i d; D s pl ay di s pl ay = new D s pl ay( ) ; i i publ i c Enabl edPanel ( i nt i , C or m { ol c) id= i; c = m c; s et Layout ( new Bor der Layout ( ) ) ; add( new M yBut t on( ) , Bor der Layout . SO UTH) ; addC ponent Li s t ener ( new C ) ) ; om L( addFocus Li s t ener ( new FL( ) ) ; addKeyLi s t ener ( new KL( ) ) ; addM eLi s t ener ( new M ) ) ; ous L( addM eM i onLi s t ener ( new M L( ) ) ; ous ot M } // To el i m nat e f l i cker : i publ i c voi d updat e( G aphi cs g) { r pai nt ( g) ; } publ i c voi d pai nt ( G aphi cs g) { r g. s et C or ( c) ; ol D m i on s = get Si ze( ) ; i ens g. f i l l Rect ( 0, 0, s . w dt h, s . hei ght ) ; i g. s et C or ( C or . bl ack) ; ol ol di s pl ay. s how g) ; ( } // D t need t o enabl e anyt hi ng f or t hi s : on' publ i c voi d pr oces s Event ( A TEvent e) { W di s pl ay. evnt [ D s pl ay. EVEN = e. t oSt r i ng( ) ; i T] r epai nt ( ) ; s uper . pr oces s Event ( e) ; } cl as s C i m em s C ponent Li s t ener { L pl ent om publ i c voi d com ponent M oved( C ponent Event e) { om di s pl ay. evnt [ D s pl ay. C M N T] = i O PO EN " C ponent m om oved" ; r epai nt ( ) ; } publ i c voi d com ponent Res i zed( C ponent Event e) { om di s pl ay. evnt [ D s pl ay. C M N T] = i O PO EN 439

" C ponent r es i zed" ; om r epai nt ( ) ; } publ i c voi d com ponent Hi dden( C ponent Event e) { om di s pl ay. evnt [ D s pl ay. C M N T] = i O PO EN " C ponent hi dden" ; om r epai nt ( ) ; } publ i c voi d com ponent Show C ponent Event e) { n( om di s pl ay. evnt [ D s pl ay. C M N T] = i O PO EN " C ponent s how ; om n" r epai nt ( ) ; } } cl as s FL i m em s Focus Li s t ener { pl ent publ i c voi d f ocus G ned( Focus Event e) { ai di s pl ay. evnt [ D s pl ay. FO US] = i C " FO US gai ned" ; C r epai nt ( ) ; } publ i c voi d f ocus Los t ( Focus Event e) { di s pl ay. evnt [ D s pl ay. FO US] = i C " FO US l os t " ; C r epai nt ( ) ; } } cl as s KL i m em s KeyLi s t ener { pl ent publ i c voi d keyPr es s ed( KeyEvent e) { di s pl ay. evnt [ D s pl ay. KEY] = i " KEY pr es s ed: " ; s how ode( e) ; C } publ i c voi d keyRel eas ed( KeyEvent e) { di s pl ay. evnt [ D s pl ay. KEY] = i " KEY r el eas ed: " ; s how ode( e) ; C } publ i c voi d keyTyped( KeyEvent e) { di s pl ay. evnt [ D s pl ay. KEY] = i " KEY t yped: " ; s how ode( e) ; C } voi d s how ode( KeyEvent e) { C i nt code = e. get KeyC ode( ) ; di s pl ay. evnt [ D s pl ay. KEY] += i KeyEvent . get KeyText ( code) ; r epai nt ( ) ; } } cl as s M i m em s M eLi s t ener { L pl ent ous publ i c voi d m eC i cked( M eEvent e) { ous l ous 440

r eques t Focus ( ) ; // G FO US on cl i ck et C di s pl ay. evnt [ D s pl ay. M USE] = i O " M USE cl i cked" ; O s how ous e( e) ; M } publ i c voi d m ePr es s ed( M eEvent e) { ous ous di s pl ay. evnt [ D s pl ay. M USE] = i O " M USE pr es s ed" ; O s how ous e( e) ; M } publ i c voi d m eRel eas ed( M eEvent e) { ous ous di s pl ay. evnt [ D s pl ay. M USE] = i O " M USE r el eas ed" ; O s how ous e( e) ; M } publ i c voi d m eEnt er ed( M eEvent e) { ous ous di s pl ay. evnt [ D s pl ay. M USE] = i O " M USE ent er ed" ; O s how ous e( e) ; M } publ i c voi d m ouseExi t ed( M eEvent e) { ous di s pl ay. evnt [ D s pl ay. M USE] = i O " M USE exi t ed" ; O s how ous e( e) ; M } voi d s how ous e( M eEvent e) { M ous di s pl ay. evnt [ D s pl ay. M USE] += i O " , x = " + e. get X( ) + " , y = " + e. get Y( ) ; r epai nt ( ) ; } } cl as s M L i m em s M eM i onLi s t ener { M pl ent ous ot publ i c voi d m eD agged( M eEvent e) { ous r ous di s pl ay. evnt [ D s pl ay. M USE_M VE] = i O O " M USE dr agged" ; O s how ous e( e) ; M } publ i c voi d m eM ous oved( M eEvent e) { ous di s pl ay. evnt [ D s pl ay. M USE_M VE] = i O O " M USE m O oved" ; s how ous e( e) ; M } voi d s how ous e( M eEvent e) { M ous di s pl ay. evnt [ D s pl ay. M USE_M VE] += i O O " , x = " + e. get X( ) + " , y = " + e. get Y( ) ; r epai nt ( ) ; } } }

441

cl as s M yBut t on ext ends But t on { i nt cl i ckC ount er ; St r i ng l abel = " " ; publ i c M yBut t on( ) { addA i onLi s t ener ( new A ) ) ; ct L( } publ i c voi d pai nt ( G aphi cs g) { r g. s et C or ( C or . gr een) ; ol ol D m i on s = get Si ze( ) ; i ens g. f i l l Rect ( 0, 0, s . w dt h, s . hei ght ) ; i g. s et C or ( C or . bl ack) ; ol ol g. dr aw Rect ( 0, 0, s . w dt h - 1, s . hei ght - 1) ; i dr aw Label ( g) ; } pr i vat e voi d dr aw Label ( G aphi cs g) { r Font M r i cs f m = g. get Font M r i cs ( ) ; et et i nt w dt h = f m s t r i ngW dt h( l abel ) ; i . i i nt hei ght = f m get Hei ght ( ) ; . i nt as cent = f m get A cent ( ) ; . s i nt l eadi ng = f m get Leadi ng( ) ; . i nt hor i zM gi n = ar ( get Si ze( ) . w dt h - w dt h) /2; i i i nt ver M gi n = ar ( get Si ze( ) . hei ght - hei ght ) /2; g. s et C or ( C or . r ed) ; ol ol g. dr aw r i ng( l abel , hor i zM gi n, St ar ver M gi n + as cent + l eadi ng) ; ar } cl as s A i m em s A i onLi s t ener { L pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct cl i ckC ount er ++; l abel = " cl i ck #" + cl i ckC ount er + " " + e. t oSt r i ng( ) ; r epai nt ( ) ; } } } publ i c cl as s G Techni que ext ends Fr am { ood e G oodTechni que( ) { s et Layout ( new G i dLayout ( 2, 2) ) ; r add( new Enabl edPanel ( 1, C or . cyan) ) ; ol add( new Enabl edPanel ( 2, C or . l i ght G ay) ) ; ol r add( new Enabl edPanel ( 3, C or . yel l ow ) ; ol ) } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Fr am f = new G e oodTechni que( ) ; f . s et Ti t l e( " G ood Techni que" ) ; f . addW ndow s t ener ( i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em out . pr i nt l n( e) ; . 442

Sys t em out . pr i nt l n( " W ndow C os i ng" ) ; . i l Sys t em exi t ( 0) ; . } }); f . s et Si ze( 700, 700) ; f . s et Vi s i bl e( t r ue) ; } } ///: ~ 这个程序例子同样证明了各种各样的发现和显示关于它们的信息的事件。这种显示是一种集中显示信息的方 法。一组字符串去获取关于每种类型的事件的信息,并且 s how )方法对任何图像对象都设置了一个句柄,我 ( 们采用并直接地写在外观代码上。这种设计是有意的被某种事件重复使用。 激活面板代表了这种新型的组件。它是一个底部有一个按钮的彩色的面板,并且它由利用接收器类为每一个 单独的事件来引发捕捉所有发生在它之上的事件,除了那些在激活面板过载的老式的 pr oces s Event ( ) 方法 (注意它应该同样调用 s uper . pr oces s Event ( ))。利用这种方法的唯一理由是它捕捉发生的每一个事件,因 此我们可以观察持续发生的每一事件。pr oces s Event ( ) 方法没有更多的展示代表每个事件的字符串,否则它 会不得不使用一串条件语句去寻找事件。在其它方面,内嵌接收类早已清晰地知道被发现的事件。(假定我 们注册它们到组件,我们不需要任何的控件的逻辑,这将成为我们的目的。)因此,它们不会去检查任何事 件;这些事件正好做它们的原材料。 每个接收器修改显示字符串和它的指定事件,并且调用重画方法 r epai nt ( ) 因此将显示这个字符串。我们同 样能注意到一个通常能消除闪烁的秘诀: publ i c voi d updat e( G aphi cs g) { r pai nt ( g) ; } 我们不会始终需要过载 updat e( ) ,但如果我们写下一些闪烁的程序,并运行它。默认的最新版本的清除背景 然后调用 pai nt ( ) 方法重新画出一些图画。这个清除动作通常会产生闪烁,但是不必要的,因为 pai nt ( )重画 了整个的外观。 我们可以看到许多的接收器——但是,对接收器输入检查指令,但我们却不能接收任何组件不支持的事件。 (不像 BadTechnuque. j ava那样我们能时时刻刻看到)。 试验这个程序是十分的有教育意义的,因为我们学习了许多的关于在 Java 中事件发生的方法。一则它展示了 大多数开窗口的系统中设计上的瑕疵:它相当的难以去单击和释放鼠标,除非移动它,并且当我们实际上正 试图用鼠标单击在某物体上时开窗口的会常常认为我们是在拖动。一个解决这个问题的方案是使用 m ePr es s ed( ) 鼠标按下方法和 m eRel eas ed( )鼠标释放方法去代替 m eC i cked( ) 鼠标单击方法,然后 ous ous ous l 判断是否去调用我们自己的以时间和 4 个像素的鼠标滞后作用的“m eReal l yC i cked( ) 真实的鼠标单击” ous l 方法。 5. 蹩脚的组件继承 另一种做法是调用 enabl eEvent ( ) 方法,并将与希望控制的事件对应的模型传递给它(许多参考书中都曾提 及这种做法)。这样做会造成那些事件被发送至老式方法(尽管它们对 Java 1. 1 来说是新的),并采用象 pr oces s Focus Event ( ) 这样的名字。也必须要记住调用基础类版本。下面是它看起来的样子。 //: BadTechni que. j ava // I t ' s pos s i bl e t o over r i de com ponent s t hi s w ay, / / but t he l i s t ener appr oach i s m uch bet t er , s o // w w d you? hy oul i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t cl as s D s pl ay { i publ i c s t at i c f i nal i nt EVEN = 0, C M N T = 1, T O PO EN M USE = 2, M USE_M VE = 3, O O O 443

FO US = 4, KEY = 5, A TI O = 6, C C N LA = 7; ST publ i c St r i ng[ ] evnt ; D s pl ay( ) { i evnt = new St r i ng[ LA ; ST] f or ( i nt i = 0; i < LA ST; i ++) evnt [ i ] = new St r i ng( ) ; } publ i c voi d s how G aphi cs g) { ( r f or ( i nt i = 0; i < LA ST; i ++) g. dr aw r i ng( evnt [ i ] , 0, 10 * i + 10) ; St } } cl a s Enabl edPanel ext ends Panel { s C or c; ol i nt i d; D s pl ay di s pl ay = new D s pl ay( ) ; i i publ i c Enabl edPanel ( i nt i , C or m { ol c) id= i; c = m c; s et Layout ( new Bor der Layout ( ) ) ; add( new M yBut t on( ) , Bor der Layout . SO UTH) ; // T ype checki ng i s l os t . You can enabl e and // pr oces s event s t hat t he com ponent does n' t // capt ur e: enabl eEvent s ( // Panel does n' t handl e t hes e: A TEvent . A TI O _EVEN A | W C N T_M SK A TEvent . A JUSTM T_EVEN A | W D EN T_M SK A TEvent . I TEM W _EVEN A | T_M SK A TEvent . TEXT_EVEN A | W T_M SK A TEvent . W N O _EVEN A | W I DW T_M SK // Panel can handl e t hes e: A TEvent . C M N T_EVEN A | W O PO EN T_M SK A TEvent . FO US_EVEN A | W C T_M SK A TEvent . KEY_EVEN A | W T_M SK A TEvent . M USE_EVEN A | W O T_M SK A TEvent . M USE_M TI O _EVEN A | W O O N T_M SK A TEvent . C N I N W O TA ER_EVEN A ; T_M SK) // You can enabl e an event w t hout i // over r i di ng i t s pr oces s m hod. et } // To el i m nat e f l i cker : i publ i c voi d updat e( G aphi cs g) { r pai nt ( g) ; } publ i c voi d pai nt ( G aphi cs g) { r g. s et C or ( c) ; ol D m i on s = get Si ze( ) ; i ens g. f i l l Rect ( 0, 0, s . w dt h, s . hei ght ) ; i g. s et C or ( C or . bl ack) ; ol ol 444

di s pl ay. s how g) ; ( } publ i c voi d pr oces s Event ( A TEvent e) { W di s pl ay. evnt [ D s pl ay. EVEN = e. t oSt r i ng( ) ; i T] r epai nt ( ) ; s up . pr oces s Event ( e) ; er } publ i c voi d pr oces s C ponent Event ( C ponent Event e) { om om s w t ch( e. get I D ) ) { i ( cas e C ponent Event . C M N T_M VED om O PO EN O : di s pl ay. evnt [ D s pl ay. C M N T] = i O PO EN " C ponent m om oved" ; br eak; cas e C ponent Event . C M N T_RESI ZED om O PO EN : di s pl ay. evnt [ D s pl ay. C M N T] = i O PO EN " C ponent r es i z ed" ; om br eak; cas e C ponent Event . C M N T_HI D EN om O PO EN D : di s pl ay. evnt [ D s pl ay. C M N T] = i O PO EN " C ponent hi dden" ; om br eak; cas e C ponent Event . C M N T_SHO N om O PO EN W: di s pl ay. evnt [ D s pl ay. C M N T] = i O PO EN " C ponent s how ; om n" br eak; def aul t : } r epai nt ( ) ; // M t al w us ays r em ber t o cal l t he " s uper " em // ver s i on of w ever you over r i de: hat s uper . p oces s C ponent Event ( e) ; r om } publ i c voi d pr oces s Focus Event ( Focus Event e) { s w t ch( e. get I D ) ) { i ( cas e Focus Event . FO US_G I N : C A ED di s pl ay. evnt [ D s pl ay. FO US] = i C " FO US gai ned" ; C br eak; cas e Focus Event . FO US_LO C ST: di s pl ay. evnt [ D s pl ay. FO US] = i C " FO US l os t " ; C br eak; def aul t : } r epai nt ( ) ; s uper . pr oces s Focus Event ( e) ; } publ i c voi d pr oces s KeyEvent ( KeyEvent e) { s w t ch( e. get I D ) ) { i ( cas e KeyEvent . KEY_PRESSED : di s pl ay. evnt [ D s pl ay. KEY] = i 445

" KEY pr es s ed: " ; br eak; cas e KeyEvent . KEY_RELEA : SED di s pl ay. evnt [ D s pl ay. KEY] = i " KEY r el eas ed: " ; br eak; cas e KeyEvent . KEY_TYPED : di s pl ay. evnt [ D s pl ay. KEY] = i " KEY t yped: " ; br eak; def aul t : } i nt code = e. get KeyC ode( ) ; di s pl ay. evnt [ D s pl ay. KEY] += i KeyEvent . get KeyText ( code) ; r epai nt ( ) ; s uper . pr oces s KeyEvent ( e) ; } publ i c voi d pr oces s M eEvent ( M eEvent e) { ous ous s w t ch( e. get I D ) ) { i ( cas e M eEvent . M USE_C C : ous O LI KED r eques t Focus ( ) ; // G FO US on cl i ck et C di s pl ay. evnt [ D s pl ay. M USE] = i O " M USE cl i cked" ; O br eak; cas e M eEvent . M USE_PRESSED ous O : di s pl ay. evnt [ D s pl ay. M USE] = i O " M USE pr es s ed" ; O br eak; cas e M eEvent . M USE_RELEA : ous O SED di s pl ay. evnt [ D s pl ay. M USE] = i O " M USE r el eas ed" ; O br eak; cas e M eEvent . M USE_EN ous O TERED : di s pl ay. evnt [ D s pl ay. M USE] = i O " M USE ent er ed" ; O br eak; cas e M eEvent . M USE_EXI TED ous O : di s pl ay. evnt [ D s pl ay. M USE] = i O " M USE exi t ed" ; O br eak; def aul t : } di s pl ay. evnt [ D s pl ay. M USE] += i O " , x = " + e. get X( ) + " , y = " + e. get Y( ) ; r epai nt ( ) ; s uper . pr oces s M eEvent ( e) ; ous } publ i c voi d pr oces s M eM i onEvent ( M eEvent e) { ous ot ous s w t ch( e. get I D ) ) { i ( 446

cas e M eEvent . M USE_D G ED ous O RA G : di s pl ay. evnt [ D s pl ay. M USE_M VE] = i O O " M USE dr agged" ; O br eak; cas e M eEvent . M USE_M VED ous O O : di s pl ay. evnt [ D s pl ay. M USE_M VE] = i O O " M USE m O oved" ; br eak; def aul t : } di s pl ay. evnt [ D s pl ay. M USE_M VE] += i O O " , x = " + e. get X( ) + " , y = " + e. get Y( ) ; r epai nt ( ) ; s uper . pr oces s M eM i onEvent ( e) ; ous ot } } cl as s M yBut t on ext ends But t on { i nt cl i ckC ount er ; St r i ng l abel = " " ; publ i c M yBut t on( ) { enabl eEvent s ( A TEvent . A TI O _EVEN A ; W C N T_M SK) } publ i c voi d pai nt ( G aphi cs g) { r g. s et C or ( C or . gr een) ; ol ol D m i on s = get Si ze( ) ; i ens g. f i l l Rect ( 0, 0, s . w dt h, s . hei ght ) ; i g. s et C or ( C or . bl ack) ; ol ol g. dr aw Rect ( 0, 0, s . w dt h - 1, s . hei ght - 1) ; i dr aw Label ( g) ; } pr i vat e voi d dr aw Label ( G aphi cs g) { r Font M r i cs f m = g. get Font M r i cs ( ) ; et et i nt w d h = f m s t r i ngW dt h( l abel ) ; i t . i i nt hei ght = f m get Hei ght ( ) ; . i nt as cent = f m get A cent ( ) ; . s i nt l eadi ng = f m get Leadi ng( ) ; . i nt hor i zM gi n = ar ( get Si ze( ) . w dt h - w dt h) /2; i i i nt ver M gi n = ar ( get Si ze( ) . hei ght - hei ght ) /2; g. s et C l or ( C or . r ed) ; o ol g. dr aw r i ng( l abel , hor i zM gi n, St ar ver M gi n + as cent + l eadi ng) ; ar } publ i c voi d pr oces s A i onEvent ( A i onEvent e) { ct ct cl i ckC ount er ++; l abel = " cl i ck #" + cl i ckC ount er + " " + e. t oSt r i ng( ) ; r epai nt ( ) ; s uper . pr oces s A i onEvent ( e) ; ct 447

} } publ i c cl as s BadTechni que ext ends Fr am { e BadTechni que( ) { s et Layout ( new G i dLayout ( 2, 2) ) ; r add( new Enabl edPanel ( 1, C or . cyan) ) ; ol add( new Enabl edPanel ( 2, C or . l i ght G ay) ) ; ol r add( new Enabl edPanel ( 3, C or . yel l ow ) ; ol ) // You can al s o do i t f or W ndow : i s enabl eEvent s ( A TEvent . W N O _EVEN A ; W I DW T_M SK) } publ i c voi d pr oces s W ndow i Event ( W ndow i Event e) { Sys t em out . pr i nt l n( e) ; . i f ( e. get I D ) == W ndow ( i Event . W N O _C SI N ) { I D W LO G Sys t em out . pr i nt l n( " W ndow C os i ng" ) ; . i l Sys t em exi t ( 0) ; . } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Fr am f = new BadTechni que( ) ; e f . s et Ti t l e( " Bad Techni que" ) ; f . s et Si ze( 700, 700) ; f . s et Vi s i bl e( t r ue) ; } } ///: ~ 的确,它能够工作。但却实在太蹩脚,而且很难编写、阅读、调试、维护以及再生。既然如此,为什么还不 使用内部接收器类呢?

13. 17 J ava 1. 1 用户接口 API
Java 1. 1 版同样增加了一些重要的新功能,包括焦点遍历,桌面色彩访问,打印“沙箱内”及早期的剪贴板 支持。 焦点遍历十分的简单,因为它显然存在于 A T 库里的组件并且我们不必为使它工作而去做任何事。如果我们 W 制造我们自己组件并且想使它们去处理焦点遍历,我们过载 i s Focus Tr aver s abl e( ) 以使它返回真值。如果我 们想在一个鼠标单击上捕捉键盘焦点,我们可以捕捉鼠标按下事件并且调用 r eques t Focus ( ) 需求焦点方法。

1 3 . 1 7 . 1 桌面颜色
利用桌面颜色,我们可知道当前用户桌面都有哪些颜色选择。这样一来,就可在必要的时候通过自己的程序 来运用那些颜色。颜色都会得以自动初始化,并置于 Sys t em ol or 的 s t at i c 成员中,所以要做的唯一事情就 C 是读取自己感兴趣的成员。各种名字的意义是不言而喻的:des kt op,act i veC i on, apt act i veC i onText ,act i veC i onBor der , i nact i veC i on, i nact i veC i onText , apt apt apt apt i nact i veC i onBor der , w ndow w ndow der , w ndow apt i , i Bor i Text , m enu,m enuText ,te , t ext Text , xt t ext Hi ghl i ght , t ext Hi ghl i ght Text ,t ext I nact i veText ,cont r ol , co r o Te , c nt r o H g i g , nt l xt o l i hl ht cont r ol Lt Hi ghl i ght ,cont r ol Shadow,cont r ol D kShadow s cr ol l bar , i nf o(用于帮助)以及 i nf o , Text (用于帮助文字)。

1 3 . 1 7 . 2 打印
非常不幸,打印时没有多少事情是可以自动进行的。相反,为完成打印,我们必须经历大量机械的、非 O O (面向对象)的步骤。但打印一个图形化的组件时,可能多少有点儿自动化的意思:默认情况下,pr i nt ( ) 方 法会调用 pai nt ( ) 来完成自己的工作。大多数时候这都已经足够了,但假如还想做一些特别的事情,就必须 448

知道页面的几何尺寸。 下面这个例子同时演示了文字和图形的打印,以及打印图形时可以采取的不同方法。此外,它也对打印支持 进行了测试: //: Pr i nt D o. j ava em // Pr i nt i ng w t h Java 1. 1 i i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t publ i c cl as s Pr i nt D o ext ends Fr am { em e But t on pr i nt Text = new But t on( " Pr i nt Text " ) , pr i nt G aphi cs = new But t on( " Pr i nt G aphi cs " ) ; r r Text Fi el d r i ngN = new Text Fi el d( 3) ; um C ce f aces = new C ce( ) ; hoi hoi G aphi cs g = nul l ; r Pl ot pl ot = new Pl ot 3( ) ; // T r y di f f er ent pl ot s Tool ki t t k = Tool ki t . get D aul t Tool ki t ( ) ; ef publ i c Pr i nt D o( ) { em r i ngN . s et Text ( " 3" ) ; um r i ngN . addText Li s t ener ( new Ri ngL( ) ) ; um Panel p = new Panel ( ) ; p. s et Layout ( new Fl ow Layout ( ) ) ; pr i nt Text . addA i onLi s t ener ( new TBL( ) ) ; ct p. add( pr i nt Text ) ; p. add( new Label ( " Font : " ) ) ; p. add( f aces ) ; pr i nt G aphi cs . addA i onLi s t ener ( new G ) ) ; r ct BL( p. add( pr i nt G aphi cs ) ; r p. add( new Label ( " Ri ngs : " ) ) ; p. add( r i ngN ) ; um s et Layout ( new Bor der Layout ( ) ) ; add( p, Bor der Layout . N RTH) ; O add( pl ot , Bor der Layout . C TER) ; EN St r i ng[ ] f ont Li s t = t k. get Font Li s t ( ) ; f or ( i nt i = 0; i < f ont Li s t . l engt h; i ++) f aces . add( f ont Li s t [ i ] ) ; f aces . s el ect ( " Ser i f " ) ; } cl as s Pr i nt D a { at publ i c Pr i nt Job pj ; publ i c i nt pageW dt h, pageHei ght ; i Pr i nt D a( St r i ng j obN e) { at am pj = get Tool ki t ( ) . get Pr i nt Job( Pr i nt D o. t hi s , j obN e, nul l ) ; em am i f ( pj ! = nul l ) { pageW dt h = pj . get PageD m i on( ) . w dt h; i i ens i pageHei ght = pj . get PageD m i on( ) . hei ght ; i ens g = pj . get G aphi cs ( ) ; r } } voi d end( ) { pj . end( ) ; } 449

} cl as s C hangeFont { pr i vat e i nt s t r i ngHei ght ; C hangeFont ( St r i ng f ace, i nt s t yl e, i nt poi nt ) { i f ( g ! = nul l ) { g. s et Font ( new Font ( f ace, s t yl e, poi nt ) ) ; s t r i ngHei ght = g. get Font M r i cs ( ) . get Hei ght ( ) ; et } } i nt s t r i ngW dt h( St r i ng s ) { i r et ur n g. get Font M r i cs ( ) . s t r i ngW dt h( s ) ; et i } i nt s t r i ngHei ght ( ) { r et ur n s t r i ngHei ght ; } } cl as s TBL i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct Pr i nt D a pd = at new Pr i nt D a( " Pr i nt Text Tes t " ) ; at // N l m ul eans pr i nt j ob cancel ed: i f ( pd == nul l ) r et ur n; St r i ng s = " Pr i nt D o" ; em C hangeFont cf = new C hangeFont ( f aces . get Sel ect edI t em ) , Font . I TA C 72) ; ( LI , g. dr aw r i ng( s , St ( pd. pageW dt h - cf . s t r i ngW dt h( s ) ) / 2, i i ( pd. pageHei ght - cf . s t r i ngHei ght ( ) ) / 3) ; s = " A s m l er poi nt s i ze" ; al cf = new C hangeFont ( f aces . get Sel ect edI t em ) , Font . BO , 48) ; ( LD g. dr aw r i ng( s , St ( pd. pageW dt h - cf . s t r i ngW dt h( s ) ) / 2, i i ( i nt ) ( ( pd. pageHei ght cf . s t r i ngHei ght ( ) ) /1. 5) ) ; g. di s pos e( ) ; pd. end( ) ; } } cl as s G i m em s A i onLi s t ener { BL pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct Pr i nt D a pd = at new Pr i nt D a( " Pr i nt G aphi cs Tes t " ) ; at r i f ( pd == nul l ) r et ur n; pl ot . pr i nt ( g) ; g. di s pos e( ) ; pd. end( ) ; } } cl as s Ri ngL i m em s Text Li s t ener { pl ent publ i c voi d t ext Val ueC hanged( Text Event e) { i nt i = 1; 450

try { i = I nt eger . par s eI nt ( r i ngN . get Text ( ) ) ; um } cat ch( N ber For m Except i on ex) { um at i = 1; } pl ot . r i ngs = i ; pl ot . r epai nt ( ) ; } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Fr am pdem = new Pr i nt D o( ) ; e o em pdem s et Ti t l e( " Pr i nt D o" ) ; o. em pdem addW ndow s t ener ( o. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); pdem s et Si ze( 500, 500) ; o. pdem s et Vi s i bl e( t r ue) ; o. } } cl as s Pl ot ext ends C anvas { publ i c i nt r i ngs = 3; } cl as s Pl ot 1 ext ends Pl ot { // D aul t pr i nt ( ) cal l s pai nt ( ) : ef publ i c voi d pai nt ( G aphi cs g) { r i nt w = get Si ze( ) . w dt h; i i nt h = get Si ze( ) . hei ght ; i nt xc = w / 2; i nt yc = w / 2; i nt x = 0, y = 0; f or ( i nt i = 0; i < r i ngs ; i ++) { i f ( x < xc & y < yc) { & g. dr aw val ( x, y, w h); O , x += 10; y += 10; w - = 20; h - = 20; } } } } cl as s Pl ot 2 ext ends Pl ot { // To f i t t he pi ct ur e t o t he page, you m t us // know w her you' r e pr i nt i ng or pai nt i ng: het publ i c voi d pai nt ( G aphi cs g) { r i nt w h; , i f ( g i nst anceof Pr i nt G aphi cs ) { r Pr i nt Job pj = 451

( ( Pr i nt G aphi cs ) g) . get Pr i nt Job( ) ; r w = pj . get PageD m i on( ) . w dt h; i ens i h = pj . get PageD m i on( ) . hei ght ; i ens } el s e { w = get Si ze( ) . w dt h; i h = get Si ze( ) . hei ght ; } i nt xc = w / 2; i nt yc = w / 2; i nt x = 0, y = 0; f or ( i nt i = 0; i < r i ngs ; i ++) { i f ( x < xc & y < yc) { & g. dr aw val ( x, y, w h) ; O , x += 10; y += 10; w - = 20; h - = 20; } } } } cl as s Pl ot 3 ext ends Pl ot { // Som hat bet t er . Separ at e ew // pr i nt i ng f r om pai nt i ng: publ i c voi d pr i nt ( G aphi cs g) { r // A s um i t ' s a Pr i nt G aphi cs obj ect : s e r Pr i nt Job pj = ( ( Pr i nt G aphi cs ) g) . get Pr i nt Job( ) ; r i nt w = pj . get PageD m i on( ) . w dt h; i ens i i nt h = pj . get PageD m i on( ) . hei ght ; i ens doG aphi cs ( g, w h) ; r , } publ i c voi d pai nt ( G aphi cs g) { r i nt w = get Si ze( ) . w dt h; i i nt h = get Si ze( ) . hei ght ; doG aphi cs ( g, w h) ; r , } pr i vat e voi d doG aphi cs ( r G aphi cs g, i nt w i nt h) { r , i nt xc = w / 2; i nt yc = w / 2; i nt x = 0, y = 0; f or ( i nt i = 0; i < r i ngs ; i ++) { i f ( x < xc & y < yc) { & g. dr aw val ( x, y, w h) ; O , x += 10; y += 10; w - = 20; h - = 20; } } } } ///: ~

452

这个程序允许我们从一个选择列表框中选择字体(并且我们会注意到很多有用的字体在 Java 1. 1 版中一直受 到严格的限制,我们没有任何可以利用的优秀字体安装在我们的机器上)。它使用这些字体去打出粗体,斜 体和不同大小的文字。另外,一个新型组件调用过的绘图被创建,以用来示范图形。当打印图形时,绘图拥 有的 r i ng 将显示在屏幕上和打印在纸上,并且这三个衍生类 Pl ot 1,Pl ot 2,Pl ot 3 用不同的方法去完成任务 以便我们可以看到我们选择的事物。同样,我们也能在一个绘图中改变一些 r i ng——这很有趣,因为它证明 了 Java 1. 1 版中打印的脆弱。在我的系统里,当 r i ng 计数显示“t oo hi gh”(究竟这是什么意思?)时, 打印机给出错误信息并且不能正确地工作,而当计数给出“l ow enough”信息时,打印机又能工作得很好。 我们也会注意到,当打印到看起来实际大小不相符的纸时页面的大小便产生了。这些特点可能被装入到将来 发行的 Java 中,我们可以使用这个程序来测试它。 这个程序为促进重复使用,不论何时都可以封装功能到内部类中。例如,不论何时我想开始打印工作(不论 图形或文字),我必须创建一个 Pr i nt Job 打印工作对象,该对象拥有它自己的连同页面宽度和高度的图形对 象。创建的 Pr i nt Job 打印工作对象和提取的页面尺寸一起被封装进 Pr i nt D a cl as s 打印类中。 at 1. 打印文字 打印文字的概念简单明了:我们选择一种字体和大小,决定字符串在页面上存在的位置,并且使用 G aphi cs . dr aw t i ng( )方法在页面上画出字符串就行了。这意味着,不管怎样我们必须精确地计算每行字符 r Sr 串在页面上存在的位置并确定字符串不会超出页面底部或者同其它行冲突。如果我们想进行字处理,我们将 进行的工作与我们很相配。C hangeFont 封装进少量从一种字体到其它的字体的变更方法并自动地创建一个新 字体对象和我们想要的字体,款式(粗体和斜体——目前还不支持下划线、空心等)以及点阵大小。它同样 会简单地计算字符串的宽度和高度。当我们按下“Pr i nt t ext ”按钮时,T BL 接收器被激活。我们可以注意 到它通过反复创建 C hangeFont 对象和调用 dr aw r i ng( )来在计算出的位置打印出字符串。注意是否这些计 St 算产生预期的结果。(我使用的版本没有出错。) 2. 打印图形 按下“Pr i nt gr aphi cs”按钮时,G 接收器会被激活。我们需要打印时,创建的 Pr i nt D a对象初始化, BL at 然后我们简单地为这个组件调用 pr i nt ( )打印方法。为强制打印,我们必须为图形对象调用 di s pos e( ) 处理方 法,并且为 Pr i nt D a对象调用 end( ) 结束方法(或改变为为 Pr i nt Job 调用 end( ) 结束方法。) at 这种工作在绘图对象中继续。我们可以看到基础类绘图是很简单的——它扩展画布并且包括一个中断调用 r i ng 来指明多少个集中的 r i ng 需要画在这个特殊的画布上。这三个衍生类展示了可达到一个目的的不同的 方法:画在屏幕上和打印的页面上。 Pl ot 1 采用最简单的编程方法:忽略绘画和打印的不同,并且过载 pai nt ( )绘画方法。使用这种工作方法的原 因是默认的 pr i nt ( )打印方法简单地改变工作方法转而调用 Pai nt ( ) 。但是,我们会注意到输出的尺寸依赖于 屏幕上画布的大小,因为宽度和高度都是在调用 C anvas . get Si ze( ) 方法时决定是,所以这是合理的。如果我 们图像的尺寸一值都是固定不变的,其它的情况都可接受。当画出的外观的大小如此的重要时,我们必须深 入了解的尺寸大小的重要性。不凑巧的是,就像我们将在 Pl ot 2 中看到的一样,这种方法变得很棘手。因为 一些我们不知道的好的理由,我们不能简单地要求图形对象以它自己的大小画出外观。这将使整个的处理工 作变得十分的优良。相反,如果我们打印而不是绘画,我们必须利用 RTTI i ns t anceof 关键字(在本书 11 章 中有相应描述)来测试 Pr i nt G api cs,然后下溯造型并调用这独特的 Pr i nt G aphi cs 方法:get Pr i nt Job( ) r r 方法。现在我们拥有 Pr i nt Job 的句柄并且我们可以发现纸张的高度和宽度。这是一种 hacky 的方法,但也许 这对它来说是合理的理由。(在其它方面,到如今我们看到一些其它的库设计,因此,我们可能会得到设计 者们的想法。) 我们可以注意到 Pl ot 2 中的 pai nt ( )绘画方法对打印和绘图的可能性进行审查。但是因为当打印时 Pr i nt ( )方 法将被调用,那么为什么不使用那种方法呢?这种方法同样也在 Pl ot 3 中也被使用,并且它消除了对 i ns t anceof 使用的需求,因为在 Pr i nt ( )方法中我们可以假设我们能对一个 Pr i nt G aphi cs 对象造型。这样 r 也不坏。这种情况被放置公共绘画代码到一个分离的 doG aphi cs ( ) 方法的办法所改进。 r 2. 在程序片内运行帧 如果我们想在一个程序片中打印会怎以样呢?很好,为了打印任何事物我们必须通过工具组件对象的 get Pr i nt Job( ) 方法拥有一个 Pr i nt Job 对象,设置唯一的一个帧对象而不是一个程序片对象。于是它似乎可 能从一个应用程序中打印,而不是从一个程序片中打印。但是,它变为我们可以从一个程序片中创建一个帧 (相反的到目前为止,我在程序片或应用程序例子中所做的,都可以生成程序片并安放帧。)。这是一个很 有用的技术,因为它允许我们在程序片中使用一些应用程序(只要它们不妨碍程序片的安全)。但是,当应 453

用程序窗口在程序片中出现时,我们会注意到 W 浏览器插入一些警告在它上面,其中一些产生 EB “W ni ng: A et W ndow (警告:程序片窗口)”的字样。 ar ppl i . 我们会看到这种技术十分直接的安放一个帧到程序片中。唯一的事是当用户关闭它时我们必须增加帧的代码 (代替调用 Sys t em exi t ( ) ): . //: Pr i nt D oA et . j ava em ppl // C eat i ng a Fr am f r om w t hi n an A et r e i ppl i m t j ava. appl et . * ; por i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t publ i c cl as s Pr i nt D oA et ext ends A et { em ppl ppl publ i c voi d i ni t ( ) { But t on b = new But t on( " Run Pr i nt D o" ) ; em b. addA i onLi s t ener ( new PD ) ) ; ct L( add( b) ; } cl as s PD i m em s A i onLi s t ener { L pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct f i nal Pr i nt D o pd = new Pr i nt D o( ) ; em em pd. addW ndow s t ener ( new W ndow dapt er ( ) { i Li i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { pd. di s pos e( ) ; } }); pd. s et Si ze( 500, 500) ; pd. s how ) ; ( } } } ///: ~ 伴随 Java 1. 1 版的打印支持功能而来的是一些混乱。一些宣传似乎声明我们能在一个程序片中打印。但 Java 的安全系统包含了一个特点,可停止一个正在初始化打印工作的程序片,初始化程序片需要通过一个 W eb浏览器或程序片浏览器来进行。在写作这本书时,这看起来像留下了一个未定的争议。当我在 W 浏览 EB 器中运行这个程序时,pr i nt dem o(打印样本)窗口正好出现,但它却根本不能从浏览器中打印。

1 3 . 1 7 . 3 剪贴板
Java 1. 1 对系统剪贴板提供有限的操作支持(在 Java. aw . dat at r ans f er package 里)。我们可以将字符串 t 作这文字对象复制到剪贴板中,并且我们可以从剪贴板中粘贴文字到字符中对角中。当然,剪贴板被设计来 容纳各种类型的数据,存在于剪贴板上的数据通过程序运行剪切和粘贴进入到程序中。虽然剪切板目前只支 持字符串数据,Java 的剪切板 A 通过“特色”概念提供了良好的可扩展性。当数据从剪贴板中出来时,它 PI 拥有一个相关的特色集,这个特色集可以被修改(例如,一个图形可以被表示成一些字符串或者一幅图像) 并且我们会注意到如果特殊的剪贴板数据支持这种特色,我们会对此十分的感兴趣。 下面的程序简单地对 T ext A ea中的字符串数据进行剪切,复制,粘贴的操作做了示范。我们将注意到的是我 r 们需要按照剪切、复制和粘贴的顺序进行工作。但如果我们看见一些其它程序中的 Text Fi el d或者 Text A ea,我们会发现它们同样也自动地支持剪贴板的操作顺序。程序中简单地增加了剪贴板的程序化控 r 制,如果我们想用它来捕捉剪贴板上的文字到一些非文字组件中就可以使用这种技术。 //: C A ut ndPas t e. j ava // Us i ng t he cl i pboar d f r om Java 1. 1 i m t j ava. aw . *; por t i m t j ava. aw . event . *; por t 454

i m t j ava. aw . dat at r ans f er . *; por t publ i c cl as s C A ut ndPas t e ext ends Fr am { e M enuBar m = new M b enuBar ( ) ; M enu edi t = new M enu( " Edi t " ) ; M enuI t em cut = new M enuI t em " C " ) , ( ut copy = new M enuI t em " C ( opy" ) , pas t e = new M enuI t em " Pas t e" ) ; ( Text A ea t ext = new Text A ea( 20, 20) ; r r C i pboar d cl i pbd = l get Tool ki t ( ) . get Sys t em l i pboar d( ) ; C publ i c C A ut ndPas t e( ) { cut . addA i onLi s t ener ( new C L( ) ) ; ct ut copy. addA i onLi s t ener ( new C ct opyL( ) ) ; pas t e. addA i onLi s t ener ( new Pas t eL( ) ) ; ct edi t . add( cut ) ; edi t . add( copy) ; edi t . add( pas t e) ; m add( edi t ) ; b. s et M enuBar ( m ; b) add( t ext , Bor der Layout . C TER) ; EN } cl as s C opyL i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct St r i ng s el ect i o = t ext . get Sel ect edText ( ) ; n St r i ngSel ect i on cl i pSt r i ng = new St r i ngSel ect i on( s el ect i on) ; cl i pbd. s et C ent s ( cl i pSt r i ng, cl i pSt r i ng) ; ont } } cl as s C L i m em s A i onLi s t ener { ut pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct St r i ng s el ect i on = t ext . get Sel ect edText ( ) ; St r i ngSel ect i on cl i pSt r i ng = new St r i ngSel ect i on( s el ect i on) ; cl i pbd. s et C ent s ( cl i pSt r i ng, cl i pSt r i ng) ; ont t ext . r epl aceRange( " " , t ext . get Sel ect i onSt ar t ( ) , t ext . get Sel ect i onEnd( ) ) ; } } cl as s Pas t eL i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct Tr ans f er abl e cl i pD a = at cl i pbd. get C ent s ( C A ont ut ndPas t e. t hi s ) ; try { St r i ng cl i pSt r i ng = ( St r i ng) cl i pD a. at get Tr ans f er D a( at D aFl avor . s t r i ngFl avor ) ; at t ext . r epl aceRange( cl i pSt r i ng, 455

t ext . get Sel ect i onSt ar t ( ) , t ext . get Sel ect i onEnd( ) ) ; } cat ch( Except i on ex) { Sys t em out . pr i nt l n( " not St r i ng f l avor " ) ; . } } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C A ut ndPas t e cp = new C A ut ndPas t e( ) ; cp. addW ndow s t ener ( i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); cp. s et Si ze( 300, 200) ; cp. s et Vi s i bl e( t r ue) ; } } ///: ~ 创建和增加菜单及 Text A ea 到如今似乎已变成一种单调的活动。这与通过工具组件创建的剪贴板字段 r cl i pbd有很大的区别。 所有的动作都安置在接收器中。C opyL 和 C 接收器同样除了最后的 C L 线以外删除被复制的线。特殊的 upl ut 两条线是 St r i ngSel ect i on 对象从字符串从创建并调用 St r i ngSel ect i on的 s et C ent s ( ) 方法。说得更准 ont 确些,就是放一个字符串到剪切板上。 在 Pas t eL 中,数据被剪贴板利用 get C ent s ( ) 进行分解。任何返回的对象都是可移动的匿名的,并且我们 ont 并不真正地知道它里面包含了什么。有一种发现的方法是调用 get Tr ans f er D eFl avor s ( ),返回一个 at D aFl avor 对象数组,表明特殊对象支持这种特点。我们同样能要求它通过我们感兴趣的特点直接地使用 at I s D aFl avor Suppor t ed( ) 。但是在这里使用一种大胆的方法:调用 get Tr ans f er D a( )方法,假设里面 at at 的内容支持字符串特色,并且它不是个被分类在异常处理器中的难题 。 在将来,我们希望更多的数据特色能够被支持。

13. 18 可视编程和 Beans
迄今为止,我们已看到 Java 对创建可重复使用的代码片工作而言是多么的有价值。“最大限度地可重复使 用”的代码单元拥有类,因为它包含一个紧密结合在一起的单元特性(字段)和单元动作(方法),它们可 以直接经过混合或通过继承被重复使用。 继承和多形态性是面向对象编程的精华,但在大多数情况下当我们创建一个应用程序时,我们真正最想要的 恰恰是我们最需要的组件。我们希望在我们的设计中设置这些部件就像电子工程师在电路板上创造集成电路 块一样(在使用 Java 的情况下,就是放到 W 页面上)。这似乎会成为加快这种“模块集合”编制程序方法 EB 的发展。 “可视化编程”最早的成功——非常的成功——要归功于微软公司的 Vi s ual Bas i c(VB,可视化 Bas i c 语 言),接下来的第二代是 Bor l and公司 D phi (一种客户/ 服务器数据库应用程序开发工具,也是 Java el Beans 设计的主要灵感)。这些编程工具的组件的像征就是可视化,这是不容置疑的,因为它们通常展示一 些类型的可视化组件,例如:一个按惯或一个 Text Fi el d。事实上,可视化通常表现为组件可以非常精确地 访问运行中程序。因此可视化编程方法的一部分包含从一个调色盘从拖放一个组件并将它放置到我们的窗体 中。应用程序创建工具像我们所做的一样编写程序代码,该代码将导致正在运行的程序中的组件被创建。 简单地拖放组件到一个窗体中通常不足以构成一个完整的程序。一般情况下,我们需要改变组件的特性,例 如组件的色彩,组件的文字,组件连结的数据库,等等。特性可以参照属性在编程时进行修改。我们可以在 应用程序构建工具中巧妙处置我们组件的属性,并且当我们创建程序时,构建数据被保存下来,所以当该程 序被启动时,数据能被重新恢复。 到如今,我们可能习惯于使用对象的多个特性,这也是一个动作集合。在设计时,可视化组件的动作可由事 件部分地代表,意味着“任何事件都可以发生在组件上”。通常,由我们决定想发生的事件,当一个事件发 456

生时,对所发生的事件连接代码。 这是关键性的部分:应用程序构建工具可以动态地询问组件(利用映象)以发现组件支持的事件和属件。一 旦它知道它们的状态,应用程序构建工具就可以显示组件的属性并允许我们修改它们的属性(当我们构建程 序时,保存它们的状态),并且也显示这些事件。一般而言,我们做一些事件像双击一个事件以及应用程序 构建工具创建一个代码并连接到事件上。当事件发生时,我们不得不编写执行代码。应用程序构建工具累计 为我们做了大量的工作。结果我们可以注意到程序看起来像它所假定的那样运行,并且依赖应用程序构建工 具去为我们管理连接的详细资料。可视化的编程工具如此成功的原因是它们明显加快构建的应用程序的处理 过程——当然,用户接口作为应用程序的一部分同样的好。

1 3 . 1 8 . 1 什么是 Bean
在经细节处理后,一个组件在类中被独特的具体化,真正地成为一块代码。关键的争议在于应用程序构建工 具发现组件的属性和事件能力。为了创建一个 VB 组件,程序开发者不得不编写正确的同时也是复杂烦琐的代 码片,接下来由某些协议去展现它们的事件和属性。D phi 是第二代的可视化编程工具并且这种开发语言主 el 动地围绕可视化编程来设计因此它更容易去创建一个可视化组件。但是,Java 带来了可视化的创作组件做为 Java Beans 最高级的“装备”,因为一个 Bean 就是一个类。我们不必再为制造任何的 Bean 而编写一些特殊 的代码或者使用特殊的编程语言。事实上,我们唯一需要做的是略微地修改我们对我们方法命名的办法。方 法名通知应用程序构建工具是否是一个属性,一个事件或是一个普通的方法。 在 Java 的文件中,命名规则被错误地曲解为“设计范式”。这十分的不幸,因为设计范式(参见第 16 章) 惹来不少的麻烦。命名规则不是设计范式,它是相当的简单: ( 1) 因为属性被命名为 xxx,我们代表性的创建两个方法:get Xxx( ) 和 s et Xxx( ) 。注意 get 或 s et 后的第一 个字母小写以产生属性名。“get ”和“s et ”方法产生同样类型的自变量。“s et ”和“get ”的属性名和类 型名之间没有关系。 ( 2) 对于布尔逻辑型属性,我们可以使用上面的“get ”和“s et ”方法,但我们也可以用“i s ”代替 “ get ”。 ( 3) Bean 的普通方法不适合上面的命名规则,但它们是公用的。 4. 对于事件,我们使用“l i s t ener (接收器)”方法。这种方法完全同我们看到过的方法相同: ( addFooBar Li s t ener ( FooBar Li s t ener )和 r em oveFooBar Li s t ener ( FooBar Li s t ener )方法用来处理 Fo Ba 事 o r 件。大多数时候内建的事件和接收器会满足我们的需要,但我们可以创建自己的事件和接收器接口。 上面的第一点回答了一个关于我们可能注意到的从 Java 1. 0 到 Java 1. 1 的改变的问题:一些方法的名字太 过于短小,显然改写名字毫无意义。现在我们可以看到为了制造 Bean 中的特殊的组件,大多数的这些修改不 得不适合于“get ”和“s et ”命名规则。 现在,我们已经可以利用上面的这些指导方针去创建一个简单的 Bean: //: Fr og. j ava // A t r i vi al Java Bean package f r ogbean; i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t cl as s Spot s { } publ i c cl as s Fr og { pr i vat e i nt j um ; ps pr i vat e C or col or ; ol pr i vat e Spot s s pot s ; pr i vat e bool ean j m ; pr publ i c i nt get Jum ( ) { r et ur n j um ; } ps ps publ i c voi d s et Jum ( i nt new ps ) { ps Jum j um = new ps ; ps Jum } publ i c C or get C or ( ) { r et ur n col or ; } ol ol publ i c voi d s et C or ( C or new ol or ) { ol ol C 457

col or = new ol or ; C } publ i c Spot s get Spot s ( ) { r et ur n s pot s ; } publ i c voi d s et Spot s ( Spot s new Spot s ) { s pot s = new Spot s ; } publ i c bool ean i s Jum ( ) { r et ur n j m ; } per pr publ i c voi d s et Jum ( bool ean j ) { j m = j ; } per pr publ i c voi d addA i onLi s t ener ( ct A i onLi s t ener l ) { ct //. . . } publ i c voi d r em oveA i onLi s t ener ( ct A i onLi s t ener l ) { ct // . . . } publ i c voi d addKeyLi s t ener ( KeyLi s t ener l ) { // . . . } publ i c voi d r em oveKeyLi s t ener ( KeyL i s t ener l ) { // . . . } // A " or di nar y" publ i c m hod: n et publ i c voi d cr oak( ) { Sys t em out . pr i nt l n( " Ri bbet ! " ) ; . } } ///: ~ 首先,我们可看到 Bean 就是一个类。通常,所有我们的字段会被作为专用,并且可以接近的唯一办法是通过 方法。紧接着的是命名规则,属性是 j um p,col or ,j um ,s pot s (注意这些修改是在第一个字母在属性名 per 的情况下进行的)。虽然内部确定的名字同最早的三个例子的属性名一样,在 j um 中我们可以看到属性名 per 不会强迫我们使用任何特殊的内部可变的名字(或者,真的拥有一些内部的可变的属性名)。 Bean 事件的句柄是 A i onEvent 和 KeyEvent ,这是根据有关接收器的“add ct ”和“r em ”命名方法得出 ove 的。最后我们可以注意到普通的方法 cr oak( ) 一直是 Bean 的一部分,仅仅是因为它是一个公共的方法,而不 是因为它符合一些命名规则。

1 3 . 1 8 . 2 用 I nt r os pect or 提取 BeanI nf o
当我们拖放一个 Bean 的调色板并将它放入到窗体中时,一个 Bean 的最关键的部分的规则发生了。应用程序 构建工具必须可以创建 Bean(如果它是默认的构建器的话,它就可以做)然后,在此范围外访问 Bean 的源 代码,提取所有的必要的信息以创立属性表和事件处理器。 解决方案的一部分在 11 章结尾部分已经显现出来:Java 1. 1 版的映象允许一个匿名类的所有方法被发现。 这完美地解决了 Bean 的难题而无需我们使用一些特殊的语言关键字像在其它的可视化编程语言中所需要的那 样。事实上,一个主要的原因是映象增加到 Java 1. 1 版中以支持 Beans (尽管映象同样支持对象串联和远程 方法调用)。因为我们可能希望应用程序构建工具的开发者将不得不映象每个 Bean 并且通过它们的方法搜索 以找到 Bean 的属性和事件。 这当然是可能的,但是 Java 的研制者们希望为每个使用它的用户提供一个标准的接口,而不仅仅是使 Bean 更为简单易用,不过他们也同样提供了一个创建更复杂的 Bean 的标准方法。这个接口就是 I nt r os pect or 类,在这个类中最重要的方法静态的 get BeanI nf o( ) 。我们通过一个类处理这个方法并且 get BeanI nf o( ) 方法 全面地对类进行查询,返回一个我们可以进行详细研究以发现其属性、方法和事件的 BeanI nf o 对象。 通常我们不会留意这样的一些事物——我们可能会使用我们大多数的现成的 Bean,并且我们不需要了解所有 的在底层运行的技术细节。我们会简单地拖放我们的 Bean 到我们窗体中,然后配置它们的属性并且为事件编 写处理器。无论如何它都是一个有趣的并且是有教育意义的使用 I nt r os pect or 来显示关于 Bean 信息的练 458

习,好啦,闲话少说,这里有一个工具请运行它(我们可以在 f or gbean 子目录中找到它): //: BeanD per . j ava um // A m hod t o i nt r os pect a Bean et i m t j ava. beans . * ; por i m t j ava. l ang. r ef l ect . *; por publ i c cl as s BeanD per { um publ i c s t at i c voi d dum C as s bean) { p( l BeanI nf o bi = nul l ; try { bi = I nt r os pect or . get BeanI nf o( bean, j ava. l ang. O ect . cl as s ) ; bj } cat ch( I nt r os pect i onExcept i on ex) { Sys t em out . pr i nt l n( " C dn' t i nt r os pect " + . oul bean. get N e( ) ) ; am Sys t em exi t ( 1) ; . } Pr oper t yD cr i pt or [ ] pr oper t i es = es bi . get Pr oper t yD cr i pt or s ( ) ; es f or ( i nt i = 0; i < pr oper t i es . l engt h; i ++) { C as s p = pr oper t i es [ i ] . get Pr oper t yType( ) ; l Sys t em out . pr i nt l n( . " Pr oper t y t ype: \n " + p. get N e( ) ) ; am Sys t em out . pr i nt l n( . " Pr oper t y nam \n " + e: pr oper t i es [ i ] . get N e( ) ) ; am M hod r eadM hod = et et pr oper t i es [ i ] . get ReadM hod( ) ; et i f ( r eadM hod ! = nul l ) et Sys t em out . pr i nt l n( . " Read m hod: \n " + et r eadM hod. t oSt r i ng( ) ) ; et M hod w i t eM hod = et r et pr oper t i es [ i ] . get W i t eM hod( ) ; r et i f ( w i t eM hod ! = nul l ) r et Sys t em out . pr i nt l n( . " W i t e m hod: \n " + r et w i t eM hod. t oSt r i ng( ) ) ; r et Sys t em out . pr i nt l n( " ====================" ) ; . } Sys t em out . pr i nt l n( " Publ i c m hods : " ) ; . et M hodD cr i pt or [ ] m hods = et es et bi . get M hodD cr i pt or s ( ) ; et es f or ( i nt i = 0; i < m hods . l engt h; i ++) et Sys t em out . pr i nt l n( . m hods [ i ] . get M hod( ) . t oSt r i ng( ) ) ; et et Sys t em out . pr i nt l n( " ======================" ) ; . Sys t em out . pr i nt l n( " Event s uppor t : " ) ; . Event Set D cr i pt or [ ] event s = es bi . get Event Set D cr i pt or s ( ) ; es f or ( i nt i = 0; i < event s . l engt h; i ++) { 459

Sys t em out . pr i nt l n( " Li s t ener t ype: \n " + . event s [ i ] . get Li s t ener Type( ) . get N e( ) ) ; am M hod[ ] l m = et event s [ i ] . get Li s t ener M hods ( ) ; et f or ( i nt j = 0; j < l m l engt h; j ++) . Sys t em out . pr i nt l n( . " Li s t ener m hod: \n " + et l m j ] . get N e( ) ) ; [ am M hodD cr i pt or [ ] l m = et es d event s [ i ] . get Li s t ener M hodD cr i pt or s ( ) ; et es f or ( i nt j = 0; j < l m l engt h; j ++) d. Sys t em out . pr i nt l n( . " M hod des cr i pt or : \n " + et l m j ] . get M hod( ) . t oSt r i ng( ) ) ; d[ et M hod addLi s t ener = et event s [ i ] . get A ddLi s t ener M hod( ) ; et Sys t em out . pr i nt l n( . " A Li s t ener M hod: \n " + dd et addLi s t ener . t oSt r i ng( ) ) ; M hod r em et oveLi s t ener = event s [ i ] . get Rem oveLi s t ener M hod( ) ; et Sys t em out . pr i nt l n( . " Rem ove Li s t ener M hod: \n " + et r em oveLi s t ener . t oSt r i ng( ) ) ; Sys t em out . pr i nt l n( " ====================" ) ; . } } // D p t he cl as s of your choi ce: um publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai i f ( ar gs . l engt h < 1) { Sys t em er r . pr i nt l n( " us age: \n" + . " BeanD per f ul l y. qual i f i e cl as s " ) ; um d. Sys t em exi t ( 0) ; . } C as s c = nul l ; l try { c = C as s . f or N e( ar gs [ 0] ) ; l am } cat ch( C as s N FoundExcept i on ex) { l ot Sys t em er r . pr i nt l n( . " C dn' t f i nd " + ar gs [ 0] ) ; oul Sys t em exi t ( 0) ; . } dum c) ; p( } } // / : ~ BeanD per . dum )是一个可以做任何工作的方法。首先它试图创建一个 BeanI nf o 对象,如果成功地调用 um p( BeanI nf o 的方法,就产生关于属性、方法和事件的信息。在 I nt r os pect or . get BeanI nf o( )中,我们会注意到 有一个另外的自变量。由它来通知 I nt r os pect or 访问继承体系的地点。在这种情况下,它在分析所有对象方 法前停下,因为我们对看到那些并不感兴趣。 因为属性,get Pr oper t yD cr i pt or s ( )返回一组的属性描述符号。对于每个描述符号我们可以调用 es get Pr oper t yType( ) 方法彻底的通过属性方法发现类的对象。这时,我们可以用 get N e( ) 方法得到每个属性 am 460

的假名(从方法名中提取),get nam ) 方法用 get ReadM hod( ) 和 get W i t eM hod( ) 完成读和写的操作。最 e( et r et 后的两个方法返回一个可以真正地用来调用在对象上调用相应的方法方法对象(这是映象的一部分)。对于 公共方法(包括属性方法),get M hodD cr i pt or s ( )返回一组方法描述字符。每一个我们都可以得到相 et es 当的方法对象并可以显示出它们的名字。 对于事件而言,get Event Set D cr i pt or s ( ) 返回一组事件描述字符。它们中的每一个都可以被查询以找出接 es 收器的类,接收器类的方法以及增加和删除接收器的方法。BeanD per 程序打印出所有的这些信息。 um 如果我们调用 BeanD per 在 Fr og 类中,就像这样: um j ava BeanD per f r ogbean. Fr og um 它的输出结果如下(已删除这儿不需要的额外细节): cl as s nam Fr og e: Pr oper t y t ype: C or ol Pr oper t y nam e: col or Read m hod: et publ i c C or get C or ( ) ol ol W i t e m hod: r et publ i c voi d s et C or ( C or ) ol ol ==================== Pr oper t y t ype: Spot s Pr oper t y nam e: s pot s Read m hod: et publ i c Spot s get Spot s ( ) W i t e m hod: r et publ i c voi d s et Spot s ( Spot s ) ==================== Pr oper t y t ype: bool ean Pr oper t y nam e: j um per Read m hod: et publ i c bool ean i s Jum ( ) per W i t e m hod: r et publ i c voi d s et Jum ( bool ean) per ==================== Pr oper t y t ype: i nt Pr oper t y nam e: j um ps Read m hod: et publ i c i nt get Jum ( ) ps W i t e m hod: r et publ i c vo d s et Jum ( i nt ) i ps ==================== Publ i c m hods : et publ i c voi d s et Jum ( i nt ) ps publ i c voi d cr oak( ) publ i c voi d r em oveA i onLi s t ener ( A i onLi s t ener ) ct ct publ i c voi d addA i onLi s t ener ( A i onLi s t ener ) ct ct 461

publ i c i nt get Jum ( ) ps publ i c voi d s et C or ( C or ) ol ol publ i c voi d s et Spot s ( Spot s ) publ i c voi d s et Jum ( bool ean) per publ i c bool ean i s Jum ( ) per publ i c voi d addKeyLi s t ener ( KeyLi s t ener ) publ i c C or get C or ( ) ol ol publ i c voi d r em oveKeyLi s t ener ( KeyLi s t ener ) publ i c Spot s get Spot s ( ) ====================== Event s uppor t : Li s t ener t ype: KeyLi s t ener Li s t ener m hod: et keyTyped Li s t ener m hod: et keyPr es s ed Li s t ener m hod: et keyRel eas ed M hod des cr i pt or : et publ i c voi d keyTyped( KeyEvent ) M hod des cr i pt or : et publ i c voi d keyPr es s ed( KeyEvent ) M hod des cr i pt or : et publ i c voi d keyRel eas ed(KeyEvent ) A Li s t ener M hod: dd et publ i c voi d addKeyLi s t ener ( KeyLi s t ener ) Rem ove Li s t ener M hod: et publ i c voi d r em oveKeyLi s t ener ( KeyLi s t ener ) ==================== Li s t ener t ype: A i onLi s t ener ct Li s t ener m hod: et act i onPer f or m ed M hod des cr i pt or : et publ i c voi d act i onPer f or m A i onEvent ) ed( ct A Li s t ener M hod: dd et publ i c voi d addA i onLi s t ener ( A i onLi s t ener ) ct ct Rem ove Li s t ener M hod: et publ i c voi d r em oveA i onLi s t ener ( A i onLi s t ener ) ct ct ==================== 这个结果揭示出了 I nt r os pect or 在从我们的 Bean 产生一个 BeanI nf o 对象时看到的大部分内容。我们可注意 到属性的类型和它们的名字是相互独立的。请注意小写的属性名。(当属性名开头在一行中有超过不止的大 写字母,这一次程序就不会被执行。)并且请记住我们在这里所见到的方法名(例如读和与方法)真正地从 一个可以被用来在对象中调用相关方法的方法对象中产生。 通用方法列表包含了不相关的事件或者属性,例如 cr oak( )。列表中所有的方法都是我们可以有计划的为 Bean 调用,并且应用程序构建工具可以选择列出所有的方法,当我们调用方法时,减轻我们的任务。 最后,我们可以看到事件在接收器中完全地分析研究它的方法、增加和减少接收器的方法。基本上,一旦我 们拥有 BeanI nf o,我们就可以找出对 Bean 来说任何重要的事物。我们同样可以为 Bean 调用方法,即使我们 除了对象外没有任何其它的信息(此外,这也是映象的特点)。

462

1 3 . 1 8 . 3 一个更复杂的 Bean
接下的程序例子稍微复杂一些,尽管这没有什么价值。这个程序是一张不论鼠标何时移动都围绕它画一个小 圆的 弧5蔽颐前聪率蟊昙 保 谄聊恢醒胂允疽桓鲎帧奥 ang! ”,并且一个动作接收器被激活。画布。当 按下鼠标键时,我们可以改变的属性是圆的大小,除此之外还有被显示文字的色彩,大小,内容。BangBean 同样拥有它自己的 addA i onLi s t ener ( ) 和 r em ct oveA i onLi s t ener ( ) 方法,因此我们可以附上自己的当用户 ct 单击在 BangBean 上时会被激活的接收器。这样,我们将能够确认可支持的属性和事件: //: BangBean. j ava // A gr aphi cal Bean package bangbean; i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. i o. * ; por i m t j ava. ut i l . * ; por publ i c cl as s BangBean ext ends C anvas i m em s Ser i al i zabl e { pl ent pr ot ect ed i nt xm ym , ; pr ot ect ed i nt cSi ze = 20; // C r cl e s i ze i pr ot ect ed St r i ng t ext = " Bang! " ; pr ot ect ed i nt f ont Si ze = 48; pr ot ect ed C or t C or = C or . r ed; ol ol ol pr ot ect ed A i onLi s t ener act i onLi s t ener ; ct publ i c BangBean( ) { addM eLi s t ener ( new M ) ) ; ous L( addM eM i onLi s t ener ( new M L( ) ) ; ous ot M } publ i c i nt get C r cl eSi ze( ) { r et ur n cSi ze; } i publ i c voi d s et C r cl eSi ze( i nt new ze) { i Si cSi z e = new z e; Si } publ i c St r i ng get BangText ( ) { r et ur n t ext ; } publ i c voi d s et BangText ( St r i ng new Text ) { t ext = new Text ; } publ i c i nt get Font Si ze( ) { r et ur n f ont Si ze; } publ i c voi d s et Font Si ze( i nt new ze) { Si f ont Si ze = new ze; Si } publ i c C or get Text C or ( ) { r et ur n t C or ; } ol ol ol publ i c voi d s et Text C or ( C or new ol or ) { ol ol C t C or = new ol or ; ol C } publ i c voi d pai nt ( G aphi cs g) { r g. s et C or ( C or . bl ack) ; ol ol g. dr aw val ( xm - cSi ze/2, ym - cSi ze/2, O cSi ze, cSi ze) ; } // Thi s i s a uni cas t l i s t ener , w ch i s hi // t he s i m es t f or m of l i s t ener m pl anagem : ent publ i c voi d addA i onLi s t ener ( ct A i onLi s t ener l ) ct 463

t hr ow TooM s anyLi s t ener s Except i on { i f ( act i onLi s t ener ! = nul l ) t hr ow new TooM anyLi s t ener s Except i on( ) ; act i onLi s t ener = l ; } publ i c voi d r em oveA i onLi s t ener ( ct A i onLi s t ener l ) { ct act i onLi s t ener = nul l ; } cl as s M ext ends M eA L ous dapt er { publ i c voi d m ePr es s ed( M eEvent e) { ous ous G aphi cs g = get G aphi cs ( ) ; r r g. s et C or ( t C or ) ; ol ol g. s et Font ( new Font ( " Ti m Rom , Font . BO , f ont Si ze) ) ; es an" LD i nt w dt h = i g. get Font M r i cs ( ) . s t r i ngW dt h( t ext ) ; et i g. dr aw r i ng( t ext , St ( get Si ze( ) . w dt h - w dt h) /2, i i get Si ze( ) . hei ght /2) ; g. di s pos e( ) ; // C l t he l i s t ener ' s m hod: al et i f ( act i onLi s t ener ! = nul l ) act i onLi s t ener . act i onPer f or m ed( new A i onEvent ( BangBean. t hi s , ct A i onEvent . A TI O _PERFO ED nul l ) ) ; ct C N RM , } } cl as s M L ext ends M eM i onA M ous ot dapt er { publ i c voi d m eM ous oved( M eEvent e) { ous xm = e. get X( ) ; ym = e. get Y( ) ; r epai nt ( ) ; } } publ i c D m i on get Pr ef er r edSi ze( ) { i ens r et ur n new D m i on( 200, 200) ; i ens } // Tes t i ng t he BangBean: publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai BangBean bb = new Bang Bean( ) ; try { bb. addA i onLi s t ener ( new BBL( ) ) ; ct } cat ch( TooM anyLi s t ener s Except i on e) { } Fr am aFr am = new Fr am " BangBean Tes t " ) ; e e e( aFr am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); 464

aFr am add( bb, Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 300, 300) ; e. aFr am s et Vi s i bl e( t r ue) ; e. } // D i ng t es t i ng, s end act i on i nf or m i on ur at // t o t he cons ol e: s t at i c cl as s BBL i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct Sys t em out . pr i nt l n( " BangBean act i on" ) ; . } } } ///: ~ 最重要的是我们会注意到 BangBean 执行了这种串联化的接口。这意味着应用程序构建工具可以在程序设计者 调整完属性值后利用串联为 BangBean 贮藏所有的信息。当 Bean 作为运行的应用程序的一部分被创建时,那 些被贮藏的属性被重新恢复,因此我们可以正确地得到我们的设计。 我们能看到通常同 Bean 一起运行的所有的字段都是专用的——允许只能通过方法来访问,通常利用“属性” 结构。 当我们注视着 addA i onLi s t ener ( ) 的签名时,我们会注意到它可以产生出一个 TooM ct anyLi s t ener Except i on (太多接收器异常)。这个异常指明它是一个单一的类型的,意味着当事件发生时,它只能通知一个接收 器。一般情况下,我们会使用具有多种类型的事件,以便一个事件通知多个的接收器。但是,那样会陷入直 到下一章我们才能准备好的结局中,因此这些内容会被重新回顾(下一个标题是“Java Beans 的重新回 顾”)。单一类型的事件回避了这个难题。 当我们按下鼠标键时,文字被安入 BangBean 中间,并且如果动作接收器字段存在,它的 act i onPer f or m ) ed( 方法就被调用,创建一个新的 A i onEvent 对象在处理过程中。无论何时鼠标移动,它的新座标将被捕捉, ct 并且画布会被重画(像我们所看到的抹去一些画布上的文字)。 m n( )方法增加了允许我们从命令行中测试程序的功能。当一个 Bean 在一个开发环境中,m n( )方法不会被 ai ai 使用,但拥有它是绝对有益的,因为它提供了快捷的测试能力。无论何时一个 A i onEvent 发生,m n( ) 方 ct ai 法都将创建了一个帧并安置了一个 BangBean 在它里面,还在 BangBean 中附上了一个简单的动作接收器以打 印到控制台。当然,一般来说应用程序构建工具将创建大多数的 Bean 的代码。当我们通过 BeanD per 或者 um 安放 BangBean 到一个可激活 Bean 的开发环境中去运行 BangBean 时,我们会注意到会有很多额外的属性和动 作明显超过了上面的代码。那是因为 BangBean 从画布中继承,并且画布就是一个 Bean,因此我们看到它的 属性和事件同样的合适。

1 3 . 1 8 . 4 Bean 的封装
在我们可以安放一个 Bean 到一个可激活 Bean 的可视化构建工具中前,它必须被放入到标准的 Bean 容器里, 也就是包含 Bean 类和一个表示“这是一个 Bean”的清单文件的 JA R(Java A Rchi ve,Java 文件)文件中。 清单文件是一个简单的紧随事件结构的文本文件。对于 BangBean 而言,清单文件就像下面这样: M f es t - Ver s i on: 1. 0 ani N e: bangbean/BangBean. cl as s am Java- Bean: Tr ue 其中,第一行指出清单文件结构的版本,这是 SUN公司在很久以前公布的版本。第二行(空行忽略)对文件 命名为 BangBean. cl as s 。第三行表示“这个文件是一个 Bean”。没有第三行,程序构建工具不会将类作为一 个 Bean 来认可。 唯一难以处理的部分是我们必须肯定“N e: ”字段中的路径是正确的。如果我们回顾 BangBean. j ava,我们 am 会看到它在 package bangbean(因为存放类路径的子目录称为“bangbean”)中,并且这个名字在清单文件 中必须包括封装的信息。另外,我们必须安放清单文件在我们封装路径的根目录上,在这个例子中意味着安 放文件在 bangbean 子目录中。这之后,我们必须从同一目录中调用 Jar 来作为清单文件,如下所示: j ar cf m BangBean. j ar BangBean. m bangbean f 465

这个例子假定我们想产生一个名为 BangBean. j ar 的文件并且我们将清单放到一个称为 BangBean. m 文件中。 f 我们可能会想“当我编译 BangBean. j ava 时,产生的其它类会怎么样呢?”哦,它们会在 bangbean 子目录中 被中止,并且我们会注意到上面 j ar 命令行的最后一个自变量就是 bangbean 子目录。当我们给 j ar 子目录名 时,它封装整个的子目录到 j ar 文件中(在这个例子中,包括 BangBean. j ava 的源代码文件——对于我们自 己的 Bean 我们可能不会去选择包含源代码文件。)另外,如果我们改变主意,解开打包的 JA R文件,我们会 发现我们清单文件并不在里面,但 j ar 创建了它自己的清单文件(部分根据我们的文件),称为 MI N A FEST. M 并且安放它到 M F ETA- I N 子目录中(代表“m a- i nf or m i on”)。如果我们打开这个清单文 F et at 件,我们同样会注意到 j ar 为每个文件加入数字签名信息,其结构如下: D ges t - A gor i t hm : SHA M 5 i l s D SHA D ges t : pD G aeC - i pEA 9N x8aFt qPI 4udSX/O 0= M 5- D ges t : O cS1hE3Sm p2hj 6qeg== D i 4N nzl 一般来说,我们不必担心这些,如果我们要做一些修改,可以修改我们的原始的清单文件并且重新调用 j ar 以为我们的 Bean 创建了一个新的 JA R文件。我们同样也可以简单地通过增加其它的 Bean 的信息到我们清单 文件来增加它们到 JA 文件中。 R 值得注意的是我们或许需要安放每个 Bean 到它自己的子目录中,因为当我们创建一个 JA 文件时,分配 JA R R 应用目录名并且 JA R放置子目录中的任何文件到 JA R文件中。我们可以看到 Fr og 和 BangBean 都在它们自己 的子目录中。 一旦我们将我们的 Bean 正确地放入一个 JA R文件中,我们就可以携带它到一个可以激活 Bean 的编程环境中 使用。使用这种方法,我们可以从一种工具到另一种工具间交替变换,但 SUN公司为 Java Beans 提供了免费 高效的测试工具在它们的“Bean D evel opm ent Ki t ,Bean 开发工具”(BD K)称为“beanbox”。(我们可以 从 w w j avas of t . com处下载。)在我们启动 beanbox 前,放置我们的 Bean 到 beanbox 中,复制 JA w. R文件到 BD 的“j ar s ”子目录中。 K

1 3 . 1 8 . 5 更复杂的 Bean 支持
我们可以看到创建一个 Bean 显然多么的简单。在程序设计中我们几乎不受到任何的限制。Java Bean 的设计 提供了一个简单的输入点,这样可以提高到更复杂的层次上。这些高层次的问题超出了这本书所要讨论的范 围,但它们会在此做简要的介绍。我们可以在 ht t p: //j ava. s un. com /beans 上找到更多的详细资料。 我们增加更加复杂的程序和它的属性到一个位置。上面的例子显示一个独特的属性,当然它也可能代表一个 数组的属性。这称为索引属性。我们简单地提供一个相应的方法(再者有一个方法名的命名规则)并且 I nt r os pect or 认可索引属性,因此我们的应用程序构建工具相应的处理。 属性可以被捆绑,这意味着它们将通过 Pr oper t yC hangeEvent 通知其它的对象。其它的对象可以随后根据对 Bean 的改变选择修改它们自己。 属性可以被束缚,这意味着其它的对象可以在一个属性的改变不能被接受时,拒绝它。其它的对象利用一个 Pr oper t yC hangeEvent 来通知,并且它们产生一个 Pr opt er t yVet oExcept i on 去阻止修改的发生,并恢复为原 来的值。 我们同样能够改变我们的 Bean 在设计时的被描绘成的方法: ( 1) 我们可以为我们特殊的 Bean 提供一个定制的属性表。这个普通的属性表将被所有的 Bean 所使用,但当 我们的 Bean 被选择时,它会自动地调用这张属性表。 ( 2) 我们可以为一个特殊的属性创建一个定制的编辑器,因此普通的属性表被使用,但当我们指定的属性被 调用时,编辑器会自动地被调用。 ( 3)我们可以为我们的 Bean 提供一个定制的 BeanI nf o 类,产生的信息不同于由 I nt r os pect or 默认产生的。 ( 4) 它同样可能在所有的 Feat ur eD cr i pt or s 中改变“exper t ”的开关模式,以辨别基本特征和更复杂的特 es 征。

1 3 . 1 8 . 6 Bean 更多的知识
另外有关的争议是 Bean 不能被编址。无论何时我们创建一个 Bean,都希望它会在一个多线程的环境中运 行。这意味着我们必须理解线程的出口,我们将在下一章中介绍。我们会发现有一段称为“Java Beans 的回 顾”的节会注意到这个问题和它的解决方案。

466

13. 19 S wi ng 入门(注释⑦)
通过这一章的学习,当我们的工作方法在 A T 中发生了巨大的改变后(如果可以回忆起很久以前,当 Java 第 W 一次面世时 SUN公司曾声明 Java 是一种“稳定,牢固”的编程语言),可能一直有 Java 还不十分的成熟的 感觉。的确,现在 Java 拥有一个不错的事件模型以及一个优秀的组件复用设计——JavaBeans 。但 G 组件 UI 看起来还相当的原始,笨拙以及相当的抽象。 ⑦:写作本节时,Sw ng库显然已被 Sun“固定”下来了,所以只要你下载并安装了 Sw ng库,就应该能正确 i i 地编译和运行这里的代码,不会出现任何问题(应该能编译 Sun配套提供的演示程序,以检测安装是否正 确)。若遇到任何麻烦,请访问 ht t p: //w w Br uceEckel . com w. ,了解最近的更新情况。 而这就是 Sw ng将要占领的领域。Sw ng库在 Java 1. 1 之后面世,因此我们可以自然而然地假设它是 Java i i 1. 2 的一部分。可是,它是设计为作为一个补充在 Java 1. 1 版中工作的。这样,我们就不必为了享用好的 UI 组件库而等待我们的平台去支持 Java 1. 2 版了。如果 Sw ng库不是我们的用户的 Java 1. 1 版所支持的一部 i 分,并且产生一些意外,那他就可能真正的需要去下载 Sw ng库了。 i Sw ng包含所有我们缺乏的组件,在整个本章余下的部分中:我们期望领会现代化的 UI ,来自按钮的任何事 i 件包括到树状和网格结构中的图片。它是一个大库,但在某些方面它为任务被设计得相应的复杂——如果任 何事都是简单的,我们不必编写更多的代码但同样设法运行我们的代码逐渐地变得更加的复杂。这意味着一 个容易的入口,如果我们需要它我们得到它的强大力量。 Sw ng相当的深奥,这一节不会去试图让读者理解,但会介绍它的能力和 Sw ng简单地使我们着手使用库。 i i 请注意我们有意识的使用这一切变得简单。如果我们需要运行更多的,这时 Sw ng能或许能给我们所想要 i 的,如果我们愿意深入地研究,可以从 SUN公司的在线文档中获取更多的资料。

1 3 . 1 9 . 1 S wi ng 有哪些优点
当我们开始使用 Sw ng库时,会注意到它在技术上向前迈出了巨大的一步。Sw ng组件是 Bean,因此他们可 i i 以支持 Bean 的任何开发环境中使用。Sw ng提供了一个完全的 UI 组件集合。因为速度的关系,所有的组件 i 都很小巧的(没有“重量级”组件被使用),Sw ng为了轻便在 Java 中整个被编写。 i 最重要的是我们会希望 Sw ng被称为“正交使用”;一旦我们采用了这种关于库的普遍的办法我们就可以在 i 任何地方应用它们。这主要是因为 Bean 的命名规则,大多数的时候在我编写这些程序例子时我可以猜到方法 名并且第一次就将它拼写正确而无需查找任何事物。这无疑是优秀库设计的品质证明。另外,我们可以广泛 地插入组件到其它的组件中并且事件会正常地工作。 键盘操作是自动被支持的——我们可以使用 Sw ng应用程序而不需要鼠标,但我们不得不做一些额外的编程 i 工作(老的 A T 中需要一些可怕的代码以支持键盘操作)。滚动被毫不费力地支持——我们简单地将我们的 W 组件到一个 JScr ol l Pane中,同样我们再增加它到我们的窗体中即可。其它的特征,例如工具提示条只需要 一行单独的代码就可执行。 Sw ng同样支持一些被称为“可插入外观和效果”的事物,这就是说 UI 的外观可以在不同的平台和不同的操 i 作系统上被动态地改变以符合用户的期望。它甚至可以创造我们自己的外观和效果。

1 3 . 1 9 . 2 方便的转换
如果我们长期艰苦不懈地利用 Java 1. 1 版构建我们的 UI ,我们并不需要扔掉它改变到 Sw ng阵营中来。幸 i 运的是,库被设计得允许容易地修改——在很多情况下我们可以简单地放一个“J”到我们老 A T 组件的每个 W 类名前面即可。下面这个例子拥有我们所熟悉的特色: //: JBut t onD o. j ava em // Looks l i ke Java 1. 1 but w t h J' s added i package c13. s w ng; i i m t j ava. aw . *; por t i m t j ava. aw . event . *; por t i m t j ava. appl et . * ; por i m t j avax. s w ng. * ; por i publ i c cl as s JBut t onD o ext ends A et { em ppl 467

JBut t on b1 = new JBut t on( " JBut t on 1" ) , b2 = new JBut t on( " JBut t on 2" ) ; JText Fi el d t = new JText Fi el d( 20) ; publ i c voi d i ni t ( ) { A i onLi s t ener al = new A i onLi s t ener ( ) { ct ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct St r i ng nam = e ( ( JBut t on) e. get Sour ce( ) ) . get Text ( ) ; t . s et Text ( nam + " Pr es s ed" ) ; e } }; b1. addA i onLi s t ener ( al ) ; ct add( b1) ; b2. addA i onLi s t ener ( al ) ; ct add( b2) ; add( t ) ; } publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) { ai JBut t onD o appl et = new JBut t onD o( ) ; em em JFr am f r am = new JFr am " Text A eaN " ) ; e e e( r ew f r am addW ndow s t ener ( new W ndow dapt er ( ) { e. i Li i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); f r am get C ent Pane( ) . add( e. ont appl et , Bor der Layout . C TER) ; EN f r am s et Si ze( 300, 100) ; e. appl et . i ni t ( ) ; appl et . s t ar t ( ) ; f r am s et Vi s i bl e( t r ue) ; e. } } ///: ~ 这是一个新的输入语句,但此外任何事物除了增加了一些“J”外,看起都像这 Java 1. 1 版的 A T。同样, W 我们不恰当的用 add( ) 方法增加到 Sw ng JFr am i e中,除此之外我们必须像上面看到的一样先准备一些 “cont ent pane”。我们可以容易地得到 Sw ng一个简单的改变所带来的好处。 i 因为程序中的封装语句,我们不得不调用像下面所写的一样调用这个程序: j ava c13. s w ng. Jbut t onD o i em 在这一节里出现的所有的程序都将需要一个相同的窗体来运行它们。

1 3 . 1 9 . 3 显示框架
尽管程序片和应用程序都可以变得很重要,但如果在任何地方都使用它们就会变得混乱和毫无用处。这一节 余下部分取代它们的是一个 Sw ng程序例子的显示框架: i //: Show j ava . // Tool f or di s pl ayi ng Sw ng dem i os package c13. s w ng; i i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j avax. s w ng. * ; por i 468

publ i c cl as s Show { publ i c s t at i c voi d i nFr am JPanel j p, i nt w dt h, i nt hei ght ) { e( i St r i ng t i t l e = j p. get C as s ( ) . t oSt r i ng( ) ; l // Rem ove t he w d " cl as s " : or i f ( t i t l e. i ndexO ( " cl as s " ) ! = - 1) f t i t l e = t i t l e. s ubs t r i ng( 6) ; JFr am f r am = new JFr am t i t l e) ; e e e( f r am addW ndow s t ener ( new W ndow dapt er ( ) { e. i Li i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); f r am get C ent Pane( ) . add( e. ont j p, Bor der Layout . C TER) ; EN f r am s et Si ze( w dt h, hei ght ) ; e. i f r am s et Vi s i bl e( t r ue) ; e. } } ///: ~ 那些想显示它们自己的类将从 JPanel 处继承并且随后为它们自己增加一些可视化的组件。最后,它们创建一 个包含下面这一行程序的 m n( ) : ai Show i nFr am new M l as s ( ) , 500, 300) ; . e( yC 最后的两个自变量是显示的宽度和高度。 注意 JFr am e的标题是用 RTTI 产生的。

1 3 . 1 9 . 4 工具提示
几乎所有我们利用来创建我们用户接口的来自于 JC ponent 的类都包含一个称为 s et Tool Ti pText ( s t r i ng) om 的方法。因此,几乎任何我们所需要表示的(对于一个对象 j c 来说就是一些来自 JC ponent 的类)都可以 om 安放在窗体中: j c. s et Tool Ti pText ( " M t i p" ) ; y 并且当鼠标停在 JC ponent 上一个超过预先设置的一个时间,一个包含我们的文字的小框就会从鼠标下弹 om 出。

1 3 . 1 9 . 5 边框
JC ponent 同样包括一个称为 s et Bor der ( ) 的方法,该方法允许我们安放一些各种各样有趣的边框到一些可 om 见的组件上。下面的程序例子利用一个创建 JPanel 并安放边框到每个例子中的被称为 s how der ( ) 的方 Bor 法,示范了一些有用的不同的边框。同样,它也使用 RTTI 来找我们使用的边框名(剔除所有的路径信息), 然后将边框名放到面板中间的 JLabl e 里: //: Bor der s . j ava // D f f er ent Sw ng bor der s i i package c13. s w ng; i i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j avax. s w ng. * ; por i i m t j avax. s w ng. bor der . *; por i publ i c cl as s Bor der s ext ends JPanel { s t at i c JPanel s how der ( Bor der b) { Bor 469

JPanel j p = new JPanel ( ) ; j p. s et Layout ( new Bor der Layout ( ) ) ; St r i ng nm = b. get C as s ( ) . t oSt r i ng( ) ; l nm = nm s ubs t r i ng( nm l as t I ndexO ( ' . ' ) + 1) ; . . f j p. add( new JLabel ( nm JLabel . C TER) , , EN Bor der Layout . C TER) ; EN j p. s et Bor der ( b) ; r et ur n j p; } publ i c Bor der s ( ) { s et Layout ( new G i dLayout ( 2, 4) ) ; r add( s how der ( new Ti t l edBor der ( " Ti t l e" ) ) ) ; Bor add( s how der ( new Et chedBor der ( ) ) ) ; Bor add( s how der ( new Li neBor der ( C or . bl ue) ) ) ; Bor ol add( s how der ( Bor new M t eBor der ( 5, 5, 30, 30, C or . gr een) ) ) ; at ol add( s how der ( Bor new Bevel Bor der ( Bevel Bor der . RA SED ) ) ; I ) add( s how der ( Bor new Sof t Bevel Bor der ( Bevel Bor der . LO ERED ) ) ; W ) add( s how der ( new C poundBor der ( Bor om new Et chedBor der ( ) , new Li neBor der ( C or . r ed) ) ) ) ; ol } publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) { ai Show i nFr am new Bor der s ( ) , 500, 300) ; . e( } } ///: ~ 这一节中大多数程序例子都使用 Ti t l edBor der ,但我们可以注意到其余的边框也同样易于使用。能创建我们 自己的边框并安放它们到按钮、标签等等内——任何来自 JC ponent 的东西。 om

1 3 . 1 9 . 6 按钮
Sw ng增加了一些不同类型的按钮,并且它同样可以修改选择组件的结构:所有的按钮、复选框、单选钮, i 甚至从 A t r act But t on 处继承的菜单项(这是因为菜单项一般被包含在其中,它可能会被改进命名为 bs “A t r act C bs hoos er ”或者相同的什么名字)。我们会注意使用菜单项的简便,下面的例子展示了不同类型的 可用的按钮: //: But t ons . j ava // Var i ous Sw ng but t ons i package c13. s w ng; i i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j avax. s w ng. * ; por i i m t j avax. s w ng. pl af . bas i c. *; por i i m t j avax. s w ng. bor der . *; por i publ i c cl as s But t ons ext ends JPanel { JBut t on j b = new JBut t on( " JBut t on" ) ; Bas i cA r ow t on r But up = new Bas i cA r ow t on( r But Bas i cA r ow t on. N RTH) , r But O 470

dow = new Bas i cA r ow t on( n r But Bas i cA r ow t on. SO r But UTH) , r i ght = new Bas i cA r ow t on( r But Bas i cA r ow t on. EA , r But ST) l ef t = new Bas i cA r ow t on( r But Bas i cA r ow t on. W r But EST) ; publ i c But t ons ( ) { add( j b) ; add( new JToggl eBut t on( " JToggl eBut t on" ) ) ; add( new JC heckBox( " JC heckBox" ) ) ; add( new JRadi oBut t on( " JRadi oBut t on" ) ) ; JPanel j p = new JPanel ( ) ; j p. s et Bor der ( new Ti t l edBor der ( " D r ect i ons " ) ) ; i j p. add( up) ; j p. add( dow ; n) j p. add( l ef t ) ; j p. add( r i ght ) ; add( j p) ; } publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) { ai Show i nFr am new But t ons ( ) , 300, 200) ; . e( } } ///: ~ JBut t on看起来像 A T 按钮,但它没有更多可运行的功能(像我们后面将看到的如加入图像等)。在 W com s un. j ava. s w ng. bas i c里,有一个更合适的 Bas i cA r ow t on按钮,但怎样测试它呢?有两种类型的 . i r But “指针”恰好请求箭头按钮使用:Spi nner 修改一个中断值,并且 St r i ngSpi nner 通过一个字符串数组来移 动(当它到达数组底部时,甚至会自动地封装)。A i onLi s t ener s 附着在箭头按钮上展示它使用的这些相 ct 关指针:因为它们是 Bean,我们将期待利用方法名,正好捕捉并设置它们的值。 当我们运行这个程序例子时,我们会发现触发按钮保持它最新状态,开或时关。但复选框和单选钮每一个动 作都相同,选中或没选中(它们从 JToggl eBut t on 处继承)。

1 3 . 1 9 . 7 按钮组
如果我们想单选钮保持“异或”状态,我们必须增加它们到一个按钮组中,这几乎同老 A T 中的方法相同但 W 更加的灵活。在下面将要证明的程序例子是,一些 A t r uact But t on能被增加到一个 But t onG oup中。 bs r 为避免重复一些代码,这个程序利用映射来生不同类型的按钮组。这会在 m akeBPanel 中看到,m akeBPanel 创建了一个按钮组和一个 JPanel ,并且为数组中的每个 St r i ng就是 m akeBPanel 的第二个自变量增加一个类 对象,由它的第一个自变量进行声明: //: But t onG oups . j ava r // Us es r ef l ect i on t o cr eat e gr oups of di f f er ent // t ypes of A t r act But t on. bs package c13. s w ng; i i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j avax. s w ng. * ; por i i m t j avax. s w ng. bor der . *; por i i m t j ava. l ang. r ef l ect . *; por publ i c cl as s But t onG oups ext ends JPanel { r s t at i c St r i ng[ ] i ds = { " June" , " W d" , " Beaver " , ar 471

" W l y" , " Eddi e" , " Lum , al py" }; s t at i c JPanel m akeBPanel ( C as s bC as s , St r i ng[ ] i ds ) { l l But t onG oup bg = new But t onG oup( ); r r JPanel j p = new JPanel ( ) ; St r i ng t i t l e = bC as s . get N e( ) ; l am t i t l e = t i t l e. s ubs t r i ng( t i t l e. l as t I ndexO ( ' . ' ) + 1) ; f j p. s et Bor der ( new Ti t l edBor der ( t i t l e) ) ; f or ( i nt i = 0; i < i ds . l engt h; i ++) { A t r act But t on ab = new JBut t on(" f ai l ed" ) ; bs try { // G t he dynam c cons t r uct or m hod et i et // t hat t akes a St r i ng ar gum : ent C t r uct or ct or = bC as s . get C t r uct or ( ons l ons new C as s [ ] { St r i ng. cl as s } ) ; l // C eat e a new obj ect : r ab = ( A t r act But t on) ct or . new ns t ance( bs I new O ect [ ] { i ds [ i ] } ) ; bj } cat ch( Except i on ex) { Sys t em out . pr i nt l n( " can' t cr eat e " + . bC as s ) ; l } bg. add( ab) ; j p. add( ab) ; } r et ur n j p; } publ i c But t onG oups ( ) { r add( m akeBPanel ( JBut t on. cl as s , i ds ) ) ; add( m akeBPanel ( JToggl eBut t on. cl as s , i ds ) ) ; add( m akeBPanel ( JC heckBox. cl as s , i ds ) ) ; add( m akeBPanel ( JRadi oBut t on. cl as s , i ds ) ) ; } publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) { ai Show i nFr am new But t onG oups ( ) , 500, 300) ; . e( r } } ///: ~ 边框标题由类名剔除了所有的路径信息而来。A t r act But t on初始化为一个 JBut t on,JBut t onr 的标签发生 bs “失效”,因此如果我们忽略这个异常信息,我们会在屏幕上一直看到这个问题。get C t r uct or ( ) 方法产 ons 生了一个通过 get C t r uct or ( ) 方法安放自变量数组类型到类数组的构建器对象,然后所有我们要做的就是 ons 调用 new ns t ance( ) ,通过它一个数组对象包含我们当前的自变量——在这种例子中,就是 i ds 数组中的字 I 符串。 这样增加了一些更复杂的内容到这个简单的程序中。为了使“异或”行为拥有按钮,我们创建一个按钮组并 增加每个按钮到我们所需的组中。当我们运行这个程序时,我们会注意到所有的按钮除了 JBut t on都会向我 们展示“异或”行为。

1 3 . 1 9 . 8 图标
我们可在一个 JLabl e 或从 A t r act But t on 处继承的任何事物中使用一个图标(包括 JBut t on,JC bs heckb , ox Jr adi oBut t on及不同类型的 JM enuI t em )。利用 JLabl es 的图标十分的简单容易(我们会在随后的一个程序例 472

子中看到)。下面的程序例子探索了我们可以利用按钮的图标和它们的衍生物的其它所有方法。 我们可以使用任何我们需要的 G F 文件,但在这个例子中使用的这个 G F 文件是这本书编码发行的一部分, I I 可以在 w w Br uceEckel . com处下载来使用。为了打开一个文件和随之带来的图像,简单地创建一个图标并分 w. 配它文件名。从那时起,我们可以在程序中使用这个产生的图标。 //: Faces . j ava // I con behavi or i n JBut t ons package c13. s w ng; i i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j avax. s w ng. *; por i publ i c cl as s Faces ext ends JPanel { s t at i c I con[ ] f aces = { new I m ageI con( " f ace0. gi f " ) , new I m ageI con( " f ace1. gi f " ) , new I m ageI con( " f ace2. gi f " ) , new I m ageI con( " f ace3. gi f " ) , new I m ageI con( " f ace4. gi f " ) , }; JBut t on j b = new JBut t on( " JBut t on" , f aces [ 3] ) , j b2 = new JBut t on( " D s abl e" ) ; i bool ean m = f al s e; ad publ i c Faces ( ) { j b. addA i onLi s t ener ( new A i onLi s t ener ( ) { ct ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct i f (m ad) { j b. s et I con( f aces [ 3] ) ; m = f al s e; ad } el s e { j b. s et I con( f aces [ 0] ) ; m = t r ue; ad } j b. s et Ver t i cal A i gnm ( JBut t on. TO ; l ent P) j b. s et Hor i zont al A i gnm ( JBut t on. LEFT) ; l ent } }); j b. s et Rol l over Enabl ed( t r ue) ; j b. s et Rol l over I con( f aces [ 1] ) ; j b. s et Pr es s edI con( f aces [ 2] ) ; j b. s et D s abl edI con( f aces [ 4] ) ; i j b. s et Tool Ti pText ( " Yow " ) ; ! add( j b) ; j b2. addA i onLi s t ener ( new A i onLi s t ener ( ) { ct ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct i f (j b. i s Enabl ed( ) ) { j b. s et Enabl ed( f al s e) ; j b2. s et Text ( " Enabl e" ) ; } el s e { j b. s et Enabl ed( t r ue) ; j b2. s et Text ( " D s abl e" ) ; i 473

} } }); add( j b2) ; } publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) { ai Show i nFr am new Faces ( ) , 300, 200) ; . e( } } ///: ~ 一个图标可以在许多的构建器中使用,但我们可以使用 s et I con( ) 方法增加或更换图标。这个例子同样展示 了当事件发生在 JBut t on(或者一些 A t r act But t on)上时,为什么它可以设置各种各样的显示图标:当 bs JBut t on被按下时,当它被失效时,或者“滚过”时(鼠标从它上面移动过但并不击它)。我们会注意到那 给了按钮一种动画的感觉。 注意工具提示条也同样增加到按钮中。

1 3 . 1 9 . 9 菜单
菜单在 Sw ng中做了重要的改进并且更加的灵活——例如,我们可以在几乎程序中任何地方使用他们,包括 i 在面板和程序片中。语法同它们在老的 A T 中是一样的,并且这样使出现在老 A T 的在新的 Sw ng也出现 W W i 了:我们必须为我们的菜单艰难地编写代码,并且有一些不再作为资源支持菜单(其它事件中的一些将使它 们更易转换成其它的编程语言)。另外,菜单代码相当的冗长,有时还有一些混乱。下面的方法是放置所有 的关于每个菜单的信息到对象的二维数组里(这种方法可以放置我们想处理的任何事物到数组里),这种方 法在解决这个问题方面领先了一步。这个二维数组被菜单所创建,因此它首先表示出菜单名,并在剩余的列 中表示菜单项和它们的特性。我们会注意到数组列不必保持一致——只要我们的代码知道将发生的一切事 件,每一列都可以完全不同。 //: M enus . j ava // A m enu- bui l di ng s ys t em al s o dem t r at es ; ons // i cons i n l abel s and m enu i t em . s package c13. s w ng; i i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j avax. s w ng. * ; por i publ i c cl as s M enus ext ends JPanel { s t at i c f i nal Bool ean bT = new Bool ean( t r ue) , bF = new Bool ean( f al s e) ; // D m cl as s t o cr eat e t ype i dent i f i er s : um y s t at i c cl as s M Type { M Type( i nt i ) { } } ; s t at i c f i nal M Type m = new M i Type( 1) , // N m m or al enu i t em cb = new M Type( 2) , // C heckbox m enu i t em r b = new M ype( 3) ; // Radi o but t on m T enu i t em JText Fi el d t = new JText Fi el d( 10) ; JLabel l = new JLabel ( " I con Sel ect ed" , Faces . f aces [ 0] , JLabel . C TER) ; EN A i onLi s t ener a1 = new A i onLi s t ener ( ) { ct ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct t . s et Text ( ( ( JM enuI t em e. get Sour ce( ) ) . get Text ( ) ) ; ) } 474

}; A i onLi s t ener a2 = new A i onLi s t ener ( ) { ct ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct JM enuI t em m = ( JM i enuI t em e. get Sour ce( ) ; ) l . s et Text ( m . get Text ( ) ) ; i l . s et I con( m . get I con( ) ) ; i } }; // St or e m enu dat a as " r es our ces " : publ i c O ect [ ] [ ] f i l eM bj enu = { // M enu nam and accel er at or : e { " Fi l e" , new C act er ( ' F' ) } , har // N e t ype accel l i s t ener enabl ed am { " N " , m , new C act er ( ' N ) , a1, bT } , ew i har ' { "O pen" , m , new C act er ( ' O ) , a1, bT } , i har ' { " Save" , m , new C act er ( ' S' ) , a1, bF } , i har { " Save A " , m , new C act er ( ' A ) , a1, bF} , s i har ' { nul l } , // Separ at or { " Exi t " , m , new C act er ( ' x' ) , a1, bT } , i har }; publ i c O ect [ ] [ ] edi t M bj enu = { // M enu nam e: { " Edi t " , new C act er ( ' E' ) } , har // N e t ype accel l i s t ener enabl ed am { " C " , m , new C act er ( ' t ' ) , a1, bT } , ut i har { "C opy" , m , new C act er ( ' C ) , a1, bT } , i har ' { " Pas t e" , m , new C act er ( ' P' ) , a1, bT } , i har { nul l } , // Separ at or { " Sel ect A l " , m , new C act er ( ' l ' ) , a1, bT} , l i har }; publ i c O ect [ ] [ ] hel pM bj enu = { // M enu nam e: { " Hel p" , new C act er ( ' H' ) } , har // N e t ype accel l i s t ener enabl ed am { " I ndex" , m , new C act er ( ' I ' ) , a1, bT } , i har { " Us i ng hel p" , m , new C act er ( ' U' ) , a1, bT} , i har { nul l } , // Separ at or { "A bout " , m , new C act er ( ' t ' ) , a1, bT } , i har }; publ i c O ect [ ] [ ] opt i onM bj enu = { // M enu nam e: { " O i ons " , new C act er ( ' O ) } , pt har ' / / N e t ype accel l i s t ener enabl ed am { " O i on 1" , cb, new C act er ( ' 1' ) , a1, bT} , pt har { " O i on 2" , cb, new C act er ( ' 2' ) , a1, bT} , pt har }; publ i c O ect [ ] [ ] f aceM bj enu = { // M enu nam e: { " Faces " , new C act er ( ' a' ) } , har // O i nal l as t el em pt ent i s i con { " Face 0" , r b, new C act er ( ' 0' ) , a2, bT, har Faces . f aces [ 0] } , 475

{ " Face 1" , r b, new C act er ( ' 1' ) , har Faces . f aces [ 1] } , { " Face 2" , r b, new C act er ( ' 2' ) , har Faces . f aces [ 2] } , { " Face 3" , r b, new C act er ( ' 3' ) , har Faces . f aces [ 3] } , { " Face 4" , r b, new C act er ( ' 4' ) , har Faces . f aces [ 4] } ,

a2, bT, a2, bT, a2, bT , a2, bT,

}; publ i c O ect [ ] m bj enuBar = { f i l eM enu, edi t M enu, f aceM enu, opt i onM enu, hel pM enu, }; s t at i c publ i c JM enuBar cr eat eM enuBar ( O ect [ ] m bj enuBar D a) { at JM enuBar m enuBar = new JM enuBar ( ) ; f or ( i nt i = 0; i < m enuBar D a. l engt h; i ++) at m enuBar . add( cr eat eM enu( ( O ect [ ] [ ] ) m bj enuBar D a[ i ] ) ) ; at r et ur n m enuBar ; } s t at i c But t onG oup bgr oup; r s t at i c publ i c JM enu cr eat eM enu( O ect [ ] [ ] m bj enuD a) { at JM enu m enu = new JM enu( ) ; m enu. s et Text ( ( St r i ng) m enuD a[ 0] [ 0] ) ; at m enu. s et M oni c( nem ( ( C act er ) m har enuD a[ 0] [ 1] ) . char Val ue( ) ) ; at // C eat e r edundant l y, i n cas e t her e ar e r // any r adi o but t ons : bgr oup = new But t onG oup( ) ; r f or ( i nt i = 1; i < m enuD a. l engt h; i ++) { at i f (m enuD a[ i ] [ 0] == nul l ) at m enu. add( new JSepar at or ( ) ) ; el s e m enu. add( cr eat eM enuI t em m ( enuD a[ i ] ) ) ; at } r et ur n m enu; } s t at i c publ i c JM enuI t em cr eat eM enuI t em bj ect [ ] dat a) { (O JM enuI t em m = nul l ; M Type t ype = ( M Type) dat a[ 1] ; i f ( t ype == m ) i m = new JM enuI t em ) ; ( el s e i f ( t ype == cb) m = new JC heckBoxM enuI t em ) ; ( el s e i f ( t ype == r b) { m = new JRadi oBut t onM enuI t em ) ; ( bgr oup. add( m ; ) } m s et Text ( ( St r i ng) dat a[ 0] ) ; . 476

m s et M oni c( . nem ( ( C act er ) dat a[ 2] ) . char Val ue( ) ) ; har m addA i onLi s t ener ( . ct ( A i onLi s t ener ) dat a[ 3] ) ; ct m s et Enabl ed( . ( ( Bool ean) dat a[ 4] ) . bool eanVal ue( ) ) ; i f ( dat a. l engt h == 6) m s et I con( ( I con) dat a[ 5] ) ; . r et ur n m ; } M enus ( ) { s et Layout ( new Bor der Layout ( ) ) ; add( cr eat eM enuBar ( m enuBar ) , Bor der Layout . N RTH) ; O JPanel p = new JPanel ( ) ; p. s et Layout ( new Bor der Layout ( ) ) ; p. add( t , Bor der Layout . N RTH) ; O p. a l , Bor der Layout . C TER) ; dd( EN add( p, Bor der Layout . C TER) ; EN } publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) { ai Show i nFr am new M . e( enus ( ) , 300, 200) ; } } ///: ~ 这个程序的目的是允许程序设计者简单地创建表格来描述每个菜单,而不是输入代码行来建立菜单。每个菜 单都产生一个菜单,表格中的第一列包含菜单名和键盘快捷键。其余的列包含每个菜单项的数据:字符串存 在在菜单项中的位置,菜单的类型,它的快捷键,当菜单项被选中时被激活的动作接收器及菜单是否被激活 等信息。如果列开始处是空的,它将被作为一个分隔符来处理。 为了预防浪费和冗长的多个 Bool ean创建的对象和类型标志,以下的这些在类开始时就作为 s t at i c f i nal 被 创建:bT 和 bF 描述 Bool eans 和哑类 M Type 的不同对象描述标准的菜单项(m ),复选框菜单项(cb),和 i 单选钮菜单项(r b )。请记住一组 O ect 可以拥有单一的 O ect 句柄,并且不再是原来的值。 bj bj 这个程序例子同样展示了 JLabl es 和 JM enuI t em (和它们的衍生事物)如何处理图标的。一个图标经由它的 s 构建器置放进 JLabl e 中并当对应的菜单项被选中时被改变。 菜单条数组控制处理所有在文件菜单清单中列出的,我们想显示在菜单条上的文件菜单。我们通过这个数组 去使用 cr eat eM enuBar ( ) ,将数组分类成单独的菜单数据数组,再通过每个单独的数组去创建菜单。这种方 法依次使用菜单数据的每一行并以该数据创建 JM enu,然后为菜单数据中剩下的每一行调用 cr eat eM enuI t em ) ( 方法。最后,cr eat eM enuI t em ) 方法分析菜单数据的每一行并且判断菜单类型和它的属性,再适当地创建菜 ( 单项。终于,像我们在菜单构建器中看到的一样,从表示 cr eat eM enuBar ( m enuBar ) 的表格中创建菜单,而所 有的事物都是采用递归方法处理的。 这个程序不能建立串联的菜单,但我们拥有足够的知识,如果我们需要的话,随时都能增加多级菜单进去。

1 3 . 1 9 . 1 0 弹出式菜单
JPopupM enu的执行看起来有一些别扭:我们必须调用 enabl eEvent s ( ) 方法并选择鼠标事件代替利用事件接收 器。它可能增加一个鼠标接收器但 M eEvent 从 i s PopupTr i gger ( )处不会返回真值——它不知道将激活一 ous 个弹出菜单。另外,当我们尝试接收器方法时,它的行为令人不可思议,这或许是鼠标单击活动引起的。在 下面的程序例子里一些事件产生了这种弹出行为: //: Popup. j ava // C eat i ng popup m r enus w t h Sw ng i i package c13. s w ng; i i m t j ava. aw . *; por t 477

i m t j ava. aw . event . * ; por t i m t j avax. s w ng. * ; por i publ i c cl as s Popup ext ends JPanel { JPopupM enu popup = new JPopupM enu( ) ; JText Fi el d t = new JText Fi el d( 10) ; publ i c Popup( ) { add( t ) ; A i onLi s t ener al = new A i onLi s t ener ( ) { ct ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct t . s et Text ( ( ( JM enuI t em e. get Sour ce( ) ) . get Text ( ) ) ; ) } }; JM enuI t em m = new JM enuI t em " Hi t her " ) ; ( m addA i onLi s t ener ( al ) ; . ct popup. add( m ; ) m = new JM enuI t em " Yon" ) ; ( m addA i onLi s t ener ( al ) ; . ct popup. add( m ; ) m = new JM enuI t em " A ar " ) ; ( f m addA i onLi s t ener ( al ) ; . ct popup. add( m ; ) popup. addSepar at or ( ) ; m = new JM enuI t em " St ay Her e" ) ; ( m addA i onLi s t ener ( al ) ; . ct popup. add( m ; ) PopupLi s t ener pl = new PopupLi s t ener ( ) ; addM eLi s t ener ( pl ) ; ous t . addM eLi s t ener ( pl ) ; ous } cl as s PopupLi s t ener ext ends M eA ous dapt er { publ i c voi d m ePr es s ed( M eEvent e) { ous ous m aybeShow Popup( e) ; } publ i c voi d m eRel eas ed( M eEvent e) { ous ous m aybeShow Popup( e) ; } pr i vat e voi d m aybeShow Popup( M eEvent e) { ous i f ( e. i s PopupTr i gger ( ) ) { popup. s how ( e. get C ponent ( ) , e. get X( ) , e. get Y( ) ) ; om } } } publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) { ai Show i nFr am new Popup( ) , 200, 150) ; . e( } } ///: ~ 相同的 A i onLi s t ener 被加入每个 JM ct enuI t em中,使其能从菜单标签中取出文字,并将文字插入 JText Fi el d。 478

1 3 . 1 9 . 1 1 列表框和组合框
列表框和组合框在 Sw ng中工作就像它们在老的 A T 中工作一样,但如果我们需要它,它们同样被增加功 i W 能。另外,它也更加的方便易用。例如,JLi s t 中有一个显示 St r i ng数组的构建器(奇怪的是同样的功能在 JC boBox 中无效!)。下面的例子显示了它们基本的用法。 om //: Li s t C bo. j ava om // Li s t boxes & C bo boxes om package c13. s w ng; i i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j avax. s w ng. * ; por i publ i c cl as s Li s t C bo ext ends JPanel { om publ i c Li s t C bo( ) { om s et Layout ( new G i dLayout ( 2, 1) ) ; r JLi s t l i s t = new JLi s t ( But t onG oups . i ds ) ; r add( new JScr ol l Pane( l i s t ) ) ; JC boBox com = new JC boBox( ) ; om bo om f or ( i nt i = 0; i < 100; i ++) com addI t em I nt eger . t oSt r i ng( i ) ) ; bo. ( add( com ; bo) } publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) { ai Show i nFr am new Li s t C bo( ) , 200, 200) ; . e( om } } ///: ~ 最开始的时候,似乎有点儿古怪的一种情况是 JLi s t s 居然不能自动提供滚动特性——即使那也许正是我们一 直所期望的。增加对滚动的支持变得十分容易,就像上面示范的一样——简单地将 JLi s t 封装到 JScr ol l Pane即可,所有的细节都自动地为我们照料到了。

1 3 . 1 9 . 1 2 滑杆和进度指示条
滑杆用户能用一个滑块的来回移动来输入数据,在很多情况下显得很直观(如声音控制)。进程条从“空” 到“满”显示相关数据的状态,因此用户得到了一个状态的透视。我最喜爱的有关这的程序例子简单地将滑 动块同进程条挂接起来,所以当我们移动滑动块时,进程条也相应的改变: //: Pr ogr es s . j ava // Us i ng pr ogr es s bar s and s l i der s package c13. s w ng; i i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j avax. s w ng. * ; por i i m t j avax. s w ng. event . *; por i i m t j avax. s w ng. bor der . *; por i publ i c cl as s Pr ogr es s ext ends JPanel { JPr ogr es s Bar pb = new JPr ogr es s Bar ( ) ; JSl i der s b = new JSl i der ( JSl i der . HO ZO TA 0, 100, 60) ; RI N L, publ i c Pr ogr es s ( ) { s et Layout ( new G i dLayout ( 2, 1) ) ; r 479

add( pb) ; s b. s et Val ue( 0) ; s b. s et Pai nt Ti cks ( t r ue) ; s b. s et M or Ti ckSpaci ng( 20) ; aj s b. s et M nor Ti ckSpaci ng( 5) ; i s b. s et Bor der ( new Ti t l edBor der ( " Sl i de M ) ) ; e" pb. s et M odel ( s b. get M odel ( ) ) ; // Shar e m odel add( s b) ; } publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) { ai Show i nFr am new Pr ogr es s ( ) , 200, 150) ; . e( } } ///: ~ JPr ogr es s Bar 十分简单,但 JSl i der 却有许多选项,例如方法、大或小的记号标签。注意增加一个带标题的 边框是多么的容易。

13. 19. 13 树
使用一个 JTr ee 可以简单地像下面这样表示: add( new JTr ee( new O ect [ ] { " t hi s " , " t hat " , " ot her " } ) ) ; bj 这个程序显示了一个原始的树状物。树状物的 A 是非常巨大的,可是——当然是在 Sw ng中的巨大。它表 PI i 明我们可以做有关树状物的任何事,但更复杂的任务可能需要不少的研究和试验。幸运的是,在库中提供了 一个妥协:“默认的”树状物组件,通常那是我们所需要的。因此大多数的时间我们可以利用这些组件,并 且只在特殊的情况下我们需要更深入的研究和理解。 下面的例子使用了“默认”的树状物组件在一个程序片中显示一个树状物。当我们按下按钮时,一个新的子 树就被增加到当前选中的结点下(如果没有结点被选中,就用根结节): //: Tr ees . j ava // Si m e Sw ng t r ee exam e. Tr ees can be m pl i pl ade // vas t l y m e com ex t han t hi s . or pl package c13. s w ng; i i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j avax. s w ng. * ; por i i m t j avax. s w ng. t r ee. * ; por i // T akes an ar r ay of St r i ngs and m akes t he f i r s t // el em ent a node and t he r es t l eaves : cl as s Br anch { D aul t M abl eTr eeN ef ut ode r ; publ i c Br anch( St r i ng[ ] dat a) { r = new D aul t M abl eTr eeN ef ut ode( dat a[ 0] ) ; f or ( i nt i = 1; i < dat a. l engt h; i ++) r . add( new D aul t M abl eTr eeN ef ut ode( dat a[ i ] ) ) ; } publ i c D aul t M abl eTr eeN ef ut ode node( ) { r et ur n r ; } } publ i c cl as s Tr ees ext ends JPanel { 480

St r i ng[ ] [ ] dat a = { { " C or s " , " Red" , " Bl ue" , " G een" } , ol r { " Fl avor s " , " Tar t " , " Sw " , " Bl and" } , eet { " Lengt h" , " Shor t " , " M um , " Long" } , edi " { " Vol um , " Hi gh" , " M um , " Low } , e" edi " " { " Tem at ur e" , " Hi gh" , " M um , " Low } , per edi " " { " I nt ens i t y" , " Hi gh" , " M um , " Low } , edi " " }; s t at i c i nt i = 0; D aul t M abl eTr eeN ef ut ode r oot , chi l d, chos en; JTr ee t r ee; D aul t Tr eeM ef odel m odel ; publ i c Tr ees ( ) { s et Layout ( new Bor der Layout ( ) ) ; r oot = new D aul t M abl eTr eeN ef ut ode( " r oot " ) ; t r ee = new JTr ee( r oot ) ; // A i t and m dd ake i t t ake car e of s cr ol l i ng: add( new JScr ol l Pane( t r ee) , Bor der Layout . C TER) ; EN // C ur e t he t r ee' s m apt odel : m odel =( D aul t Tr eeM ef odel ) t r ee. get M odel ( ) ; JBut t on t es t = new JBut t on( " Pr es s m ) ; e" t es t . addA i onLi s t ener ( new A i onLi s t ener ( ) { ct ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct i f ( i < dat a. l engt h) { chi l d = new Br anch( dat a[ i ++] ) . node( ) ; // W ' s t he l as t one you cl i cked? hat chos en = ( D aul t M abl eTr eeN ef ut ode) t r ee. get Las t Sel ect edPat hC ponent ( ) ; om i f ( chos en == nul l ) chos en = r oot ; // The m odel w l l cr eat e t he i // appr opr i at e event . I n r es pons e, t he // t r ee w l l updat e i t s el f : i m odel . i ns er t N odeI nt o( chi l d, chos en, 0) ; // Thi s put s t he new node on t he // cur r ent l y chos en node. } } }); // C hange t he but t on' s col or s : t es t . s et Backgr ound( C or . bl ue) ; ol t es t . s et For egr ound( C or . w t e) ; ol hi JPanel p = new JPanel ( ) ; p. add( t es t ) ; add( p, Bor der Layout . SO UTH) ; } publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) { ai Show i nFr am new Tr ees ( ) , 200, 500) ; . e( } } ///: ~ 最重要的类就是分支,它是一个工具,用来获取一个字符串数组并为第一个字符串建立一个 481

D aul t M abl eTr eeN ef ut ode 作为根,其余在数组中的字符串作为叶。然后 node( )方法被调用以产生“分支”的 根。树状物类包括一个来自被制造的分支的二维字符串数组,以及用来统计数组的一个静态中断 i 。 D aul t M abl eTr eeN ef ut ode 对象控制这个结节,但在屏幕上表示的是被 JTr ee 和它的相关 (D aul t Tr eeM ef odel )模式所控制。注意当 JTr ee 被增加到程序片时,它被封装到 JScr ol l Pane中——这就 是它全部提供的自动滚动。 JTr ee 通过它自己的模型来控制。当我们修改这个模型时,模型产生一个事件,导致 JTr ee对可以看见的树 状物完成任何必要的升级。在 i ni t ( ) 中,模型由调用 get M odel ( )方法所捕捉。当按钮被按下时,一个新的分 支被创建了。然后,当前选择的组件被找到(如果没有选择就是根)并且模型的 i ns er t N odeI nt o( ) 方法做所 有的改变树状物和导致它升级的工作。 大多数的时候,就像上面的例子一样,程序将给我们在树状物中所需要的一切。不过,树状物拥有力量去做 我们能够想像到的任何事——在上面的例子中我们到处都可看到“def aul t (默认)”字样,我们可以取代我 们自己的类来获取不同的动作。但请注意:几乎所有这些类都有一个具大的接口,因此我们可以花一些时间 努力去理解这些错综复杂的树状物。

1 3 . 1 9 . 1 4 表格
和树状物一样,表格在 Sw ng相当的庞大和强大。它们最初有意被设计成以 Java 数据库连结(JD ,在 15 i BC 章有介绍)为媒介的“网格”数据库接口,并且因此它们拥有的巨大的灵活性,使我们不再感到复杂。无 疑,这是足以成为成熟的电子数据表的基础条件而且可能为整本书提供很好的根据。但是,如果我们理解这 个的基础条件,它同样可能创建相关的简单的 Jt abl e 。 JTabl e控制数据的显示方式,但 Tabl eM odel 控制它自己的数据。因此在我们创建 JTabl e 前,应先创建一个 Tabl eM odel 。我们可以全部地执行 Tabl eM odel 接口,但它通常从 hel per 类的 A r a Ta eM el 处简单地 bst ct bl od 继承: //: Tabl e. j ava // Si m e dem t r at i on of JTabl e pl ons package c13. s w ng; i i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j avax. s w ng. * ; por i i m t j avax. s w ng. t abl e. *; por i i m t j avax. s w ng. event . *; por i // T he Tabl eM odel cont r ol s al l t he dat a: cl as s D aM at odel ext ends A t r act Tabl eM bs odel { O ect [ ] [ ] dat a = { bj { " one" , " t w , " t hr ee" , " f our " } , o" { " f i ve" , " s i x" , " s even" , " ei ght " } , { " ni ne" , " t en" , " el even" , " t w ve" } , el }; // Pr i nt s dat a w hen t abl e changes : cl as s TM i m em s Tabl eM L pl ent odel Li s t ener { publ i c voi d t abl eC hanged( Tabl eM odel Event e) { f or ( i nt i = 0; i < dat a. l engt h; i ++) { f or ( i nt j = 0; j < dat a[ 0] . l engt h; j ++) Sys t em out . pr i nt ( dat a[ i ] [ j ] + " " ) ; . Sys t em o . pr i nt l n( ) ; . ut } } } D aM at odel ( ) { addTabl eM odel Li s t ener ( new TM ) ) ; L( } 482

publ i c i nt get C um ount ( ) { ol nC r et ur n dat a[ 0] . l engt h; } publ i c i nt get Row ount ( ) { C r et ur n dat a. l engt h; } publ i c O ect get Val ueA ( i nt r ow i nt col ) { bj t , r et ur n dat a[ r ow [ col ] ; ] } publ i c voi d s et Val ueA ( O ect val , i nt r ow i nt col ) { t bj , dat a[ r ow [ col ] = val ; ] // I ndi cat e t he change has happened: f i r eTabl eD aC at hanged( ) ; } publ i c bool ean i s C l Edi t abl e( i nt r ow i nt col ) { el , r et ur n t r ue; } }; publ i c cl as s Tabl e ext ends JPanel { publ i c Tabl e( ) { s et Layout ( new Bor der Layout ( ) ) ; JTabl e t abl e = new JTabl e( new D aM at odel ( ) ) ; JScr ol l Pane s cr ol l pane = JTabl e. cr eat eScr ol l PaneFor Tabl e( t abl e) ; add( s cr ol l pane, Bor der Layout . C TER) ; EN } publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) { ai Show i nFr am new Tabl e( ) , 200, 200) ; . e( } } ///: ~ D eM at odel 包括一组数据,但我们同样能从其它的地方得到数据,例如从数据库中。构建器增加了一个 Tabl eM odel Li s t ener 用来在每次表格被改变后打印数组。剩下的方法都遵循 Bean 的命名规则,并且当 JTabl e需要在 D eM at odel 中显示信息时调用。A t r act Tabl eM bs odel 提供了默认的 s et Val ueA ( )和 t i s C l Edi t abl e( )方法以防止修改这些数据,因此如果我们想修改这些数据,就必须过载这些方法。 el 一旦我们拥有一个 Tabl eM odel ,我们只需要将它分配给 JTabl e构建器即可。所有有关显示,编辑和更新的 详细资料将为我们处理。注意这个程序例子同样将 JTabl e放置在 JScr ol l Pane 中,这是因为 JScr ol l Pane需 要一个特殊的 JTabl e 方法。

1 3 . 1 9 . 1 5 卡片式对话框
在本章的前部,向我们介绍了老式的 C dLayout ,并且注意到我们怎样去管理我们所有的卡片开关。有趣的 ar 是,有人现在认为这是一种不错的设计。幸运的是,Sw ng用 JTabbedPane对它进行了修补,由 JTa i bbed ne Pa 来处理这些卡片,开关和其它的任何事物。对比 C dLayout 和 JTabbedPane,我们会发现惊人的差异。 ar 下面的程序例子十分的有趣,因为它利用了前面例子的设计。它们都是做为 JPanel 的衍生物来构建的,因此 这个程序将安放前面的每个例子到它自己在 JTabbedPane的窗格中。我们会看到利用 RTTI 制造的程序十分的 小巧精致: //: Tabbed. j ava // Us i ng t abbed panes 483

package c13. s w ng; i i m t j ava. aw . *; por t i m t j avax. s w ng. * ; por i i m t j avax. s w ng. bor der . * ; por i publ i c cl as s Tabbed ext ends JPanel { s t at i c O ect [ ] [ ] q = { bj { " Fel i x" , Bor der s . cl as s } , { " The Pr of es s or " , But t ons . cl as s } , { " Rock Bot t om , But t onG oups . cl as s } , " r { " Theodor e" , Faces . cl as s } , { " Si m , M on" enus . cl as s } , { " A vi n" , Popup. cl as s } , l { " Tom , Li s t C bo. cl as s } , " om { " Jer r y" , Pr ogr es s . cl as s } , { " Bugs " , Tr ees . cl as s } , { " D f y" , Tabl e. cl as s } , af }; s t at i c JPanel m akePanel ( C as s c) { l St r i ng t i t l e = c. get N e( ) ; am t i t l e = t i t l e. s ubs t r i ng( t i t l e. l as t I ndexO ( ' . ' ) + 1) ; f JPanel s p = nul l ; try { s p = ( JPanel ) c. new ns t ance( ) ; I } cat ch( Except i on e) { Sys t em out . pr i nt l n( e) ; . } s p. s et Bor der ( new Ti t l edBor der ( t i t l e) ) ; r et ur n s p; } publ i c Tabbed( ) { s et Layout ( new Bor der Layout ( ) ) ; JTabbedPane t abbed = new JTabbedPane( ) ; f or ( i nt i = 0; i < q. l engt h; i ++) t abbed. addTab( ( St r i ng) q[ i ] [ 0] , m akePanel ( ( C as s ) q[ i ] [ 1] ) ) ; l add( t abbed, Bor der Layout . C TER) ; EN t abbed. s et Sel ect edI ndex( q. l engt h/2) ; } publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) { ai Show i nFr am new Tabbed( ) , 460, 350) ; . e( } } ///: ~ 再者,我们可以注意到使用的数组构造式样:第一个元素是被置放在卡片上的 St r i ng,第二个元素是将被显 示在对应窗格上 JPanel 类。在 Tabbed( ) 构建器里,我们可以看到两个重要的 JTabbedPane方法被使用: addTab( ) 插入一个新的窗格,s et Sel ect edI ndex( ) 选择一个窗格并从它开始。(一个在中间被选中的窗格证 明我们不必从第一个窗格开始)。 当我们调用 addTab( ) 方法时,我们为它提供卡片的 St r i ng和一些组件(也就是说,一个 A T 组件,而不是 W 一个来自 A T 的 JC ponent )。这个组件会被显示在窗格中。一旦我们这样做了,自然而然的就不需要更多 W om 管理了——JTabbedPane会为我们处理其它的任何事。 484

m akePanel ( )方法获取我们想创建的类 C as s 对象和用 new ns t ance( ) 去创建并造型为 JPanel (当然,假定那 l I 些类是必须从 JPanel 继承才能增加的类,除非在这一节中为程序例子的结构所使用)。它增加了一个包括类 名并返回结果的 Ti t l edBor der ,以作为一个 JPanel 在 addTab( ) 被使用。 当我们运行程序时,我们会发现如果卡片太多,填满了一行,JTabbedPane自动地将它们堆积起来。

1 3 . 1 9 . 1 6 S wi ng 消息框
开窗的环境通常包含一个标准的信息框集,允许我们很快传递消息给用户或者从用户那里捕捉消息。在 Sw ng里,这些信息窗被包含在 JO i onPane 里的。我们有一些不同的可能实现的事件(有一些十分复 i pt 杂),但有一点,我们必须尽可能的利用 s t at i c JO i onPane. s how es s ageD al og( ) 和 pt M i JO i onPane. s how onf i r m i al og( ) 方法,调用消息对话框和确认对话框。 pt C D

1 3 . 1 9 . 1 7 S wi ng 更多的知识
这一节意味着唯一向我们介绍的是 Sw ng的强大力量和我们的着手处,因此我们能注意到通过库,我们会感 i 觉到我们的方法何等的简单。到目前为止,我们已看到的可能足够满足我们 UI 设计需要的一部分。不过,这 里有许多有关 Sw ng额外的情况——它有意成为一全功能的 UI 设计工具箱。如果我们没有发现我们所需要 i 的,请到 SUN公司的在线文件中去查找,并搜索 W EB。这个方法几乎可以完成我们能想到的任何事。 本节中没有涉及的一些要点: ■更多特殊的组件,例如 JC or C ol hoos er , JFi l eC hoos er , JPas s w dFi el d, JHTM or LPane(完成简单的 H L 格式 TM 化和显示)以及 JText Pane(一个支持格式化,字处理和图像的文字编辑器)。它们都非常易用。 ■Sw ng的新的事件类型。在一些方法中,它们看起来像违例:类型非常的重要,名字可以被用来表示除了 i 它们自己之外的任何事物。 ■新的布局管理:Spr i ngs & St r ut s 以及 BoxLayout ■分裂控制:一个间隔物式的分裂条,允许我们动态地处理其它组件的位置。 ■JLayer edPane 和 JI nt er nal Fr am 被一起用来在当前帧中创建子帧,以产生多文件接口(M I )应用程序。 e D ■可插入的外观和效果,因此我们可以编写单个的程序可以像期望的那样动态地适合不同的平台和操作系 统。 ■自定义光标。 ■JTool bar A 提供的可拖动的浮动工具条。 PI ■双缓存和为平整屏幕重新画线的自动重画批次。 ■内建“取消”支持。 ■拖放支持。

13. 20 总结
对于 A T 而言,Java 1. 1 到 Java 1. 2 最大的改变就是 Java 中所有的库。Java 1. 0 版的 A T 曾作为目前见过 W W 的最糟糕的一个设计被彻底地批评,并且当它允许我们在创建小巧精致的程序时,产生的 G “在所有的平 UI 台上都同样的平庸”。它与在特殊平台上本地应用程序开发工具相比也是受到限制的,笨拙的并且也是不友 好的。当 Java 1. 1 版纳入新的事件模型和 Java Beans 时,平台被设置——现在它可以被拖放到可视化的应 用程序构建工具中,创建 G 组件。另外,事件模型的设计和 Bean 无疑对轻松的编程和可维护的代码都非常 UI 的在意(这些在 Java 1. 0 A T 中不那么的明显)。但直至 G 组件-JFC i ng类-显示工作结束它才这 W UI /Sw 样。对于 Sw ng组件而言,交叉平台 G 编程可以变成一种有教育意义的经验。 i UI 现在,唯一的情况是缺乏应用程序构建工具,并且这就是真正的变革的存在之处。微软的 Vi s ual Bas i c和 Vi s ual C ++需要它们的应用程序构建工具,同样的是 Bor l and的 D phi 和 C el ++构建器。如果我们需要应用程 序构建工具变得更好,我们不得不交叉我们的指针并且希望自动授权机会给我们所需要的。Java 是一个开放 的环境,因此不但考虑到同其它的应用程序构建环境竞争,而且 Java 还促进它们的发展。这些工具被认真地 使用,它们必须支持 Java Beans 。这意味着一个平等的应用领域:如果一个更好的应用程序构建工具出现, 我们不需要去约束它就可以使用——我们可以采用并移动到新的工具上工作即可,这会提高我们的工作效 率。这种竞争的环境对应用程序构建工具来说从未出现过,这种竞争能真正提高程序设计者的工作效率。

485

13. 21 练习
( 1)创建一个有文字字段和三个按钮的程序片。当我们按下每个按钮时,使不同的文字显示在文字段中。 ( 2)增加一个复选框到练习 1 创建的程序中,捕捉事件,并插入不同的文字到文字字段中。 ( 3)创建一个程序片并增加所有导致 act i on( ) 被调用的组件,然后捕捉他们的事件并在文字字段中为每个组 件显示一个特定的消息。 ( 4)增加可以被 handl eEvent ( ) 方法测试事件的组件到练习 3 中。过载 handl eEvent ( ) 并在文字字段中为每个 组件显示特定的消息。 ( 5)创建一个有一个按钮和一个 Text Fi el d的程序片。编写一个 handl eEvent ( ) ,以便如果按钮有焦点,输入 字符到将显示的 Text Fi el d中。 ( 6)创建一个应用程序并将本章所有的组件增加主要的帧,包括菜单和对话框。 ( 7)修改 Text N . j ava,以便字母在 t 2 中保持输入时的样子,取代自动变成大写。 ew ( 8)修改 C dLayout 1. j ava以便它使用 Java 1. 1 的事件模型。 ar ( 9)增加 Fr og. cl as s 到本章出现的清单文件中并运行 j ar 以创建一个包括 Fr og 和 BangBean 的 JA R文件。现 在从 SUN公司处下载并安装 BD 或者使用我们自己的可激活 Bean 的程序构建工具并增加 JA K R文件到我们的环 境中,因此我们可以测试两个 Bean。 ( 10) 创建我们自己的包括两个属性:一个布尔值为“on”,另一个为整型“l evel ”,称为 Val ve 的 Java Bean。创建一个清单文件,利用 j ar 打包我们的 Bean,然后读入它到 beanbox 或到我们自己的激活程序构建 工具里,因此我们可以测试它。 ( 11) 修改 M enus . j ava,以便它处理多级菜单。这要假设读者已经熟悉了 HTM 的基础知识。但那些东西并不 L 难理解,而且有一些书和资料可供参考。

486

第 14 章 多线程
利用对象,可将一个程序分割成相互独立的区域。我们通常也需要将一个程序转换成多个独立运行的子任 务。 象这样的每个子任务都叫作一个“线程”(Thr ead)。编写程序时,可将每个线程都想象成独立运行,而且 都有自己的专用 C PU。一些基础机制实际会为我们自动分割 C PU的时间。我们通常不必关心这些细节问题, 所以多线程的代码编写是相当简便的。 这时理解一些定义对以后的学习狠有帮助。“进程”是指一种“自包容”的运行程序,有自己的地址空间。 “多任务”操作系统能同时运行多个进程(程序)——但实际是由于 C 分时机制的作用,使每个进程都能 PU 循环获得自己的 C PU时间片。但由于轮换速度非常快,使得所有程序好象是在“同时”运行一样。“线程” 是进程内部单一的一个顺序控制流。因此,一个进程可能容纳了多个同时执行的线程。 多线程的应用范围很广。但在一般情况下,程序的一些部分同特定的事件或资源联系在一起,同时又不想为 它而暂停程序其他部分的执行。这样一来,就可考虑创建一个线程,令其与那个事件或资源关联到一起,并 让它独立于主程序运行。一个很好的例子便是“Q t ”或“退出”按钮——我们并不希望在程序的每一部分 ui 代码中都轮询这个按钮,同时又希望该按钮能及时地作出响应(使程序看起来似乎经常都在轮询它)。事实 上,多线程最主要的一个用途就是构建一个“反应灵敏”的用户界面。

14. 1 反应灵敏的用户界面
作为我们的起点,请思考一个需要执行某些 C PU密集型计算的程序。由于 C PU“全心全意”为那些计算服 务,所以对用户的输入十分迟钝,几乎没有什么反应。在这里,我们用一个合成的 appl et /appl i cat i on(程 序片/应用程序)来简单显示出一个计数器的结果: //: C ount er 1. j ava // A non- r es pons i ve us er i nt er f ace package c14; i m t j ava. aw . *; por t i m t j ava. aw . event . *; por t i m t j ava. appl et . * ; por publ i c cl as s C ount er 1 ext ends A et { ppl pr i vat e i nt count = 0; pr i vat e But t on onO f = new But t on( " Toggl e" ) , f s t ar t = new But t on( " St ar t " ) ; pr i vat e Text Fi el d t = new Text Fi el d( 10) ; pr i vat e bool ean r unFl ag = t r ue; publ i c voi d i ni t ( ) { add( t ) ; s t ar t . addA i onLi s t ener ( new St ar t L( ) ) ; ct add( s t ar t ) ; onO f . addA i onLi s t ener ( new O f f L( ) ) ; f ct nO add( onO f ) ; f } publ i c voi d go( ) { w l e ( t r ue) { hi try { Thr ead. cur r ent Thr ead( ) . s l eep( 100) ; } cat ch ( I nt er r upt edExcept i on e) { } i f ( r unFl ag) t . s et Text ( I nt eger . t oSt r i ng( count ++) ) ; 487

} } cl as s St ar t L i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct go( ) ; } } cl as s O f f L i m em s A i onLi st ener { nO pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct r unFl ag = ! r unFl ag; } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C ount er 1 appl et = new C ount er 1( ) ; Fr am aFr am = new Fr am " C e e e( ount er 1" ) ; aFr am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); aFr am add( appl et , Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 300, 200) ; e. appl et . i ni t ( ) ; appl et . s t ar t ( ) ; aFr am s et Vi s i bl e( t r ue) ; e. } } ///: ~ 在这个程序中,A T 和程序片代码都应是大家熟悉的,第 13 章对此已有很详细的交待。go( ) 方法正是程序全 W 心全意服务的对待:将当前的 count (计数)值置入 Text Fi el d(文本字段)t ,然后使 count 增值。 go( ) 内的部分无限循环是调用 s l eep( ) 。s l eep( ) 必须同一个 Thr ead (线程)对象关联到一起,而且似乎每个 应用程序都有部分线程同它关联(事实上,Java 本身就是建立在线程基础上的,肯定有一些线程会伴随我们 写的应用一起运行)。所以无论我们是否明确使用了线程,都可利用 Thr ead. cur r ent Thr ead( ) 产生由程序使 用的当前线程,然后为那个线程调用 s l eep( ) 。注意,Thr ead. cur r ent Thr ead( ) 是 Thr ead类的一个静态方 法。 注意 s l eep( )可能“掷”出一个 I nt er r upt Except i on(中断违例)——尽管产生这样的违例被认为是中止线 程的一种“恶意”手段,而且应该尽可能地杜绝这一做法。再次提醒大家,违例是为异常情况而产生的,而 不是为了正常的控制流。在这里包含了对一个“睡眠”线程的中断,以支持未来的一种语言特性。 一旦按下 s t ar t 按钮,就会调用 go( ) 。研究一下 go( ) ,你可能会很自然地(就象我一样)认为它该支持多线 程,因为它会进入“睡眠”状态。也就是说,尽管方法本身“睡着”了,C PU仍然应该忙于监视其他按钮 “按下”事件。但有一个问题,那就是 go( ) 是永远不会返回的,因为它被设计成一个无限循环。这意味着 act i onPer f or m )根本不会返回。由于在第一个按键以后便陷入 act i onPer f or m )中,所以程序不能再对 ed( ed( 其他任何事件进行控制(如果想出来,必须以某种方式“杀死”进程——最简便的方式就是在控制台窗口按 C r l +C键)。 t 这里最基本的问题是 go( )需要继续执行自己的操作,而与此同时,它也需要返回,以便 act i onPer f or m ) ed( 能够完成,而且用户界面也能继续响应用户的操作。但对象 go( ) 这样的传统方法来说,它却不能在继续的同 时将控制权返回给程序的其他部分。这听起来似乎是一件不可能做到的事情,就象 C PU必须同时位于两个地 方一样,但线程可以解决一切。“线程模型”(以及 Java 中的编程支持)是一种程序编写规范,可在单独一 个程序里实现几个操作的同时进行。根据这一机制,C 可为每个线程都分配自己的一部分时间。每个线程 PU 都“感觉”自己好象拥有整个 C PU,但 C PU的计算时间实际却是在所有线程间分摊的。 线程机制多少降低了一些计算效率,但无论程序的设计,资源的均衡,还是用户操作的方便性,都从中获得 488

了巨大的利益。综合考虑,这一机制是非常有价值的。当然,如果本来就安装了多块 C PU,那么操作系统能 够自行决定为不同的 C PU分配哪些线程,程序的总体运行速度也会变得更快(所有这些都要求操作系统以及 应用程序的支持)。多线程和多任务是充分发挥多处理机系统能力的一种最有效的方式。

1 4 . 1 . 1 从线程继承
为创建一个线程,最简单的方法就是从 Thr ead类继承。这个类包含了创建和运行线程所需的一切东西。 Thr ead最重要的方法是 r un( ) 。但为了使用 r un( ) ,必须对其进行过载或者覆盖,使其能充分按自己的吩咐 行事。因此,r un( ) 属于那些会与程序中的其他线程“并发”或“同时”执行的代码。 下面这个例子可创建任意数量的线程,并通过为每个线程分配一个独一无二的编号(由一个静态变量产 生),从而对不同的线程进行跟踪。Thr ead的 r un( ) 方法在这里得到了覆盖,每通过一次循环,计数就减 1——计数为 0 时则完成循环(此时一旦返回 r un( ) ,线程就中止运行)。 //: Si m eThr ead. j ava pl // Ver y s i m e Thr eadi ng exam e pl pl publ i c cl as s Si m eThr ead ext ends Thr ead { pl pr i vat e i nt count D n = 5; ow pr i vat e i nt t hr eadN ber ; um pr i vat e s t at i c i nt t hr eadC ount = 0; publ i c Si m eThr ead( ) { pl t hr eadN ber = ++t hr eadC um ount ; Sys t em out . p i nt l n( " M ng " + t hr eadN ber ) ; . r aki um } publ i c voi d r un( ) { w l e( t r ue) { hi Sys t em out . pr i nt l n( " Thr ead " + . t hr eadN ber + " ( " + count D n + " ) " ) ; um ow i f ( - - count D n == 0) r et ur n; ow } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai f or ( i nt i = 0; i < 5; i ++) new Si m eThr ead( ) . s t ar t ( ) ; pl Sys t em out . pr i nt l n( " A l Thr eads St ar t ed" ) ; . l } } ///: ~ r un( ) 方法几乎肯定含有某种形式的循环——它们会一直持续到线程不再需要为止。因此,我们必须规定特定 的条件,以便中断并退出这个循环(或者在上述的例子中,简单地从 r un( ) 返回即可)。r un( ) 通常采用一种 无限循环的形式。也就是说,通过阻止外部发出对线程的 s t op( )或者 des t r oy( ) 调用,它会永远运行下去 (直到程序完成)。 在 m n( ) 中,可看到创建并运行了大量线程。Thr ead包含了一个特殊的方法,叫作 s t ar t ( ),它的作用是对 ai 线程进行特殊的初始化,然后调用 r un( )。所以整个步骤包括:调用构建器来构建对象,然后用 s t ar t ( )配置 线程,再调用 r un( ) 。如果不调用 s t ar t ( )——如果适当的话,可在构建器那样做——线程便永远不会启动。 下面是该程序某一次运行的输出(注意每次运行都会不同): M ng 1 aki M ng 2 aki M ng 3 aki M ng 4 aki M ng 5 aki T hr ead 1( 5) 489

T hr ead 1( 4) T hr ead 1( 3) T hr ead 1( 2) T hr ead 2( 5) T hr ead 2( 4) T hr ead 2( 3) T hr ead 2( 2) T hr ead 2( 1) T hr ead 1( 1) A l Thr eads St ar t ed l T hr ead 3( 5) T hr ead 4( 5) T hr ead 4( 4) T hr ead 4( 3) T hr ead 4( 2) T hr ead 4( 1) T hr ead 5( 5) T hr ead 5( 4) T hr ead 5( 3) T hr ead 5( 2) T hr ead 5( 1) T hr ead 3( 4) T hr ead 3( 3) T hr ead 3( 2) T hr ead 3( 1) 可注意到这个例子中到处都调用了 s l eep( ),然而输出结果指出每个线程都获得了属于自己的那一部分 C PU 执行时间。从中可以看出,尽管 s l eep( )依赖一个线程的存在来执行,但却与允许或禁止线程无关。它只不 过是另一个不同的方法而已。 亦可看出线程并不是按它们创建时的顺序运行的。事实上,C PU处理一个现有线程集的顺序是不确定的—— 除非我们亲自介入,并用 Thr ead的 s et Pr i or i t y( ) 方法调整它们的优先级。 m n( )创建 Thr ead对象时,它并未捕获任何一个对象的句柄。普通对象对于垃圾收集来说是一种“公平竞 ai 赛”,但线程却并非如此。每个线程都会“注册”自己,所以某处实际存在着对它的一个引用。这样一来, 垃圾收集器便只好对它“瞠目以对”了。

1 4 . 1 . 2 针对用户界面的多线程
现在,我们也许能用一个线程解决在 C ount er 1. j ava 中出现的问题。采用的一个技巧便是在一个线程的 r un( ) 方法中放置“子任务”——亦即位于 go( ) 内的循环。一旦用户按下 St ar t 按钮,线程就会启动,但马上结束 线程的创建。这样一来,尽管线程仍在运行,但程序的主要工作却能得以继续(等候并响应用户界面的事 件)。下面是具体的代码: / /: C ount er 2. j ava // A r es pons i ve us er i nt er f ace w t h t hr eads i i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. appl et . * ; por cl as s Separ at eSubTas k ext ends Thr ead { pr i vat e i nt count = 0; pr i vat e C ount er 2 c2; pr i vat e bool ean r unFl ag = t r ue; publ i c Separ at eSubTas k( C ount er 2 c2) { 490

t hi s . c2 = c2; s t ar t ( ) ; } publ i c voi d i nver t Fl ag( ) { r unFl ag = ! r unFl ag; } publ i c voi d r un( ) { w l e ( t r ue) { hi try { s l eep( 100) ; } cat ch ( I nt er r upt edExcept i on e) { } i f ( r unFl ag) c2. t . s et Text ( I nt eger . t oSt r i ng( count ++) ) ; } } } publ i c cl as s C ount er 2 ext ends A et { ppl Text Fi el d t = new Text Fi el d( 10) ; pr i vat e Separ at eSubTas k s p = nul l ; pr i vat e But t on onO f = new But t on( " Toggl e" ) , f s t ar t = new But t on( " St ar t " ) ; publ i c voi d i ni t ( ) { add( t ) ; s t ar t . addA i onLi s t ener ( new St ar t L( ) ) ; ct add( s t ar t ) ; onO f . addA i onLi s t ener ( new O f f L( ) ) ; f ct nO add( onO f ) ; f } cl as s St ar t L i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct i f ( s p == nul l ) s p = new Separ at eSubTas k( C ount er 2. t hi s ) ; } } cl as s O f f L i m em s A i onLi s t ener { nO pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct i f ( s p ! = nul l ) s p. i nver t Fl ag( ) ; } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C ount er 2 appl et = new C ount er 2( ) ; Fr am aFr am = new Fr am " C e e e( ount er 2" ) ; aFr am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); aFr am add( appl et , Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 300, 200) ; e. appl et . i ni t ( ) ; 491

appl et . s t ar t ( ) ; aFr am s et Vi s i bl e( t r ue) ; e. } } ///: ~ 现在,C ount er 2 变成了一个相当直接的程序,它的唯一任务就是设置并管理用户界面。但假若用户现在按下 St ar t 按钮,却不会真正调用一个方法。此时不是创建类的一个线程,而是创建 Separ at eSubTas k,然后继续 C ount er 2 事件循环。注意此时会保存 Separ at eSubTas k 的句柄,以便我们按下 onO f 按钮的时候,能正常地 f 切换位于 Separ at eSubTas k 内部的 r unFl ag(运行标志)。随后那个线程便可启动(当它看到标志的时 候),然后将自己中止(亦可将 Separ at eSubTas k 设为一个内部类来达到这一目的)。 Separ at eSubTas k 类是对 Thr ead的一个简单扩展,它带有一个构建器(其中保存了 C ount er 2 句柄,然后通 过调用 s t ar t ( ) 来运行线程)以及一个 r un( ) ——本质上包含了 C ount er 1. j ava 的 go( ) 内的代码。由于 Separ at eSubTas k 知道自己容纳了指向一个 C ount er 2 的句柄,所以能够在需要的时候介入,并访问 C unt er 2 o 的 Tes t Fi el d(文本字段)。 按下 onO f 按钮,几乎立即能得到正确的响应。当然,这个响应其实并不是“立即”发生的,它毕竟和那种 f 由“中断”驱动的系统不同。只有线程拥有 C PU的执行时间,并注意到标记已发生改变,计数器才会停止。 1. 用内部类改善代码 下面说说题外话,请大家注意一下 Separ at eSubTas k 和 C ount er 2 类之间发生的结合行为。Separ at eSub sk Ta 同C ount er 2 “亲密”地结合到了一起——它必须持有指向自己“父”C ount er 2 对象的一个句柄,以便自己能 回调和操纵它。但两个类并不是真的合并为单独一个类(尽管在下一节中,我们会讲到 Java 确实提供了合并 它们的方法),因为它们各自做的是不同的事情,而且是在不同的时间创建的。但不管怎样,它们依然紧密 地结合到一起(更准确地说,应该叫“联合”),所以使程序代码多少显得有些笨拙。在这种情况下,一个 内部类可以显著改善代码的“可读性”和执行效率: //: C ount er 2i . j ava // C ount er 2 us i ng an i nner cl as s f or t he t hr ead i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. appl et . * ; por publ i c cl as s C ount er 2i ext ends A et { ppl pr i vat e cl as s Separ at eSubTas k ext ends Thr ead { i nt count = 0; bool ean r unFl ag = t r ue; Separ at eSubTas k( ) { s t ar t ( ) ; } publ i c voi d r un( ) { w l e ( t r ue) { hi try { s l eep( 100) ; } cat ch ( I nt er r upt edExcept i on e) { } i f ( r unFl ag) t . s et Text ( I nt eger . t oSt r i ng( count ++) ) ; } } } pr i vat e Separ at eSubTas k s p = nul l ; pr i vat e Text Fi el d t = new Text Fi el d( 10) ; pr i vat e But t on onO f = new But t on( " Toggl e" ) , f s t ar t = new But t on( " St ar t " ) ; publ i c voi d i ni t ( ) { 492

add( t ) ; s t ar t . addA i onLi s t ener ( new St ar t L( ) ) ; ct add( s t ar t ) ; onO f . addA i onLi s t ener ( new O f f L( ) ) ; f ct nO add( onO f ) ; f } cl as s St ar t L i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct i f ( s p == nul l ) s p = new Separ at eSubTas k( ) ; } } cl as s O f f L i m em s A i onLi s t ener { nO pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct i f ( s p ! = nul l ) s p. r unFl ag = ! s p. r unFl ag; // i nver t Fl ag( ) ; } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C ount er 2i appl et = new C ount er 2i ( ) ; Fr am aFr am = new Fr am " C e e e( ount er 2i " ) ; aFr am a i ndow s t ener ( e. ddW Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); aFr am add( appl et , Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 300, 200) ; e. appl et . i ni t ( ) ; appl et . s t ar t ( ) ; aFr am s et Vi s i bl e( t r ue) ; e. } } ///: ~ 这个 Separ at eSubTas k 名字不会与前例中的 Separ at eSubTas k 冲突——即使它们都在相同的目录里——因为 它已作为一个内部类隐藏起来。大家亦可看到内部类被设为 pr i vat e(私有)属性,这意味着它的字段和方 法都可获得默认的访问权限(r un( ) 除外,它必须设为 publ i c,因为它在基础类中是公开的)。除 C ount er 2i 之外,其他任何方面都不可访问 pr i vat e内部类。而且由于两个类紧密结合在一起,所以很容易放宽它们之 间的访问限制。在 Separ at eSubTas k 中,我们可看到 i nver t Fl ag( )方法已被删去,因为 C ount er 2i 现在可以 直接访问 r unFl ag。 此外,注意 Separ at eSubTas k 的构建器已得到了简化——它现在唯一的用外就是启动线程。C ount er 2i 对象 的句柄仍象以前那样得以捕获,但不再是通过人工传递和引用外部对象来达到这一目的,此时的内部类机制 可以自动照料它。在 r un( ) 中,可看到对 t 的访问是直接进行的,似乎它是 Separ at eSubTas k 的一个字段。 父类中的 t 字段现在可以变成 pr i vat e,因为 Separ at eSubTas k 能在未获任何特殊许可的前提下自由地访问 它——而且无论如何都该尽可能地把字段变成“私有”属性,以防来自类外的某种力量不慎地改变它们。 无论在什么时候,只要注意到类相互之间结合得比较紧密,就可考虑利用内部类来改善代码的编写与维护。

1 4 . 1 . 3 用主类合并线程
在上面的例子中,我们看到线程类(Thr ead)与程序的主类(M n)是分隔开的。这样做非常合理,而且易 ai 于理解。然而,还有另一种方式也是经常要用到的。尽管它不十分明确,但一般都要更简洁一些(这也解释 了它为什么十分流行)。通过将主程序类变成一个线程,这种形式可将主程序类与线程类合并到一起。由于 493

对一个 G 程序来说,主程序类必须从 Fr am 或 A et 继承,所以必须用一个接口加入额外的功能。这个接 UI e ppl 口叫作 Runnabl e,其中包含了与 Thr ead一致的基本方法。事实上,Thr ead也实现了 Runnabl e,它只指出有 一个 r un( ) 方法。 对合并后的程序/线程来说,它的用法不是十分明确。当我们启动程序时,会创建一个 Runnabl e(可运行 的)对象,但不会自行启动线程。线程的启动必须明确进行。下面这个程序向我们演示了这一点,它再现了 C ount er 2 的功能: //: C ount er 3. j ava // Us i ng t he Runnabl e i nt er f ace t o t ur n t he // m n cl as s i nt o a t hr ead. ai i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. appl et . * ; por publ i c cl as s C ount er 3 ext ends A et i m em s Runnabl e { ppl pl ent pr i vat e i nt count = 0; pr i vat e bool ean r unFl ag = t r ue; pr i vat e Thr ead s el f Thr ead = nul l ; pr i vat e But t on onO f = new But t on( " Toggl e" ) , f s t ar t = new But t on( " St ar t " ) ; pr i vat e Text Fi el d t = new Text Fi el d( 10) ; publ i c voi d i ni t ( ) { add( t ) ; s t ar t . addA i onLi s t ener ( new St ar t L( ) ) ; ct add( s t ar t ) ; onO f . addA i onLi s t ener ( new O f f L( ) ) ; f ct nO add( onO f ) ; f } publ i c voi d r un( ) { w l e ( t r ue) { hi try { s el f Thr ead. s l eep( 100) ; } cat ch ( I nt er r upt edExcept i on e) { } i f ( r unFl ag) t . s et Text ( I nt eger . t oSt r i ng( count ++) ) ; } } cl as s St ar t L i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct i f ( s el f Thr ead == nul l ) { s el f Thr ead = new Thr ead( C ount er 3. t hi s ) ; s el f Thr ead. s t ar t ( ) ; } } } cl as s O f f L i m em s A i onLi s t ener { nO pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct r unFl ag = ! r unFl ag; } } 494

publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C ount er 3 appl et = new C ount er 3( ) ; Fr am aFr am = new Fr am C e e e(" ount er 3" ) ; aFr am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); aFr am add( appl et , Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 300, 200) ; e. appl et . i ni t ( ) ; appl et . s t ar t ( ) ; aFr am s et Vi s i bl e( t r ue) ; e. } } ///: ~ 现在 r un( ) 位于类内,但它在 i ni t ( ) 结束以后仍处在“睡眠”状态。若按下启动按钮,线程便会用多少有些 暧昧的表达方式创建(若线程尚不存在): new Thr ead( C ount er 3. t hi s ) ; 若某样东西有一个 Runnabl e 接口,实际只是意味着它有一个 r un( ) 方法,但不存在与之相关的任何特殊东 西——它不具有任何天生的线程处理能力,这与那些从 Thr ead继承的类是不同的。所以为了从一个 Runnabl e 对象产生线程,必须单独创建一个线程,并为其传递 Runnabl e 对象;可为其使用一个特殊的构建 器,并令其采用一个 Runnabl e 作为自己的参数使用。随后便可为那个线程调用 s t ar t ( ),如下所示: s el f Thr ead. s t ar t ( ) ; 它的作用是执行常规初始化操作,然后调用 r un( ) 。 Runnabl e 接口最大的一个优点是所有东西都从属于相同的类。若需访问什么东西,只需简单地访问它即可, 不需要涉及一个独立的对象。但为这种便利也是要付出代价的——只可为那个特定的对象运行单独一个线程 (尽管可创建那种类型的多个对象,或者在不同的类里创建其他对象)。 注意 Runnabl e 接口本身并不是造成这一限制的罪魁祸首。它是由于 Runnabl e 与我们的主类合并造成的,因 为每个应用只能主类的一个对象。

1 4 . 1 . 4 制作多个线程
现在考虑一下创建多个不同的线程的问题。我们不可用前面的例子来做到这一点,所以必须倒退回去,利用 从 Thr ead继承的多个独立类来封装 r un( ) 。但这是一种更常规的方案,而且更易理解,所以尽管前例揭示了 我们经常都能看到的编码样式,但并不推荐在大多数情况下都那样做,因为它只是稍微复杂一些,而且灵活 性稍低一些。 下面这个例子用计数器和切换按钮再现了前面的编码样式。但这一次,一个特定计数器的所有信息(按钮和 文本字段)都位于它自己的、从 Thr ead继承的对象内。Ti cker 中的所有字段都具有 pr i vat e(私有)属性, 这意味着 Ti cker 的具体实现方案可根据实际情况任意修改,其中包括修改用于获取和显示信息的数据组件的 数量及类型。创建好一个 Ti cker 对象以后,构建器便请求一个 A T 容器(C ai ner )的句柄——Ti cker 用 W ont 自己的可视组件填充那个容器。采用这种方式,以后一旦改变了可视组件,使用 Ti cker 的代码便不需要另行 修改一道。 //: C ount er 4. j ava // I f you s epar at e your t hr ead f r om t he m n ai // cl as s , you can have as m any t hr eads as you // w . ant i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. appl et . * ; por

495

cl as s Ti cker ext ends Thr ead { pr i vat e But t on b = new But t on( " Toggl e" ) ; pr i vat e Text Fi el d t = new Text Fi e d( 10) ; l pr i vat e i nt count = 0; pr i vat e bool ean r unFl ag = t r ue; publ i c Ti cker ( C ai ner c) { ont b. addA i onLi s t ener ( new Toggl eL( ) ) ; ct Panel p = new Panel ( ) ; p. add( t ) ; p. add( b) ; c. add( p) ; } cl as s Toggl eL i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct r unFl ag = ! r unFl ag; } } publ i c voi d r un( ) { w l e ( t r ue) { hi i f ( r unFl ag) t . s et Text ( I nt eger . t oSt r i ng( count ++) ) ; try { s l eep( 100) ; } cat ch ( I nt er r upt edExcept i o e) { } n } } } publ i c cl as s C ount er 4 ext ends A et { ppl pr i vat e But t on s t ar t = new But t on( " St ar t " ) ; pr i vat e bool ean s t ar t ed = f al s e; pr i vat e Ti cker [ ] s ; pr i vat e bool ean i s A et = t r ue; ppl pr i vat e i nt s i ze; publ i c voi d i ni t ( ) { // G par am er " s i z e" f r om W page: et et eb i f ( i s A et ) ppl s i ze = I nt eger . par s eI nt ( get Par am er ( " s i ze" ) ) ; et s = new Ti cker [ s i z e] ; f or ( i nt i = 0; i < s . l engt h; i ++) s [ i ] = new Ti cker ( t hi s ) ; s t ar t . addA i onLi s t ener ( new St ar t L( ) ) ; ct add( s t ar t ) ; } cl as s St ar t L i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct i f ( ! s t ar t ed) { s t ar t ed = t r ue; f or ( i nt i = 0; i < s . l engt h; i ++) s [ i ] . s t ar t ( ) ; } 496

} } publ i c s t at i c voi d m n( S t r i ng[ ] ar gs ) { ai C ount er 4 appl et = new C ount er 4( ) ; // Thi s i s n' t an appl et , s o s et t he f l ag and // pr oduce t he par am er val ues f r om ar gs : et appl et . i s A et = f al s e; ppl appl et . s i ze = ( ar gs . l engt h == 0 ? 5 : I nt eger . par s eI nt ( ar gs [ 0] ) ) ; Fr am aFr am = new Fr am " C e e e( ount er 4" ) ; aFr am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); aFr am add( appl et , Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 200, appl et . s i ze * 50) ; e. appl et . i ni t ( ) ; appl et . s t ar t ( ) ; aFr am s et Vi s i bl e( t r ue) ; e. } } ///: ~ Ti cker 不仅包括了自己的线程处理机制,也提供了控制与显示线程的工具。可按自己的意愿创建任意数量的 线程,毋需明确地创建窗口化组件。 在C ount er 4 中,有一个名为 s 的 Ti cker 对象的数组。为获得最大的灵活性,这个数组的长度是用程序片参 数接触 W eb页而初始化的。下面是网页中长度参数大致的样子,它们嵌于对程序片(appl et )的描述内容 中: i e=s 其中,par am ,nam 和 val ue是所有 W e eb页都适用的关键字。nam 是指程序中对参数的一种引用称谓,va ue e l 可以是任何字串(并不仅仅是解析成一个数字的东西)。 我们注意到对数组 s 长度的判断是在 i ni t ( ) 内部完成的,它没有作为 s 的内嵌定义的一部分提供。换言之, 不可将下述代码作为类定义的一部分使用(应该位于任何方法的外部): i ns t s i ze = I nt eger . par s eI nt ( get Par am er ( " Si ze" ) ) ; et Ti cker [ ] s = new Ti cker [ s i ze] 可把它编译出来,但会在运行期得到一个空指针违例。但若将 get Par am er ( ) 初始化移入 i ni t ( ),则可正常 et 工作。程序片框架会进行必要的启动工作,以便在进入 i ni t ( ) 前收集好一些参数。 此外,上述代码被同时设置成一个程序片和一个应用(程序)。在它是应用程序的情况下,s i ze 参数可从命 令行里提取出来(否则就提供一个默认的值)。 数组的长度建好以后,就可以创建新的 Ti cker 对象;作为 Ti cker 构建器的一部分,用于每个 Ti cker 的按钮 和文本字段就会加入程序片。 按下 St ar t 按钮后,会在整个 Ti cker 数组里遍历,并为每个 Ti cker 调用 s t ar t ( ) 。记住,s t ar t ( ) 会进行必 要的线程初始化工作,然后为那个线程调用 r un( ) 。 T oggl eL 监视器只是简单地切换 Ti cker 中的标记,一旦对应线程以后需要修改这个标记,它会作出相应的反 应。 这个例子的一个好处是它使我们能够方便地创建由单独子任务构成的大型集合,并以监视它们的行为。在这 种情况下,我们会发现随着子任务数量的增多,机器显示出来的数字可能会出现更大的分歧,这是由于为线 程提供服务的方式造成的。 497

亦可试着体验一下 s l eep( 100) 在 Ti cker . r un( )中的重要作用。若删除 s l eep( ),那么在按下一个切换按钮 前,情况仍然会进展良好。按下按钮以后,那个特定的线程就会出现一个失败的 r unFl ag,而且 r un( ) 会深深 地陷入一个无限循环——很难在多任务处理期间中止退出。因此,程序对用户操作的反应灵敏度会大幅度降 低。

1 4 . 1 . 5 Daem 线程 on
“D on”线程的作用是在程序的运行期间于后台提供一种“常规”服务,但它并不属于程序的一个基本部 aem 分。因此,一旦所有非 D on线程完成,程序也会中止运行。相反,假若有任何非 D on线程仍在运行 aem aem (比如还有一个正在运行 m n( ) 的线程),则程序的运行不会中止。 ai 通过调用 i s D on( ) ,可调查一个线程是不是一个 D on,而且能用 s et D on( ) 打开或者关闭一个线程的 aem aem aem D on状态。如果是一个 D on 线程,那么它创建的任何线程也会自动具备 D on属性。 aem aem aem 下面这个例子演示了 D on 线程的用法: aem //: D ons . j ava aem // D oni c behavi or aem i m t j ava. i o. * ; por cl as s D on ext ends Thr ead { aem pr i vat e s t at i c f i nal i nt SI ZE = 10; pr i vat e Thr ead[ ] t = new Thr ead[ SI ZE] ; publ i c D on( ) { aem s et D on( t r ue) ; aem s t ar t ( ) ; } publ i c voi d r un( ) { f or ( i nt i = 0; i < SI ZE; i ++) t [ i ] = new D onSpaw i ) ; aem n( f or ( i nt i = 0; i < SI ZE; i ++) Sys t em out . pr i nt l n( . " t [ " + i + " ] . i s D on( ) = " aem + t [ i ] . i s D on( ) ) ; aem w l e( t r ue) hi yi el d( ) ; } } cl as s D onSpaw ext ends Thr ead { aem n publ i c D onSpaw i nt i ) { aem n( Sys t em out . pr i nt l n( . " D onSpaw " + i + " s t ar t ed" ) ; aem n s t ar t ( ) ; } publ i c voi d r un( ) { w l e( t r ue) hi yi el d( ) ; } } publ i c cl as s D ons { aem publ i c s t at i c voi d m n( St r i ng[ ] ar g ) { ai s Thr ead d = new D on( ) ; aem Sys t em out . pr i nt l n( . 498

" d. i s D on( ) = " + d. i s D on( ) ) ; aem aem // A l ow t he daem t hr eads t o f i ni s h l on // t hei r s t ar t up pr oces s es : Buf f er edReader s t di n = new Buf f er edReader ( new I nput St r eam Reader (Sys t em i n) ) ; . Sys t em out . pr i nt l n( " W t i ng f or C ) ; . ai R" try { s t di n. r eadLi ne( ) ; } cat ch( I O Except i on e) { } } } ///: ~ D on线程可将自己的 D on 标记设置成“真”,然后产生一系列其他线程,而且认为它们也具有 D on aem aem aem 属性。随后,它进入一个无限循环,在其中调用 yi el d( ) ,放弃对其他进程的控制。在这个程序早期的一个 版本中,无限循环会使 i nt 计数器增值,但会使整个程序都好象陷入停顿状态。换用 yi el d( )后,却可使程 序充满“活力”,不会使人产生停滞或反应迟钝的感觉。 一旦 m n( ) 完成自己的工作,便没有什么能阻止程序中断运行,因为这里运行的只有 D on 线程。所以能 ai aem 看到启动所有 D on 线程后显示出来的结果,Sys t em i n 也进行了相应的设置,使程序中断前能等待一个回 aem . 车。如果不进行这样的设置,就只能看到创建 D on线程的一部分结果(试试将 r eadLi ne( ) 代码换成不同 aem 长度的 s l eep( ) 调用,看看会有什么表现)。

14. 2 共享有限的资源
可将单线程程序想象成一种孤立的实体,它能遍历我们的问题空间,而且一次只能做一件事情。由于只有一 个实体,所以永远不必担心会有两个实体同时试图使用相同的资源,就象两个人同时都想停到一个车位,同 时都想通过一扇门,甚至同时发话。 进入多线程环境后,它们则再也不是孤立的。可能会有两个甚至更多的线程试图同时同一个有限的资源。必 须对这种潜在资源冲突进行预防,否则就可能发生两个线程同时访问一个银行帐号,打印到同一台计算机, 以及对同一个值进行调整等等。

1 4 . 2 . 1 资源访问的错误方法
现在考虑换成另一种方式来使用本章频繁见到的计数器。在下面的例子中,每个线程都包含了两个计数器, 它们在 r un( ) 里增值以及显示。除此以外,我们使用了 W cher 类的另一个线程。它的作用是监视计数器, at 检查它们是否保持相等。这表面是一项无意义的行动,因为如果查看代码,就会发现计数器肯定是相同的。 但实际情况却不一定如此。下面是程序的第一个版本: //: Shar i ng1. j ava // Pr obl em w t h r es our ce s har i ng w l e t hr eadi ng s i hi i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. appl et . * ; por cl as s Tw ount er ext ends Thr ead { oC pr i vat e bool ean s t ar t ed = f al s e; pr i vat e Text Fi el d t 1 = new Text Fi el d( 5) , t 2 = new T ext Fi el d( 5) ; pr i vat e Label l = new Label ( " count 1 == count 2" ) ; pr i vat e i nt count 1 = 0, count 2 = 0; // A t he di s pl ay com dd ponent s as a panel 499

// t o t he gi ven cont ai ner : publ i c Tw ount er ( C ai ner c) { oC ont Panel p = new Panel ( ) ; p. add( t 1) ; p. add( t 2) ; p. add( l ) ; c. add( p) ; } publ i c voi d s t ar t ( ) { i f ( ! s t ar t ed) { s t ar t ed = t r ue; s uper . s t ar t ( ) ; } } publ i c voi d r un( ) { w l e ( t r ue) { hi t 1. s et Text ( I nt eger . t oSt r i ng( count 1++) ) ; t 2. s et Text ( I nt eger . t oSt r i ng( count 2++) ) ; try { s l eep( 500) ; } cat ch ( I nt er r upt edExcept i on e) { } } } publ i c voi d s ynchTes t ( ) { Shar i ng1. i ncr em A ent cces s ( ) ; i f ( count 1 ! = count 2) l . s et Text ( " Uns ynched" ) ; } } cl as s W cher ext ends Thr ead { at pr i vat e Shar i ng1 p; publ i c W cher ( Shar i ng1 p) { at t hi s . p = p; s t ar t ( ) ; } publ i c voi d r un( ) { w l e( t r ue) { hi f or ( i nt i = 0; i < p. s . l engt h; i ++) p. s [ i ] . s ynchTes t ( ) ; try { s l eep( 500) ; } cat ch ( I nt er r upt edExcept i on e) { } } } } publ i c cl as s Shar i ng1 ext ends A et { ppl Tw ount er [ ] s ; oC pr i vat e s t at i c i nt acces s C ount = 0; pr i vat e s t at i c Text Fi el d aC ount = new Text Fi el d( " 0" , 10) ; 500

publ i c s t at i c voi d i ncr em A ent cces s ( ) { acces s C ount ++; aC ount . s et Text ( I nt eger . t oSt r i ng( acces s C ount ) ) ; } pr i vat e But t on s t ar t = new But t on( " St ar t " ) , obs er ver = new But t on( " O er ve" ) ; bs pr i vat e bool ean i s A et = t r ue; ppl pr i vat e i nt num ount er s = 0; C pr i vat e i nt num bs er ver s = 0; O publ i c voi d i ni t ( ) { i f ( i s A et ) { ppl num ount er s = C I nt eger . par s eI nt ( get Par am er ( " s i ze" ) ) ; et num bs er ver s = O I nt eger . par s eI nt ( get Par am er ( " obs er ver s " ) ) ; et } s = new Tw ount er [ num ount er s ] ; oC C f or ( i nt i = 0; i < s . l engt h; i ++) s [ i ] = new Tw ount er ( t hi s ) ; oC Panel p = new Panel ( ) ; s t ar t . addA i onLi s t ener ( new St ar t L( ) ) ; ct p. add( s t ar t ) ; obs er ver . addA i onLi s t ener ( new O er ver L( ) ) ; ct bs p. add( obs er ver ) ; p. add( new Label ( " A cces s C ount " ) ) ; p. add( aC ount ) ; add( p) ; } cl as s St ar t L i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct f or ( i nt i = 0; i < s . l engt h; i ++) s [ i ] . s t ar t ( ) ; } } cl as s O er ver L i m em s A i onLi s t ener { bs pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct f or ( i nt i = 0; i < num bs er ver s ; i ++) O new W cher ( Shar i ng1. t hi s ) ; at } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Shar i ng1 appl et = new Shar i ng1( ) ; // Thi s i s n' t an appl et , s o s et t he f l ag and // pr oduce t he par am er val ues f r om ar gs : et appl et . i s A et = f al s e; ppl appl et . num ount er s = C ( ar gs . l engt h == 0 ? 5 : I nt eger . par s eI nt ( ar gs [ 0] ) ) ; appl et . num bs er ver s = O ( ar gs . l engt h < 2 ? 5 : 501

I nt eger . par s eI nt (ar gs [ 1] ) ) ; Fr am aFr am = new Fr am " Shar i ng1" ) ; e e e( aFr am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); aFr am add( appl et , Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 350, appl et . num ount er s *100) ; e. C appl et . i ni t ( ) ; appl et . s t ar t ( ) ; aFr am s et Vi s i bl e( t r ue) ; e. } } ///: ~ 和往常一样,每个计数器都包含了自己的显示组件:两个文本字段以及一个标签。根据它们的初始值,可知 道计数是相同的。这些组件在 Tw ount er 构建器加入 C ai ner 。由于这个线程是通过用户的一个“按下按 oC ont 钮”操作启动的,所以 s t ar t ( )可能被多次调用。但对一个线程来说,对 Thr ead. s t ar t ( ) 的多次调用是非法 的(会产生违例)。在 s t ar t ed标记和过载的 s t ar t ( ) 方法中,大家可看到针对这一情况采取的防范措施。 在 r un( ) 中,count 1 和 count 2 的增值与显示方式表面上似乎能保持它们完全一致。随后会调用 s l eep( );若 没有这个调用,程序便会出错,因为那会造成 C PU难于交换任务。 s ynchTes t ( )方法采取的似乎是没有意义的行动,它检查 count 1 是否等于 count 2;如果不等,就把标签设为 “Uns ynched”(不同步)。但是首先,它调用的是类 Shar i ng1 的一个静态成员,以便增值和显示一个访问 计数器,指出这种检查已成功进行了多少次(这样做的理由会在本例的其他版本中变得非常明显)。 W cher 类是一个线程,它的作用是为处于活动状态的所有 Tw ount er 对象都调用 s ynchTes t ( )。其间,它 at oC 会对 Shar i ng1 对象中容纳的数组进行遍历。可将 W cher 想象成它掠过 Tw ount er 对象的肩膀不断地“偷 at oC 看”。 Shar i ng1 包含了 Tw ount er 对象的一个数组,它通过 i ni t ( )进行初始化,并在我们按下“s t ar t ”按钮后作 oC 为线程启动。以后若按下“O er ve”(观察)按钮,就会创建一个或者多个观察器,并对毫不设防的 bs Tw ount er 进行调查。 oC 注意为了让它作为一个程序片在浏览器中运行,W eb页需要包含下面这几行: i e=s 可自行改变宽度、高度以及参数,根据自己的意愿进行试验。若改变了 s i ze 和 obs er ver s ,程序的行为也会 发生变化。我们也注意到,通过从命令行接受参数(或者使用默认值),它被设计成作为一个独立的应用程 序运行。 下面才是最让人“不可思议”的。在 Tw ount er . r un( ) 中,无限循环只是不断地重复相邻的行: oC t 1. s et Text ( I nt eger . t oSt r i ng( count 1++) ) ; t 2. s et Text ( I nt eger . t oSt r i ng( count 2++) ) ; (和“睡眠”一样,不过在这里并不重要)。但在程序运行的时候,你会发现 count 1 和 count 2 被“观察” (用 W cher 观察)的次数是不相等的!这是由线程的本质造成的——它们可在任何时候挂起(暂停)。所 at 以在上述两行的执行时刻之间,有时会出现执行暂停现象。同时,W cher 线程也正好跟随着进来,并正好 at 在这个时候进行比较,造成计数器出现不相等的情况。 本例揭示了使用线程时一个非常基本的问题。我们跟无从知道一个线程什么时候运行。想象自己坐在一张桌 子前面,桌上放有一把叉子,准备叉起自己的最后一块食物。当叉子要碰到食物时,食物却突然消失了(因 为这个线程已被挂起,同时另一个线程进来“偷”走了食物)。这便是我们要解决的问题。 有的时候,我们并不介意一个资源在尝试使用它的时候是否正被访问(食物在另一些盘子里)。但为了让多 502

线程机制能够正常运转,需要采取一些措施来防止两个线程访问相同的资源——至少在关键的时期。 为防止出现这样的冲突,只需在线程使用一个资源时为其加锁即可。访问资源的第一个线程会其加上锁以 后,其他线程便不能再使用那个资源,除非被解锁。如果车子的前座是有限的资源,高喊“这是我的!”的 孩子会主张把它锁起来。

1 4 . 2 . 2 J av a 如何共享资源
对一种特殊的资源——对象中的内存——Java 提供了内建的机制来防止它们的冲突。由于我们通常将数据元 素设为从属于 pr i vat e(私有)类,然后只通过方法访问那些内存,所以只需将一个特定的方法设为 s ynchr oni z ed (同步的),便可有效地防止冲突。在任何时刻,只可有一个线程调用特定对象的一个 s ynchr oni z ed方法(尽管那个线程可以调用多个对象的同步方法)。下面列出简单的 s ynchr oni z ed方法: s ynchr oni zed voi d f ( ) { /* . . . */ } s ynchr oni zed voi d g( ) { /* . . . */ } 每个对象都包含了一把锁(也叫作“监视器”),它自动成为对象的一部分(不必为此写任何特殊的代 码)。调用任何 s ynchr oni z ed方法时,对象就会被锁定,不可再调用那个对象的其他任何 s ynchr oni z ed方 法,除非第一个方法完成了自己的工作,并解除锁定。在上面的例子中,如果为一个对象调用 f ( ),便不能 再为同样的对象调用 g( ),除非 f ( )完成并解除锁定。因此,一个特定对象的所有 s ynchr oni z ed方法都共享 着一把锁,而且这把锁能防止多个方法对通用内存同时进行写操作(比如同时有多个线程)。 每个类也有自己的一把锁(作为类的 C as s 对象的一部分),所以 s ynchr oni zed s t at i c 方法可在一个类的 l 范围内被相互间锁定起来,防止与 s t at i c数据的接触。 注意如果想保护其他某些资源不被多个线程同时访问,可以强制通过 s ynchr oni z ed方访问那些资源。 1. 计数器的同步 装备了这个新关键字后,我们能够采取的方案就更灵活了:可以只为 Tw ount er 中的方法简单地使用 oC s ynchr oni z ed关键字。下面这个例子是对前例的改版,其中加入了新的关键字: //: Shar i ng2. j ava // Us i ng t he s ynchr oni zed keyw d t o pr event or // m t i pl e acces s t o a par t i cul ar r es our ce. ul i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. appl et . * ; por cl as s Tw ount er 2 ext ends Thr ead { oC pr i vat e bool ean s t ar t ed = f al s e; pr i vat e Text Fi el d t 1 = new T ext Fi el d( 5) , t 2 = new T ext Fi el d( 5) ; pr i vat e Label l = new Label ( " count 1 == count 2" ) ; pr i vat e i nt count 1 = 0, count 2 = 0; publ i c Tw ount er 2( C ai ner c) { oC ont Panel p = new Panel ( ) ; p. add( t 1) ; p. add( t 2) ; p. add( l ) ; c. add( p) ; } publ i c voi d s t ar t ( ) { i f ( ! s t ar t ed) { s t ar t ed = t r ue; s uper . s t ar t ( ) ; } 503

} publ i c s ynchr oni zed voi d r un( ) { w l e ( t r ue) { hi t 1. s et Text ( I nt eger . t oSt r i ng( count 1++) ) ; t 2. s et Text ( I nt eger . t oSt r i ng( count 2++) ) ; try { s l eep( 500) ; } cat ch ( I nt er r upt edExcept i on e) { } } } publ i c s ynchr oni zed voi d s ynchTes t ( ) { Shar i ng2. i ncr em A ent cces s ( ) ; i f ( count 1 ! = count 2) l . s et Text ( " Uns ynched" ) ; } } cl as s W cher 2 ext ends Thr ead { at pr i vat e Shar i ng2 p; publ i c W cher 2( Shar i ng2 p) { at t hi s . p = p; s t ar t ( ) ; } publ i c voi d r un( ) { w l e( t r ue) { hi f or ( i nt i = 0; i < p. s . l engt h; i ++) p. s [ i ] . s ynchTes t ( ) ; try { s l eep( 500) ; } cat ch ( I nt er r upt edExcept i on e) { } } } } publ i c cl as s Shar i ng2 ext ends A et { ppl Tw ount er 2[ ] s ; oC pr i vat e s t at i c i nt acces s C ount = 0; pr i vat e s t at i c Text Fi el d aC ount = new Text Fi el d( " 0" , 10) ; publ i c s t at i c voi d i ncr em A ent cces s ( ) { acces s C ount ++; aC ount . s et Text ( I nt eger . t oSt r i ng( acc s C es ount ) ) ; } pr i vat e But t on s t ar t = new But t on( " St ar t " ) , obs er ver = new But t on( " O er ve" ) ; bs pr i vat e bool ean i s A et = t r ue; ppl pr i vat e i nt num ount er s = 0; C pr i vat e i nt num bs er ver s = 0; O publ i c voi d i ni t ( ) { i f ( i s A et ) { ppl num o er s = C unt 504

I nt eger . par s eI nt ( get Par am er ( " s i ze" ) ) ; et num bs er ver s = O I nt eger . par s eI nt ( get Par am er ( " obs er ver s " ) ) ; et } s = new Tw ount er 2[ num ount er s ] ; oC C f or ( i nt i = 0; i < s . l engt h; i ++) s [ i ] = new Tw ount er 2( t hi s ) ; oC Panel p = new Panel ( ) ; s t ar t . addA i onLi s t ener ( new St ar t L( ) ) ; ct p. add( s t ar t ) ; obs er ver . addA i onLi s t ener ( new O er ver L( ) ) ; ct bs p. add( obs er ver ) ; p. add( new Label ( " A cces s C ount " ) ) ; p. add( aC ount ) ; add( p) ; } cl as s St ar t L i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct f or ( i nt i = 0; i < s . l engt h; i ++) s [ i ] . s t ar t ( ) ; } } cl as s O er ver L i m em s A i onLi s t ener { bs pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct f or ( i nt i = 0; i < num bs er ver s ; i ++) O new W cher 2( Shar i ng2. t hi s ) ; at } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Shar i ng2 appl et = new Shar i ng2( ) ; // Thi s i s n' t an appl et , s o s et t he f l ag and // pr oduce t he par am er val ues f r om ar gs : et appl et . i s A et = f al s e; ppl appl et . num ount er s = C ( ar gs . l engt h == 0 ? 5 : I nt eger . par s eI nt ( ar gs [ 0] ) ) ; appl et . num bs er ver s = O ( ar gs . l engt h < 2 ? 5 : I nt eger . par s eI nt ( ar gs [ 1] ) ) ; Fr am aFr am = new Fr am " Shar i ng2" ) ; e e e( aFr am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); aFr am add( appl et , Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 350, appl et . num ount er s *100) ; e. C appl et . i ni t ( ) ; appl et . s t ar t ( ) ; aFr am s et Vi s i bl e( t r ue) ; e. 505

} } ///: ~ 我们注意到无论 r un( ) 还是 s ynchTes t ( )都是“同步的”。如果只同步其中的一个方法,那么另一个就可以自 由忽视对象的锁定,并可无碍地调用。所以必须记住一个重要的规则:对于访问某个关键共享资源的所有方 法,都必须把它们设为 s ynchr oni z ed ,否则就不能正常地工作。 现在又遇到了一个新问题。W cher 2 永远都不能看到正在进行的事情,因为整个 r un( ) 方法已设为“同 at 步”。而且由于肯定要为每个对象运行 r un( ) ,所以锁永远不能打开,而 s ynchTes t ( )永远不会得到调用。之 所以能看到这一结果,是因为 acces s C ount 根本没有变化。 为解决这个问题,我们能采取的一个办法是只将 r un( ) 中的一部分代码隔离出来。想用这个办法隔离出来的 那部分代码叫作“关键区域”,而且要用不同的方式来使用 s ynchr oni z ed关键字,以设置一个关键区域。 Java 通过“同步块”提供对关键区域的支持;这一次,我们用 s ynchr oni z ed关键字指出对象的锁用于对其 中封闭的代码进行同步。如下所示: s ynchr oni zed( s yncO ect ) { bj // Thi s code can be acces s ed by onl y // one t hr ead at a t i m as s um ng al l e, i // t hr eads r es pect s yncO ect ' s l ock bj } 在能进入同步块之前,必须在 s ynchO ect 上取得锁。如果已有其他线程取得了这把锁,块便不能进入,必 bj 须等候那把锁被释放。 可从整个 r un( ) 中删除 s ynchr oni z ed关键字,换成用一个同步块包围两个关键行,从而完成对 Shar i ng2 例 子的修改。但什么对象应作为锁来使用呢?那个对象已由 s ynchTes t ( )标记出来了——也就是当前对象 (t hi s )!所以修改过的 r un( ) 方法象下面这个样子: publ i c voi d r un( ) { w l e ( t r ue) { hi s ynchr oni zed( t hi s ) { t 1. s et Text ( I nt eger . t oSt r i ng( count 1++) ) ; t 2. s et Text ( I nt eger . t oSt r i ng( count 2++) ) ; } try { s l eep( 500) ; } cat ch ( I nt er r upt edExcept i on e) { } } } 这是必须对 Shar i ng2. j ava 作出的唯一修改,我们会看到尽管两个计数器永远不会脱离同步(取决于允许 W cher 什么时候检查它们),但在 r un( ) 执行期间,仍然向 W cher 提供了足够的访问权限。 at at 当然,所有同步都取决于程序员是否勤奋:要访问共享资源的每一部分代码都必须封装到一个适当的同步块 里。 2. 同步的效率 由于要为同样的数据编写两个方法,所以无论如何都不会给人留下效率很高的印象。看来似乎更好的一种做 法是将所有方法都设为自动同步,并完全消除 s ynchr oni z ed关键字(当然,含有 s ynchr oni zed r un( ) 的例 子显示出这样做是很不通的)。但它也揭示出获取一把锁并非一种“廉价”方案——为一次方法调用付出的 代价(进入和退出方法,不执行方法主体)至少要累加到四倍,而且根据我们的具体现方案,这一代价还有 可能变得更高。所以假如已知一个方法不会造成冲突,最明智的做法便是撤消其中的 s ynchr oni z ed关键字。

1 4 . 2 . 3 回顾 J av a Beans
我们现在已理解了同步,接着可换从另一个角度来考察 Java Beans 。无论什么时候创建了一个 Bean,就必须 假定它要在一个多线程的环境中运行。这意味着: 506

( 1) 只要可行,Bean 的所有公共方法都应同步。当然,这也带来了“同步”在运行期间的开销。若特别在意 这个问题,在关键区域中不会造成问题的方法就可保留为“不同步”,但注意这通常都不是十分容易判断。 有资格的方法倾向于规模很小(如下例的 get C r cl eSi ze( ) )以及/或者“微小”。也就是说,这个方法调 i 用在如此少的代码片里执行,以至于在执行期间对象不能改变。如果将这种方法设为“不同步”,可能对程 序的执行速度不会有明显的影响。可能也将一个 Bean的所有 publ i c方法都设为 s ynchr oni z ed,并只有在保 证特别必要、而且会造成一个差异的情况下,才将 s ynchr oni z ed关键字删去。 ( 2) 如果将一个多造型事件送给一系列对那个事件感兴趣的“听众”,必须假在列表中移动的时候可以添加 或者删除。 第一点很容易处理,但第二点需要考虑更多的东西。让我们以前一章提供的 BangBean. j ava 为例。在那个例 子中,我们忽略了 s ynchr oni z ed关键字(那时还没有引入呢),并将造型设为单造型,从而回避了多线程的 问题。在下面这个修改过的版本中,我们使其能在多线程环境中工作,并为事件采用了多造型技术: //: BangBean2. j ava // You s houl d w i t e your Beans t hi s w s o t hey r ay // can r un i n a m t i t hr eaded envi r onm . ul ent i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. ut i l . * ; por i m t j ava. i o. * ; por publ i c cl as s BangBean2 ext ends C anvas i m em s Ser i al i zabl e { pl ent pr i vat e i nt xm ym , ; pr i vat e i nt cSi ze = 20; // C r cl e s i ze i pr i vat e St r i ng t ext = " Bang! " ; pr i vat e i nt f ont Si ze = 48; pr i vat e C or t C or = C or . r ed; ol ol ol pr i vat e Vect or act i onLi s t ener s = new Vect or ( ) ; publ i c BangBean2( ) { addM eLi s t ener ( new M ) ) ; ous L( addM eM i onLi s t ener ( new M ( ) ) ; ous ot M } publ i c s ynchr oni zed i nt get C r cl eSi ze( ) { i r et ur n cSi ze; } publ i c s ynchr oni zed voi d s et C r cl eSi ze( i nt new ze) { i Si cSi z e = new z e; Si } publ i c s ynchr oni zed St r i ng get BangText ( ) { r et ur n t ext ; } publ i c s ynchr oni zed voi d s et BangText ( St r i ng new Text ) { t ext = new Text ; } publ i c s ynchr oni zed i nt get Font Si ze( ) { r et ur n f ont Si ze; } publ i c s ynchr oni zed voi d s et Font Si ze( i nt new ze) { Si 507

f ont Si ze = new ze; Si } publ i c s ynchr oni zed C or get Text C or ( ) { ol ol r et ur n t C or ; ol } publ i c s ynchr oni zed voi d s et Text C or ( C or new ol or ) { ol ol C t C or = new ol or ; ol C } publ i c voi d pai nt ( G aphi cs g) { r g. s et C or ( C or . bl ack) ; ol ol g. dr aw val ( xm - cSi ze/2, ym - cSi ze/2, O cSi ze, cSi ze) ; } // Thi s i s a m t i cas t l i s t ener , w ch i s ul hi // m e t ypi cal l y us ed t han t he uni cas t or // appr oach t aken i n BangBean. j ava: publ i c s ynchr oni zed voi d addA i onLi s t ener ( ct A i onLi s t ener l ) { ct act i onLi s t ener s . addEl em ( l ) ; ent } publ i c s ynchr oni zed voi d r em oveA i onLi s t ener ( ct A i onLi s t ener l ) { ct act i onLi s t ener s . r em oveEl em ( l ) ; ent } // N i ce t hi s i s n' t s ynchr oni zed: ot publ i c voi d not i f yLi s t ener s ( ) { A i onEvent a = ct new A i onEvent ( BangBean2. t hi s , ct A i onEvent . A TI O _PERFO ED nul l ) ; ct C N RM , Vect or l v = nul l ; // M ake a copy of t he vect or i n cas e s om eone // adds a l i s t ener w l e w r e hi e' // cal l i ng l i s t ener s : s ynchr oni zed( t hi s ) { l v = ( Vect or ) act i onLi s t ener s . cl one( ) ; } // C l al l t he l i s t ener m hods : al et f or ( i nt i = 0; i < l v. s i ze( ) ; i ++) { A i onLi s t ener al = ct ( A i onLi s t ener ) l v. el em A ( i ) ; ct ent t al . act i onPer f or m a) ; ed( } } cl as s M ext ends M eA L ous dapt er { publ i c voi d m ePr es s ed( M eEvent e) { ous ous G aphi cs g = get G aphi cs ( ) ; r r g. s et C or ( t C or ) ; ol ol g. s et Font ( new Font ( " Ti m Rom , Font . BO , f ont Si ze) ) ; es an" LD i nt w dt h = i 508

g. get Font M r i cs ( ) . s t r i ngW dt h( t ext ) ; et i g. dr aw r i ng( t ext , St ( get Si ze( ) . w dt h - w dt h) /2, i i get Si ze( ) . hei ght /2) ; g. di s pos e( ) ; not i f yLi s t ener s ( ) ; } } cl as s M ext ends M eM i onA M ous ot dapt er { publ i c voi d m eM ous oved( M eEvent e) { ous xm = e. get X( ) ; ym = e. get Y( ) ; r epai nt ( ) ; } } // Tes t i ng t he BangBean2: publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai BangBean2 bb = new BangBean2( ) ; bb. addA i onLi s t ener ( new A i onLi s t ener ( ) { ct ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct Sys t em out . pr i nt l n( " A i onEvent " + e) ; . ct } }); bb. addA i onLi s t ener ( new A i onLi s t ener ( ) { ct ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct Sys t em out . pr i nt l n( " BangBean2 act i on" ) ; . } }); bb. addA i onLi s t ener ( new A i onLi s t ener ( ) { ct ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct Sys t em out . pr i nt l n( " M e act i on" ) ; . or } }); Fr am aFr am = new Fr am " BangBean2 Tes t " ) ; e e e( aFr am addW ndow s t ener ( new W ndow dapt er ( ) { e. i Li i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); aFr am add( bb, Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 300, 300) ; e. aFr am s et Vi s i bl e( t r ue) ; e. } } ///: ~ 很容易就可以为方法添加 s ynchr oni z ed。但注意在 addA i onLi s t ener ( ) 和 r em ct i onLi st ener ( )中,现 ct oveA 在添加了 A i onLi s t ener ,并从一个 Vect or 中移去,所以能够根据自己愿望使用任意多个。 ct 我们注意到,not i f yLi s t ener s ( ) 方法并未设为“同步”。可从多个线程中发出对这个方法的调用。另外,在 对 not i f yLi s t ener s ( ) 调用的中途,也可能发出对 addA i onLi s t ener ( )和 r em ct oveA i onLi s t ener ( ) 的调 ct 用。这显然会造成问题,因为它否定了 Vect or act i onLi s t ener s 。为缓解这个问题,我们在一个 s ynchr oni z ed从句中“克隆”了 Vect or ,并对克隆进行了否定。这样便可在不影响 not i f yLi s t ener s ( )的前 提下,对 Vect or 进行操纵。 509

pai nt ( )方法也没有设为“同步”。与单纯地添加自己的方法相比,决定是否对过载的方法进行同步要困难得 多。在这个例子中,无论 pai nt ( ) 是否“同步”,它似乎都能正常地工作。但必须考虑的问题包括: ( 1) 方法会在对象内部修改“关键”变量的状态吗?为判断一个变量是否“关键”,必须知道它是否会被程 序中的其他线程读取或设置(就目前的情况看,读取或设置几乎肯定是通过“同步”方法进行的,所以可以 只对它们进行检查)。对 pai nt ( ) 的情况来说,不会发生任何修改。 ( 2) 方法要以这些“关键”变量的状态为基础吗?如果一个“同步”方法修改了一个变量,而我们的方法要 用到这个变量,那么一般都愿意把自己的方法也设为“同步”。基于这一前提,大家可观察到 cSi ze 由“同 步”方法进行了修改,所以 pai nt ( )应当是“同步”的。但在这里,我们可以问:“假如 cSi ze 在 pai nt ( )执 行期间发生了变化,会发生的最糟糕的事情是什么呢?”如果发现情况不算太坏,而且仅仅是暂时的效果, 那么最好保持 pai nt ( ) 的“不同步”状态,以避免同步方法调用带来的额外开销。 ( 3) 要留意的第三条线索是 pai nt ( )基础类版本是否“同步”,在这里它不是同步的。这并不是一个非常严 格的参数,仅仅是一条“线索”。比如在目前的情况下,通过同步方法(好 cSi ze)改变的一个字段已合成 到 pai nt ( ) 公式里,而且可能已改变了情况。但请注意,s ynchr oni z ed不能继承——也就是说,假如一个方 法在基础类中是“同步”的,那么在衍生类过载版本中,它不会自动进入“同步”状态。 Tes t BangBean2 中的测试代码已在前一章的基础上进行了修改,已在其中加入了额外的“听众”,从而演示 了 BangBean2 的多造型能力。

14. 3 堵塞
一个线程可以有四种状态: ( 1) 新(N ):线程对象已经创建,但尚未启动,所以不可运行。 ew ( 2) 可运行(Runnabl e):意味着一旦时间分片机制有空闲的 C PU周期提供给一个线程,那个线程便可立即 开始运行。因此,线程可能在、也可能不在运行当中,但一旦条件许可,没有什么能阻止它的运行——它既 没有“死”掉,也未被“堵塞”。 ( 3) 死(D ead):从自己的 r un( ) 方法中返回后,一个线程便已“死”掉。亦可调用 s t op( )令其死掉,但会 产生一个违例——属于 Er r or 的一个子类(也就是说,我们通常不捕获它)。记住一个违例的“掷”出应当 是一个特殊事件,而不是正常程序运行的一部分。所以不建议你使用 s t op( ) (在 Java 1. 2 则是坚决反 对)。另外还有一个 des t r oy( ) 方法(它永远不会实现),应该尽可能地避免调用它,因为它非常武断,根 本不会解除对象的锁定。 ( 4) 堵塞(Bl ocked ):线程可以运行,但有某种东西阻碍了它。若线程处于堵塞状态,调度机制可以简单地 跳过它,不给它分配任何 C PU时间。除非线程再次进入“可运行”状态,否则不会采取任何操作。

1 4 . 3 . 1 为何会堵塞
堵塞状态是前述四种状态中最有趣的,值得我们作进一步的探讨。线程被堵塞可能是由下述五方面的原因造 成的: ( 1) 调用 s l eep( 毫秒数) ,使线程进入“睡眠”状态。在规定的时间内,这个线程是不会运行的。 ( 2) 用 s us pend( ) 暂停了线程的执行。除非线程收到 r es um ) 消息,否则不会返回“可运行”状态。 e( ( 3) 用 w t ( )暂停了线程的执行。除非线程收到 nof i f y( ) 或者 not i f yA l ( ) 消息,否则不会变成“可运行” ai l (是的,这看起来同原因 2 非常相象,但有一个明显的区别是我们马上要揭示的)。 ( 4) 线程正在等候一些 I O (输入输出)操作完成。 ( 5) 线程试图调用另一个对象的“同步”方法,但那个对象处于锁定状态,暂时无法使用。 亦可调用 yi el d( ) (Thr ead类的一个方法)自动放弃 C PU,以便其他线程能够运行。然而,假如调度机制觉 得我们的线程已拥有足够的时间,并跳转到另一个线程,就会发生同样的事情。也就是说,没有什么能防止 调度机制重新启动我们的线程。线程被堵塞后,便有一些原因造成它不能继续运行。 下面这个例子展示了进入堵塞状态的全部五种途径。它们全都存在于名为 Bl ocki ng. j ava 的一个文件中,但 在这儿采用散落的片断进行解释(大家可注意到片断前后的“C i nued”以及“C i nui ng”标志。利用第 ont ont 17 章介绍的工具,可将这些片断连结到一起)。首先让我们看看基本的框架: //: Bl ocki ng. j ava // D ons t r at es t he var i ous w em ays a t hr ead / / can be bl ocked. 510

i i i i

m t por m t por m t por m t por

j ava. aw . *; t j ava. aw . event . * ; t j ava. appl et . * ; j ava. i o. * ;

//////////// The bas i c f r am or k /////////// ew cl as s Bl ockabl e ext ends Thr ead { pr i vat e Peeker peeker ; pr ot ect ed Text Fi el d s t at e = new Text Fi el d( 40) ; pr ot ect ed i nt i ; publ i c Bl ockabl e( C ai ner c) { ont c. add( s t at e) ; peeker = new Peeker ( t hi s , c) ; } publ i c s ynchr oni zed i nt r ead( ) { r et ur n i ; } pr ot ect ed s ynchr oni zed voi d updat e( ) { s t at e. s et Text ( get C as s ( ) . get N e( ) l am + " s t at e: i = " + i ) ; } publ i c voi d s t opPeeker ( ) { // peeker . s t op( ) ; D ecat ed i n Java 1. 2 epr peeker . t er m nat e( ) ; // The pr ef er r ed appr oach i } } cl as s Peeker ext ends Thr ead { pr i vat e Bl ockabl e b; pr i vat e i nt s es s i on; pr i vat e Text Fi el d s t at us = new Text Fi el d( 40) ; pr i vat e bool ean s t op = f al s e; publ i c Peeker ( Bl ockabl e b, C ai ner c) { ont c. add( s t at us ) ; t hi s . b = b; s t ar t ( ) ; } publ i c voi d t er m nat e( ) { s t op = t r ue; } i publ i c voi d r un( ) { w l e ( ! s t op) { hi s t at us . s et Text ( b. get C as s ( ) . g N e( ) l et am + " Peeker " + ( ++s es s i on) + " ; val ue = " + b. r ead( ) ) ; try { s l eep( 100) ; } cat ch ( I nt er r upt edExcept i on e) { } } } } ///: C i nued ont Bl ockabl e 类打算成为本例所有类的一个基础类。一个 Bl ockabl e 对象包含了一个名为 s t at e 的 Text Fi el d (文本字段),用于显示出对象有关的信息。用于显示这些信息的方法叫作 updat e( ) 。我们发现它用 get C as s . get N e( ) 来产生类名,而不是仅仅把它打印出来;这是由于 updat e( 0 不知道自己为其调用的那个 l am 类的准确名字,因为那个类是从 Bl ockabl e 衍生出来的。 511

在 Bl ockabl e 中,变动指示符是一个 i nt i ;衍生类的 r un( ) 方法会为其增值。 针对每个 Bl oackabl e 对象,都会启动 Peeker 类的一个线程。Peeker 的任务是调用 r ead( )方法,检查与自己 关联的 Bl ockabl e 对象,看看 i 是否发生了变化,最后用它的 s t at us 文本字段报告检查结果。注意 r ead( ) 和 updat e( ) 都是同步的,要求对象的锁定能自由解除,这一点非常重要。 1. 睡眠 这个程序的第一项测试是用 s l eep( )作出的: ///: C i nui ng ont ///////////// Bl ocki ng vi a s l eep( ) /////////// cl as s Sl eeper 1 ext ends Bl ockabl e { publ i c Sl eeper 1( C ai ner c) { s uper ( c) ; } ont publ i c s ynchr oni zed voi d r un( ) { w l e( t r ue) { hi i ++; updat e( ) ; try { s l eep( 1000) ; } cat ch ( I nt er r upt edExcept i on e) { } } } } cl as s Sl eeper 2 ext ends Bl ockabl e { publ i c Sl eeper 2( C ai ner c) { s uper ( c) ; } ont publ i c voi d r un( ) { w l e( t r ue) { hi change( ) ; try { s l eep( 1000) ; } cat ch ( I nt er r upt edExcept i on e) { } } } publ i c s ynchr oni zed voi d change( ) { i ++; updat e( ) ; } } ///: C i nued ont 在 Sl eeper 1 中,整个 r un( ) 方法都是同步的。我们可看到与这个对象关联在一起的 Peeker 可以正常运行, 直到我们启动线程为止,随后 Peeker 便会完全停止。这正是“堵塞”的一种形式:因为 Sl eeper 1. r un( ) 是 同步的,而且一旦线程启动,它就肯定在 r un( ) 内部,方法永远不会放弃对象锁定,造成 Peeker 线程的堵 塞。 Sl eeper 2 通过设置不同步的运行,提供了一种解决方案。只有 change( ) 方法才是同步的,所以尽管 r un( ) 位 于 s l eep( ) 内部,Peeker 仍然能访问自己需要的同步方法——r ead( )。在这里,我们可看到在启动了 Sl eeper 2 线程以后,Peeker 会持续运行下去。 2. 暂停和恢复 这个例子接下来的一部分引入了“挂起”或者“暂停”(Sus pend )的概述。Thr ead类提供了一个名为 s us pend( ) 的方法,可临时中止线程;以及一个名为 r es um ) 的方法,用于从暂停处开始恢复线程的执行。 e( 显然,我们可以推断出 r es um ) 是由暂停线程外部的某个线程调用的。在这种情况下,需要用到一个名为 e( Res um (恢复器)的独立类。演示暂停/恢复过程的每个类都有一个相关的恢复器。如下所示: er 512

///: C i nui ng ont /////////// Bl ocki ng vi a s us pend( ) /////////// cl as s Sus pendRes um ext ends Bl ockabl e { e publ i c Sus pendRes um C ai ner c) { e( ont s uper ( c) ; new Res um ( t hi s ) ; er } } cl as s Sus pendRes um ext ends Sus pendRes um { e1 e publ i c Sus pendRes um C ai ner c) { s uper ( c) ; } e1( ont publ i c s ynchr oni zed voi d r un( ) { w l e( t r ue) { hi i ++; updat e( ) ; s us pend( ) ; // D ecat ed i n Java 1. 2 epr } } } cl as s Sus pendRes um ext ends Sus pendRes um { e2 e publ i c Sus pendRes um C ai ner c) { s uper ( c) ; } e2( ont publ i c voi d r un( ) { w l e( t r ue) { hi change( ) ; s us pend( ) ; // D ecat ed i n Java 1. 2 epr } } publ i c s ynchr oni zed voi d change( ) { i ++; updat e( ) ; } } cl as s Res um ext ends Thr ead { er pr i vat e Sus pendRes um s r ; e publ i c Res um ( Sus pendRes um s r ) { er e t hi s . s r = s r ; s t ar t ( ) ; } publ i c voi d r un( ) { w l e( t r ue) { hi try { s l eep( 1000) ; } cat ch ( I nt er r upt edExcept i on e) { } s r . r es um ) ; // D ecat ed i n Java 1. 2 e( epr } } } ///: C i nued ont SuspendRes um 也提供了一个同步的 r un( ) 方法。同样地,当我们启动这个线程以后,就会发现与它关联的 e1 513

Peeker 进入“堵塞”状态,等候对象锁被释放,但那永远不会发生。和往常一样,这个问题在 Sus pendRes um 里得到了解决,它并不同步整个 r un( ) 方法,而是采用了一个单独的同步 change( ) 方法。 e2 对于 Java 1. 2,大家应注意 s us pend( ) 和 r es um ) 已获得强烈反对,因为 s us pend( ) 包含了对象锁,所以极 e( 易出现“死锁”现象。换言之,很容易就会看到许多被锁住的对象在傻乎乎地等待对方。这会造成整个应用 程序的“凝固”。尽管在一些老程序中还能看到它们的踪迹,但在你写自己的程序时,无论如何都应避免。 本章稍后就会讲述正确的方案是什么。 3. 等待和通知 通过前两个例子的实践,我们知道无论 s l eep( )还是 s us pend( ) 都不会在自己被调用的时候解除锁定。需要用 到对象锁时,请务必注意这个问题。在另一方面,w t ( ) 方法在被调用时却会解除锁定,这意味着可在执行 ai w t ( )期间调用线程对象中的其他同步方法。但在接着的两个类中,我们看到 r un( ) 方法都是“同步”的。 ai 在 w t ( ) 期间,Peeker 仍然拥有对同步方法的完全访问权限。这是由于 w t ( )在挂起内部调用的方法时, ai ai 会解除对象的锁定。 我们也可以看到 w t ( ) 的两种形式。第一种形式采用一个以毫秒为单位的参数,它具有与 s l eep( )中相同的 ai 含义:暂停这一段规定时间。区别在于在 w t ( ) 中,对象锁已被解除,而且能够自由地退出 w t ( ) ,因为一 ai ai 个 not i f y( ) 可强行使时间流逝。 第二种形式不采用任何参数,这意味着 w t ( )会持续执行,直到 not i f y( ) 介入为止。而且在一段时间以后, ai 不会自行中止。 w t ( )和 not i f y( ) 比较特别的一个地方是这两个方法都属于基础类 O ect 的一部分,不象 s l eep( ) , ai bj s us pend( ) 以及 r es um ) 那样属于 Thr ead的一部分。尽管这表面看有点儿奇怪——居然让专门进行线程处理 e( 的东西成为通用基础类的一部分——但仔细想想又会释然,因为它们操纵的对象锁也属于每个对象的一部 分。因此,我们可将一个 w t ( ) 置入任何同步方法内部,无论在那个类里是否准备进行涉及线程的处理。事 ai 实上,我们能调用 w t ( ) 的唯一地方是在一个同步的方法或代码块内部。若在一个不同步的方法内调用 ai w t ( )或者 not i f y( ) ,尽管程序仍然会编译,但在运行它的时候,就会得到一个 ai I l l egal M t or St at eExcept i on(非法监视器状态违例),而且会出现多少有点莫名其妙的一条消息: oni “cur r ent t hr ead not ow ”(当前线程不是所有人”。注意 s l eep( ),s us pend( ) 以及r esum ner e()都能在不 同步的方法内调用,因为它们不需要对锁定进行操作。 只能为自己的锁定调用 w t ( )和 not i f y( ) 。同样地,仍然可以编译那些试图使用错误锁定的代码,但和往常 ai 一样会产生同样的 I l l egal M t or St at eExcept i on违例。我们没办法用其他人的对象锁来愚弄系统,但可要 oni 求另一个对象执行相应的操作,对它自己的锁进行操作。所以一种做法是创建一个同步方法,令其为自己的 对象调用 not i f y( ) 。但在 N i f i er 中,我们会看到一个同步方法内部的 not i f y( ) : ot s ynchr oni zed( w n2) { w not i f y( ) ; n2. } 其中,w 是类型为 W t N i f y2 的对象。尽管并不属于 W t N i f y2 的一部分,这个方法仍然获得了 w n2 ai ot ai ot n2 对象的锁定。在这个时候,它为 w 调用 not i f y( ) 是合法的,不会得到 I l l egal M t or St at eExcept i on违 n2 oni 例。 ///: C i nui ng ont /////////// Bl ocki ng vi a w t ( ) /////////// ai cl as s W t N i f y1 ext ends Bl ockabl e { ai ot publ i c W t N i f y1( C ai ner c) { s uper ( c) ; } ai ot ont publ i c s ynchr oni zed voi d r un( ) { w l e( t r ue) { hi i ++; updat e( ) ; try { w t ( 1000) ; ai } cat ch ( I nt er r upt edExcept i on e) { } } 514

} } cl as s W t N i f y2 ext ends Bl ockabl e { ai ot publ i c W t N i f y2( C ai ner c) { ai ot ont s uper ( c) ; new N i f i er ( t hi s ) ; ot } publ i c s ynchr oni zed voi d r un( ) { w l e( t r ue) { hi i ++; updat e( ) ; try { w t(); ai } cat ch ( I nt er r upt edExcept i on e) { } } } } cl as s N i f i er ext ends Thr ead { ot pr i vat e W t N i f y2 w ai ot n2; publ i c N i f i er ( W t N i f y2 w ot ai ot n2) { t hi s . w = w n2 n2; s t ar t ( ) ; } publ i c voi d r un( ) { w l e( t r ue) { hi try { s l eep( 2000) ; } cat ch ( I nt er r upt edExcept i on e) { } s ynchr oni zed( w n2) { w not i f y( ) ; n2. } } } } ///: C i nued ont 若必须等候其他某些条件(从线程外部加以控制)发生变化,同时又不想在线程内一直傻乎乎地等下去,一 般就需要用到 w t ( ) 。w t ( )允许我们将线程置入“睡眠”状态,同时又“积极”地等待条件发生改变。而 ai ai 且只有在一个 not i f y( ) 或 not i f yA l ( ) 发生变化的时候,线程才会被唤醒,并检查条件是否有变。因此,我 l 们认为它提供了在线程间进行同步的一种手段。 4. I O堵塞 若一个数据流必须等候一些 I O活动,便会自动进入“堵塞”状态。在本例下面列出的部分中,有两个类协同 通用的 Reader 以及 W i t er 对象工作(使用 Java 1. 1 的流)。但在测试模型中,会设置一个管道化的数据 r 流,使两个线程相互间能安全地传递数据(这正是使用管道流的目的)。 Sender 将数据置入 W i t er ,并“睡眠”随机长短的时间。然而,Recei ver 本身并没有包括 s l eep( ) , r s us pend( ) 或者 w t ( ) 方法。但在执行 r ead( )的时候,如果没有数据存在,它会自动进入“堵塞”状态。如 ai 下所示: ///: C i nui ng ont cl as s Sender ext ends Bl ockabl e { // s end 515

pr i vat e W i t er out ; r publ i c Sender ( C ai ner c, W i t er out ) { ont r s uper ( c) ; t hi s . out = out ; } publ i c voi d r un( ) { w l e( t r ue) { hi f or ( char c = ' A ; c Thr ead. M X_PRI O TY) Pr A RI new i or i t y = Thr ead. M X_PRI O TY; Pr A RI s et Pr i or i t y( new i or i t y) ; Pr } } cl as s D nL i m em s A i onLi s t ener { ow pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct i nt new i or i t y = get Pr i or i t y( ) - 1; Pr i f ( new i or i t y < Thr ead. M N Pr I _PRI O TY) RI new i or i t y = Thr ead. M N Pr I _PRI O TY; RI s et Pr i or i t y( new i or i t y) ; Pr } } publ i c voi d r un( ) { w l e ( t r ue) { hi i f ( r unFl ag) { t . s et Text ( I nt eger . t oSt r i ng( count ++) ) ; pr . s et Text ( I nt eger . t oSt r i ng( get Pr i or i t y( ) ) ) ; } 522

yi el d( ) ; } } } publ i c cl as s C ount er 5 ext ends A et { ppl pr i vat e But t on s t ar t = new But t on( " St ar t " ) , upM = new But t on( " I nc M Pr i or i t y" ) , ax ax dow ax = new But t on( " D M Pr i or i t y" ) ; nM ec ax pr i vat e bool ean s t ar t ed = f al s e; pr i vat e s t at i c f i nal i nt SI ZE = 10; pr i vat e Ti cker 2[ ] s = new Ti cker 2[ SI ZE] ; pr i vat e Text Fi el d m = new Text Fi el d( 3) ; p publ i c voi d i ni t ( ) { f or ( i nt i = 0; i < s . l engt h; i ++) s [ i ] = new Ti cker 2( t hi s ) ; add( new Label ( " M X_PRI O TY = " A RI + Thr ead. M X_PRI O TY) ) ; A RI add( new Label ( " M N I _PRI O TY = " RI + Thr ead. M N I _PRI O TY) ) ; RI add( new Label ( " G oup M Pr i or i t y = " ) ) ; r ax add( m ; p) add( s t ar t ) ; add( upM ; add( dow ax) ; ax) nM s t ar t . addA i onLi s t ener ( new St ar t L( ) ) ; ct upM addA i onLi s t ener ( new UpM ax. ct axL( ) ) ; dow ax. addA i onLi s t ener ( new D nM nM ct ow axL( ) ) ; s how axPr i or i t y( ) ; M // Recur s i vel y di s pl ay par ent t hr ead gr oups : Thr eadG oup par ent = r s [ 0] . get Thr eadG oup( ) . get Par ent ( ) ; r w l e( par ent ! = nul l ) { hi a new Label ( dd( " Par ent t hr eadgr oup m pr i or i t y = " ax + par ent . get M axPr i or i t y( ) ) ) ; par ent = par ent . get Par ent ( ) ; } } publ i c voi d s how axPr i or i t y( ) { M m s et Text ( I nt eger . t oSt r i ng( p. s [ 0] . get Thr eadG oup( ) . get M r axPr i or i t y( ) ) ) ; } cl as s St ar t L i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct i f ( ! s t ar t ed) { s t ar t ed = t r ue; f or ( i nt i = 0; i < s . l engt h; i ++) s [ i ] . s t ar t ( ) ; } } } 523

cl as s UpM axL i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct i nt m axp = s [ 0] . get Thr eadG oup( ) . get M r axPr i or i t y( ) ; i f ( ++m axp > Thr ead. M X_PRI O TY) A RI m axp = Thr ead. M X_PRI O TY; A RI s [ 0] . get Thr eadG oup( ) . s et M r axPr i or i t y( m axp) ; s how axPr i or i t y( ) ; M } } cl as s D nM ow axL i m em s A i onLi s t ener { pl ent ct publ i c voi d act i onPer f or m A i onEvent e) { ed( ct i nt m axp = s [ 0] . get Thr eadG oup( ) . get M r axPr i or i t y( ) ; i f (- -m axp < Thr ead. M N I _PRI O TY) RI m axp = Thr ead. M N I _PRI O TY; RI s [ 0] . get Thr eadG oup( ) . s et M r axPr i or i t y( m axp) ; s how axPr i or i t y( ) ; M } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai C ount er 5 appl et = new C ount er 5( ) ; Fr am aFr am = new Fr am " C e e e( ount er 5" ) ; aFr am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); aFr am add( appl et , Bor der Layout . C TER) ; e. EN aFr am s et Si ze( 300, 600) ; e. appl et . i ni t ( ) ; appl et . s t ar t ( ) ; aFr am s et Vi s i bl e( t r ue) ; e. } } ///: ~ Ti cker 采用本章前面构造好的形式,但有一个额外的 Text Fi el d(文本字段),用于显示线程的优先级;以 及两个额外的按钮,用于人为提高及降低优先级。 也要注意 yi el d( ) 的用法,它将控制权自动返回给调试程序(机制)。若不进行这样的处理,多线程机制仍 会工作,但我们会发现它的运行速度慢了下来(试试删去对 yi el d( ) 的调用)。亦可调用 s l eep( ),但假若那 样做,计数频率就会改由 s l eep( ) 的持续时间控制,而不是优先级。 C ount er 5 中的 i ni t ( ) 创建了由 10 个 T i cker 2 构成的一个数组;它们的按钮以及输入字段(文本字段)由 T i cker 2 构建器置入窗体。C ount er 5 增加了新的按钮,用于启动一切,以及用于提高和降低线程组的最大优 先级。除此以外,还有一些标签用于显示一个线程可以采用的最大及最小优先级;以及一个特殊的文本字 段,用于显示线程组的最大优先级(在下一节里,我们将全面讨论线程组的问题)。最后,父线程组的优先 级也作为标签显示出来。 按下“up ”(上)或“dow n”(下)按钮的时候,会先取得 T i cker 2 当前的优先级,然后相应地提高或者降 低。 运行该程序时,我们可注意到几件事情。首先,线程组的默认优先级是 5。即使在启动线程之前(或者在创 建线程之前,这要求对代码进行适当的修改)将最大优先级降到 5 以下,每个线程都会有一个 5 的默认优先 级。 524

最简单的测试是获取一个计数器,将它的优先级降低至 1,此时应观察到它的计数频率显著放慢。现在试着 再次提高优先级,可以升高回线程组的优先级,但不能再高了。现在将线程组的优先级降低两次。线程的优 先级不会改变,但假若试图提高或者降低它,就会发现这个优先级自动变成线程组的优先级。此外,新线程 仍然具有一个默认优先级,即使它比组的优先级还要高(换句话说,不要指望利用组优先级来防止新线程拥 有比现有的更高的优先级)。 最后,试着提高组的最大优先级。可以发现,这样做是没有效果的。我们只能减少线程组的最大优先级,而 不能增大它。

1 4 . 4 . 1 线程组
所有线程都隶属于一个线程组。那可以是一个默认线程组,亦可是一个创建线程时明确指定的组。在创建之 初,线程被限制到一个组里,而且不能改变到一个不同的组。每个应用都至少有一个线程从属于系统线程 组。若创建多个线程而不指定一个组,它们就会自动归属于系统线程组。 线程组也必须从属于其他线程组。必须在构建器里指定新线程组从属于哪个线程组。若在创建一个线程组的 时候没有指定它的归属,则同样会自动成为系统线程组的一名属下。因此,一个应用程序中的所有线程组最 终都会将系统线程组作为自己的“父”。 之所以要提出“线程组”的概念,很难从字面上找到原因。这多少为我们讨论的主题带来了一些混乱。一般 地说,我们认为是由于“安全”或者“保密”方面的理由才使用线程组的。根据 A nol d和 G l i ng的说法: r os “线程组中的线程可以修改组内的其他线程,包括那些位于分层结构最深处的。一个线程不能修改位于自己 所在组或者下属组之外的任何线程”(注释①)。然而,我们很难判断“修改”在这儿的具体含义是什么。 下面这个例子展示了位于一个“叶子组”内的线程能修改它所在线程组树的所有线程的优先级,同时还能为 这个“树”内的所有线程都调用一个方法。 ①:《The Java Pr ogr am i ng Language m 》第 179 页。该书由 A nol d和 Jam G l i ng编著,A d so e e r s os d i n-Wsl y 于 1996 年出版 //: Tes t A cces s . j ava // How t hr eads can acces s ot her t hr eads // i n a par ent t hr ead gr oup publ i c cl as s Tes t A cces s { publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai Thr eadG oup r x = new Thr eadG oup( " x" ) , r y = new Thr eadG oup( x, " y" ) , r z = new Thr eadG oup( y, " z" ) ; r Thr ead one = new Tes t Thr ead1( x, " one" ) , t w = new Tes t Thr ead2( z, " t w ) ; o o" } } cl as s Tes t Thr ead1 ext ends Thr ead { pr i vat e i nt i ; Tes t Thr ead1( Thr eadG oup g, St r i ng nam { r e) s uper ( g, nam ; e) } voi d f ( ) { i ++; // m f y t hi s t hr ead odi Sys t em out . pr i nt l n( get N e( ) + " f ( ) " ) ; . am } }

525

cl as s Tes t Thr ead2 ext ends Tes t Thr ead1 { Tes t Thr ead2( Thr eadG oup g, St r i ng nam { r e) s uper ( g, nam ; e) s t ar t ( ) ; } publ i c voi d r un( ) { Thr eadG oup g = r get Thr eadG oup( ) . get Par ent ( ) . get Par ent ( ) ; r g. l i s t ( ) ; Thr ead[ ] gA l = new Thr ead[ g. act i veC l ount ( ) ] ; g. enum at e( gA l ) ; er l f or ( i nt i = 0; i < gA l . l engt h; i ++) { l gA l [ i ] . s et Pr i or i t y( Thr ead. M N l I _PRI O TY) ; RI ( ( Tes t Thr ead1) gA l [ i ] ) . f ( ) ; l } g. l i s t ( ) ; } } ///: ~ 在 m n( ) 中,我们创建了几个 Thr eadG oup(线程组),每个都位于不同的“叶”上:x 没有参数,只有它 ai r 的名字(一个 St r i ng),所以会自动进入“s ys t em ”(系统)线程组;y 位于 x 下方,而 z 位于 y 下方。注 意初始化是按照文字顺序进行的,所以代码合法。 有两个线程创建之后进入了不同的线程组。其中,Tes t Thr ead1 没有一个 r un( ) 方法,但有一个 f ( ),用于通 知线程以及打印出一些东西,以便我们知道它已被调用。而 Tes t Thr ead2 属于 Tes t Thr ead1 的一个子类,它 的 r un( ) 非常详尽,要做许多事情。首先,它获得当前线程所在的线程组,然后利用 get Par ent ( )在继承树中 向上移动两级(这样做是有道理的,因为我想把 Tes t Thr ead2 在分级结构中向下移动两级)。随后,我们调 用方法 act i veC ount ( ) ,查询这个线程组以及所有子线程组内有多少个线程,从而创建由指向 Thr ead的句柄 构成的一个数组。enum at e( )方法将指向所有这些线程的句柄置入数组 gA l 里。然后在整个数组里遍历, er l 为每个线程都调用 f ( ) 方法,同时修改优先级。这样一来,位于一个“叶子”线程组里的线程就修改了位于 父线程组的线程。 调试方法 l i s t ( ) 打印出与一个线程组有关的所有信息,把它们作为标准输出。在我们对线程组的行为进行调 查的时候,这样做是相当有好处的。下面是程序的输出: j ava. l ang. Thr eadG oup[ nam r e=x, m axpr i =10] Thr ead[ one, 5, x] j ava. l ang. Thr eadG oup[ nam r e=y, m axpr i =10] j ava. l ang. Thr eadG oup[ nam r e=z, m axpr i =10] T hr ead[ t w 5, z] o, one f ( ) tw f () o j ava. l ang. Thr eadG oup[ nam r e=x, m axpr i =10] Thr ead[ one, 1, x] j ava. l ang. Thr eadG oup[ nam r e=y, m axpr i =10] j ava. l ang. Thr eadG oup[ nam r e=z, m axpr i =10] Thr ead[ t w 1, z] o, l i s t ( )不仅打印出 Thr eadG oup或者 Thr ead的类名,也打印出了线程组的名字以及它的最高优先级。对于线 r 程,则打印出它们的名字,并接上线程优先级以及所属的线程组。注意 l i s t ( ) 会对线程和线程组进行缩排处 理,指出它们是未缩排的线程组的“子”。 大家可看到 f ( )是由 Tes t Thr ead2 的 r un( ) 方法调用的,所以很明显,组内的所有线程都是相当脆弱的。然 而,我们只能访问那些从自己的 s ys t em线程组树分支出来的线程,而且或许这就是所谓“安全”的意思。我 们不能访问其他任何人的系统线程树。 526

1. 线程组的控制 抛开安全问题不谈,线程组最有用的一个地方就是控制:只需用单个命令即可完成对整个线程组的操作。下 面这个例子演示了这一点,并对线程组内优先级的限制进行了说明。括号内的注释数字便于大家比较输出结 果: //: Thr eadG oup1. j ava r // How t hr ead gr oups cont r ol pr i or i t i es // of t he t hr eads i ns i de t hem . publ i c cl as s Thr eadG oup1 { r publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai // G t he s ys t em t hr ead & pr i nt i t s I nf o: et Thr eadG oup s ys = r Thr ead. cur r ent Thr ead( ) . get Thr eadG oup( ) ; r s ys . l i s t ( ) ; // ( 1) // Reduce t he s ys t em t hr ead gr oup pr i or i t y: s ys . s et M axPr i or i t y( Thr ead. M X_PRI O TY - 1) ; A RI // I ncr eas e t he m n t hr ead pr i or i t y: ai Thr ead cur r = Thr ead. cur r ent Thr ead( ) ; cur r . s et Pr i or i t y( cur r . get Pr i or i t y( ) + 1) ; s ys . l i s t ( ) ; // ( 2) // A t em t o s et a new gr oup t o t he m t pt ax: Thr eadG oup g1 = new Thr eadG oup( " g1" ) ; r r g1. s et M axPr i or i t y( Thr ead. M X_PRI O TY) ; A RI // A t em t o s et a new t hr ead t o t he m t pt ax: Thr ead t = new Thr ead( g1, " A ) ; " t . s et Pr i or i t y( Thr ead. M X_PRI O TY) ; A RI g1. l i s t ( ) ; // ( 3) // Reduce g1' s m pr i or i t y, t hen at t em ax pt // t o i ncr eas e i t : g1. s et M axPr i or i t y( Thr ead. M X_PRI O TY - 2) ; A RI g1. s et M axPr i or i t y( Thr ead. M X_PRI O TY) ; A RI g1. l i s t ( ) ; // ( 4) // A t em t o s et a new t hr ead t o t he m t pt ax: t = new Thr ead( g1, " B" ) ; t . s et Pr i or i t y( Thr ead. M X_PRI O TY) ; A RI g1. l i s t ( ) ; // ( 5) // Low t he m pr i or i t y bel ow t he def aul t er ax // t hr ead pr i or i t y: g1. s et M axPr i or i t y( Thr ead. M N I _PRI O TY + 2) ; RI // Look at a new t hr ead' s pr i or i t y bef or e // and af t er changi ng i t : t = new Thr ead( g1, " C ) ; " g1. l i s t ( ) ; // ( 6) t . s et Pr i or i t y( t . get Pr i or i t y( ) - 1) ; g1. l i s t ( ) ; // ( 7) // M ake g2 a chi l d Thr eadgr oup of g1 and // t r y t o i ncr eas e i t s pr i or i t y: Thr eadG oup g2 = new Thr eadG oup( g1, " g2" ) ; r r g2. l i s t ( ) ; // ( 8) g2. s et M axPr i or i t y( Thr ead. M X_PRI O TY) ; A RI 527

g2. l i s t ( ) ; // ( 9) // A a bunch of new t hr eads t o g2: dd f or ( i nt i = 0; i < 5; i ++) new Thr ead( g2, I nt eger . t oSt r i ng( i ) ) ; // Show i nf or m i on about al l t hr eadgr oups at // and t hr eads : s ys . l i s t ( ) ; // ( 10) Sys t em out . pr i nt l n( " St ar t i ng al l t hr eads : " ) ; . Thr ead[ ] al l = new Thr ead[ s ys . act i veC ount ( ) ] ; s ys. enum at e( al l ) ; er f or ( i nt i = 0; i < al l . l engt h; i ++) i f ( ! al l [ i ] . i s A i ve( ) ) l al l [ i ] . s t ar t ( ) ; // Sus pends & St ops al l t hr eads i n // t hi s gr oup and i t s s ubgr oups : Sys t em out . pr i nt l n( " A l t hr eads s t ar t ed" ) ; . l s ys . s us pend( ) ; // D ecat ed i n Java 1. 2 epr // N ever get s her e. . . Sys t em out . pr i nt l n( " A l t hr eads s us pended" ) ; . l s ys . s t op( ) ; // D ecat ed i n Java 1. 2 epr Sys t em out . pr i nt l n( " A l t hr eads s t opped" ) ; . l } } ///: ~ 下面的输出结果已进行了适当的编辑,以便用一页能够装下(j ava. l ang. 已被删去),而且添加了适当的数 字,与前面程序列表中括号里的数字对应: ( 1) Thr eadG oup[ nam ys t em m r e=s , axpr i =10] Thr ead[ m n, 5, s ys t em ai ] ( 2) Thr eadG oup[ nam ys t em m r e=s , axpr i =9] Thr ead[ m n, 6, s ys t em ai ] ( 3) Thr eadG oup[ nam r e=g1, m axpr i =9] Thr ead[ A 9, g1] , ( 4) Thr eadG oup[ nam r e=g1, m axpr i =8] Thr ead[ A 9, g1] , ( 5) Thr eadG oup[ nam r e=g1, m axpr i =8] Thr ead[ A 9, g1] , Thr ead[ B, 8, g1] ( 6) Thr eadG oup[ nam r e=g1, m axpr i =3] Thr ead[ A 9, g1] , Thr ead[ B, 8, g1] Thr ead[ C 6, g1] , ( 7) Thr eadG oup[ nam r e=g1, m axpr i =3] Thr ead[ A 9, g1] , Thr ead[ B, 8, g1] Thr ead[ C 3, g1] , ( 8) Thr eadG oup[ nam r e=g2, m axpr i =3] ( 9) Thr eadG oup[ nam r e=g2, m axpr i =3] ( 10) Thr eadG oup[ nam ys t em m r e=s , axpr i =9] Thr ead[ m n, 6, s ys t em ai ] Thr eadG oup[ nam r e=g1, m axpr i =3] Thr ead[ A 9, g1] , 528

Thr ead[ B, 8, g1] Thr ead[ C 3, g1] , Thr eadG oup[ nam r e=g2, m axpr i =3] Thr ead[ 0, 6, g2] Thr ead[ 1, 6, g2] Thr ead[ 2, 6, g2] Thr ead[ 3, 6, g2] Thr ead[ 4, 6, g2] St ar t i ng al l t hr eads : A l t hr eads s t ar t ed l 所有程序都至少有一个线程在运行,而且 m n( ) 采取的第一项行动便是调用 Thr ead的一个 s t at i c(静态) ai 方法,名为 cur r ent Thr ead( )。从这个线程开始,线程组将被创建,而且会为结果调用 l i s t ( ) 。输出如下: ( 1) Thr eadG oup[ nam ys t em m r e=s , axpr i =10] Thr ead[ m n, 5, s ys t em ai ] 我们可以看到,主线程组的名字是 s ys t em ,而主线程的名字是 m n,而且它从属于 s ys t em ai 线程组。 第二个练习显示出 s ys t em组的最高优先级可以减少,而且 m n 线程可以增大自己的优先级: ai ( 2) Thr eadG oup[ nam ys t em m r e=s , axpr i =9] Thr ead[ m n, 6, s ys t em ai ] 第三个练习创建一个新的线程组,名为 g1;它自动从属于 s ys t em线程组,因为并没有明确指定它的归属关 系。我们在 g1 内部放置了一个新线程,名为 A 。随后,我们试着将这个组的最大优先级设到最高的级别,并 将 A的优先级也设到最高一级。结果如下: ( 3) Thr eadG oup[ nam r e=g1, m axpr i =9] Thr ead[ A 9, g1] , 可以看出,不可能将线程组的最大优先级设为高于它的父线程组。 第四个练习将 g1 的最大优先级降低两级,然后试着把它升至 Thr ead. M X_PRI O TY。结果如下: A RI ( 4) Thr eadG oup[ nam r e=g1, m axpr i =8] Thr ead[ A 9, g1] , 同样可以看出,提高最大优先级的企图是失败的。我们只能降低一个线程组的最大优先级,而不能提高它。 此外,注意线程 A的优先级并未改变,而且它现在高于线程组的最大优先级。也就是说,线程组最大优先级 的变化并不能对现有线程造成影响。 第五个练习试着将一个新线程设为最大优先级。如下所示: ( 5) Thr eadG oup[nam r e=g1, m axpr i =8] Thr ead[ A 9, g1] , Thr ead[ B, 8, g1] 因此,新线程不能变到比最大线程组优先级还要高的一级。 这个程序的默认线程优先级是 6;若新建一个线程,那就是它的默认优先级,而且不会发生变化,除非对优 先级进行了特别的处理。练习六将把线程组的最大优先级降至默认线程优先级以下,看看在这种情况下新建 一个线程会发生什么事情: ( 6) Thr eadG oup[ nam r e=g1, m axpr i =3] Thr ead[ A 9, g1] , 529

Thr ead[ B, 8, g1] Thr ead[ C 6, g1] , 尽管线程组现在的最大优先级是 3,但仍然用默认优先级 6 来创建新线程。所以,线程组的最大优先级不会 影响默认优先级(事实上,似乎没有办法可以设置新线程的默认优先级)。 改变了优先级后,接下来试试将其降低一级,结果如下: ( 7) Thr eadG oup[ nam r e=g1, m axpr i =3] Thr ead[ A 9, g1] , Thr ead[ B, 8, g1] Thr ead[ C 3, g1] , 因此,只有在试图改变优先级的时候,才会强迫遵守线程组最大优先级的限制。 我们在( 8)和( 9) 中进行了类似的试验。在这里,我们创建了一个新的线程组,名为 g2,将其作为 g1 的一个 子组,并改变了它的最大优先级。大家可以看到,g2 的优先级无论如何都不可能高于 g1: ( 8) Thr eadG oup[ nam r e=g2, m axpr i =3] ( 9) Thr eadG oup[ nam r e=g2, m axpr i =3] 也要注意在 g2 创建的时候,它会被自动设为 g1 的线程组最大优先级。 经过所有这些实验以后,整个线程组和线程系统都会被打印出来,如下所示: ( 10) Thr eadG oup[ nam ys t em m r e=s , axpr i =9] Thr ead[ m n, 6, s ys t em ai ] Thr eadG oup[ nam r e=g1, m axpr i =3] Thr ead[ A 9, g1] , Thr ead[ B, 8, g1] Thr ead[ C 3, g1] , Thr eadG oup[ nam r e=g2, m axpr i =3] Thr ead[ 0, 6, g2] Thr ead[ 1, 6, g2] Thr ead[ 2, 6, g2] Thr ead[ 3, 6, g2] Thr ead[ 4, 6, g2] 所以由线程组的规则所限,一个子组的最大优先级在任何时候都只能低于或等于它的父组的最大优先级。 本程序的最后一个部分演示了用于整组线程的方法。程序首先遍历整个线程树,并启动每一个尚未启动的线 程。例如,s ys t em 组随后会被挂起(暂停),最后被中止(尽管用 s us pend( ) 和 s t op( ) 对整个线程组进行操 作看起来似乎很有趣,但应注意这些方法在 Java 1. 2 里都是被“反对”的)。但在挂起 s ys t em组的同时, 也挂起了 m n 线程,而且整个程序都会关闭。所以永远不会达到让线程中止的那一步。实际上,假如真的中 ai 止了 m n 线程,它会“掷”出一个 Thr eadD h 违例,所以我们通常不这样做。由于 Thr eadG oup是从 ai eat r O ect 继承的,其中包含了 w t ( )方法,所以也能调用 w t (秒数×1000) ,令程序暂停运行任意秒数的时 bj ai ai 间。当然,事前必须在一个同步块里取得对象锁。 Thr eadG oup类也提供了 s us pend( ) 和 r es um ) 方法,所以能中止和启动整个线程组和它的所有线程,也能 r e( 中止和启动它的子组,所有这些只需一个命令即可(再次提醒,s us pend( ) 和 r es um ) 都是 Java 1. 2 所“反 e( 对”的)。 从表面看,线程组似乎有些让人摸不着头脑,但请注意我们很少需要直接使用它们。

14. 5 回顾 r unnabl e
在本章早些时候,我曾建议大家在将一个程序片或主 Fr am 当作 Runnabl e 的实现形式之前,一定要好好地想 e 一想。若采用那种方式,就只能在自己的程序中使用其中的一个线程。这便限制了灵活性,一旦需要用到属 于那种类型的多个线程,就会遇到不必要的麻烦。 530

当然,如果必须从一个类继承,而且想使类具有线程处理能力,则 Runnabl e 是一种正确的方案。本章最后一 个例子对这一点进行了剖析,制作了一个 Runnabl eC anvas 类,用于为自己描绘不同的颜色(C anvas 是“画 布”的意思)。这个应用被设计成从命令行获得参数值,以决定颜色网格有多大,以及颜色发生变化之间的 s l eep( )有多长。通过运用这些值,大家能体验到线程一些有趣而且可能令人费解的特性: //: C or Boxes . j ava ol // Us i ng t he Runnabl e i nt er f ace i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t cl as s C Box ext ends C anvas i m em s Runnabl e { pl ent pr i vat e Thr ead t ; pr i vat e i nt paus e; pr i vat e s t at i c f i nal C or [ ] col or s = { ol C or . bl ack, C or . bl ue, C or . cyan, ol ol ol C or . dar kG ay, C or . gr ay, C or . gr een, ol r ol ol C or . l i ght G ay, C or . m ol r ol agent a, C or . or ange, C or . pi nk, C o . r ed, ol ol ol r C or . w t e, C or . yel l ow ol hi ol }; pr i vat e C or cC or = new ol or ( ) ; ol ol C pr i vat e s t at i c f i nal C or new ol or ( ) { ol C r et ur n col or s [ ( i nt ) ( M h. r andom ) * col or s . l engt h) at ( ]; } publ i c voi d pai nt ( G aphi cs g) { r g. s et C or ( cC or ) ; ol ol D m i on s = get Si ze( ) ; i ens g. f i l l Rect ( 0, 0, s . w dt h, s . hei ght ) ; i } publ i c C Box( i nt paus e) { t hi s . paus e = paus e; t = new Thr ead( t hi s ) ; t . s t ar t ( ) ; } publ i c voi d r un( ) { w l e( t r ue) { hi cC or = new ol or ( ) ; ol C r epai nt ( ) ; try { t . s l eep( paus e) ; } cat ch( I nt er r upt edExcept i on e) { } } } } publ i c cl as s C or Boxes ext ends Fr am { ol e publ i c C or Boxes ( i nt paus e, i nt gr i d) { ol s et Ti t l e( " C or Boxes " ) ; ol s et Layout ( new G i dLayout ( gr i d, gr i d) ) ; r f or ( i nt i = 0; i < gr i d * gr i d; i ++) 531

add( new C Box( paus e) ) ; addW ndow s t ener ( new W ndow dapt er ( ) { i Li i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai i nt paus e = 50; i nt gr i d = 8; i f ( ar gs . l engt h > 0) paus e = I nt eger . par s eI nt ( ar gs [ 0] ) ; i f ( ar gs . l engt h > 1) gr i d = I nt eger . par s eI nt ( ar gs [ 1] ) ; Fr am f = new C or Boxes ( paus e, gr i d) ; e ol f . s et Si ze( 500, 400) ; f . s et Vi s i bl e( t r ue) ; } } ///: ~ C or Boxes 是一个典型的应用(程序),有一个构建器用于设置 G 。这个构建器采用 i nt gr i d 的一个参 ol UI 数,用它设置 G i dLayout (网格布局),使每一维里都有一个 gr i d 单元。随后,它添加适当数量的 C 对 r Box 象,用它们填充网格,并为每一个都传递 paus e 值。在 m n( ) 中,我们可看到如何对 paus e 和 gr i d 的默认 ai 值进行修改(如果用命令行参数传递)。 C 是进行正式工作的地方。它是从 C Box anvas 继承的,并实现了 Runnabl e 接口,使每个 C anvas 也能是一个 Thr ead 。记住在实现 Runnabl e 的时候,并没有实际产生一个 Thr ead对象,只是一个拥有 r un( )方法的类。 因此,我们必须明确地创建一个 Thr ead对象,并将 Runnabl e 对象传递给构建器,随后调用 s t ar t ( ) (在构 建器里进行)。在 C 里,这个线程的名字叫作 t 。 Box 请留意数组 col or s,它对 C or 类中的所有颜色进行了列举(枚举)。它在 new ol or ( ) 中用于产生一种随机 ol C 选择的颜色。当前的单元(格)颜色是 cC or 。 ol pai nt ( )则相当简单——只是将颜色设为 cC or ,然后用那种颜色填充整张画布(C ol anvas )。 在 r un( ) 中,我们看到一个无限循环,它将 cC or 设为一种随机颜色,然后调用 r epai nt ( ) 把它显示出来。 ol 随后,对线程执行 s l eep( ) ,使其“休眠”由命令行指定的时间长度。 由于这种设计方案非常灵活,而且线程处理同每个 C anvas 元素都紧密结合在一起,所以在理论上可以生成任 意多的线程(但在实际应用中,这要受到 JVM能够从容对付的线程数量的限制)。 这个程序也为我们提供了一个有趣的评测基准,因为它揭示了不同 JVM 机制在速度上造成的戏剧性的差异。

1 4 . 5 . 1 过多的线程
有些时候,我们会发现 C or Boxes 几乎陷于停顿状态。在我自己的机器上,这一情况在产生了 10×10 的网 ol 格之后发生了。为什么会这样呢?自然地,我们有理由怀疑 A T 对它做了什么事情。所以这里有一个例子能 W 够测试那个猜测,它产生了较少的线程。代码经过了重新组织,使一个 Vect or 实现了 Runnabl e,而且那个 Vect or 容纳了数量众多的色块,并随机挑选一些进行更新。随后,我们创建大量这些 Vect or 对象,数量大 致取决于我们挑选的网格维数。结果便是我们得到比色块少得多的线程。所以假如有一个速度的加快,我们 就能立即知道,因为前例的线程数量太多了。如下所示: //: C or Boxes 2. j ava ol // Bal anci ng t hr ead us e i m t j ava. aw . *; por t i m t j ava. aw . event . * ; por t i m t j ava. ut i l . * ; por cl as s C Box2 ext ends C anvas { 532

pr i vat e s t at i c f i nal C or [ ] col or s = { ol C or . bl ack, C or . bl ue, C or . cyan, ol ol ol C or . dar kG ay, C or . gr ay, C or . gr een, ol r ol ol C or . l i ght G ay, C or . m ol r ol agent a, C or . or ange, C or . pi nk, C or . r ed, ol ol ol C or . w t e, C or . yel l ow ol hi ol }; pr i vat e C or cC or = new ol or ( ) ; ol ol C pr i vat e s t at i c f i nal C or new ol or ( ) { ol C r et ur n col or s [ ( i nt ) ( M h. r andom ) * col or s . l engt h) at ( ]; } voi d next C or ( ) { ol cC or = new ol or ( ) ; ol C r epai nt ( ) ; } publ i c voi d pai nt ( G aphi cs g) { r g. s et C or ( cC or ) ; ol ol D m i on s = get Si ze( ) ; i ens g. f i l l Rect ( 0, 0, s . w dt h, s . hei ght ) ; i } } cl as s C BoxVect o r ext ends Vect or i m em s Runnabl e { pl ent pr i vat e Thr ead t ; pr i vat e i nt paus e; publ i c C BoxVect or ( i nt paus e) { t hi s . paus e = paus e; t = new Thr ead( t hi s ) ; } publ i c voi d go( ) { t . s t ar t ( ) ; } publ i c voi d r un( ) { w l e( t r ue) { hi i nt i = ( i nt ) ( M h. r andom ) * s i ze( ) ) ; at ( ((C Box2) el em A ( i ) ) . next C or ( ) ; ent t ol try { t . s l eep( paus e) ; } cat ch( I nt er r upt edExcept i on e) { } } } } publ i c cl as s C or Boxes 2 ext ends Fr am { ol e pr i vat e C BoxVect or [ ] v; publ i c C or Boxes 2( i nt paus e, i nt gr i d) { ol s et Ti t l e( " C or Boxes 2" ) ; ol s et Layout ( new G i dLayout ( gr i d, gr i d) ) ; r v = new C BoxVect or [ gr i d] ; f or ( i nt i = 0; i < gr i d; i ++) v[ i ] = new C BoxVect or ( paus e) ; 533

f or ( i nt i = 0; i < gr i d * gr i d; i ++) { v[ i % gr i d] . ad em ( new C dEl ent Box2( ) ) ; add( ( C Box2) v[ i % gr i d] . l as t El em ( ) ) ; ent } f or ( i nt i = 0; i < gr i d; i ++) v[ i ] . go( ) ; addW ndow s t ener ( new W ndow dapt er ( ) { i Li i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } } ); } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai // Shor t er def aul t paus e t han C or Boxes : ol i nt paus e = 5; i nt gr i d = 8; i f ( ar gs . l engt h > 0) paus e = I nt eger . par s eI nt ( ar gs [ 0] ) ; i f ( ar gs . l engt h > 1) gr i d = I nt eger . par s eI nt ( ar gs [ 1] ) ; Fr am f = new C or Boxes 2( paus e, gr i d) ; e ol f . s et Si ze( 500, 400) ; f . s et Vi s i bl e( t r ue) ; } } ///: ~ 在 C or Boxes 2 中,我们创建了 C ol BoxVect or 的一个数组,并对其初始化,使其容下各个 C BoxVect or 网格。 每个网格都知道自己该“睡眠”多长的时间。随后为每个 C BoxVect or 都添加等量的 C box2 对象,而且将每个 Vect or 都告诉给 go( ) ,用它来启动自己的线程。 C Box2 类似 C Box——能用一种随机选择的颜色描绘自己。但那就是 C Box2 能够做的全部工作。所有涉及线程 的处理都已移至 C BoxVect or 进行。 C BoxVect or 也可以拥有继承的 Thr ead,并有一个类型为 Vect or 的成员对象。这样设计的好处就是 addEl em ( )和 el em A ( )方法可以获得特定的参数以及返回值类型,而不是只能获得常规 O ect (它们 ent ent t bj 的名字也可以变得更短)。然而,这里采用的设计表面上看需要较少的代码。除此以外,它会自动保留一个 Vect or 的其他所有行为。由于 el em A ( ) 需要大量进行“封闭”工作,用到许多括号,所以随着代码主体 ent t 的扩充,最终仍有可能需要大量代码。 和以前一样,在我们实现 Runnabl e 的时候,并没有获得与 Thr ead配套提供的所有功能,所以必须创建一个 新的 Thr ead,并将自己传递给它的构建器,以便正式“启动”——s t ar t ( )——一些东西。大家在 C BoxVect or 构建器和 go( ) 里都可以体会到这一点。r un( ) 方法简单地选择 Vect or 里的一个随机元素编号,并 为那个元素调用 next C or ( ),令其挑选一种新的随机颜色。 ol 运行这个程序时,大家会发现它确实变得更快,响应也更迅速(比如在中断它的时候,它能更快地停下 来)。而且随着网格尺寸的壮大,它也不会经常性地陷于“停顿”状态。因此,线程的处理又多了一项新的 考虑因素:必须随时检查自己有没有“太多的线程”(无论对什么程序和运行平台)。若线程太多,必须试 着使用上面介绍的技术,对程序中的线程数量进行“平衡”。如果在一个多线程的程序中遇到了性能上的问 题,那么现在有许多因素需要检查: ( 1) 对 s l eep,yi el d( )以及/或者 w t ( ) 的调用足够多吗? ai ( 2) s l eep( )的调用时间足够长吗? ( 3) 运行的线程数是不是太多? ( 4) 试过不同的平台和 JVM 吗? 象这样的一些问题是造成多线程应用程序的编制成为一种“技术活”的原因之一。

534

14. 6 总结
何时使用多线程技术,以及何时避免用它,这是我们需要掌握的重要课题。骼它的主要目的是对大量任务进 行有序的管理。通过多个任务的混合使用,可以更有效地利用计算机资源,或者对用户来说显得更方便。资 源均衡的经典问题是在 I O等候期间如何利用 C PU。至于用户方面的方便性,最经典的问题就是如何在一个长 时间的下载过程中监视并灵敏地反应一个“停止”(s t op)按钮的按下。 多线程的主要缺点包括: ( 1) 等候使用共享资源时造成程序的运行速度变慢。 ( 2) 对线程进行管理要求的额外 C PU开销。 ( 3) 复杂程度无意义的加大,比如用独立的线程来更新数组内每个元素的愚蠢主意。 ( 4) 漫长的等待、浪费精力的资源竞争以及死锁等多线程症状。 线程另一个优点是它们用“轻度”执行切换(100 条指令的顺序)取代了“重度”进程场景切换(1000 条指 令)。由于一个进程内的所有线程共享相同的内存空间,所以“轻度”场景切换只改变程序的执行和本地变 量。而在“重度”场景切换时,一个进程的改变要求必须完整地交换内存空间。 线程处理看来好象进入了一个全新的领域,似乎要求我们学习一种全新的程序设计语言——或者至少学习一 系列新的语言概念。由于大多数微机操作系统都提供了对线程的支持,所以程序设计语言或者库里也出现了 对线程的扩展。不管在什么情况下,涉及线程的程序设计: ( 1) 刚开始会让人摸不着头脑,要求改换我们传统的编程思路; ( 2) 其他语言对线程的支持看来是类似的。所以一旦掌握了线程的概念,在其他环境也不会有太大的困难。 尽管对线程的支持使 Java 语言的复杂程度多少有些增加,但请不要责怪 Java。毕竟,利用线程可以做许多 有益的事情。 多个线程可能共享同一个资源(比如一个对象里的内存),这是运用线程时面临的最大的一个麻烦。必须保 证多个线程不会同时试图读取和修改那个资源。这要求技巧性地运用 s ynchr oni z ed(同步)关键字。它是一 个有用的工具,但必须真正掌握它,因为假若操作不当,极易出现死锁。 除此以外,运用线程时还要注意一个非常特殊的问题。由于根据 Java 的设计,它允许我们根据需要创建任意 数量的线程——至少理论上如此(例如,假设为一项工程方面的有限元素分析创建数以百万的线程,这对 Java 来说并非实际)。然而,我们一般都要控制自己创建的线程数量的上限。因为在某些情况下,大量线程 会将场面变得一团糟,所以工作都会几乎陷于停顿。临界点并不象对象那样可以达到几千个,而是在 100 以 下。一般情况下,我们只创建少数几个关键线程,用它们解决某个特定的问题。这时数量的限制问题不大。 但在较常规的一些设计中,这一限制确实会使我们感到束手束脚。 大家要注意线程处理中一个不是十分直观的问题。由于采用了线程“调度”机制,所以通过在 r un( ) 的主循 环中插入对 s l eep( )的调用,一般都可以使自己的程序运行得更快一些。这使它对编程技巧的要求非常高, 特别是在更长的延迟似乎反而能提高性能的时候。当然,之所以会出现这种情况,是由于在正在运行的线程 准备进入“休眠”状态之前,较短的延迟可能造成“s l eep( ) 结束”调度机制的中断。这便强迫调度机制将其 中止,并于稍后重新启动,以便它能做完自己的事情,再进入休眠状态。必须多想一想,才能意识到事情真 正的麻烦程度。 本章遗漏的一件事情是一个动画例子,这是目前程序片最流行的一种应用。然而,Java JD 配套提供了解决 K 这个问题的一整套方案(并可播放声音),大家可到 j ava. s un. com的演示区域下载。此外,我们完全有理由 相信未来版本的 Java 会提供更好的动画支持——尽管目前的 W eb涌现出了与传统方式完全不同的非 Java、 非程序化的许多动画方案。如果想系统学习 Java 动画的工作原理,可参考《C e Java——核心 Java》一 or 书,由 C nel l & s t m 编著,Pr ent i ce- Hal l 于 1997 年出版。若欲更深入地了解线程处理,请参考 or Hor ann 《C oncur r ent Pr ogr am i ng i n Java——Java 中的并发编程》,由 D m oug Lea 编著,A s on- W s el ey 于 ddi i 1997 年出版;或者《Java Thr eads——Java 线程》,O & ong编著,O Rei l l y 于 1997 年出版。 aks W '

14. 7 练习
( 1) 从 Thr ead继承一个类,并(过载)覆盖 r un( ) 方法。在 r un( ) 内,打印出一条消息,然后调用 s l eep() 。重复三遍这些操作,然后从 r un( ) 返回。在构建器中放置一条启动消息,并覆盖 f i nal i ze( ),打印 一条关闭消息。创建一个独立的线程类,使它在 r un( ) 内调用 Sys t em gc( )和 Sys t em r unFi nal i zat i on( ) , . . 并打印一条消息,表明调用成功。创建这两种类型的几个线程,然后运行它们,看看会发生什么。 ( 2) 修改 C ount er 2. j ava,使线程成为一个内部类,而且不需要明确保存指向 C ount er 2 的一个。 ( 3) 修改 Shar i ng2. j ava,在 Tw ount er 的 r un( ) 方法内部添加一个 s ynchr oni z ed oC (同步)块,而不是同步 整个 r un( ) 方法。 535

( 4) 创建两个 Thr ead子类,第一个的 r un( ) 方法用于最开始的启动,并捕获第二个 Thr ead对象的句柄,然 后调用 w t ( ) 。第二个类的 r un( ) 应在过几秒后为第一个线程调用 m f yA l ( ),使第一个线程能打印出一 ai odi l 条消息。 ( 5) 在 T i cker 2 内的 C ount er 5. j ava 中,删除 yi el d( ) ,并解释一下结果。用一个 s l eep( )换掉yi el d (),再 解释一下结果。 ( 6) 在 Thr eadG oup1. j ava中,将对 s ys . s us pend( ) 的调用换成对线程组的一个 w t ( ) 调用,令其等候2 秒 r ai 钟。为了保证获得正确的结果,必须在一个同步块内取得 s ys 的对象锁。 ( 7) 修改 D ons . j ava aem ,使 m n( ) 有一个 s l eep( ),而不是一个 r eadLi ne( ) 。实验不同的睡眠时间,看看会 ai 有什么发生。 ( 8) 到第 7 章(中间部分)找到那个 G eenhous eC r ol s . j ava例子,它应该由三个文件构成。在 r ont Event . j ava中,Event 类建立在对时间的监视基础上。修改这个 Event ,使其成为一个线程。然后修改其余 的设计,使它们能与新的、以线程为基础的 Event 正常协作。

536

第 15 章 网络编程
历史上的网络编程都倾向于困难、复杂,而且极易出错。 程序员必须掌握与网络有关的大量细节,有时甚至要对硬件有深刻的认识。一般地,我们需要理解连网协议 中不同的“层”(Layer )。而且对于每个连网库,一般都包含了数量众多的函数,分别涉及信息块的连接、 打包和拆包;这些块的来回运输;以及握手等等。这是一项令人痛苦的工作。 但是,连网本身的概念并不是很难。我们想获得位于其他地方某台机器上的信息,并把它们移到这儿;或者 相反。这与读写文件非常相似,只是文件存在于远程机器上,而且远程机器有权决定如何处理我们请求或者 发送的数据。 Java 最出色的一个地方就是它的“无痛苦连网”概念。有关连网的基层细节已被尽可能地提取出去,并隐藏 在 JVM以及 Java 的本机安装系统里进行控制。我们使用的编程模型是一个文件的模型;事实上,网络连接 (一个“套接字”)已被封装到系统对象里,所以可象对其他数据流那样采用同样的方法调用。除此以外, 在我们处理另一个连网问题——同时控制多个网络连接——的时候,Java 内建的多线程机制也是十分方便 的。 本章将用一系列易懂的例子解释 Java 的连网支持。

15. 1 机器的标识
当然,为了分辨来自别处的一台机器,以及为了保证自己连接的是希望的那台机器,必须有一种机制能独一 无二地标识出网络内的每台机器。早期网络只解决了如何在本地网络环境中为机器提供唯一的名字。但 Java 面向的是整个因特网,这要求用一种机制对来自世界各地的机器进行标识。为达到这个目的,我们采用了 I P (互联网地址)的概念。I P 以两种形式存在着: ( 1) 大家最熟悉的 D S(域名服务)形式。我自己的域名是 br uceeckel . com N 。所以假定我在自己的域内有一 台名为 O 的计算机,它的域名就可以是 O . br uceeckel . com pus pus 。这正是大家向其他人发送电子函件时采用 的名字,而且通常集成到一个万维网(W W W )地址里。 ( 2) 此外,亦可采用“四点”格式,亦即由点号(. )分隔的四组数字,比如 202. 98. 32. 111。 不管哪种情况,I P 地址在内部都表达成一个由 32 个二进制位(bi t )构成的数字(注释①),所以 I P 地址 的每一组数字都不能超过 255。利用由 j ava. net 提供的 s t at i c I net A es s . get ByN e( ),我们可以让一个 ddr am 特定的 Java 对象表达上述任何一种形式的数字。结果是类型为 I net A es s 的一个对象,可用它构成一个 ddr “套接字”(Socket ),大家在后面会见到这一点。 ①:这意味着最多只能得到 40 亿左右的数字组合,全世界的人很快就会把它用光。但根据目前正在研究的新 I P 编址方案,它将采用 128 bi t 的数字,这样得到的唯一性 I P 地址也许在几百年的时间里都不会用完。 作为运用 I net A es s . get ByN e( ) 一个简单的例子,请考虑假设自己有一家拨号连接因特网服务提供者 ddr am (I SP),那么会发生什么情况。每次拨号连接的时候,都会分配得到一个临时 I P 地址。但在连接期间,那 个 I P 地址拥有与因特网上其他 I P 地址一样的有效性。如果有人按照你的 I P 地址连接你的机器,他们就有可 能使用在你机器上运行的 W eb或者 FT P 服务器程序。当然这有个前提,对方必须准确地知道你目前分配到的 I P。由于每次拨号连接获得的 I P 都是随机的,怎样才能准确地掌握你的 I P 呢? 下面这个程序利用 I net A es s . get ByN e( ) 来产生你的 I P 地址。为了让它运行起来,事先必须知道计算机 ddr am 的名字。该程序只在 W ndow 95 中进行了测试,但大家可以依次进入自己的“开始”、“设置”、“控制面 i s 板”、“网络”,然后进入“标识”卡片。其中,“计算机名称”就是应在命令行输入的内容。 //: W m . j ava hoA I // Fi nds out your net w k addr es s w or hen you' r e // connect ed t o t he I nt er net . package c15; i m t j ava. net . *; por publ i c cl as s W m { hoA I publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) ai 537

t hr ow Except i on { s i f ( ar gs . l engt h ! = 1) { Sys t em er r . pr i nt l n( . " Us age: W m M hoA I achi neN e" ) ; am Sys t em exi t ( 1) ; . } I net A es s a = ddr I net A es s . get ByN e( ar gs [ 0] ) ; ddr am Sys t em out . pr i nt l n( a) ; . } } ///: ~ 就我自己的情况来说,机器的名字叫作“C os s us ”(来自同名电影,“巨人”的意思。我在这台机器上有 ol 一个很大的硬盘)。所以一旦连通我的 I SP,就象下面这样执行程序: j ava w m C os s us hoA I ol 得到的结果象下面这个样子(当然,这个地址可能每次都是不同的): C os s us /202. 98. 41. 151 ol 假如我把这个地址告诉一位朋友,他就可以立即登录到我的个人 W eb服务器,只需指定目标地址 ht t p: //202. 98. 41. 151 即可(当然,我此时不能断线)。有些时候,这是向其他人发送信息或者在自己的 W eb站点正式出台以前进行测试的一种方便手段。

1 5 . 1 . 1 服务器和客户机
网络最基本的精神就是让两台机器连接到一起,并相互“交谈”或者“沟通”。一旦两台机器都发现了对 方,就可以展开一次令人愉快的双向对话。但它们怎样才能“发现”对方呢?这就象在游乐园里那样:一台 机器不得不停留在一个地方,侦听其他机器说:“嘿,你在哪里呢?” “停留在一个地方”的机器叫作“服务器”(Ser ver );到处“找人”的机器则叫作“客户机”(C i ent ) l 或者“客户”。它们之间的区别只有在客户机试图同服务器连接的时候才显得非常明显。一旦连通,就变成 了一种双向通信,谁来扮演服务器或者客户机便显得不那么重要了。 所以服务器的主要任务是侦听建立连接的请求,这是由我们创建的特定服务器对象完成的。而客户机的任务 是试着与一台服务器建立连接,这是由我们创建的特定客户机对象完成的。一旦连接建好,那么无论在服务 器端还是客户机端,连接只是魔术般地变成了一个 I O数据流对象。从这时开始,我们可以象读写一个普通的 文件那样对待连接。所以一旦建好连接,我们只需象第 10 章那样使用自己熟悉的 I O命令即可。这正是 Java 连网最方便的一个地方。 1. 在没有网络的前提下测试程序 由于多种潜在的原因,我们可能没有一台客户机、服务器以及一个网络来测试自己做好的程序。我们也许是 在一个课堂环境中进行练习,或者写出的是一个不十分可靠的网络应用,还能拿到网络上去。I P 的设计者注 意到了这个问题,并建立了一个特殊的地址——l ocal hos t ——来满足非网络环境中的测试要求。在 Java 中 产生这个地址最一般的做法是: I net A es s addr = I net A es s . get ByN e( nul l ) ; ddr ddr am 如果向 get ByN e( ) 传递一个 nul l (空)值,就默认为使用 l ocal hos t 。我们用 I net A es s 对特定的机器 am ddr 进行索引,而且必须在进行进一步的操作之前得到这个 I net A es s (互联网地址)。我们不可以操纵一个 ddr I net A es s 的内容(但可把它打印出来,就象下一个例子要演示的那样)。创建 I net A es s 的唯一途径 ddr ddr 就是那个类的 s t at i c(静态)成员方法 get ByN e( )(这是最常用的)、get A l ByN e( )或者 am l am get Local Hos t ( ) 。 为得到本地主机地址,亦可向其直接传递字串" l ocal hos t " : I net A es s . get ByN e( " l ocal hos t " ) ; ddr am 或者使用它的保留 I P 地址(四点形式),就象下面这样: I net A es s . get ByN e( " 127. 0. 0. 1" ) ; ddr am 这三种方法得到的结果是一样的。

538

1 5 . 1 . 2 端口:机器内独一无二的场所
有些时候,一个 I P 地址并不足以完整标识一个服务器。这是由于在一台物理性的机器中,往往运行着多个服 务器(程序)。由 I P 表达的每台机器也包含了“端口”(Por t )。我们设置一个客户机或者服务器的时候, 必须选择一个无论客户机还是服务器都认可连接的端口。就象我们去拜会某人时,I P 地址是他居住的房子, 而端口是他在的那个房间。 注意端口并不是机器上一个物理上存在的场所,而是一种软件抽象(主要是为了表述的方便)。客户程序知 道如何通过机器的 I P 地址同它连接,但怎样才能同自己真正需要的那种服务连接呢(一般每个端口都运行着 一种服务,一台机器可能提供了多种服务,比如 HTTP 和 FT P 等等)?端口编号在这里扮演了重要的角色,它 是必需的一种二级定址措施。也就是说,我们请求一个特定的端口,便相当于请求与那个端口编号关联的服 务。“报时”便是服务的一个典型例子。通常,每个服务都同一台特定服务器机器上的一个独一无二的端口 编号关联在一起。客户程序必须事先知道自己要求的那项服务的运行端口号。 系统服务保留了使用端口 1 到端口 1024 的权力,所以不应让自己设计的服务占用这些以及其他任何已知正在 使用的端口。本书的第一个例子将使用端口 8080(为追忆我的第一台机器使用的老式 8 位 I nt el 8080 芯 片,那是一部使用 C P/M操作系统的机子)。

15. 2 套接字
“套接字”或者“插座”(Socket )也是一种软件形式的抽象,用于表达两台机器间一个连接的“终端”。 针对一个特定的连接,每台机器上都有一个“套接字”,可以想象它们之间有一条虚拟的“线缆”。线缆的 每一端都插入一个“套接字”或者“插座”里。当然,机器之间的物理性硬件以及电缆连接都是完全未知 的。抽象的基本宗旨是让我们尽可能不必知道那些细节。 在 Java 中,我们创建一个套接字,用它建立与其他机器的连接。从套接字得到的结果是一个 I nput St r eam以 及 O put St r eam ut (若使用恰当的转换器,则分别是 Reader 和 W i t er ),以便将连接作为一个 I O流对象对 r 待。有两个基于数据流的套接字类:Ser ver Socket ,服务器用它“侦听”进入的连接;以及 Socket ,客户用 它初始一次连接。一旦客户(程序)申请建立一个套接字连接,Ser ver Socket 就会返回(通过 accept ( ) 方 法)一个对应的服务器端套接字,以便进行直接通信。从此时起,我们就得到了真正的“套接字-套接字” 连接,可以用同样的方式对待连接的两端,因为它们本来就是相同的!此时可以利用 get I nput St r eam )以及 ( get O put St r eam )从每个套接字产生对应的 I nput St r eam和 O put St r eam对象。这些数据流必须封装到缓 ut ( ut 冲区内。可按第 10 章介绍的方法对类进行格式化,就象对待其他任何流对象那样。 对于 Java 库的命名机制,Ser ver Socket (服务器套接字)的使用无疑是容易产生混淆的又一个例证。大家可 能认为 Ser ver Socket 最好叫作“Ser ver C onnect or ”(服务器连接器),或者其他什么名字,只是不要在其 中安插一个“Socket ”。也可能以为 Ser ver Socket 和 Socket 都应从一些通用的基础类继承。事实上,这两 种类确实包含了几个通用的方法,但还不够资格把它们赋给一个通用的基础类。相反,Ser ver Socket 的主要 任务是在那里耐心地等候其他机器同它连接,再返回一个实际的 Socket 。这正是“Ser ver Socket ”这个命名 不恰当的地方,因为它的目标不是真的成为一个 Socket ,而是在其他人同它连接的时候产生一个 Socket 对 象。 然而,Ser ver Socket 确实会在主机上创建一个物理性的“服务器”或者侦听用的套接字。这个套接字会侦听 进入的连接,然后利用 accept ( ) 方法返回一个“已建立”套接字(本地和远程端点均已定义)。容易混淆的 地方是这两个套接字(侦听和已建立)都与相同的服务器套接字关联在一起。侦听套接字只能接收新的连接 请求,不能接收实际的数据包。所以尽管 Ser ver Socket 对于编程并无太大的意义,但它确实是“物理性” 的。 创建一个 Ser ver Socket 时,只需为其赋予一个端口编号。不必把一个 I P 地址分配它,因为它已经在自己代 表的那台机器上了。但在创建一个 Socket 时,却必须同时赋予 I P 地址以及要连接的端口编号(另一方面, 从 Ser ver Socket . accept ( ) 返回的 Socket 已经包含了所有这些信息)。

1 5 . 2 . 1 一个简单的服务器和客户机程序
这个例子将以最简单的方式运用套接字对服务器和客户机进行操作。服务器的全部工作就是等候建立一个连 接,然后用那个连接产生的 Socket 创建一个 I np St r eam以及一个 O put St r eam ut ut 。在这之后,它从 I nput St r eam 读入的所有东西都会反馈给 O put St r eam ut ,直到接收到行中止(EN )为止,最后关闭连接。 D 客户机连接与服务器的连接,然后创建一个 O put St r eam ut 。文本行通过 O put St r eam ut 发送。客户机也会创 建一个 I nput St r eam ,用它收听服务器说些什么(本例只不过是反馈回来的同样的字句)。 服务器与客户机(程序)都使用同样的端口号,而且客户机利用本地主机地址连接位于同一台机器中的服务 539

器(程序),所以不必在一个物理性的网络里完成测试(在某些配置环境中,可能需要同真正的网络建立连 接,否则程序不能工作——尽管实际并不通过那个网络通信)。 下面是服务器程序: //: Jabber Ser ver . j ava // Ver y s i m e s er ver t hat j us t pl // echoes w ever t he cl i ent s ends . hat i m t j ava. i o. * ; por i m t j ava. net . *; por publ i c cl as s Jabber Ser ver { // C hoos e a por t out s i de of t he r ange 1- 1024: publ i c s t at i c f i nal i nt PO = 8080; RT publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) ai t hr ow I O s Except i on { Ser ver Socket s = new Ser ver Socket ( PO ; RT) Sys t em out . pr i nt l n( " St ar t ed: " + s ) ; . try { // Bl ocks unt i l a connect i on occur s : Socket s ocket = s . accept ( ) ; try { Sys t em out . pr i nt l n( . "C onnect i on accept ed: " + s ocket ) ; Buf f er edReader i n = new Buf f er edReader ( new I nput St r eam Reader ( s ocket . get I nput St r eam ) ) ) ; ( // O put i s aut om i cal l y f l us hed ut at // by Pr i nt W i t er : r Pr i nt W i t er out = r new Pr i nt W i t er ( r new Buf f er edW i t er ( r new O put St r eam r i t er ( ut W s ocket . get O put St r eam ) ) ) , t r ue) ; ut ( w l e ( t r ue) { hi St r i ng s t r = i n. r eadLi ne( ) ; i f ( s t r . equal s ( " EN " ) ) br eak; D Sys t em out . pr i nt l n( " Echoi ng: " + s t r ) ; . out . pr i nt l n( s t r ) ; } // A w l ays cl os e t he t w s ocket s . . . o } f i nal l y { Sys t em out . pr i nt l n( " cl os i ng. . . " ) ; . s ocket . cl os e( ) ; } } f i nal l y { s . cl os e( ) ; } } } ///: ~ 可以看到,Ser ver Socket 需要的只是一个端口编号,不需要 I P 地址(因为它就在这台机器上运行)。调用 540

accept ( ) 时,方法会暂时陷入停顿状态(堵塞),直到某个客户尝试同它建立连接。换言之,尽管它在那里 等候连接,但其他进程仍能正常运行(参考第 14 章)。建好一个连接以后,accept ( ) 就会返回一个 Socket 对象,它是那个连接的代表。 清除套接字的责任在这里得到了很艺术的处理。假如 Ser ver Socket 构建器失败,则程序简单地退出(注意必 须保证 Ser ver Socket 的构建器在失败之后不会留下任何打开的网络套接字)。针对这种情况,m n( )会 ai “掷”出一个 I O Except i on 违例,所以不必使用一个 t r y 块。若 Ser ver Socket 构建器成功执行,则其他所有 方法调用都必须到一个 t r y- f i nal l y 代码块里寻求保护,以确保无论块以什么方式留下,Ser ver Socket 都能 正确地关闭。 同样的道理也适用于由 accept ( ) 返回的 Socket 。若 accept ( ) 失败,那么我们必须保证 Socket 不再存在或者 含有任何资源,以便不必清除它们。但假若执行成功,则后续的语句必须进入一个 t r y- f i nal l y 块内,以保 障在它们失败的情况下,Socket 仍能得到正确的清除。由于套接字使用了重要的非内存资源,所以在这里必 须特别谨慎,必须自己动手将它们清除(Java 中没有提供“破坏器”来帮助我们做这件事情)。 无论 Ser ver Socket 还是由 accept ( ) 产生的 Socket 都打印到 Sys t em out 里。这意味着它们的 t o r i ng方法 . St 会得到自动调用。这样便产生了: Ser ver Socket [ addr =0. 0. 0. 0, PO RT=0, l ocal por t =8080] Socket [ addr =127. 0. 0. 1, PO RT=1077, l ocal por t =8080] 大家不久就会看到它们如何与客户程序做的事情配合。 程序的下一部分看来似乎仅仅是打开文件,以便读取和写入,只是 I nput St r eam O put St r eam 和 ut 是从 Socket 对象创建的。利用两个“转换器”类 I nput St r eam Reader 和 O put St r eam r i t er ,I nput St r eam ut W 和 O put St r eam ut 对象已经分别转换成为 Java 1. 1 的 Reader 和 W i t er 对象。也可以直接使用 Java1. 0 的 r I nput St r eam O put St r eam 和 ut 类,但对输出来说,使用 W i t er 方式具有明显的优势。这一优势是通过 r Pr i nt W i t er 表现出来的,它有一个过载的构建器,能获取第二个参数——一个布尔值标志,指向是否在每 r 一次 pr i nt l n( ) 结束的时候自动刷新输出(但不适用于 pr i nt ( )语句)。每次写入了输出内容后(写进 out ),它的缓冲区必须刷新,使信息能正式通过网络传递出去。对目前这个例子来说,刷新显得尤为重要, 因为客户和服务器在采取下一步操作之前都要等待一行文本内容的到达。若刷新没有发生,那么信息不会进 入网络,除非缓冲区满(溢出),这会为本例带来许多问题。 编写网络应用程序时,需要特别注意自动刷新机制的使用。每次刷新缓冲区时,必须创建和发出一个数据包 (数据封)。就目前的情况来说,这正是我们所希望的,因为假如包内包含了还没有发出的文本行,服务器 和客户机之间的相互“握手”就会停止。换句话说,一行的末尾就是一条消息的末尾。但在其他许多情况 下,消息并不是用行分隔的,所以不如不用自动刷新机制,而用内建的缓冲区判决机制来决定何时发送一个 数据包。这样一来,我们可以发出较大的数据包,而且处理进程也能加快。 注意和我们打开的几乎所有数据流一样,它们都要进行缓冲处理。本章末尾有一个练习,清楚展现了假如我 们不对数据流进行缓冲,那么会得到什么样的后果(速度会变慢)。 无限 w l e 循环从 Buf f er edReader i n 内读取文本行,并将信息写入 Sys t em out ,然后写入 hi . Pr i nt W i t er . out 。注意这可以是任何数据流,它们只是在表面上同网络连接。 r 客户程序发出包含了" EN " 的行后,程序会中止循环,并关闭 Socket 。 D 下面是客户程序的源码: //: Jabber C i ent . j ava l // Ver y s i m e cl i ent t hat j us t s ends pl // l i nes t o t he s er ver and r eads l i nes // t hat t he s er ver s ends . i m t j ava. net . *; por i m t j ava. i o. * ; por publ i c cl as s Jabber C i ent { l publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) ai t hr ow I O s Except i on { // Pas s i ng nul l t o get ByN e( ) pr oduces t he am // s peci al " Local Loopback" I P addr es s , f or 541

// t es t i ng on one m achi ne w a net w k: /o or I net A es s addr = ddr I net A es s . get ByN e( nul l ) ; ddr am // A t er nat i vel y, you can us e l // t he addr es s or nam e: // I net A es s addr = ddr // I net A es s . get ByN e( " 127. 0. 0. 1" ) ; ddr am // I net A es s addr = ddr // I net A es s . get ByN e( " l ocal hos t " ) ; ddr am Sys t em out . pr i nt l n( " addr = " + addr ) ; . Socket s ocket = new Socket ( addr , Jabber Ser ver . PO ; RT) // G d ever yt hi ng i n a t r y- f i nal l y t o m uar ake // s ur e t hat t he s ocket i s cl os ed: try { Sys t em out . pr i nt l n( " s ocket = " + s ocket ) ; . Buf f er edReader i n = new Buf f er edReader ( new I nput St r eam Reader ( s ocket . get I nput St r eam ) ) ) ; ( // O put i s aut om i cal l y f l us hed ut at // by Pr i nt W i t er : r Pr i nt W i t er out = r new Pr i nt W i t er ( r new Buf f er edW i t er ( r new O put St r eam r i t er ( ut W s ocket . get O put St r eam ) ) ) , t r ue) ; ut ( f or ( i nt i = 0; i < 10; i ++) { out . pr i nt l n( " how " + i ) ; dy St r i ng s t r = i n. r eadLi ne( ) ; Sys t em out . pr i nt l n( s t r ) ; . } out . pr i nt l n( " EN " ) ; D } f i nal l y { Sys t em out . pr i nt l n( " cl os i ng. . . " ) ; . s ocket . cl os e( ) ; } } } ///: ~ 在 m n( ) 中,大家可看到获得本地主机 I P 地址的 I net A es s 的三种途径:使用 nul l ,使用 l ocal hos t , ai ddr 或者直接使用保留地址 127. 0. 0. 1。当然,如果想通过网络同一台远程主机连接,也可以换用那台机器的 I P 地址。打印出 I net A es s addr 后(通过对 t oSt r i ng( )方法的自动调用),结果如下: ddr l ocal hos t /127. 0. 0. 1 通过向 get ByN e( ) 传递一个 nul l ,它会默认寻找 l ocal hos t ,并生成特殊的保留地址 127. 0. 0. 1。注意在名 am 为 s ocket 的套接字创建时,同时使用了 I net A es s 以及端口号。打印这样的某个 Socket 对象时,为了真 ddr 正理解它的含义,请记住一次独一无二的因特网连接是用下述四种数据标识的:cl i ent Hos t (客户主机)、 cl i ent Por t N ber (客户端口号)、s er ver Hos t (服务主机)以及 s er ver Por t N ber (服务端口号)。服务 um um 程序启动后,会在本地主机(127. 0. 0. 1)上建立为它分配的端口(8080)。一旦客户程序发出请求,机器上 下一个可用的端口就会分配给它(这种情况下是 1077),这一行动也在与服务程序相同的机器 (127. 0. 0. 1)上进行。现在,为了使数据能在客户及服务程序之间来回传送,每一端都需要知道把数据发到 哪里。所以在同一个“已知”服务程序连接的时候,客户会发出一个“返回地址”,使服务器程序知道将自 542

己的数据发到哪儿。我们在服务器端的示范输出中可以体会到这一情况: Socket [ addr =127. 0. 0. 1, por t =1077, l ocal por t =8080] 这意味着服务器刚才已接受了来自 127. 0. 0. 1 这台机器的端口 1077 的连接,同时监听自己的本地端口 (8080)。而在客户端: Socket [ addr =l ocal hos t /127. 0. 0. 1, PO RT=8080, l ocal por t =1077] 这意味着客户已用自己的本地端口 1077 与 127. 0. 0. 1 机器上的端口 8080 建立了 连接。 大家会注意到每次重新启动客户程序的时候,本地端口的编号都会增加。这个编号从 1025(刚好在系统保留 的 1- 1024 之外)开始,并会一直增加下去,除非我们重启机器。若重新启动机器,端口号仍然会从 1025 开 始增值(在 Uni x 机器中,一旦超过保留的套按字范围,数字就会再次从最小的可用数字开始)。 创建好 Socket 对象后,将其转换成 Buf f er edReader 和 Pr i nt W i t er 的过程便与在服务器中相同(同样地, r 两种情况下都要从一个 Socket 开始)。在这里,客户通过发出字串" how ,并在后面跟随一个数字,从而 dy" 初始化通信。注意缓冲区必须再次刷新(这是自动发生的,通过传递给 Pr i nt W i t er 构建器的第二个参 r 数)。若缓冲区没有刷新,那么整个会话(通信)都会被挂起,因为用于初始化的“how dy”永远不会发送出 去(缓冲区不够满,不足以造成发送动作的自动进行)。从服务器返回的每一行都会写入 Sys t em out ,以验 . 证一切都在正常运转。为中止会话,需要发出一个" EN " 。若客户程序简单地挂起,那么服务器会“掷”出一 D 个违例。 大家在这里可以看到我们采用了同样的措施来确保由 Socket 代表的网络资源得到正确的清除,这是用一个 t r y- f i nal l y 块实现的。 套接字建立了一个“专用”连接,它会一直持续到明确断开连接为止(专用连接也可能间接性地断开,前提 是某一端或者中间的某条链路出现故障而崩溃)。这意味着参与连接的双方都被锁定在通信中,而且无论是 否有数据传递,连接都会连续处于开放状态。从表面看,这似乎是一种合理的连网方式。然而,它也为网络 带来了额外的开销。本章后面会介绍进行连网的另一种方式。采用那种方式,连接的建立只是暂时的。

15. 3 服务多个客户
Jabber Ser ver 可以正常工作,但每次只能为一个客户程序提供服务。在典型的服务器中,我们希望同时能处 理多个客户的请求。解决这个问题的关键就是多线程处理机制。而对于那些本身不支持多线程的语言,达到 这个要求无疑是异常困难的。通过第 14 章的学习,大家已经知道 Java 已对多线程的处理进行了尽可能的简 化。由于 Java 的线程处理方式非常直接,所以让服务器控制多名客户并不是件难事。 最基本的方法是在服务器(程序)里创建单个 Ser ver Socket ,并调用 accept ( ) 来等候一个新连接。一旦 accept ( ) 返回,我们就取得结果获得的 Socket ,并用它新建一个线程,令其只为那个特定的客户服务。然后 再调用 accept ( ) ,等候下一次新的连接请求。 对于下面这段服务器代码,大家可发现它与 Jabber Ser ver . j ava例子非常相似,只是为一个特定的客户提供 服务的所有操作都已移入一个独立的线程类中: //: M t i Jabber Ser ver . j ava ul // A s er ver t hat us es m t i t hr eadi ng t o handl e ul // any num ber of cl i ent s . i m t j ava. i o. * ; por i m t j ava. net . *; por cl as s Ser veO neJabber ext ends Thr ead { pr i vat e Socket s ocket ; pr i vat e Buf f er edReader i n; pr i vat e Pr i nt W i t er out ; r publ i c Ser veO neJabber ( Socket s ) t hr ow I O s Except i on { s ocket = s ; in = new Buf f er edReader ( new I nput St r eam Reader ( s ocket . get I nput St r eam ) ) ) ; ( // Enabl e aut o- f l us h: 543

out = new Pr i nt W i t er ( r new Buf f er edW i t er ( r new O put St r eam r i t er ( ut W s ocket . get O put St r eam ) ) ) , t r ue) ; ut ( // I f any of t he above cal l s t hr ow an // except i on, t he cal l er i s r es pons i bl e f or // cl os i ng t he s ocket . O her w s e t he t hr ead t i // w l l cl os e i t . i s t ar t ( ) ; // C l s r un( ) al } publ i c voi d r un( ) { try { w l e ( t r ue) { hi St r i ng s t r = i n. r eadLi ne( ) ; i f ( s t r . equal s ( " EN " ) ) br eak; D Sys t em out . pr i nt l n( " Echoi ng: " + s t r ) ; . out . pr i nt l n( s t r ) ; } Sys t em out . pr i nt l n( " cl os i ng. . . " ) ; . } cat ch ( I O Except i on e) { } f i nal l y { try { s ocket . cl os e( ) ; } cat ch( I O Except i on e) { } } } } publ i c cl as s M t i Jabber Ser ver { ul s t at i c f i nal i nt PO = 8080; RT publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) ai t hr ow I O s Except i on { Ser ver Socket s = new Ser ver Socket ( PO ; RT) Sys t em out . pr i nt l n( " Ser ver St ar t ed" ) ; . try { w l e( t r ue) { hi // Bl ocks unt i l a connect i on occur s : Socket s ocket = s . accept ( ) ; try { new Ser veO neJabber ( s ocket ) ; } cat ch( I O Except i on e) { // I f i t f ai l s , cl os e t he s ocket , // ot her w s e t he t hr ead w l l cl os e i t : i i s ocket . cl os e( ) ; } } } f i nal l y { s . cl os e( ) ; } }

544

} ///: ~ 每次有新客户请求建立一个连接时,Ser veO neJabber 线程都会取得由 accept ( ) 在 m i n( ) 中生成的 Socket 对 a 象。然后和往常一样,它创建一个 Buf f er edReader ,并用 Socket 自动刷新 Pr i nt W i t er 对象。最后,它调 r 用 Thr ead的特殊方法 s t ar t ( ),令其进行线程的初始化,然后调用 r un( ) 。这里采取的操作与前例是一样 的:从套扫字读入某些东西,然后把它原样反馈回去,直到遇到一个特殊的" EN " 结束标志为止。 D 同样地,套接字的清除必须进行谨慎的设计。就目前这种情况来说,套接字是在 Ser veO neJabber 外部创建 的,所以清除工作可以“共享”。若 Ser veO neJabber 构建器失败,那么只需向调用者“掷”出一个违例即 可,然后由调用者负责线程的清除。但假如构建器成功,那么必须由 Ser veO neJabber 对象负责线程的清除, 这是在它的 r un( ) 里进行的。 请注意 M t i Jabber Ser ver 有多么简单。和以前一样,我们创建一个 Ser ver Socket ,并调用 a ul ccep ()允许一 t 个新连接的建立。但这一次,accept ( ) 的返回值(一个套接字)将传递给用于 Ser veO neJabber 的构建器,由 它创建一个新线程,并对那个连接进行控制。连接中断后,线程便可简单地消失。 如果 Ser ver Socket 创建失败,则再一次通过 m n( )掷出违例。如果成功,则位于外层的 t r y- f i nal l y 代码 ai 块可以担保正确的清除。位于内层的 t r y- cat ch 块只负责防范 Ser veO neJabber 构建器的失败;若构建器成 功,则 Ser veO neJabber 线程会将对应的套接字关掉。 为了证实服务器代码确实能为多名客户提供服务,下面这个程序将创建许多客户(使用线程),并同相同的 服务器建立连接。每个线程的“存在时间”都是有限的。一旦到期,就留出空间以便创建一个新线程。允许 创建的线程的最大数量是由 f i nal i nt m hr eads 决定的。大家会注意到这个值非常关键,因为假如把它设 axt 得很大,线程便有可能耗尽资源,并产生不可预知的程序错误。 //: M t i Jabber C i ent . j ava ul l // C i ent t hat t es t s t he M t i Jabber Ser ver l ul // by s t ar t i ng up m t i pl e cl i ent s . ul i m t j ava. net . *; por i m t j ava. i o. * ; por cl as s Jabber C i ent Thr ead ext ends Thr ead { l pr i vat e Socket s ocket ; pr i vat e Buf f er edReader i n; pr i vat e Pr i nt W i t er out ; r pr i vat e s t at i c i nt count er = 0; pr i vat e i nt i d = count er ++; pr i vat e s t at i c i nt t hr eadcount = 0; publ i c s t at i c i nt t hr eadC ount ( ) { r et ur n t hr eadcount ; } p i c Jabber C i ent Thr ead( I net A es s addr ) { ubl l ddr Sys t em out . pr i nt l n( " M ng cl i ent " + i d) ; . aki t hr eadcount ++; try { s ocket = new Socket ( addr , M t i Jabber Ser ver . PO ; ul RT) } cat ch( I O Except i on e) { // I f t he cr eat i on of t he s ocket f ai l s , // not hi ng needs t o be cl eaned up. } try { in = new Buf f er edReader ( new I nput St r eam Reader ( s ocket . get I nput St r eam ) ) ) ; ( 545

// Enabl e aut o f l us h: out = new Pr i nt W i t er ( r new Buf f er edW i t er ( r new O put St r eam r i t er ( ut W s ocket . get O put St r eam ) ) ) , t r ue) ; ut ( s t ar t ( ) ; } cat ch( I O Except i on e) { // The s ocket s houl d be cl os ed on any // f ai l ur es ot her t han t he s ocket // cons t r uct or : try { s ocket . cl os e( ) ; } cat ch( I O Except i on e2) { } } // O her w s e t he s ocket w l l be cl os ed by t i i // t he r un( ) m hod of t he t hr ead. et } publ i c voi d r un( ) { try { f or ( i nt i = 0; i < 25; i ++) { out . pr i nt l n( " C i ent " + i d + " : " + i ) ; l St r i ng s t r = i n. r eadLi ne( ) ; Sys t em out . pr i nt l n( s t r ) ; . } out . pr i nt l n( " EN " ) ; D } cat ch( I O Except i on e) { } f i nal l y { // A w l ays cl os e i t : try { s ocket . cl os e( ) ; } cat ch( I O Except i on e) { } t hr eadcount - - ; // Endi ng t hi s t hr ead } } } publ i c cl as s M t i Jabber C i ent { ul l s t at i c f i nal i nt M X_THREA S = 40; A D publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) ai t hr ow I O s Except i on, I nt er r upt edExcept i on { I net A es s addr = ddr I net A es s . get ByN e( nul l ) ; ddr am w l e( t r ue) { hi i f ( Jabber C i ent Thr ead. t hr eadC l ount ( ) < M X_THREA S) A D new Jabber C i ent Thr ead( addr ) ; l Thr ead. cur r ent Thr ead( ) . s l eep( 100) ; } } } ///: ~

546

Jabber C i ent Thr ead 构建器获取一个 I net A es s,并用它打开一个套接字。大家可能已看出了这样的一个 l ddr 套路:Socket 肯定用于创建某种 Reader 以及/或者 W i t er (或者 I nput St r eam r 和/或 O put St r eam ut )对 象,这是运用 Socket 的唯一方式(当然,我们可考虑编写一、两个类,令其自动完成这些操作,避免大量重 复的代码编写工作)。同样地,s t ar t ( ) 执行线程的初始化,并调用 r un( )。在这里,消息发送给服务器,而 来自服务器的信息则在屏幕上回显出来。然而,线程的“存在时间”是有限的,最终都会结束。注意在套接 字创建好以后,但在构建器完成之前,假若构建器失败,套接字会被清除。否则,为套接字调用 cl os e( )的 责任便落到了 r un( ) 方法的头上。 t hr eadcount 跟踪计算目前存在的 Jabber C i ent Thr ead对象的数量。它将作为构建器的一部分增值,并在 l r un( ) 退出时减值(r un( ) 退出意味着线程中止)。在 M t i Jabber C i ent . m n( ) 中,大家可以看到线程的数 ul l ai 量会得到检查。若数量太多,则多余的暂时不创建。方法随后进入“休眠”状态。这样一来,一旦部分线程 最后被中止,多作的那些线程就可以创建了。大家可试验一下逐渐增大 M X_THREA S,看看对于你使用的系 A D 统来说,建立多少线程(连接)才会使您的系统资源降低到危险程度。

15. 4 数据报
大家迄今看到的例子使用的都是“传输控制协议”(T C P),亦称作“基于数据流的套接字”。根据该协议的 设计宗旨,它具有高度的可靠性,而且能保证数据顺利抵达目的地。换言之,它允许重传那些由于各种原因 半路“走失”的数据。而且收到字节的顺序与它们发出来时是一样的。当然,这种控制与可靠性需要我们付 出一些代价:T C 具有非常高的开销。 P 还有另一种协议,名为“用户数据报协议”(UD P),它并不刻意追求数据包会完全发送出去,也不能担保它 们抵达的顺序与它们发出时一样。我们认为这是一种“不可靠协议”(T C 当然是“可靠协议”)。听起来 P 似乎很糟,但由于它的速度快得多,所以经常还是有用武之地的。对某些应用来说,比如声音信号的传输, 如果少量数据包在半路上丢失了,那么用不着太在意,因为传输的速度显得更重要一些。大多数互联网游 戏,如 D abl o,采用的也是 UD 协议通信,因为网络通信的快慢是游戏是否流畅的决定性因素。也可以想想 i P 一台报时服务器,如果某条消息丢失了,那么也真的不必过份紧张。另外,有些应用也许能向服务器传回一 条 UD 消息,以便以后能够恢复。如果在适当的时间里没有响应,消息就会丢失。 P Java 对数据报的支持与它对 T C 套接字的支持大致相同,但也存在一个明显的区别。对数据报来说,我们在 P 客户和服务器程序都可以放置一个 D agr am at Socket (数据报套接字),但与 Ser ver Socket 不同,前者不会 干巴巴地等待建立一个连接的请求。这是由于不再存在“连接”,取而代之的是一个数据报陈列出来。另一 项本质的区别的是对 T C 套接字来说,一旦我们建好了连接,便不再需要关心谁向谁“说话”——只需通过 P 会话流来回传送数据即可。但对数据报来说,它的数据包必须知道自己来自何处,以及打算去哪里。这意味 着我们必须知道每个数据报包的这些信息,否则信息就不能正常地传递。 D agr am at Socket 用于收发数据包,而 D agr am at Packet 包含了具体的信息。准备接收一个数据报时,只需提 供一个缓冲区,以便安置接收到的数据。数据包抵达时,通过 D agr am at Socket ,作为信息起源地的因特网地 址以及端口编号会自动得到初化。所以一个用于接收数据报的 D agr am at Packet 构建器是: D agr am at Packet ( buf , buf . l engt h) 其中,buf 是一个字节数组。既然 buf 是个数组,大家可能会奇怪为什么构建器自己不能调查出数组的长度 呢?实际上我也有同感,唯一能猜到的原因就是 C风格的编程使然,那里的数组不能自己告诉我们它有多 大。 可以重复使用数据报的接收代码,不必每次都建一个新的。每次用它的时候(再生),缓冲区内的数据都会 被覆盖。 缓冲区的最大容量仅受限于允许的数据报包大小,这个限制位于比 64KB 稍小的地方。但在许多应用程序中, 我们都宁愿它变得还要小一些,特别是在发送数据的时候。具体选择的数据包大小取决于应用程序的特定要 求。 发出一个数据报时,D agr am at Packet 不仅需要包含正式的数据,也要包含因特网地址以及端口号,以决定它 的目的地。所以用于输出 D agr am at Packet 的构建器是: D agr am at Packet ( buf , l engt h, i net A es s , por t ) ddr 这一次,buf (一个字节数组)已经包含了我们想发出的数据。 l engt h可以是 buf 的长度,但也可以更短一 些,意味着我们只想发出那么多的字节。另两个参数分别代表数据包要到达的因特网地址以及目标机器的一 个目标端口(注释②)。 ②:我们认为 T C 和 UD 端口是相互独立的。也就是说,可以在端口 8080 同时运行一个 T C 和 UD 服务程 P P P P 序,两者之间不会产生冲突。 547

大家也许认为两个构建器创建了两个不同的对象:一个用于接收数据报,另一个用于发送它们。如果是好的 面向对象的设计方案,会建议把它们创建成两个不同的类,而不是具有不同的行为的一个类(具体行为取决 于我们如何构建对象)。这也许会成为一个严重的问题,但幸运的是,D agr am at Packet 的使用相当简单,我 们不需要在这个问题上纠缠不清。这一点在下例里将有很明确的说明。该例类似于前面针对 T C 套接字的 P M t i Jabber Ser ver 和 M t i Jabber C i ent 例子。多个客户都会将数据报发给服务器,后者会将其反馈回最 ul ul l 初发出消息的同样的客户。 为简化从一个 St r i ng里创建 D agr am at Packet 的工作(或者从 D agr am at Packet 里创建 St r i ng),这个例子 首先用到了一个工具类,名为 D am gr : //: D am j ava gr . // A ut i l i t y cl as s t o conver t back and f or t h / / Bet w een St r i ngs and D aG am at r Packet s . i m t j ava. net . *; por publ i c cl as s D am { gr publ i c s t at i c D agr am at Packet t oD agr am at ( St r i ng s , I net A es s des t I A i nt des t Por t ) { ddr , // D ecat ed i n Java 1. 1, but i t w ks : epr or byt e[ ] buf = new byt e[ s . l engt h( ) + 1] ; s . get Byt es ( 0, s . l engt h( ) , buf , 0) ; // The cor r ect Java 1. 1 appr oach, but i t ' s // Br oken ( i t t r uncat es t he St r i ng) : // byt e[ ] buf = s . get Byt es ( ) ; r et ur n new D agr am at Packet ( buf , buf . l engt h, des t I A des t Por t ) ; , } publ i c s t at i c St r i ng t oSt r i ng( D agr am at Packet p) { // The Java 1. 0 appr oach: // r et ur n new St r i ng( p. get D a( ) , at // 0, 0, p. get Lengt h( ) ) ; // The Java 1. 1 appr oach: r et ur n new St r i ng( p. get D a( ) , 0, p. get Lengt h( ) ) ; at } } ///: ~ D am的第一个方法采用一个 St r i ng、一个 I net A es s 以及一个端口号作为自己的参数,将 St r i ng的内容 gr ddr 复制到一个字节缓冲区,再将缓冲区传递进入 D agr am at Packet 构建器,从而构建一个 D agr am at Packet 。注 意缓冲区分配时的" +1" ——这对防止截尾现象是非常重要的。St r i ng的 get Byt e( ) 方法属于一种特殊操作, 能将一个字串包含的 char 复制进入一个字节缓冲。该方法现在已被“反对”使用;Java 1. 1 有一个“更 好”的办法来做这个工作,但在这里却被当作注释屏蔽掉了,因为它会截掉 St r i ng的部分内容。所以尽管我 们在 Java 1. 1 下编译该程序时会得到一条“反对”消息,但它的行为仍然是正确无误的(这个错误应该在你 读到这里的时候修正了)。 D am t oSt r i ng( )方法同时展示了 Java 1. 0 的方法和 Java 1. 1 的方法(两者是不同的,因为有一种新类型 gr . 的 St r i ng构建器)。 下面是用于数据报演示的服务器代码: //: C t er Ser ver . j ava hat // A s er ver t hat echoes dat agr am s i m t j ava. net . *; por i m t j ava. i o. * ; por i m t j ava. ut i l . *; por 548

publ i c cl as s C t er Ser ver { hat s t at i c f i nal i nt I N RT = 1711; PO pr i vat e byt e[ ] buf = new byt e[ 1000] ; pr i vat e D agr am at Packet dp = new D agr am at Packet ( buf , buf . l engt h) ; // C l i s t en & s end on t he s am s ocket : an e pr i vat e D agr am at Socket s ocket ; publ i c C t er Ser ver ( ) { hat try { s ocket = new D agr am at Socket ( I N RT) ; PO Sys t em out . pr i nt l n( " Ser ver s t ar t ed" ) ; . w l e( t r ue) { hi // Bl ock unt i l a dat agr am appear s : s ocket . r ecei ve( dp) ; St r i ng r cvd = D am t o r i ng( dp) + gr . St " , f r om addr es s : " + dp. get A es s ( ) + ddr " , por t : " + dp. get Por t ( ) ; Sys t em out . pr i nt l n( r cvd) ; . St r i ng echoSt r i ng = " Echoed: " + r cvd; // Ext r act t he addr es s and por t f r om t he // r ecei ved dat agr am t o f i nd out w e t o her // s end i t back: D agr am at Packet echo = D am t oD agr am echoSt r i ng, gr . at ( dp. get A es s ( ) , dp. get Por t ( ) ) ; ddr s ocket . s end( echo) ; } } cat ch( Socket Except i on e) { Sys t em er r . pr i nt l n( " C t open s ocket " ) ; . an' Sys t em exi t ( 1) ; . } cat ch( I O Except i on e) { Sys t em er r . pr i nt l n( " C m cat i on er r or " ) ; . om uni e. pr i nt St ackTr ace( ) ; } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai new C t er Ser ver ( ) ; hat } } ///: ~ C t e Ser ver 创建了一个用来接收消息的 D agr am hat r at Socket (数据报套接字),而不是在我们每次准备接收 一条新消息时都新建一个。这个单一的 D agr am at Socket 可以重复使用。它有一个端口号,因为这属于服务 器,客户必须确切知道自己把数据报发到哪个地址。尽管有一个端口号,但没有为它分配因特网地址,因为 它就驻留在“这”台机器内,所以知道自己的因特网地址是什么(目前是默认的 l ocal hos t )。在无限 w l e hi 循环中,套接字被告知接收数据(r ecei ve( ) )。然后暂时挂起,直到一个数据报出现,再把它反馈回我们希 望的接收人——D agr am at Packet dp ——里面。数据包(Packet )会被转换成一个字串,同时插入的还有数据 包的起源因特网地址及套接字。这些信息会显示出来,然后添加一个额外的字串,指出自己已从服务器反馈 回来了。 大家可能会觉得有点儿迷惑。正如大家会看到的那样,许多不同的因特网地址和端口号都可能是消息的起源 549

地——换言之,客户程序可能驻留在任何一台机器里(就这一次演示来说,它们都驻留在 l ocal hos t 里,但 每个客户使用的端口编号是不同的)。为了将一条消息送回它真正的始发客户,需要知道那个客户的因特网 地址以及端口号。幸运的是,所有这些资料均已非常周到地封装到发出消息的 D agr am at Packet 内部,所以我 们要做的全部事情就是用 get A es s ( ) 和 get Por t ( ) 把它们取出来。利用这些资料,可以构建 ddr D agr am at Packet echo——它通过与接收用的相同的套接字发送回来。除此以外,一旦套接字发出数据报,就 会添加“这”台机器的因特网地址及端口信息,所以当客户接收消息时,它可以利用 get A es s ( )和 ddr get Por t ( ) 了解数据报来自何处。事实上,get A es s ( )和 get Por t ( ) 唯一不能告诉我们数据报来自何处的前 ddr 提是:我们创建一个待发送的数据报,并在正式发出之前调用了 get A es s ( ) 和 get Por t ( ) 。到数据报正式 ddr 发送的时候,这台机器的地址以及端口才会写入数据报。所以我们得到了运用数据报时一项重要的原则:不 必跟踪一条消息的来源地!因为它肯定保存在数据报里。事实上,对程序来说,最可靠的做法是我们不要试 图跟踪,而是无论如何都从目标数据报里提取出地址以及端口信息(就象这里做的那样)。 为测试服务器的运转是否正常,下面这程序将创建大量客户(线程),它们都会将数据报包发给服务器,并 等候服务器把它们原样反馈回来。 //: C t er C i ent . j ava hat l // Tes t s t he C t er Ser ver by s t ar t i ng m t i pl e hat ul // cl i ent s , each of w ch s ends dat agr am . hi s i m t j ava. l ang. Thr ead; por i m t j ava. net . *; por i m t j ava. i o. * ; por publ i c cl as s C t er C i ent ext ends Thr ead { hat l // C l i st en & s end on t he s am s ocket : an e pr i vat e D agr am at Socket s ; pr i vat e I net A es s hos t A es s ; ddr ddr pr i vat e byt e[ ] buf = new byt e[ 1000] ; pr i vat e D agr am at Packet dp = new D agr am at Packet ( buf , buf . l engt h) ; pr i vat e i nt i d; publ i c C t er C i ent ( i nt i dent i f i er ) { hat l i d = i dent i f i er ; try { // A o- as s i gn por t num : ut ber s = new D agr am at Socket ( ) ; hos t A es s = ddr I net A es s . get ByN e( " l ocal hos t " ) ; ddr am } cat ch( Unknow nHos t Except i on e) { Sys t em er r . pr i nt l n( " C . annot f i nd hos t " ) ; Sys t em exi t ( 1) ; . } cat ch( Socket Except i on e) { Sys t em er r . pr i nt l n( " C t open s ocket " ) ; . an' e. pr i nt St ackTr ace( ) ; Sys t em exi t ( 1) ; . } Sys t em out . pr i nt l n( " C t er C i ent s t ar t i ng" ) ; . hat l } publ i c voi d r un( ) { try { f or ( i nt i = 0; i < 25; i ++) { St r i ng out M s age = " C i ent #" + es l i d + " , m s age # + i ; es " 550

// M ake and s end a dat agr am : s . s end( D am t oD agr am out M s age, gr . at ( es hos t A es s , ddr C t er Ser ver . I N RT) ) ; hat PO // Bl ock unt i l i t echoes back: s . r ecei ve( dp) ; // Pr i nt out t he echoed cont ent s : St r i ng r cvd = " C i ent #" + i d + l " , r cvd f r om " + dp. get A es s ( ) + " , " + ddr dp. get Por t ( ) + " : " + D am t oSt r i ng( dp) ; gr . Sys t em out . pr i nt l n( r cvd) ; . } } cat ch( I O Except i on e) { e. pr i nt St ackTr ace( ) ; Sys t em exi t ( 1) ; . } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai f or ( i nt i = 0; i < 10; i ++) new C t er C i ent ( i ) . s t ar t ( ) ; hat l } } ///: ~ C t er C i ent 被创建成一个线程(Thr ead),所以可以用多个客户来“骚扰”服务器。从中可以看到,用 hat l 于接收的 D agr am at Packet 和用于 C t er Ser ver 的那个是相似的。在构建器中,创建 D agr am hat at Packet 时没 有附带任何参数(自变量),因为它不需要明确指出自己位于哪个特定编号的端口里。用于这个套接字的因 特网地址将成为“这台机器”(比如 l ocal hos t ),而且会自动分配端口编号,这从输出结果即可看出。同 用于服务器的那个一样,这个 D agr am at Packet 将同时用于发送和接收。 hos t A es s 是我们想与之通信的那台机器的因特网地址。在程序中,如果需要创建一个准备传出去的 ddr D agr am at Packet ,那么必须知道一个准确的因特网地址和端口号。可以肯定的是,主机必须位于一个已知的 地址和端口号上,使客户能启动与主机的“会话”。 每个线程都有自己独一无二的标识号(尽管自动分配给线程的端口号是也会提供一个唯一的标识符)。在 r un( ) 中,我们创建了一个 St r i ng消息,其中包含了线程的标识编号以及该线程准备发送的消息编号。我们 用这个字串创建一个数据报,发到主机上的指定地址;端口编号则直接从 C t er Ser ver 内的一个常数取 hat 得。一旦消息发出,r ecei ve( ) 就会暂时被“堵塞”起来,直到服务器回复了这条消息。与消息附在一起的所 有信息使我们知道回到这个特定线程的东西正是从始发消息中投递出去的。在这个例子中,尽管是一种“不 可靠”协议,但仍然能够检查数据报是否到去过了它们该去的地方(这在 l ocal hos t 和 LA N环境中是成立 的,但在非本地连接中却可能出现一些错误)。 运行该程序时,大家会发现每个线程都会结束。这意味着发送到服务器的每个数据报包都会回转,并反馈回 正确的接收者。如果不是这样,一个或更多的线程就会挂起并进入“堵塞”状态,直到它们的输入被显露出 来。 大家或许认为将文件从一台机器传到另一台的唯一正确方式是通过 T C 套接字,因为它们是“可靠”的。然 P 而,由于数据报的速度非常快,所以它才是一种更好的选择。我们只需将文件分割成多个数据报,并为每个 包编号。接收机器会取得这些数据包,并重新“组装”它们;一个“标题包”会告诉机器应该接收多少个 包,以及组装所需的另一些重要信息。如果一个包在半路“走丢”了,接收机器会返回一个数据报,告诉发 送者重传。

15. 5 一个 W 应用 eb
现在让我们想想如何创建一个应用,令其在真实的 W eb环境中运行,它将把 Java 的优势表现得淋漓尽致。这 个应用的一部分是在 W eb服务器上运行的一个 Java 程序,另一部分则是一个“程序片”或“小应用程序” 551

(A et ),从服务器下载至浏览器(即“客户”)。这个程序片从用户那里收集信息,并将其传回 W ppl eb服 务器上运行的应用程序。程序的任务非常简单:程序片会询问用户的 E- m l 地址,并在验证这个地址合格后 ai (没有包含空格,而且有一个@ 符号),将该 E- m l 发送给 W ai eb服务器。服务器上运行的程序则会捕获传回 的数据,检查一个包含了所有 E- m l 地址的数据文件。如果那个地址已包含在文件里,则向浏览器反馈一条 ai 消息,说明这一情况。该消息由程序片负责显示。若是一个新地址,则将其置入列表,并通知程序片已成功 添加了电子函件地址。 若采用传统方式来解决这个问题,我们要创建一个包含了文本字段及一个“提交”(Subm t )按钮的 HTM i L 页。用户可在文本字段里键入自己喜欢的任何内容,并毫无阻碍地提交给服务器(在客户端不进行任何检 查)。提交数据的同时,W eb页也会告诉服务器应对数据采取什么样的操作——知会“通用网关接口” (C I )程序,收到这些数据后立即运行服务器。这种 C I 程序通常是用 Per l 或 C写的(有时也用 C G G ++,但 要求服务器支持),而且必须能控制一切可能出现的情况。它首先会检查数据,判断是否采用了正确的格 式。若答案是否定的,则 C I 程序必须创建一个 HTM 页,对遇到的问题进行描述。这个页会转交给服务器, G L 再由服务器反馈回用户。用户看到出错提示后,必须再试一遍提交,直到通过为止。若数据正确,C I 程序 G 会打开数据文件,要么把电子函件地址加入文件,要么指出该地址已在数据文件里了。无论哪种情况,都必 须格式化一个恰当的 HTM 页,以便服务器返回给用户。 L 作为 Java 程序员,上述解决问题的方法显得非常笨拙。而且很自然地,我们希望一切工作都用 Java 完成。 首先,我们会用一个 Java 程序片负责客户端的数据有效性校验,避免数据在服务器和客户之间传来传去,浪 费时间和带宽,同时减轻服务器额外构建 HTM 页的负担。然后跳过 Per l C I 脚本,换成在服务器上运行一 L G 个 Java 应用。事实上,我们在这儿已完全跳过了 W eb服务器,仅仅需要从程序片到服务器上运行的 Java 应 用之间建立一个连接即可。 正如大家不久就会体验到的那样,尽管看起来非常简单,但实际上有一些意想不到的问题使局面显得稍微有 些复杂。用 Java 1. 1 写程序片是最理想的,但实际上却经常行不通。到本书写作的时候,拥有 Java 1. 1 能 力的浏览器仍为数不多,而且即使这类浏览器现在非常流行,仍需考虑照顾一下那些升级缓慢的人。所以从 安全的角度看,程序片代码最好只用 Java 1. 0 编写。基于这一前提,我们不能用 JA R文件来合并(压缩)程 序片中的. cl as s 文件。所以,我们应尽可能减少. cl as s 文件的使用数量,以缩短下载时间。 好了,再来说说我用的 W eb服务器(写这个示范程序时用的就是它)。它确实支持 Java,但仅限于 Java 1. 0!所以服务器应用也必须用 Java 1. 0 编写。

1 5 . 5 . 1 服务器应用
现在讨论一下服务器应用(程序)的问题,我把它叫作 N eC l ecor (名字收集器)。假如多名用户同时尝 am ol 试提交他们的 E- m l 地址,那么会发生什么情况呢?若 N eC l ect or 使用 TC P套接字,那么必须运用 ai am ol P/I 早先介绍的多线程机制来实现对多个客户的并发控制。但所有这些线程都试图把数据写到同一个文件里,其 中保存了所有 E- m l 地址。这便要求我们设立一种锁定机制,保证多个线程不会同时访问那个文件。一个 ai “信号机”可在这里帮助我们达到目的,但或许还有一种更简单的方式。 如果我们换用数据报,就不必使用多线程了。用单个数据报即可“侦听”进入的所有数据报。一旦监视到有 进入的消息,程序就会进行适当的处理,并将答复数据作为一个数据报传回原先发出请求的那名接收者。若 数据报半路上丢失了,则用户会注意到没有答复数据传回,所以可以重新提交请求。 服务器应用收到一个数据报,并对它进行解读的时候,必须提取出其中的电子函件地址,并检查本机保存的 数据文件,看看里面是否已经包含了那个地址(如果没有,则添加之)。所以我们现在遇到了一个新的问 题。Java 1. 0 似乎没有足够的能力来方便地处理包含了电子函件地址的文件(Java 1. 1 则不然)。但是,用 C轻易就可以解决这个问题。因此,我们在这儿有机会学习将一个非 Java 程序同 Java 程序连接的最简便方 式。程序使用的 Runt i m e对象包含了一个名为 exec( ) 的方法,它会独立机器上一个独立的程序,并返回一个 Pr oces s(进程)对象。我们可以取得一个 O put St r eam ut ,它同这个单独程序的标准输入连接在一起;并取 得一个 I nput St r eam ,它则同标准输出连接到一起。要做的全部事情就是用任何语言写一个程序,只要它能 从标准输入中取得自己的输入数据,并将输出结果写入标准输出即可。如果有些问题不能用 Java 简便与快速 地解决(或者想利用原有代码,不想改写),就可以考虑采用这种方法。亦可使用 Java 的“固有方法” (N i ve M hod),但那要求更多的技巧,大家可以参考一下附录 A at et 。 1. C程序 这个非 Java 应用是用 C写成,因为 Java 不适合作 C I 编程;起码启动的时间不能让人满意。它的任务是管 G 理电子函件(E- m l )地址的一个列表。标准输入会接受一个 E- m l 地址,程序会检查列表中的名字,判断 ai ai 是否存在那个地址。若不存在,就将其加入,并报告操作成功。但假如名字已在列表里了,就需要指出这一 552

点,避免重复加入。大家不必担心自己不能完全理解下列代码的含义。它仅仅是一个演示程序,告诉你如何 用其他语言写一个程序,并从 Java 中调用它。在这里具体采用何种语言并不重要,只要能够从标准输入中读 取数据,并能写入标准输出即可。 //: Li s t m . c gr // Us ed by N eC l ect or . j ava t o m am ol anage // t he em l l i s t f i l e on t he s er ver ai # ncl ude i # ncl ude i #i ncl ude #def i ne BSI ZE 250 i nt al r eadyI nLi s t ( FI LE* l i s t , char * nam { e) char l buf [ BSI ZE] ; // G t o t he begi nni ng of t he l i s t : o f s eek( l i s t , 0, SEEK_SET) ; // Read each l i ne i n t he l i s t : w l e( f get s ( l buf , BSI ZE, l i s t ) ) { hi // St r i p of f t he new i ne: l char * new i ne = s t r chr ( l buf , ' \n' ) ; l i f ( new i ne ! = 0) l *new i ne = ' \0' ; l i f ( s t r cm l buf , nam == 0) p( e) r et ur n 1; } r et ur n 0; } i nt m n( ) { ai char buf [ BSI ZE] ; FI LE* l i s t = f open( " em i s t . t xt " , " a+t " ) ; l i f ( l i s t == 0) { per r or ( " coul d not open em i s t . t xt " ) ; l exi t ( 1) ; } w l e( 1) { hi get s ( buf ) ; /* Fr om s t di n */ i f ( al r eadyI nLi s t ( l i s t , buf ) ) { pr i nt f ( " A r eady i n l i s t : % " , buf ) ; l s f f l us h( s t dout ) ; } el s e { f s eek( l i s t , 0, SEEK_EN ) ; D f pr i nt f ( l i s t , " % \n" , buf ) ; s f f l us h( l i s t ) ; pr i nt f ( " % added t o l i s t " , buf ) ; s f f l us h( s t dout ) ; } } } ///: ~ 该程序假设 C编译器能接受' //' 样式注释(许多编译器都能,亦可换用一个 C ++编译器来编译这个程序)。 553

如果你的编译器不能接受,则简单地将那些注释删掉即可。 文件中的第一个函数检查我们作为第二个参数(指向一个 char 的指针)传递给它的名字是否已在文件中。在 这儿,我们将文件作为一个 FI LE 指针传递,它指向一个已打开的文件(文件是在 m n( ) 中打开的)。函数 ai f s eek( )在文件中遍历;我们在这儿用它移至文件开头。f get s ( )从文件 l i s t 中读入一行内容,并将其置入缓 冲区 l buf ——不会超过规定的缓冲区长度 BSI ZE。所有这些工作都在一个 w l e循环中进行,所以文件中的 hi 每一行都会读入。接下来,用 s t r chr ( ) 找到新行字符,以便将其删掉。最后,用 s t r cm ) 比较我们传递给函 p( 数的名字与文件中的当前行。若找到一致的内容,s t r cm ) 会返回 0。函数随后会退出,并返回一个 1,指出 p( 该名字已经在文件里了(注意这个函数找到相符内容后会立即返回,不会把时间浪费在检查列表剩余内容的 上面)。如果找遍列表都没有发现相符的内容,则函数返回 0。 在 m n( ) 中,我们用 f open( )打开文件。第一个参数是文件名,第二个是打开文件的方式;a+表示“追 ai 加”,以及“打开”(或“创建”,假若文件尚不存在),以便到文件的末尾进行更新。f open( )函数返回的 是一个 FI LE 指针;若为 0,表示打开操作失败。此时需要用 per r or ( ) 打印一条出错提示消息,并用 exi t ( ) 中止程序运行。 如果文件成功打开,程序就会进入一个无限循环。调用 get s ( buf ) 的函数会从标准输入中取出一行(记住标 准输入会与 Java 程序连接到一起),并将其置入缓冲区 buf 中。缓冲区的内容随后会简单地传递给 al r eadyI nLi s t ( )函数,如内容已在列表中,pr i nt f () 就会将那条消息发给标准输出(Java 程序正在监视 它)。f f l us h( ) 用于对输出缓冲区进行刷新。 如果名字不在列表中,就用 f s eek( )移到列表末尾,并用 f pr i nt f ( ) 将名字“打印”到列表末尾。随后,用 pr i nt f ( ) 指出名字已成功加入列表(同样需要刷新标准输出),无限循环返回,继续等候一个新名字的进 入。 记住一般不能先在自己的计算机上编译此程序,再把编译好的内容上载到 W eb服务器,因为那台机器使用的 可能是不同类的处理器和操作系统。例如,我的 W eb服务器安装的是 I nt el 的 C PU,但操作系统是 Li nux,所 以必须先下载源码,再用远程命令(通过 t el net )指挥 Li nux 自带的 C编译器,令其在服务器端编译好程 序。 2. Java程序 这个程序先启动上述的 C程序,再建立必要的连接,以便同它“交谈”。随后,它创建一个数据报套接字, 用它“监视”或者“侦听”来自程序片的数据报包。 //: N eC l ect or . j ava am ol // Ext r act s em l nam f r om dat agr am and s t or es ai es s // t hem i ns i de a f i l e, us i ng Java 1. 02. i m or t j ava. net . *; p i m t j ava. i o. * ; por i m t j ava. ut i l . * ; por publ i c cl as s N eC l ect or { am ol f i nal s t at i c i nt C LLEC R_PO = 8080; O TO RT f i nal s t at i c i nt BUFFER_SI ZE = 1000; byt e[ ] buf = new byt e[ BUFFER_SI ZE] ; D agr am at Packet dp = new D agr am at Packet ( buf , buf . l engt h) ; // C l i s t en & s end on t he s am s ocket : an e D agr am at Socket s ocket ; Pr oces s l i s t m ; gr Pr i nt St r eam nam s t ; eLi D aI nput St r eam addRes ul t ; at publ i c N eC l ect or ( ) { am ol try { l i st m = gr Runt i m get Runt i m ) . exec( " l i s t m . exe" ) ; e. e( gr nam s t = new Pr i nt St r eam eLi ( 554

new Buf f er edO put St r eam ut ( l i s t m . get O put St r eam ) ) ) ; gr ut ( addRes ul t = new D aI nput St r eam at ( new Buf f er edI nput St r eam ( l i s t m . get I nput St r eam ) ) ) ; gr ( } cat ch( I O Except i on e) { Sys t e . er r . pr i nt l n( m "C annot s t ar t l i s t m . exe" ) ; gr Sys t em exi t ( 1) ; . } try { s ocket = new D agr am at Socket ( C LLEC R_PO ; O TO RT) Sys t em out . pr i nt l n( . " N eC l ect or Ser ver s t ar t ed" ) ; am ol w l e( t r ue) { hi // Bl ock unt i l a dat agr am appear s : s ocket . r ecei ve( dp) ; St r i ng r cvd = new St r i ng( dp. get D a( ) , at 0, 0, dp. get Lengt h( ) ) ; // Send t o l i s t m . exe s t andar d i nput : gr nam s t . pr i nt l n( r cvd. t r i m ) ) ; eLi ( nam s t . f l us h( ) ; eLi byt e[ ] r es ul t Buf = new byt e[ BUFFER_SI ZE] ; i nt byt eC ount = addRes ul t . r ead( r es ul t Buf ) ; i f ( byt eC ount ! = - 1) { St r i ng r es ul t = new St r i ng( r es ul t Buf , 0) . t r i m ) ; ( // Ext r act t he addr es s and por t f r om // t he r ecei ved dat agr am t o f i nd out // w e t o s end t he r epl y: her I net A es s s ender A es s = ddr ddr dp. get A es s ( ) ; ddr i nt s ender Por t = dp. get Por t ( ) ; byt e[ ] echoBuf = new byt e[ BUFFER_SI ZE] ; r es ul t . get Byt es ( 0, byt eC ount , echoBuf , 0) ; D agr am at Packet echo = new D agr am at Packet ( echoBuf , echoBuf . l engt h, s ender A es s , s ender Por t ) ; ddr s ocket . s end( echo) ; } el s e Sys t em out . pr i nt l n( . " Unexpect ed l ack of r es ul t f r om " + " l i s t m . exe" ) ; gr } } cat ch( Socket Except i on e) { Sys t em er r . pr i nt l n( " C t open s ocket " ) ; . an' 555

Sys t em exi t ( 1) ; . } cat ch( I O Except i on e) { Sys t em er r . pr i nt l n( " C m cat i on er r or " ) ; . om uni e. pr i nt St ackTr ace( ) ; } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) { ai new N eC l ect or ( ) ; am ol } } ///: ~ N eC l ect or 中的第一个定义应该是大家所熟悉的:选定端口,创建一个数据报包,然后创建指向一个 am ol D agr am at Socket 的句柄。接下来的三个定义负责与 C程序的连接:一个 Pr oces s 对象是 C程序由 Java 程序 启动之后返回的,而且那个 Pr oces s 对象产生了 I nput St r eam和 O put St r eam ut ,分别代表 C程序的标准输出 和标准输入。和 Java I O一样,它们理所当然地需要“封装”起来,所以我们最后得到的是一个 Pr i nt St r eam D aI nput St r eam 和 at 。 这个程序的所有工作都是在构建器内进行的。为启动 C程序,需要取得当前的 Runt i m e对象。我们用它调用 exec( ),再由后者返回 Pr oces s 对象。在 Pr oces s 对象中,大家可看到通过一简单的调用即可生成数据流: get O put St r eam )和 get I nput St r eam )。从这个时候开始,我们需要考虑的全部事情就是将数据传给数据 ut ( ( 流 nam s t ,并从 addRes ul t 中取得结果。 eLi 和往常一样,我们将 D agr am at Socket 同一个端口连接到一起。在无限 w l e 循环中,程序会调用 hi r ecei ve( ) ——除非一个数据报到来,否则 r ecei ve( ) 会一起处于“堵塞”状态。数据报出现以后,它的内容 会提取到 St r i ng r cvd里。我们首先将该字串两头的空格剔除(t r i m ),再将其发给 C程序。如下所示: nam s t . pr i nt l n( r cvd. t r i m ) ) ; eLi ( 之所以能这样编码,是因为 Java 的 exec( ) 允许我们访问任何可执行模块,只要它能从标准输入中读,并能 向标准输出中写。还有另一些方式可与非 Java 代码“交谈”,这将在附录 A中讨论。 从 C程序中捕获结果就显得稍微麻烦一些。我们必须调用 r ead( ),并提供一个缓冲区,以便保存结果。 r ead( )的返回值是来自 C程序的字节数。若这个值为- 1,意味着某个地方出现了问题。否则,我们就将 r es ul t Buf (结果缓冲区)转换成一个字串,然后同样清除多余的空格。随后,这个字串会象往常一样进入一 个 D agr am at Packet ,并传回当初发出请求的那个同样的地址。注意发送方的地址也是我们接收到的 D agr am at Packet 的一部分。 记住尽管 C程序必须在 W eb服务器上编译,但 Java 程序的编译场所可以是任意的。这是由于不管使用的是什 么硬件平台和操作系统,编译得到的字节码都是一样的。就就是 Java 的“跨平台”兼容能力。

1 5 . 5 . 2 Nam ender 程序片 eS
正如早先指出的那样,程序片必须用 Java 1. 0 编写,使其能与绝大多数的浏览器适应。也正是由于这个原 因,我们产生的类数量应尽可能地少。所以我们在这儿不考虑使用前面设计好的 D am类,而将数据报的所 gr 有维护工作都转到代码行中进行。此外,程序片要用一个线程监视由服务器传回的响应信息,而非实现 Runnabl e 接口,用集成到程序片的一个独立线程来做这件事情。当然,这样做对代码的可读性不利,但却能 产生一个单类(以及单个服务器请求)程序片: / /: N eSender . j ava am // A appl et t hat s ends an em l addr es s n ai // as a dat agr am us i ng Java 1. 02. , i m t j ava. aw . *; por t i m t j ava. appl et . * ; por i m t j ava. net . *; por i m t j ava. i o. * ; por publ i c cl as s N eSender ext ends A et am ppl i m em s Runnabl e { pl ent pr i vat e Thr ead pl = nul l ; 556

pr i vat e But t on s end = new But t on( " A em l addr es s t o m l i ng l i s t " ) ; dd ai ai pr i vat e Text Fi el d t = new Text Fi el d( " t ype your em l addr es s her e" , 40) ; ai pr i vat e St r i ng s t r = new St r i ng( ) ; pr i vat e Label l = new Label ( ) , l 2 = new Label ( ) ; pr i vat e D agr am at Socket s ; pr i vat e I net A es s hos t A es s ; ddr ddr pr i vat e byt e[ ] buf = new byt e[ N eC l ect or . BUFFER_SI ZE] ; am ol pr i vat e D agr am at Packet dp = new D agr am at Packet ( buf , buf . l engt h) ; pr i vat e i nt vcount = 0; publ i c voi d i ni t ( ) { s et Layout ( new Bor der Layout ( ) ) ; Panel p = new Panel ( ) ; p. s et Layout ( new G i dLayout ( 2, 1) ) ; r p. add( t ) ; p. add( s end) ; add( " N t h" , p) ; or Panel l abel s = new Panel ( ) ; l abel s . s et Layout ( new G i dLayout ( 2, 1) ) ; r l abel s . add( l ) ; l abel s . add( l 2) ; add( " C er " , l abel s ) ; ent try { // A o- as s i gn por t num : ut ber s = new D agr am at Socket ( ) ; hos t A es s = I net A es s . get ByN e( ddr ddr am get C odeBas e( ) . get Hos t ( ) ) ; } cat ch( Unknow nHos t Except i on e) { l . s et Text ( " C annot f i nd hos t " ) ; } cat ch( Socket Except i on e) { l . s et Text ( " C t open s ocket " ) ; an' } l . s et Text ( " Ready t o s end your em l addr es s " ) ; ai } publ i c bool ean act i on ( Event evt , O ect ar g) { bj i f ( evt . t ar get . equal s ( s end) ) { i f ( pl ! = nul l ) { // pl . s t op( ) ; D ecat ed i n Java 1. 2 epr Thr ead r em ove = pl ; pl = nul l ; r em ove. i nt er r upt ( ) ; } l 2. s et Text ( " " ) ; // C heck f or er r or s i n em l nam ai e: s t r = t . get Text ( ) . t oLow C e( ) . t r i m ) ; er as ( i f ( s t r . i ndexO ( ' ' ) ! = - 1) { f l . s et Text ( " Spaces not al l ow i n nam ) ; ed e" r et ur n t r ue; 557

} i f ( s t r . i ndexO ( ' , ' ) ! = - 1) { f l . s et Text ( " C m not al l ow i n nam ) ; om as ed e" r et ur n t r ue; } i f ( s t r . i ndexO ( ' @ ) == - 1) { f ' l . s et Text ( " N e m t i ncl ude ' @ " ) ; am us ' l 2. s et Text ( " " ) ; r et ur n t r ue; } i f ( s t r . i ndexO ( ' @ ) == 0) { f ' l . s et Text ( " N e m t pr eceed ' @ " ) ; am us ' l 2. s et Text ( " " ) ; r et ur n t r ue; } St r i ng end = s t r . s ubs t r i ng( s t r . i ndexO ( ' @ ) ) ; f ' i f ( end. i ndexO ( ' . ' ) == - 1) { f l . s et Text ( " Por t i on af t er ' @ m t " + ' us " have an ext ens i on, s uch as ' . com " ) ; ' l 2. s et Text ( " " ) ; r et ur n t r ue; } // Ever yt hi ng' s O s o s end t he nam G a K, e. et // f r es h buf f er , s o i t ' s zer oed. For s om e // r eas on you m t us e a f i xed s i ze r at her us // t han cal cul at i ng t he s i z e dynam cal l y: i byt e[ ] s buf = new byt e[ N eC l ect or . BUFFER_SI ZE] ; am ol s t r . get Byt es ( 0, s t r . l engt h( ) , s buf , 0) ; D agr am at Packet t oSend = new D agr am at Packet ( s buf , 100, hos t A es s , ddr N eC l ect or . C LLEC R_PO ; am ol O TO RT) try { s . s end( t oSend) ; } cat ch( Except i on e) { l . s et Text ( " C dn' t s end dat agr am ) ; oul " r et ur n t r ue; } l . s et Text ( " Sent : " + s t r ) ; s end. s et Label ( " Re- s end" ) ; pl = new Thr ead( t hi s ) ; pl . s t ar t ( ) ; l 2. s et Text ( " W t i ng f or ver i f i cat i on " + ++vcount ) ; ai } el s e r et ur n s uper . act i on( evt , ar g) ; r et ur n t r ue; } // The t hr ead por t i on of t he appl et w ches f or at // t he r epl y t o com back f r om t he s er ver : e 558

publ i c voi d r un( ) { try { s . r ecei ve( dp) ; } cat ch( Except i on e) { l 2. s et Text ( " C dn' t r ecei ve dat agr am ) ; oul " r et ur n; } l 2. s et Text ( new St r i ng( dp. get D a( ) , at 0, 0, dp. get Lengt h( ) ) ) ; } } ///: ~ 程序片的 UI (用户界面)非常简单。它包含了一个 Tes t Fi el d(文本字段),以便我们键入一个电子函件地 址;以及一个 But t on(按钮),用于将地址发给服务器。两个 Label (标签)用于向用户报告状态信息。 到现在为止,大家已能判断出 D agr am at Socket 、I net A es s 、缓冲区以及 D agr am ddr at Packet 都属于网络连接 中比较麻烦的部分。最后,大家可看到 r un( ) 方法实现了线程部分,使程序片能够“侦听”由服务器传回的 响应信息。 i ni t ( )方法用大家熟悉的布局工具设置 G ,然后创建 D agr am UI at Socket ,它将同时用于数据报的收发。 act i on( ) 方法只负责监视我们是否按下了“发送”(s end)按钮。记住,我们已被限制在 Java 1. 0 上面,所 以不能再用较灵活的内部类了。按钮按下以后,采取的第一项行动便是检查线程 pl ,看看它是否为 nul l (空)。如果不为 nul l ,表明有一个活动线程正在运行。消息首次发出时,会启动一个新线程,用它监视来 自服务器的回应。所以假若有个线程正在运行,就意味着这并非用户第一次发送消息。pl 句柄被设为 nul l , 同时中止原来的监视者(这是最合理的一种做法,因为 s t op( ) 已被 Java 1. 2“反对”,这在前一章已解释过 了)。 无论这是否按钮被第一次按下,I 2 中的文字都会清除。 下一组语句将检查 E- m l 名字是否合格。St r i ng. i ndexO ( ) 方法的作用是搜索其中的非法字符。如果找到一 ai f 个,就把情况报告给用户。注意进行所有这些工作时,都不必涉及网络通信,所以速度非常快,而且不会影 响带宽和服务器的性能。 名字校验通过以后,它会打包到一个数据报里,然后采用与前面那个数据报示例一样的方式发到主机地址和 端口编号。第一个标签会发生变化,指出已成功发送出去。而且按钮上的文字也会改变,变成“重发” (r es end)。这时会启动线程,第二个标签则会告诉我们程序片正在等候来自服务器的回应。 线程的 r un( ) 方法会利用 N eSender 中包含的 D agr am am at Socket 来接收数据(r ecei ve( ) ),除非出现来自服 务器的数据报包,否则 r ecei ve( ) 会暂时处于“堵塞”或者“暂停”状态。结果得到的数据包会放进 N eSender 的 D agr am am at Packet dp中。数据会从包中提取出来,并置入 N eSender 的第二个标签。随后,线 am 程的执行将中断,成为一个“死”线程。若某段时间里没有收到来自服务器的回应,用户可能变得不耐烦, 再次按下按钮。这样做会中断当前线程(数据发出以后,会再建一个新的)。由于用一个线程来监视回应数 据,所以用户在监视期间仍然可以自由使用 UI 。 1. W eb页 当然,程序片必须放到一个 W eb页里。下面列出完整的 W eb页源码;稍微研究一下就可看出,我用它从自己 开办的邮寄列表(M l l i ng Li s t )里自动收集名字。 ai D O TEN l A Your s el f t o Br uce Eckel ' s Java M l i ng Li s t dd ai D D K=" K=" C LO N A Your s el f t o Br uce Eckel ' s Java M l i ng Li s t dd ai N 559

The appl et on t hi s page w l l aut om i cal l y add your em l addr es s t o t he m l i ng l i s t , s o you i at ai ai w l l r ecei ve updat e i nf or m i on about changes t o t he onl i ne ver s i on of " Thi nki ng i n Java, " i at not i f i cat i on w hen t he book i s i n pr i nt , i nf or m i on about upcom ng Java s em nar s , and at i i not i f i cat i on about t he “Hands- on Java Sem nar ” M t i m a C . Type i n your em l addr es s and i ul edi D ai pr es s t he but t on t o aut om i cal l y add your s el f t o t hi s m l i ng l i s t . at ai am i I f af t er s ever al t r i es , you do not get ver i f i cat i on i t m eans t hat t he Java appl i cat i on on t he s er ver i s havi ng pr obl em . I n t hi s cas e, you can add your s el f t o t he l i s t by s endi ng em l t o s ai bj 0) gr i d = I nt eger . par s eI nt ( ar gs [ 0] ) ; Fr am f = new BoxO er ver ( gr i d) ; e bs f . s et Si ze( 500, 400) ; f . s et Vi s i bl e( t r ue) ; f . addW ndow s t ener ( i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); } } cl as s O Box ext ends C C anvas i m em s O er ver { pl ent bs O er vabl e not i f i er ; bs i nt x, y; // Locat i ons i n gr i d C or cC or = new ol or ( ) ; ol ol C s t at i c f i nal C or [ ] col or s = { ol C or . bl ack, C or . bl ue, C or . cyan, ol ol ol C or . dar kG ay, C or . gr ay, C or . gr een, ol r ol ol C or . l i ght G ay, C or . m ol r ol agent a, C or . or ange, C or . pi nk, C or . r ed, ol ol ol C or . w t e, C or . yel l ow ol hi ol }; s t at i c f i nal C or new ol or ( ) { ol C r et ur n col or s [ ( i nt ) ( M h. r andom ) * col or s . l engt h) at ( ]; } O Box( i nt x, i nt y, O er vabl e not i f i er ) { C bs t hi s . x = x; t hi s . y = y; not i f i er . addO er ver ( t hi s ) ; bs t hi s . not i f i er = not i f i er ; addM eLi s t ener ( new M ) ) ; ous L( } publ i c voi d p nt ( G aphi cs g) { ai r g. s et C or ( cC or ) ; ol ol D m i on s = get Si ze( ) ; i ens g. f i l l Rect ( 0, 0, s . w dt h, s . hei ght ) ; i } cl as s M ext ends M eA L ous dapt er { publ i c voi d m ePr es s ed( M eEvent e) { ous ous not i f i er . not i f yO er ver s ( O Box. t hi s ) ; bs C 591

} } publ i c voi d updat e( O er vabl e o, O ect ar g) { bs bj O Box cl i cked = ( O Box) ar g; C C i f ( next To( cl i cked) ) { cC or = cl i cked. cC or ; ol ol r epai nt ( ) ; } } pr i vat e f i nal bool ean next To( O Box b) { C r et ur n M h. abs ( x - b. x) 0) { // G her dat a on t hos e you s ee at i nt t ot al Speed = 0; f l oat t ot al Bear i ng = 0. 0f ; f l oat di s t anceToN es t = 100000. 0f ; ear Beas t near es t Beas t = ( Beas t ) s een. el em A ( 0) ; ent t Enum at i on e = s een. el em s ( ) ; er ent w l e( e. has M eEl em s ( ) ) { hi or ent Beas t aBeas t = ( Beas t ) e. next El em ( ) ; ent t ot al Speed += aBeas t . cur r ent Speed; f l oat bear i ng = aBeas t . bear i ngFr om nt A ongA s ( Poi l xi x, y, cur r ent D r ect i on) ; i t ot al Bear i ng += bear i ng; f l oat di s t anceToBeas t = aBeas t . di s t anceFr om nt ( x, y) ; Poi i f ( di s t anceToBeas t < di s t anceToN es t ) { ear near es t Beas t = aBeas t ; di s t anceToN es t = di s t anceToBeas t ; ear } } // Rul e 1: M ch aver age s peed of t hos e at // i n t he l i s t : cur r ent Speed = t ot al Speed / s een. s i ze( ) ; // Rul e 2: M ove t ow ds t he per cei ved ar // cent er of gr avi t y of t he her d: cur r ent D r ect i on = i t ot al Bear i ng / s een. s i z e( ) ; // Rul e 3: M nt ai n a m ni m di s t ance ai i um // f r om t hos e ar ound you: 644

i f ( di s t anceToN es t f i el d. m axSpeed) { cur r ent Speed = f i el d. m axSpeed; } } } el s e { // You ar e i n f r ont , s o s l ow dow n cur r ent Speed = ( i nt ) ( cur r ent Speed * f i el d. decayRat e) ; } // M ake t he beas t m ove: x += ( i nt ) ( M h. cos ( cur r ent D r ect i on) at i * cur r ent Speed) ; y += ( i nt ) ( M h. s i n( cur r ent D r ect i on) at i * cur r ent Speed) ; x % f i el d. xExt ent ; = y % f i el d. yExt ent ; = i f ( x < 0) x += f i el d. xExt ent ; i f ( y < 0) y += f i el d. yExt ent ; } publ i c f l oat bear i ngFr om nt A ongA s ( Poi l xi i nt or i gi nX, i nt or i gi nY, f l oat axi s ) { / / Ret ur ns bear i ng angl e of t he cur r ent Beas t // i n t he w l d coor di ant e s ys t em or try { doubl e bear i ngI nRadi ans = M h. at an( at ( t hi s . y - or i gi nY) / ( t hi s . x - or i gi nX) ) ; // I nver s e t an has t w s ol ut i ons , s o you o // have t o cor r ect f or ot her quar t er s : i f ( x < or i gi nX) { i f ( y < or i gi nY) { bear i ngI nRadi ans += - ( f l oat ) M h. PI ; at } el s e { bear i ngI nRadi ans = ( f l oat ) M h. PI - bear i ngI nRadi ans ; at } } // Jus t s ubt r act t he axi s ( i n r adi ans ) : r et ur n ( f l oat ) ( axi s - bear i ngI nRadi ans ) ; } cat ch( A i t hm i cExcept i on aE) { r et // D vi de by 0 er r or pos s i bl e on t hi s i i f ( x > or i gi nX) { r et ur n 0; 645

} el s e r et ur n ( f l oat ) M h. PI ; at } } publ i c f l oat di s t anceFr om nt ( i nt x1, i nt y1) { Poi r et ur n ( f l oat ) M h. s qr t ( at M h. pow x1 - x, 2) + at ( M h. pow y1 - y, 2) ) ; at ( } publ i c Poi nt pos i t i on( ) { r et ur n new Poi nt ( x, y) ; } // Beas t s know how t o dr aw t hem el ves : s publ i c voi d dr aw G aphi cs g) { ( r g. s et C or ( col or ) ; ol i nt di r ect i onI nD ees = ( i nt ) ( egr ( cur r ent D r ect i on * 360) / ( 2 * M h. PI ) ) ; i at i nt s t ar t A e = di r ect i onI nD ees ngl egr Fi el dO Beas t s . hal f Fi el dO Vi ew f ; i nt endA e = 90; ngl g. f i l l A c( x, y, G ZE, G ZE, r SI SI s t ar t A e, endA e) ; ngl ngl } } publ i c cl as s Fi el dO Beas t s ext ends A et ppl i m em s Runnabl e { pl ent pr i vat e Vect or beas t s ; s t at i c f l oat f i el dO Vi ew = f ( f l oat ) ( M h. PI / 4) , // I n r adi ans at // D ecel er at i on % per s econd: decayRat e = 1. 0f , m ni m D s t ance = 10f ; // I n pi xel s i um i s t at i c i nt hal f Fi el dO Vi ew = ( i nt ) ( f ( f i el dO Vi ew * 360) / ( 2 * M h. PI ) ) , f at xExt ent = 0, yExt ent = 0, num Beas t s = 50, m axSpeed = 20; // Pi xel s /s econd bool ean uni queC or s = t r ue; ol Thr ead t hi s Thr ead; i nt del ay = 25; publ i c voi d i ni t ( ) { i f ( xExt ent == 0 & yExt ent == 0) { & xExt ent = I nt eger . par s eI nt ( get Par am er ( " xExt ent " ) ) ; et yExt ent = I nt eger . par s eI nt ( get Par am er ( " yExt ent " ) ) ; et } 646

beas t s = m akeBeas t Vect or ( num Beas t s , uni queC or s ) ; ol // N s t ar t t he beas t s a r ovi n' : ow t hi s T hr ead = new T hr ead( t hi s ) ; t hi s Thr ead. s t ar t ( ) ; } publ i c voi d r un( ) { w l e( t r ue) { hi f or ( i nt i = 0; i < beas t s . s i ze( ) ; i ++) { Beas t b = ( Beas t ) beas t s . el em A ( i ) ; ent t b. s t ep( ) ; } try { t hi s Thr ead. s l eep( del ay) ; } cat ch( I nt er r upt edExcept i on ex) { } r epai nt ( ) ; // O her w s e i t w t updat e t i on' } } Vect or m akeBea t Vect or ( s i nt quant i t y, bool ean uni queC or s ) { ol Vect or new Beas t s = new Vect or ( ) ; Random gener at or = new Random ) ; ( // Us ed onl y i f uni queC or s i s on: ol doubl e cubeRoot O Beas t N ber = f um M h. pow ( doubl e) num at ( Beas t s , 1. 0 / 3. 0) ; f l oat col or C ubeSt epSi ze = ( f l oat ) ( 1. 0 / cubeRoot O Beas t N ber ) ; f um f l oat r = 0. 0f ; f l oat g = 0. 0f ; f l oat b = 0. 0f ; f or ( i nt i = 0; i < quant i t y; i ++) { i nt x = ( i nt ) ( gener at or . next Fl oat ( ) * xExt ent ) ; i f ( x > xExt ent - Beas t . G ZE) SI x - = Beas t . G ZE; SI i nt y = ( i nt ) ( gener at or . next Fl oat ( ) * yExt ent ) ; i f ( y > yExt ent - Beas t . G ZE) SI y - = Beas t . G ZE; SI f l oat di r ect i on = ( f l oat ) ( gener at or . next Fl oat ( ) * 2 * M h. PI ) ; at i nt s peed = ( i nt ) ( gener at or . next Fl oat ( ) * ( f l oat ) m axSpeed) ; i f ( uni queC or s ) { ol r += col or C ubeSt epSi ze; i f ( r > 1. 0) { r - = 1. 0f ; g += col or C ubeSt epSi ze; i f ( g > 1. 0) { g - = 1. 0f ; b += col or C ubeSt epSi ze; i f ( b > 1. 0) 647

b - = 1. 0f ; } } } new Beas t s . addEl em ( ent new Beas t ( t hi s , x, y, di r ect i on, s peed, new C or ( r , g, b) ) ) ; ol } r et ur n new Beas t s ; } publ i c Vect or beas t Li s t I nSect or ( Beas t vi ew ) { er Vect or out put = new Vect or ( ) ; Enum at i on e = beas t s . el em s ( ) ; er ent Beas t aBeas t = ( Beas t ) beas t s . el em A ( 0) ; ent t i nt count er = 0; w l e( e. has M eEl em s ( ) ) { hi or ent aBeas t = ( Beas t ) e. next El em ( ) ; ent i f ( aBeas t ! = vi ew ) { er Poi nt p = aBeas t . pos i t i on( ) ; Poi nt v = vi ew . pos i t i on( ) ; er f l oat bear i ng = aBeas t . bear i ngFr om nt A ongA s ( Poi l xi v. x, v. y, vi ew . cur r ent D r ect i on) ; er i i f ( M h. abs ( bear i ng) < f i el dO Vi ew / 2) at f out put . addEl em ( aBeas t ) ; ent } } r et ur n out put ; } publ i c voi d pai nt ( G aphi cs g) { r Enum at i on e = beas t s . el em s ( ) ; er ent w l e( e. has M eEl em s ( ) ) { hi or ent ( ( Beas t ) e. next El em ( ) ) . dr aw g) ; ent ( } } publ i c s t at i c voi d m n( St r i ng[ ] ar gs ) ai { Fi el dO Beas t s f i el d = new Fi el dO Beas t s ( ) ; f i el d. xExt ent = 640; f i el d. yExt ent = 480; Fr am f r am = new Fr am " Fi el d ' O Beas t s " ) ; e e e( // O i onal l y us e a com and- l i ne ar gum pt m ent // f or t he s l eep t i m e: i f ( ar gs . l engt h >= 1) f i el d. del ay = I nt eger . par s eI nt ( ar gs [ 0] ) ; f r am addW ndow s t ener ( e. i Li new W ndow dapt er ( ) { i A publ i c voi d w ndow l os i ng( W ndow i C i Event e) { Sys t em exi t ( 0) ; . } }); f r am add( f i el d, Bor der Layout . C TER) ; e. EN f r am s et Si ze( 640, 480) ; e. 648

f i el d. i ni t ( ) ; f i el d. s t ar t ( ) ; f r am s et Vi s i bl e( t r ue) ; e. } } ///: ~ 尽管这并非对 C ai g Reynol d 的“Boi ds”例子中的行为完美重现,但它却展现出了自己独有的迷人之外。通 r 过对数字进行调整,即可进行全面的修改。至于与这种群聚行为有关的更多的情况,大家可以访问 C ai g r Reynol d的主页——在那个地方,甚至还提供了 Boi ds 一个公开的 3D展示版本: ht t p: //w w hm . com r /boi ds . ht m w. t /cw l 为了将这个程序作为一个程序片运行,请在 HTM 文件中设置下述程序片标志: L

17. 4 总结
通过本章的学习,大家知道运用 Java 可做到一些较复杂的事情。通过这些例子亦可看出,尽管 Java 必定有 自己的局限,但受那些局限影响的主要是性能(比如写好文字处理程序后,会发现 C ++的版本要快得多—— 这部分是由于 I O库做得不完善造成的;而在你读到本书的时候,情况也许已发生了变化。但 Java 的局限也 仅此而已,它在语言表达方面的能力是无以伦比的。利用 Java,几乎可以表达出我们想得到的任何事情。而 与此同时,Java 在表达的方便性和易读性上,也做足了功夫。所以在使用 Java 时,一般不会陷入其他语言 常见的那种复杂境地。使用那些语言时,会感觉它们象一个爱唠叨的老太婆,哪有 Java 那样清纯、简练!而 且通过 Java 1. 2 的 JFC i ng库,A T 的表达能力和易用性甚至又得到了进一步的增强。 /Sw W

17. 5 练习
( 1) (稍微有些难度)改写 Fi el dO Beas t s . j ava ,使它的状态能够保持固定。加上一些按钮,允许用户保存 和恢复不同的状态文件,并从它们断掉的地方开始继续运行。请先参考第 10 章的 C D at e. j ava,再决定具 A St 体怎样做。 ( 2) (大作业)以 Fi el dO Beas t s . j ava作为起点,构造一个自动化交通仿真系统。 ( 3) (大作业)以 C as s Scanner . j ava作为起点,构造一个特殊的工具,用它找出那些虽然定义但从未用过 l 的方法和字段。 ( 4) (大作业)利用 JD ,构造一个联络管理程序。让这个程序以一个平面文件数据库为基础,其中包含了 BC 名字、地址、电话号码、E- m l 地址等联系资料。应该能向数据库里方便地加入新名字。键入要查找的名字 ai 时,请采用在第 15 章的 VLookup. j ava里介绍过的那种名字自动填充技术。

649

附录 A 使用非 J AVA 代码
JA A语言及其标准 A (应用程序编程接口)应付应用程序的编写已绰绰有余。但在某些情况下,还是必须 V PI 使用非 JA A编码。例如,我们有时要访问操作系统的专用特性,与特殊的硬件设备打交道,重复使用现有的 V 非 Java 接口,或者要使用“对时间敏感”的代码段,等等。与非 Java 代码的沟通要求获得编译器和“虚拟 机”的专门支持,并需附加的工具将 Java 代码映射成非 Java 代码(也有一个简单方法:在第 15 章的“一个 W eb应用”小节中,有个例子解释了如何利用标准输入输出同非 Java 代码连接)。目前,不同的开发商为我 们提供了不同的方案:Java 1. 1 有“Java 固有接口”(Java N i ve I nt er f ace at ,JN ),网景提出了自己的 I “Java 运行期接口”(Java Runt i m I nt er f ace)计划,而微软提供了 J/D r ect 、“本源接口”(Raw e i N i ve I nt er f ace,RN )以及 Java/C M集成方案。 at I O 各开发商在这个问题上所持的不同态度对程序员是非常不利的。若 Java 应用必须调用固有方法,则程序员或 许要实现固有方法的不同版本——具体由应用程序运行的平台决定。程序员也许实际需要不同版本的 Java 代 码,以及不同的 Java 虚拟机。 另一个方案是 C RBA O (通用对象请求代理结构),这是由 O G M (对象管理组,一家非赢利性的公司协会)开发 的一种集成技术。C RBA并非任何语言的一部分,只是实现通用通信总线及服务的一种规范。利用它可在由 O 不同语言实现的对象之间实现“相互操作”的能力。这种通信总线的名字叫作 O RB(对象请求代理),是由 其他开发商实现的一种产品,但并不属于 Java 语言规范的一部分。 本附录将对 JN ,J/D REC ,RN ,JA /C M集成和 C RBA进行概述。但不会作更深层次的探讨,甚至有时还 I I T I VA O O 假定读者已对相关的概念和技术有了一定程度的认识。但到最后,大家应该能够自行比较不同的方法,并根 据自己要解决的问题挑选出最恰当的一种。

A. 1 J ava 固有接口
JN 是一种包容极广的编程接口,允许我们从 Java 应用程序里调用固有方法。它是在 Java 1. 1 里新增的, I 维持着与 Java 1. 0 的相应特性——“固有方法接口”(N I )——某种程度的兼容。N I 设计上一些特点使 M M 其未获所有虚拟机的支持。考虑到这个原因,Java 语言将来的版本可能不再提供对 N I 的支持,这儿也不准 M 备讨论它。 目前,JN 只能与用 C或 C I ++写成的固有方法打交道。利用 JN ,我们的固有方法可以: I ■创建、检查及更新 Java 对象(包括数组和字串) ■调用 Java 方法 ■俘获和丢弃“异常” ■装载类并获取类信息 ■进行运行期类型检查 所以,原来在 Java 中能对类及对象做的几乎所有事情在固有方法中同样可以做到。

A. 1 . 1 调用固有方法
我们先从一个简单的例子开始:一个 Java 程序调用固有方法,后者再调用 W n32 的 A 函数 i PI M s ageBox( ),显示出一个图形化的文本框。这个例子稍后也会与 J/D r ect 一志使用。若您的平台不是 es i W n32,只需将包含了下述内容的 C头: i #i ncl ude i s 替换成: # ncl ude i 并将对 M s ageBox( ) 的调用换成调用 pr i nt f ( ) 即可。 es 第一步是写出对固有方法及它的自变量进行声明的 Java 代码: cl as s Show s gBox { M publ i c s t at i c voi d m n( St r i ng [ ] ar gs ) { ai Show s gBox app = new Show s gBox( ) ; M M app. Show es s age( " G M ener at ed w t h JN " ) ; i I } 650

pr i vat e nat i ve voi d Show es s age( St r i ng m g) ; M s s t at i c { Sys t em l oadLi br ar y( " M gI m " ) ; . s pl } } 在固有方法声明的后面,跟随有一个 s t at i c 代码块,它会调用 Sys t em l oadLi br ar y( ) (可在任何时候调用 . 它,但这样做更恰当)Sys t em l oadLi br ar y( )将一个 D 载入内存,并建立同它的链接。D 必须位于您的 . LL LL 系统路径,或者在包含了 Java 类文件的目录中。根据具体的平台,JVM 会自动添加适当的文件扩展名。 1. C头文件生成器:j avah 现在编译您的 Java 源文件,并对编译出来的. cl as s 文件运行 j avah。j avah 是在 1. 0 版里提供的,但由于我 们要使用 Java 1. 1 JN ,所以必须指定- j ni 参数: I j avah - j ni Show s gBox M j avah 会读入类文件,并为每个固有方法声明在 C或 C ++头文件里生成一个函数原型。下面是输出结果—— Show s gBox. h源文件(为符合本书的要求,稍微进行了一下修改): M /* D N T ED T THI S FI LE O O I - it is m achi ne gener at ed */ #i ncl ude /* Header f or cl as s Show s gBox */ M #i f ndef _I ncl uded_Show s gBox M #def i ne _I ncl uded_Show s gBox M #i f def __cpl us pl us ext er n " C { " #endi f /* * C as s : l Show s gBox M * M hod: et Show es s age M * Si gnat ur e: ( Lj ava/l ang/St r i ng; ) V */ JN EXPO voi d JN C LL I RT I A Java_Show s gBox_Show es s age M M ( JN Env *, j obj ect , j s t r i ng) ; I #i f def __cpl us pl us } #endi f #endi f 从“#i f def _cpl us pl us”这个预处理引导命令可以看出,该文件既可由 C编译器编译,亦可由 C ++编译器编 译。第一个# ncl ude 命令包括 j ni . h——一个头文件,作用之一是定义在文件其余部分用到的类型; i JN EXPO 和 JN C LL 是一些宏,它们进行了适当的扩充,以便与那些不同平台专用的引导命令配合; I RT I A JN Env,j obj ect 以及 j s t r i ng则是 JN 数据类型定义。 I I 2. 名称管理和函数签名 JN 统一了固有方法的命名规则;这一点是非常重要的,因为它属于虚拟机将 Java 调用与固有方法链接起来 I 的机制的一部分。从根本上说,所有固有方法都要以一个“Java”起头,后面跟随 Java 方法的名字;下划线 字符则作为分隔符使用。若 Java 固有方法“过载”(即命名重复),那么也把函数签名追加到名字后面。在 原型前面的注释里,大家可看到固有的签名。欲了解命名规则和固有方法签名更详细的情况,请参考相应的 JN 文档。 I 651

3. 实现自己的 D LL 此时,我们要做的全部事情就是写一个 C或 C ++源文件,在其中包含由 j avah 生成的头文件;并实现固有方 法;然后编译它,生成一个动态链接库。这一部分的工作是与平台有关的,所以我假定读者已经知道如何创 建一个 D LL。通过调用一个 W n32 A ,下面的代码实现了固有方法。随后,它会编译和链接到一个名为 i PI M gI m . dl l 的文件里: s pl #i ncl ude i s #i ncl ude " Show s gBox. h" M BO L A EN O PI TRY D l M n( HA D hM l ai N LE odul e, D O dw W RD Reas on, voi d** l pRes er ved) { r et ur n TRUE; } JN EXPO voi d JN C LL I RT I A Java_Show s gBox_Show es s age( JN Env * j Env, M M I j obj ect t hi s , j s t r i ng j M g) { s cons t char * m g; s m g = ( *j Env)- >G St r i ngUTFC s ( j Env, j M g, 0) ; s et har s M s ageBox( HW D ESKTO m g, es N _D P, s " Thi nki ng i n Java: JN " , I M K | M C N LA A O ) ; B_O B_I O EXC M TI N ( *j Env) - >Rel eas eSt r i ngU har s ( j Env, j M g, m g) ; TFC s s } 若对 W n32 没有兴趣,只需跳过 M s ageBox( )调用;最有趣的部分是它周围的代码。传递到固有方法内部的 i es 自变量是返回 Java 的大门。第一个自变量是类型 JN Env 的,其中包含了回调 JVM I 需要的所有挂钩(下一节 再详细讲述)。由于方法的类型不同,第二个自变量也有自己不同的含义。对于象上例那样的非 s t at i c方法 (也叫作实例方法),第二个自变量等价于 C ++的“t hi s ”指针,并类似于 Java 的“t hi s ”:都引用了调用 固有方法的那个对象。对于 s t at i c方法,它是对特定 C as s 对象的一个引用,方法就是在那个 C as s 对象里 l l 实现的。 剩余的自变量代表传递到固有方法调用里的 Java 对象。主类型也是以这种形式传递的,但它们进行的“按 值”传递。 在后面的小节里,我们准备讲述如何从一个固有方法的内部访问和控制 JVM ,同时对上述代码进行更详尽的 解释。

A. 1 . 2 访问 J NI 函数:J NI E nv 自变量
利用 JN 函数,程序员可从一个固有方法的内部与 JVM I 打交道。正如大家在前面的例子中看到的那样,每个 JN 固有方法都会接收一个特殊的自变量作为自己的第一个参数:JN Env 自变量——它是指向类型为 I I JN Env_的一个特殊 JN 数据结构的指针。JN 数据结构的一个元素是指向由 JVM I I I 生成的一个数组的指针;该 数组的每个元素都是指向一个 JN 函数的指针。可从固有方法的内部发出对 JN 函数的调用,做法是撤消对 I I 这些指针的引用(具体的操作实际很简单)。每种 JVM 都以自己的方式实现了 JN 函数,但它们的地址肯定 I 位于预先定义好的偏移处。 利用 JN Env 自变量,程序员可访问一系列函数。这些函数可划分为下述类别: I ■获取版本信息 ■进行类和对象操作 ■控制对 Java 对象的全局和局部引用 ■访问实例字段和静态字段 ■调用实例方法和静态方法 ■执行字串和数组操作 ■产生和控制 Java 异常 JN 函数的数量相当多,这里不再详述。相反,我会向大家揭示使用这些函数时背后的一些基本原理。欲了 I 652

解更详细的情况,请参阅自己所用编译器的 JN 文档。 I 若观察一下 j ni . h头文件,就会发现在#i f def _cpl us pl us 预处理器条件的内部,当由 C ++编译器编译时, JN Env_结构被定义成一个类。这个类包含了大量内嵌函数。通过一种简单而且熟悉的语法,这些函数让我们 I 可以从容访问 JN 函数。例如,前例包含了下面这行代码: I ( * j Env)- >Rel eas eSt r i ngUTFC s ( j Env, j M g, m g) ; har s s 它在 C ++里可改写成下面这个样子: j Env- >Rel eas eSt r i ngUTFC s ( j M g, m g) ; har s s 大家可注意到自己不再需要同时撤消对 j Env 的两个引用,相同的指针不再作为第一个参数传递给 JN 函数调 I 用。在这些例子剩下的地方,我会使用 C ++风格的代码。 1. 访问 Java 字串 作为访问 JN 函数的一个例子,请思考上述的代码。在这里,我们利用 JN Env 的自变量 j Env 来访问一个 I I Java 字串。Java 字串采取的是 Uni code格式,所以假若收到这样一个字串,并想把它传给一个非 Uni code函 数(如 pr i nt f ( ) ),首先必须用 JN 函数 G St r i ngUTFC s ( ) 将其转换成 A I I 字符。该函数能接收一个 I et har SC Java 字串,然后把它转换成 UT F- 8 字符(用 8 位宽度容纳 A I I 值,或用 16 位宽度容纳 Uni code;若原始字 SC 串的内容完全由 A I I 构成,那么结果字串也是 A I I )。 SC SC G St r i ngUTFC s 是 JN Env 间接指向的那个结构里的一个字段,而这个字段又是指向一个函数的指针。为 et har I 访问 JN 函数,我们用传统的 C语法来调用一个函数(通过指针)。利用上述形式可实现对所有 JN 函数的 I I 访问。

A. 1 . 3 传递和使用 J av a 对象
在前例中,我们将一个字串传递给固有方法。事实上,亦可将自己创建的 Java 对象传递给固有方法。 在我们的固有方法内部,可访问已收到的那些对象的字段及方法。 为传递对象,声明固有方法时要采用原始的 Java 语法。如下例所示,M yJavaC as s 有一个 publ i c(公共)字 l 段,以及一个 publ i c 方法。Us eO ect s 类声明了一个固有方法,用于接收 M bj yJavaC as s 类的一个对象。为 l 调查固有方法是否能控制自己的自变量,我们设置了自变量的 publ i c字段,调用固有方法,然后打印出 publ i c字段的值。 cl as s M yJavaC as s { l publ i c voi d di vByTw ) { aVal ue /= 2; } o( publ i c i nt aVal ue; } publ i c cl as s Us eO ect s { bj publ i c s t at i c voi d m n( St r i ng [ ] ar gs ) { ai Us eO ect s app = new Us eO ect s ( ) ; bj bj M yJavaC as s anO = new M l bj yJavaC as s ( ) ; l anO . aVal ue = 2; bj app. changeO ect ( anO ) ; bj bj Sys t em out . pr i nt l n( " Java: " + anO . aVal ue) ; . bj } pr i vat e nat i ve voi d changeO ect ( M bj yJavaC as s obj ) ; l s t at i c { Sys t em l oadLi br ar y( " Us eO I m " ) ; . bj pl } } 编译好代码,并将. cl ass 文件传递给 j avah 后,就可以实现固有方法。在下面这个例子中,一旦取得字段和 方法 I D ,就会通过 JN 函数访问它们。 I JN EXPO voi d JN C LL I RT I A 653

Java_Us eO ect s _changeO ect ( bj bj JN Env * env, j obj ect j Thi s , j obj ect obj ) { I j cl as s cl s ; j f i el dI D f i d; j m hodI D m d; et i i nt val ue; cl s = env- >G O ect C as s ( obj ) ; et bj l f i d = env- >G Fi el dI D cl s , et ( " aVal ue" , " I " ) ; m d = env- >G M hodI D cl s , i et et ( " di vByTw , " ( ) V" ) ; o" val ue = env- >G I nt Fi el d( obj , f i d) ; et pr i nt f ( " N i ve: % \n" , val ue) ; at d env- >Set I nt Fi el d( obj , f i d, 6) ; env- >C l Voi dM hod( obj , m d) ; al et i val ue = e >G I nt Fi el d( obj , f i d) ; nv- et pr i nt f ( " N i ve: % \n" , val ue) ; at d } 除第一个自变量外,C ++函数会接收一个 j obj ect ,它代表 Java 对象引用“固有”的那一面——那个引用是 我们从 Java 代码里传递的。我们简单地读取 aVal ue ,把它打印出来,改变这个值,调用对象的 di vByTw ) o( 方法,再将值重新打印一遍。 为访问一个字段或方法,首先必须获取它的标识符。利用适当的 JN 函数,可方便地取得类对象、元素名以 I 及签名信息。这些函数会返回一个标识符,利用它可访问对应的元素。尽管这一方式显得有些曲折,但我们 的固有方法确实对 Java 对象的内部布局一无所知。因此,它必须通过由 JVM 返回的索引访问字段和方法。这 样一来,不同的 JVM 就可实现不同的内部对象布局,同时不会对固有方法造成影响。 若运行 Java 程序,就会发现从 Java 那一侧传来的对象是由我们的固有方法处理的。但传递的到底是什么 呢?是指针,还是 Java 引用?而且垃圾收集器在固有方法调用期间又在做什么呢? 垃圾收集器会在固有方法执行期间持续运行,但在一次固有方法调用期间,我们的对象可保证不会被当作 “垃圾”收集去。为确保这一点,事先创建了“局部引用”,并在固有方法调用之后立即清除。由于它们的 “生命期”与调用过程息息相关,所以能够保证对象在固有方法调用期间的有效性。 由于这些引用会在每次函数调用的时候创建和破坏,所以不可在 s t at i c 变量中制作固有方法的局部副本(本 地拷贝)。若希望一个引用在函数存在期间持续有效,就需要一个全局引用。全局引用不是由 JVM 创建的, 但通过调用特定的 JN 函数,程序员可将局部引用扩展为全局引用。创建一个全局引用时,需对引用对象的 I “生存时间”负责。全局引用(以及它引用的对象)会一直留在内存里,直到用特定的 JN 函数明确释放了 I 这个引用。它类似于 C的 m l oc( ) 和 f r ee( ) 。 al

A. 1 . 4 J NI 和 J av a 异常
利用 JN ,可丢弃、捕捉、打印以及重新丢弃 Java 异常,就象在一个 Java 程序里那样。但对程序员来说, I 需自行调用专用的 JN 函数,以便对异常进行处理。下面列出用于异常处理的一些 JN 函数: I I ■T hr ow ) :丢弃一个现有的异常对象;在固有方法中用于重新丢弃一个异常。 ( ■Thr ow ew ) :生成一个新的异常对象,并将其丢弃。 N ( ■Except i onO ccur r ed( ) :判断一个异常是否已被丢弃,但尚未清除。 ■Except i onD cr i be( ) :打印一个异常和堆栈跟踪信息。 es ■Except i onC ear ( ) :清除一个待决的异常。 l ■Fat al Er r or ( ) :造成一个严重错误,不返回。 在所有这些函数中,最不能忽视的就是 Except i onO ccur r ed( ) 和 Except i onC ear ( ) 。大多数 JN 函数都能产 l I 生异常,而且没有象在 Java 的 t r y 块内的那种语言特性可供利用。所以在每一次 JN 函数调用之后,都必须 I 调用 Except i onO ccur r ed( ) ,了解异常是否已被丢弃。若侦测到一个异常,可选择对其加以控制(可能时还 要重新丢弃它)。然而,必须确保异常最终被清除。这可以在自己的函数中用 Except i onC ear ( )来实现;若 l 异常被重新丢弃,也可能在其他某些函数中进行。但无论如何,这一工作是必不可少的。

654

我们必须保证异常被彻底清除。否则,假若在一个异常待决的情况下调用一个 JN 函数,获得的结果往往是 I 无法预知的。也有少数几个 JN 函数可在异常时安全调用;当然,它们都是专门的异常控制函数。 I

A. 1 . 5 J NI 和线程处理
由于 Java 是一种多线程语言,几个线程可能同时发出对一个固有方法的调用(若另一个线程发出调用,固有 方法可能在运行期间暂停)。此时,完全要由程序员来保证固有调用在多线程的环境中安全进行。例如,要 防范用一种未进行监视的方法修改共享数据。此时,我们主要有两个选择:将固有方法声明为“同步”,或 在固有方法内部采取其他某些策略,确保数据处理正确地并发进行。 此外,绝对不要通过线程传递 JN Env,因为它指向的内部结构是在“每线程”的基础上分配的,而且包含了 I 只对那些特定的线程才有意义的信息。

A. 1 . 6 使用现成代码
为实现 JN 固有方法,最简单的方法就是在一个 Java 类里编写固有方法的原型,编译那个类,再通过 j avah I 运行. cl as s 文件。但假若我们已有一个大型的、早已存在的代码库,而且想从 Java 里调用它们,此时又该 如何是好呢?不可将 D 中的所有函数更名,使其符合 JN 命名规则,这种方案是不可行的。最好的方法是 LL I 在原来的代码库“外面”写一个封装 D LL。Java 代码会调用新 D 里的函数,后者再调用原始的 D 函数。 LL LL 这个方法并非仅仅是一种解决方案;大多数情况下,我们甚至必须这样做,因为必须面向对象引用调用 JN I 函数,否则无法使用它们。

A. 2 微软的解决方案
到本书完稿时为止,微软仍未提供对 JN 的支持,只是用自己的专利方法提供了对非 Java 代码调用的支持。 I 这一支持内建到编译器 M cr os of t JVM以及外部工具中。只有程序用 M cr os of t Java编译器编译,而且只有 i i 在 M cr os of t Java虚拟机(JVM i )上运行的时候,本节讲述的特性才会有效。若计划在因特网上发行自己的 应用,或者本单位的内联网建立在不同平台的基础上,就可能成为一个严重的问题。 微软与 W n32 代码的接口为我们提供了连接 W n32 的三种途径: i i ( 1) J/D r ect :方便调用 W n32 D 函数的一种途径,具有某些限制。 i i LL ( 2) 本原接口(RN ):可调用 W n32 D 函数,但必须自行解决“垃圾收集”问题。 I i LL ( 3) Java/C M集成:可从 Java 里直接揭示或调用 C M O O 服务。 后续的小节将分别探讨这三种技术。 写作本书的时候,这些特性均通过了 M cr os of t SD f or Java 2. 0 bet a 2 的支持。可从微软公司的 W i K eb站 点下载这个开发平台(要经历一个痛苦的选择过程,他们叫作“A i ve Set up”)。Java SD 是一套命令行 ct K 工具的集合,但编译引擎可轻易嵌入 D evel oper St udi o环境,以便我们用 Vi s ual J++ 1. 1 来编译 Java 1. 1 代码。

A. 3 J / Di r ect
J/D r ect 是调用 W n32 D 函数最简单的方式。它的主要设计目标是与 W n32A 打交道,但完全可用它调 i i LL i PI 用其他任何 A 。但是,尽管这一特性非常方便,但它同时也造成了某些限制,且降低了性能(与 RN 相 PI I 比)。但 J/D r ect 也有一些明显的优点。首先,除希望调用的那个 D 里的代码之外,没有必要再编写额外 i LL 的非 Java 代码,换言之,我们不需要一个封装器或者代理/存根 D LL。其次,函数自变量与标准数据类型之 间实现了自动转换。若必须传递用户自定义的数据类型,那么 J/D r ect 可能不按我们的希望工作。第三,就 i 象下例展示的那样,它非常简单和直接。只需少数几行,这个例子便能调用 W n32 A 函数 M s ageBox( ) , i PI es 它能弹出一个小的模态窗口,并带有一个标题、一条消息、一个可选的图标以及几个按钮。 publ i c cl as s Show s gBox { M publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) ai t hr ow Uns at i s f i edLi nkEr r or s { M s ageBox( 0, es " C eat ed by t he M s ageBox( ) W n32 f unc" , r es i " Thi nki ng i n Java" , 0) ; } 655

/** @ l . i m t ( " USER32" ) */ dl por pr i vat e s t at i c nat i ve i nt M s ageBox( i nt hw w , St r i ng t ext , es ndO ner St r i ng t i t l e, i nt f uSt yl e) ; } 令人震惊的是,这里便是我们利用 J/D r ect 调用 W n32 D 函数所需的全部代码。其中的关键是位于示范代 i i LL 码底部的 M s ageBox( )声明之前的@ l . i m t 引导命令。它表面上看是一条注释,但实际并非如此。它的 es dl por 作用是告诉编译器:引导命令下面的函数是在 USER32 D 里实现的,而且应相应地调用。我们要做的全部事 LL 情就是提供与 D 内实现的函数相符的一个原型,并调用函数。但是毋需在 Java 版本里手工键入需要的每一 LL 个 W n32 A 函数,一个 M cr os of t Java包会帮我们做这件事情(很快就会详细解释)。为了让这个例子正 i PI i 常工作,函数必须“按名称”由 D 导出。但是,也可以用@ l . i m t 引导命令“按顺序”链接。举个例 LL dl por 子来说,我们可指定函数在 D 里的入口位置。稍后还会具体讲述@ l . i m t 引导命令的特性。 LL dl por 用非 Java 代码进行链接的一个重要问题就是函数参数的自动配置。正如大家看到的那样,M s ageBox( )的 es Java 声明采用了两个字串自变量,但原来的 C方案则采用了两个 char 指针。编译器会帮助我们自动转换标 准数据类型,同时遵照本章后一节要讲述的规则。 最好,大家或许已注意到了 m n( )声明中的 Uns at i s f i edLi nkEr r or 异常。在运行期的时候,一旦链接程序 ai 不能从非 Java 函数里解析出符号,就会触发这一异常(事件)。这可能是由多方面的原因造成的:. dl l 文 件未找到;不是一个有效的 D LL;或者 J/D r ect 未获您所使用的虚拟机的支持。为了使 D 能被找到,它必 i LL 须位于 W ndow 或 W ndow \Sys t em i s i s 目录下,位于由 PA 环境变量列出的一个目录中,或者位于和. cl ass 文 TH 件相同的目录。J/D r ect 获得了 M cr os of t Java编译器 1. 02. 4213 版本及更高版本的支持,也获得了 i i M cr os of t JVM 4. 79. 2164 及更高版本的支持。为了解自己编译器的版本号,请在命令行下运行 JVC i ,不要加 任何参数。为了解 JVM的版本号,请找到 m j ava. dl l 的图标,并利用右键弹出菜单观察它的属性。 s

A. 3 . 1 @dl l . i m por t 引导命令
作为使用 J/D r ect 唯一的途径,@ l . i m t 引导命令相当灵活。它提供了为数众多的修改符,可用它们自 i dl por 定义同非 Java 代码建立链接关系的方式。它亦可应用于类内的一些方法,或应用于整个类。也就是说,我们 在那个类内声明的所有方法都是在相同的 D 里实现的。下面让我们具体研究一下这些特性。 LL 1. 别名处理和按顺序链接 为了使@ l . i m t 引导命令能象上面显示的那样工作,D 内的函数必须按名字导出。然而,我们有时想使 dl por LL 用与 D 里原始名字不同的一个名字(别名处理),否则函数就可能按编号(比如按顺序)导出,而不是按 LL 名字导出。下面这个例子声明了 Fi nes t r aD M s aggi o( )(用意大利语说的“M s ageBox”)。正如大家看 i es es 到的那样,使用的语法是非常简单的。 publ i c cl as s A i as i ng { l publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) ai t hr ow Uns at i s f i edLi nkEr r or s { Fi nes t r aD M s aggi o( 0, i es " C eat ed by t he M s ageBox( ) W n32 f unc" , r es i " Thi nki ng i n Java" , 0) ; } /** @ l . i m t ( " USER32" , dl por ent r ypoi nt =" M s ageBox" ) */ es pr i vat e s t at i c nat i ve i nt Fi nes t r aD M s aggi o( i nt hw w , St r i ng t ext , i es ndO ner St r i ng t i t l e, i nt f uSt yl e) ; } 下面这个例子展示了如何同 D 里并非按名字导出的一个函数建立链接,那个函数事实是按它们在 D 里的 LL LL 位置导出的。这个例子假设有一个名为 M A YM TH的 D LL,这个 D 在位置编号 3 处包含了一个函数。那个函数 LL 获取两个整数作为自变量,并返回两个整数的和。 656

publ i c cl as s ByO di nal { r publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) ai t hr ow Uns at i s f i edLi nkEr r or { s i nt j =3, k=9; Sys t em o . pr i nt l n( " Res ul t of D f unct i on: " . ut LL + A j , k) ) ; dd( } /** @ l . i m t ( " M A , ent r ypoi nt = " #3" ) */ dl por YM TH" pr i vat e s t at i c nat i ve i nt A i nt op1, i nt op2) ; dd( } 可以看出,唯一的差异就是 ent r ypoi nt 自变量的形式。 2. 将@ l . i m t 应用于整个类 dl por @ l . i m t 引导命令可应用于整个类。也就是说,那个类的所有方法都是在相同的 D 里实现的,并具有 dl por LL 相同的链接属性。引导命令不会由子类继承;考虑到这个原因,而且由于 D 里的函数是自然的 s t at i c函 LL 数,所以更佳的设计方案是将 A 函数封装到一个独立的类里,如下所示: PI /** @ l . i m t ( " USER32" ) */ dl por cl as s M er 32A yUs cces s { publ i c s t at i c nat i ve i nt M s ageBox( i nt hw w , St r i ng t ext , es ndO ner St r i ng t i t l e, i nt f uSt yl e) ; p i c nat i ve s t at i c bool ean ubl M s ageBeep( i nt uType) ; es } publ i c cl as s W eC as s { hol l publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) ai t hr ow Uns at i s f i edLi nkEr r or { s M er 32A yUs cces s . M s ageBeep( 4) ; es M er 32A yUs cces s . M s ageBox( 0, es " C eat ed by t he M s ageBox( ) W n32 f unc" , r es i " Thi nki ng i n Java" , 0) ; } } 由于 M s ageBeep( ) 和 M s ageBox( )函数已在不同的类里被声明成 s t at i c函数,所以必须在调用它们时规定 es es 作用域。大家也许认为必须用上述的方法将所有 W n32 A (函数、常数和数据类型)都映射成 Java 类。但 i PI 幸运的是,根本不必这样做。

A. 3 . 2 com m . wi n3 2 包 . s
W n32 A 的体积相当庞大——包含了数以千计的函数、常数以及数据类型。当然,我们并不想将每个 W n32 i PI i A 函数都写成对应 Java 形式。微软考虑到了这个问题,发行了一个 Java 包,可通过 J/D r ect 将 W n32 PI i i A 映射成 Java 类。这个包的名字叫作 com m . w n32。安装 Java SD 2. 0 时,若在安装选项中进行了相应 PI . s i K 的设置,这个包就会安装到我们的类路径中。这个包由大量 Java 类构成,它们完整再现了 W n32 A 的常 i PI 数、数据类型以及函数。包容能力最大的三个类是 Us er 32. cl as s,Ker nel . cl as s 以及 G 32. cl as s 。它们包 di 含的是 W n32 A 的核心内容。为使用它们,只需在自己的 Java 代码里导入即可。前面的 Show s gBox 示例 i PI M 可用 com m . w n32 改写成下面这个样子(这里也考虑到了用更恰当的方式使用 Uns at i s f i edLi nkEr r or ): . s i i m t com m . w n32. *; por . s i 657

publ i c cl as s Us eW n32Package { i publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) { ai try { Us er 32. M s ageBeep( es w nm M C N LA A O ) ; i . B_I O EXC M TI N Us er 32. M s ageBox( 0, es " C eat ed by t he M s ageBox( ) W n32 f unc" , r es i " Thi nki ng i n Java" , w nm M KC N EL | i . B_O A C w nm M C N LA A O ) ; i . B_I O EXC M TI N } cat ch( Uns at i s f i edLi nkEr r or e) { Sys t em out . pr i nt l n( " C . an’t l i nk W n32 A " ) ; i PI Sys t em out . pr i nt l n( e) ; . } } } Java 包是在第一行导入的。现在,可在不进行其他声明的前提下调用 M s ageBeep( ) 和 M s ageBox( )函数。 es es 在 M s ageBeep( ) 里,我们可看到包导入时也声明了 W n32 常数。这些常数是在大量 Java 接口里定义的,全 es i 部命名为 w nx(x 代表欲使用之常数的首字母)。 i 写作本书时,com m . w n32 包的开发仍未正式完成,但已可堪使用。 . s i

A. 3 . 3 汇集
“汇集”(M s hal i ng)是指将一个函数自变量从它原始的二进制形式转换成与语言无关的某种形式,再将 ar 这种通用形式转换成适合调用函数采用的二进制格式。在前面的例子中,我们调用了 M s ageBox( )函数,并 es 向它传递了两个字串。M s ageBox( )是个 C函数,而且 Java 字串的二进制布局与 C字串并不相同。但尽管如 es 此,自变量仍获得了正确的传递。这是由于在调用 C代码前,J/D r ect 已帮我们考虑到了将 Java 字串转换 i 成 C字串的问题。这种情况适合所有标准的 Java 类型。下面这张表格总结了简单数据类型的默认对应关系: Java C byt e BYTE 或 C R HA s hor t SHO 或 W RD RT O i nt I N T,UI N ,LO G T N ,ULO G或 D O N W RD char TC R HA l ong __i nt 64 f l oat Fl oat doubl e D oubl e bool ean BO L O St r i ng LPC ST R(只允许在 O 模式中作为返回值) T LE byt e[ ] BYTE * s hor t [ ] W RD * O char [ ] TC R * HA i nt [ ] D O * W RD 这个列表还可继续下去,但已很能说明问题了。大多数情况下,我们不必关心与简单数据类型之间的转换问 题。但一旦必须传递用户自定义类型的自变量,情况就立即变得不同了。例如,可能需要传递一个结构化 的、用户自定义的数据类型,或者需要把一个指针传给原始内存区域。在这些情况下,有一些特殊的编译引 导命令标记一个 Java 类,使其能作为一个指针传给结构(@ l . s t r uct 引导命令)。欲知使用这些关键字的 dl 细节,请参考产品文档。

658

A. 3 . 4 编写回调函数
有些 W n32 A 函数要求将一个函数指针作为自己的参数使用。W ndow A 函数随后就可以调用自变量函 i PI i s PI 数(通常是在以后发生特定的事件时)。这一技术就叫作“回调函数”。回调函数的例子包括窗口进程以及 我们在打印过程中设置的回调(为后台打印程序提供回调函数的地址,使其能更新状态,并在必要的时候中 止打印)。 另一个例子是 A 函数 Enum i ndow ( ) ,它能枚举目前系统内所有顶级窗口。Enum i ndow ( ) 要求获取一个函 PI W s W s 数指针作为自己的参数,然后搜索由 W ndow 内部维护的一个列表。对于列表内的每个窗口,它都会调用回 i s 调函数,将窗口句柄作为一个自变量传给回调。 为了在 Java 里达到同样的目的,必须使用 com m . dl l 包里的 C l back 类。我们从 C l back 里继承,并取 . s al al 消 cal l back( ) 。这个方法只能接近 i nt 参数,并会返回 i nt 或 voi d。方法签名和具体的实施取决于使用这个 回调的 W ndow A 函数。 i s PI 现在,我们要进行的全部工作就是创建这个 C l back 衍生类的一个实例,并将其作为函数指针传递给 A 函 al PI 数。随后,J/D r ect 会帮助我们自动完成剩余的工作。 i 下面这个例子调用了 W n32 A 函数 Enum i ndow ( ) ;Enum i ndow Pr oc类里的 cal l back( ) 方法会获取每个 i PI W s W s 顶级窗口的句柄,获取标题文字,并将其打印到控制台窗口。 i m t com m . dl l . * ; por . s i m t com m . w n32. *; por . s i cl as s Enum i ndow Pr oc ext ends C l back { W s al publ i c bool ean cal l back( i nt hw nd, i nt l par am { ) St r i ngBuf f er t ext = new St r i ngBuf f er ( 50) ; Us er 32. G W ndow et i Text ( hw nd, t ext , t ext . capaci t y( ) +1) ; i f ( t ext . l engt h( ) ! = 0) Sys t em out . pr i nt l n( t ext ) ; . r et ur n t r ue; // t o cont i nue enum at i on. er } } publ i c cl as s Show al l back { C publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) ai t hr ow I nt er r upt edExcept i on { s bool ean ok = Us er 32. Enum i ndow ( W s new Enum i ndow Pr oc( ) , 0) ; W s i f ( ! ok) Sys t em er r . pr i nt l n( " Enum i ndow f ai l ed. " ) ; . W s Thr ead. cur r ent Thr ead( ) . s l eep( 3000) ; } } 对 s l eep( ) 的调用允许窗口进程在 m n( )退出前完成。 ai

A. 3 . 5 其他 J / Di r ect 特性
通过@ l . i m t 引导命令内的修改符(标记),还可用到 J/D r ect 的另两项特性。第一项是对 O 函数的 dl por i LE 简化访问,第二项是选择 A 函数的 A SI 及 Uni code版本。 PI N 根据约定,所有 O 函数都会返回类型为 HRESULT 的一个值,它是由 C M定义的一个结构化整数值。若在 LE O CM O 那一级编写程序,并希望从一个 O 函数里返回某些不同的东西,就必须将一个特殊的指针传递给它— LE —该指针指向函数即将在其中填充数据的那个内存区域。但在 Java 中,我们没有指针可用;此外,这种方法 并不简练。利用 J/D r ect ,我们可在@ l . i m t 引导命令里使用 ol e修改符,从而方便地调用 O 函数。 i dl por LE 标记为 ol e 函数的一个固有方法会从 Java 形式的方法签名(通过它决定返回类型)自动转换成 C M O 形式的函 659

数。 第二项特性是选择 A SI 或者 Uni code字串控制方法。对字串进行控制的大多数 W n32 A 函数都提供了两个 N i PI 版本。例如,假设我们观察由 USER32. D 导出的符号,那么不会找到一个 M s ageBox( ) 函数,相反会看到 LL es M s ageBoxA ) 和 M s ageBoxW ) 函数——分别是该函数的 A SI 和 Uni code版本。如果在@l l . i mo t 引导命 es ( es ( N d pr 令里不规定想调用哪个版本,JVM就会试着自行判断。但这一操作会在程序执行时花费较长的时间。所以, 我们一般可用 ans i ,uni code或 aut o 修改符硬性规定。 欲了解这些特性更详细的情况,请参考微软公司提供的技术文档。

A. 4 本原接口(RNI )
同 J/D r ect 相比,RN 是一种比非 Java 代码复杂得多的接口;但它的功能也十分强大。RN 比 J/D r ect 更 i I I i 接近于 JVM ,这也使我们能写出更有效的代码,能处理固有方法中的 Java 对象,而且能实现与 JVM 内部运行 机制更紧密的集成。 RN 在概念上类似 Sun 公司的 JN 。考虑到这个原因,而且由于该产品尚未正式完工,所以我只在这里指出它 I I 们之间的主要差异。欲了解更详细的情况,请参考微软公司的文档。 JN 和 RN 之间存在几方面引人注目的差异。下面列出的是由 m j avah生成的 C头文件(微软提供的 m j avah I I s s 在功能上相当于 Sun的 j avah),应用于前面在 JN 例子里使用的 Java 类文件 Show s gBox。 I M /* D N T ED T O O I aut om i cal l y gener at ed by m j avah */ at s #i ncl ude #pr agm w ni ng( di s abl e: 4510) a ar #pr agm w ni ng( di s abl e: 4512) a ar #pr agm w ni ng( di s abl e: 4610) a ar s t r uct C as s j ava_l ang_St r i ng; l #def i ne Hj ava_l ang_St r i ng C as s j ava_l ang_St r i ng l /* Header f or cl as s Show s gBox M */

#i f ndef _I ncl uded_Show s gBox M #def i ne _I ncl uded_Show s gBox M #def i ne HShow s gBox C as s Show s gBox M l M t ypedef s t r uct C as s Show s gBox { l M # ncl ude i l ong M SRes er ved; #i ncl ude } C as s Show s gBox; l M #i f def __cpl us pl us ext er n " C { " #endi f __decl s pec( dl l expor t ) voi d __cdecl Show s gBox_Show es s age ( s t r uct HShow s gBox *, M M M s t r uct Hj ava_l ang_St r i ng *) ; #i f def __cpl us pl us } #endi f #endi f /* _I ncl uded_Show s gBox */ M

#pr agm w ni ng( def aul t : 4510) a ar 660

#pr agm w ni ng( def aul t : 4512) a ar #pr agm w ni ng( def aul t : 4610) a ar 除可读性较差外,代码里还隐藏着一些技术性问题,待我一一道来。 在 RN 中,固有方法的程序员知道对象的二进制布局。这样便允许我们直接访问自己希望的信息;我们不必 I 象在 JN 里那样获得一个字段或方法标识符。但由于并非所有虚拟机都需要将相同的二进制布局应用于自己 I 的对象,所以上面的固有方法只能在 M cr os of t JVM下运行。 i 在 JN 中,通过 JN Env 自变量可访问大量函数,以便同 JVM I I 打交道。在 RN 中,用于控制 JVM运作的函数变 I 成了可直接调用。它们中的某一些(如控制异常的那一个)类似于它们的 JN “兄弟”。但大多数 RN 函数 I I 都有与 JN 中不同的名字和用途。 I JN 和 RN 最重大的一个区别是“垃圾收集”的模型。在 JN 中,垃圾收集在固有方法执行期间遵守与 Java I I I 代码执行时相同的规则。而在 RN 中,要由程序员在固有方法活动期间自行负责“垃圾收集器”器的启动与 I 中止。默认情况下,垃圾收集器在进入固有方法前处于不活动状态;这样一来,程序员就可假定准备使用的 对象用不着在那个时间段内进行垃圾收集。然而一旦固有方法准备长时间执行,程序员就应考虑激活垃圾收 集器——通过调用 G Enabl e( ) 这个 RN 函数(G C I C是“G bage C l ect or ”的缩写,即“垃圾收集”)。 ar ol 也存在与全局句柄特性类似的机制——程序员可利用可保证特定的对象在 G C活动期间不至于被当作“垃圾” 收掉。概念是类似的,但名称有所差异——在 RN 中,人们把它叫作 G Fr am 。 I C es

A. 4 . 1 RNI 总结
RN 与 M cr os of t JV I i M紧密集成这一事实既是它的优点,也是它的缺点。RN 比 JN 复杂得多,但它也为我们 I I 提供了对 JVM 内部活动的高度控制;其中包括垃圾收集。此外,它显然针对速度进行了优化,采纳了 C程序 员熟悉的一些折衷方案和技术。但除了微软的 JVM 之外,它并不适于其他 JVM 。

A. 5 J ava/ COM集成
CM O (以前称为 O LE)代表微软公司的“组件对象模型”(C ponent O ect M om bj odel ),它是所有 A i veX 技 ct 术(包括 A i veX控件、A om i on 以及 A i veX文档)的基础。但 C M ct ut at ct O 还包含了更多的东西。它是一种特 殊的规范,按照它开发出来的组件对象可通过操作系统的专门特性实现“相互操作”。在实际应用中,为 W n32 系统开发的所有新软件都与 C M i O 有着一定的关系——操作系统通过 C M O 对象揭示出自己的一些特性。 由其他厂商开发的组件也可以建立在 C M O 的基础上,我们能创建和注册自己的 C M O 组件。通过这样或那样的 形式,如果我们想编写 W n32 代码,那么必须和 C M i O 打交道。在这里,我们将仅仅重述 C M O 编程的基本概 念,而且假定读者已掌握了 C M O 服务器(能为 C M O 客户提供服务的任何 C M O 对象)以及 C M O 客户(能从 C M O 服务器那里申请服务的一个 C M O 对象)的概念。本节将尽可能地使叙述变得简单。工具实际的功能要强大得 多,而且我们可通过更高级的途径来使用它们。但这也要求对 C M O 有着更深刻的认识,那已经超出了本附录 的范围。如果您对这个功能强大、但与不同平台有关的特性感兴趣,应该研究 C M O 和微软公司的文档资料, 仔细阅读有关 Java/C M集成的那部分内容。如果想获得更多的资料,向您推荐 D e Roger s on 编著的 O al 《I ns i de C M O 》,该书由 M cr os of t Pr es s 于 1997 年出版。 i 由于 C M O 是所有新型 W n32 应用程序的结构核心,所以通过 Java 代码使用(或揭示)C M服务的能力就显得 i O 尤为重要。Java/C M集成无疑是 M cr os of t Java编译器以及虚拟机最有趣的特性。Java 和 C M O i O 在它们的模 型上是如此相似,所以这个集成在概念上是相当直观的,而且在技术上也能轻松实现无缝结合——为访问 CM O ,几乎不需要编写任何特殊的代码。大多数技术细节都是由编译器和/或虚拟机控制的。最终的结果便是 Java 程序员可象对待原始 Java 对象那样对待 C M O 对象。而且 C M O 客户可象使用其他 C M O 服务器那样使用由 Java 实现的 C M服务器。在这里提醒大家,尽管我使用的是通用术语“C M O O ”,但根据扩展,完全可用 Java 实现一个 A i veX A om i on 服务器,亦可在 Java 程序中使用一个 A i veX 控件。 ct ut at ct Java 和 C M最引人注目的相似之处就是 C M O O 接口与 Java 的“i nt er f ace”关键字的关系。这是接近完美的一 种相符,因为: ■C M对象揭示出了接口(也只有接口) O ■C M接口本身并不具备实施方案;要由揭示出接口的那个 C M O O 对象负责它的实施 ■C M接口是对语义上相关的一组函数的说明;不会揭示出任何数据 O ■C M类将 C M O O 接口组合到了一起。Java 类可实现任意数量的 Java 接口。 ■C M有一个引用对象模型;程序员永远不可能“拥有”一个对象,只能获得对对象一个或多个接口的引 O 用。Java 也有一个引用对象模型——对一个对象的引用可“造型”成对它的某个接口的引用。 661

■C M对象在内存里的“生存时间”取决于使用对象的客户数量;若这个数量变成零,对象就会将自己从内 O 存中删去。在 Java 中,一个对象的生存时间也由客户的数量决定。若不再有对那个对象的引用,对象就会等 候垃圾收集器的处理。 Java 与 C M之间这种紧密的对应关系不仅使 Java 程序员可以方便地访问 C M O O 特性,也使 Java 成为编写 C M O 代码的一种有效语言。C M O 是与语言无关的,但 C M O 开发事实上采用的语言是 C ++和 Vi s ual Bas i c。同 Java 相比,C ++在进行 C M O 开发时显得更加强大,并可生成更有效的代码,只是它很难使用。Vi s ual Bas i c比 Java 简单得多,但它距离基础操作系统太远了,而且它的对象模型并未实现与 C M O 很好的对应(映射)关 系。Java 是两者之间一种很好的折衷方案。 接下来,让我们对 C M开发的一些关键问题进行讨论。编写 Java/C M客户和服务器时,这些问题是首先需要 O O 弄清楚的。

A. 5 . 1 COM基础
CM O 是一种二进制规范,致力于实施可相互操作的对象。例如,C M认为一个对象的二进制布局必须能够调用 O 另一个 C M对象里的服务。由于是对二进制布局的一种描述,所以只要某种语言能生成这样的一种布局,就 O 可通过它实现 C M O 对象。通常,程序员不必关注象这样的一些低级细节,因为编译器可自动生成正确的布 局。例如,假设您的程序是用 C ++写的,那么大多数编译器都能生成符合 C M O 规范的一张虚拟函数表格。对 那些不生成可执行代码的语言,比如 VB 和 Java,在运行期则会自动挂接到 C M O。 CM O 库也提供了几个基本的函数,比如用于创建对象或查找系统中一个已注册 C M O 类的函数。 一个组件对象模型的基本目标包括: ■让对象调用其他对象里的服务 ■允许新类型对象(或更新对象)无缝插入环境 第一点正是面向对象程序设计要解决的问题:我们有一个客户对象,它能向一个服务器对象发出请求。在这 种情况下,“客户”和“服务器”这两个术语是在常规意义上使用的,并非指一些特定的硬件配置。对于任 何面向对象的语言,第一个目标都是很容易达到的——只要您的代码是一个完整的代码块,同时实现了服务 器对象代码以及客户对象代码。若改变了客户和服务器对象相互间的沟通形式,只需简单地重新编译和链接 一遍即可。重新启动应用程序时,它就会自动采用组件的最新版本。 但假若应用程序由一些未在自己控制之下的组件对象构成,情况就会变得迥然有异——我们不能控制它们的 源码,而且它们的更新可能完全独立于我们的应用程序进行。例如,当我们在自己的程序里使用由其他厂商 开发的 A i veX 控件时,就会面临这一情况。控件会安装到我们的系统里,我们的程序能够(在运行期)定 ct 位服务器代码,激活对象,同它建立链接,然后使用它。以后,我们可安装控件的新版本,我们的应用程序 应该仍然能够运行;即使在最糟的情况下,它也应礼貌地报告一条出错消息,比如“控件未找到”等等;一 般不会莫名其妙地挂起或死机。 在这些情况下,我们的组件是在独立的可执行代码文件里实现的:D 或 EXE。若服务器对象在一个独立的可 LL 执行代码文件里实现,就需要由操作系统提供的一个标准方法,从而激活这些对象。当然,我们并不想在自 己的代码里使用 D 或 EXE 的物理名称及位置,因为这些参数可能经常发生变化。此时,我们想使用的是由 LL 操作系统维护的一些标识符。另外,我们的应用程序需要对服务器展示出来的服务进行的一个描述。下面这 两个小节将分别讨论这两个问题。 1. G D和注册表 UI CM O 采用结构化的整数值(长度为 128 位)唯一性地标识系统中注册的 C M O 项目。这些数字的正式名称叫作 G D(G obal l y Uni que I D i f i er ,全局唯一标识符),可由特殊的工具生成。此外,这些数字可以保证 UI l ent 在“任何空间和时间”里独一无二,没有重复。在空间,是由于数字生成器会读取网卡的 I D号码;在时间, 是由于同时会用到系统的日期和时间。可用 G D标识 C M UI O 类(此时叫作 C D LSI )或者 C M接口(I I D O )。尽 管名字不同,但基本概念与二进制结构都是相同的。G D亦可在其他环境中使用,这里不再赘述。 UI G D以及相关的信息都保存在 W ndow 注册表中,或者说保存在“注册数据库”(Regi s t r at i on UI i s D abas e)中。这是一种分级式的数据库,内建于操作系统中,容纳了与系统软硬件配置有关的大量信息。 at 对于 C M O ,注册表会跟踪系统内安装的组件,比如它们的 C D LSI 、实现它们的可执行文件的名字及位置以及其 他大量细节。其中一个比较重要的细节是组件的 Pr ogI D ;Pr ogI D在概念上类似于 G D,因为它们都标识着 UI 一个 C M O 组件。区别在于 G D是一个二进制的、通过算法生成的值。而 Pr ogI D则是由程序员定义的字串 UI 值。Pr ogI D是随同一个 C D分配的。 LSI 我们说一个 C M O 组件已在系统内注册,最起码的一个条件就是它的 C D和它的执行文件已存在于注册表中 LSI 662

(Pr ogI D通常也已就位)。在后面的例子里,我们主要任务就是注册与使用 C M O 组件。 注册表的一项重要特点就是它作为客户和服务器对象之间的一个去耦层使用。利用注册表内保存的一些信 息,客户会激活服务器;其中一项信息是服务器执行模块的物理位置。若这个位置发生了变动,注册表内的 信息就会相应地更新。但这个更新过程对于客户来说是“透明”或者看不见的。后者只需直接使用 Pr ogI D或 C D即可。换句话说,注册表使服务器代码的位置透明成为了可能。随着 D O (分布式 C M LSI CM O )的引入,在 本地机器上运行的一个服务器甚至可移到网络中的一台远程机器,整个过程甚至不会引起客户对它的丝毫注 意(大多数情况下如此)。 2. 类型库 由于 C M O 具有动态链接的能力,同时由于客户和服务器代码可以分开独立发展,所以客户随时都要动态侦测 由服务器展示出来的服务。这些服务是用“类型库”(Type Li br ar y)中一种二进制的、与语言无关的形式 描述的(就象接口和方法签名)。它既可以是一个独立的文件(通常采用. TLB 扩展名),也可以是链接到执 行程序内部的一种 W n32 资源。运行期间,客户会利用类型库的信息调用服务器中的函数。 i 我们可以写一个 M cr os of t I nt er f ace D i ni t i on Language i ef (微软接口定义语言,M D )源文件,用 MD I L I L 编译器编译它,从而生成一个. TLB 文件。M D 语言的作用是对 C M类、接口以及方法进行描述。它在名称、 I L O 语法以及用途上都类似 O B/C RBA I D 。然而,Java 程序员不必使用 M D 。后面还会讲到另一种不同的 M O L I L M cr os of t 工具,它能读入 Java 类文件,并能生成一个类型库。 i 3. C M HRESULT 中的函数返回代码 O: 由服务器展示出来的 C M O 函数会返回一个值,采用预先定义好的 HRESULT 类型。HRESULT 代表一个包含了三 个字段的整数。这样便可使用多个失败和成功代码,同时还可以使用其他信息。由于 C M O 函数返回的是一个 HRESULT,所以不能用返回值从函数调用里取回原始数据。若必须返回数据,可传递指向一个内存区域的指 针,函数将在那个区域里填充数据。我们把这称为“外部参数”。作为 Java/C M程序员,我们不必过于关注 O 这个问题,因为虚拟机会帮助我们自动照管一切。这个问题将在后续的小节里讲述。

A. 5 . 2 M J av a/ COM集成 S
同C ++/C M程序员相比,M cr os of t Java编译器、虚拟机以及各式各样的工具极大简化了 Java/C M程序员 O i O 的工作。编译器有特殊的引导命令和包,可将 Java 类当作 C M类对待。但在大多数情况下,我们只需依赖 O M cr os of t JVM为 C M提供的支持,同时利用两个有力的外部工具。 i O M cr os of t Java Vi r t ual M i achi ne(JVM )在 C M和 Java 对象之间扮演了一座桥梁的角色。若将 Ja O va对象创 建成一个 C M O 服务器,那么我们的对象仍然会在 JVM 内部运行。M cr os of t JVM是作为一个 D 实现的,它 i LL 向操作系统展示出了 C M O 接口。在内部,JVM将对这些 C M O 接口的函数调用映射成 Java 对象中的方法调用。 当然,JVM 必须知道哪个 Java 类文件对应于服务器执行模块;之所以能够找出这方面的信息,是由于我们事 前已用 Javar eg在 W ndow 注册表内注册了类文件。Javar eg是与 M cr os of t Java SD 配套提供的一个工具 i s i K 程序,能读入一个 Java 类文件,生成相应的类型库以及一个 G D,并可将类注册到系统内。亦可用 Javar eg UI 注册远程服务器。例如,可用它注册在不同机器上运行的一个服务器。 如果想写一个 Java/C M客户,必须经历一系列不同的步骤。Java/C M O O “客户”是一些特殊的 Java 代码,它 们想激活和使用系统内注册的一个 C M O 服务器。同样地,虚拟机会与 C M服务器沟通,并将它提供的服务作 O 为 Java 类内的各种方法展示(揭示)出来。另一个 M cr os of t 工具是 j act i vex ,它能读取一个类型库,并 i 生成相应的 Java 源文件,在其中包含特殊的编译器引导命令。生成的源文件属于我们在指定类型库之后命名 的一个包的一部分。下一步是在自己的 C M O 客户 Java 源文件中导入那个包。 接下来让我们讨论两个例子。

A. 5 . 3 用 J av a 设计 COM服务器
本节将介绍 A i veX控件、A om i on 服务器或者其他任何符合 C M ct ut at O 规范的服务器的开发过程。下面这个例 子实现了一个简单的 A om i on 服务器,它能执行整数加法。我们用 s et A ut at ddend( ) 方法设置 addend的值。 每次调用 s um ) 方法的时候,ad ( dend 就会添加到当前 r es ul t 里。我们用 get Res ul t ( )获得 r es ul t 值,并用 cl ear ( )重新设置值。用于实现这一行为的 Java 类是非常简单的: publ i c cl as s A dder { pr i vat e i nt addend; 663

pr i vat e i nt r es ul t ; publ i c voi d s et A ddend( i nt a) { addend = a; } publ i c i nt get A ddend( ) { r et ur n addend; } publ i c i nt get Res ul t ( ) { r et ur n r es ul t ; } publ i c voi d s um ) { r es ul t += addend; } ( publ i c voi d cl ear ( ) { r es ul t = 0; addend = 0; } } 为了将这个 Java 类作为一个 C M O 对象使用,我们将 Javar eg工具应用于编译好的 A dder . cl as s 文件。这个工 具提供了一系列选项;在这种情况下,我们指定 Java 类文件名(" A dder " ),想为这个服务器在注册表里置 入的 Pr ogI D JavaA (" dder . A dder . 1" ),以及想为即将生成的类型库指定的名字(" JavaA dder . t l b" )。由于 尚未给出 C D LSI ,所以 Javar eg会自动生成一个。若我们再次对同样的服务器调用 Javar eg ,就会直接使用现 成的 C D LSI 。 j avar eg /r egi s t er /cl as s : A dder /pr ogi d: JavaA dder . A dder . 1 /t ypel i b: JavaA dder . t l b Javar eg也会将新服务器注册到 W ndow 注册表。此时,我们必须记住将 A i s dder . cl as s 复制到 W ndow i s\Java\t r us t l i b 目录。考虑到安全方面的原因(特别是涉及程序片调用 C M O 服务的问题),只有在 CM O 服务器已安装到 t r us t l i b 目录的前提下,这些服务器才会被激活。 现在,我们已在自己的系统中安装了一个新的 A om i on服务器。为进行测试,我们需要一个 A om i on ut at ut at 控制器,而 A om i on控制器就是 Vi s ual Bas i c(VB)。在下面,大家会看到几行 VB 代码。按照 VB 的格 ut at 式,我设置了一个文本框,用它从用户那里接收要相加的值。并用一个标签显示结果,用两个下推按钮分别 调用 s um ) 和 cl ear ( ) 方法。最开始,我们声明了一个名为 A ( dder 的对象变量。在 For m _Load子例程中(在窗 体首次显示时载入),会调用 A dder 自动服务器的一个新实例,并对窗体的文本字段进行初始化。一旦用户 按下“Sum ”或者“C ear ”按钮,就会调用服务器中对应的方法。 l D mA i dder A O ect s bj Pr i vat e Sub For m _Load( ) Set A dder = C eat eO ect ( " JavaA r bj dder . A dder . 1" ) A ddend. Text = A dder . get A ddend Res ul t . C i on = A apt dder . get Res ul t End Sub Pr i vat e Sub Sum n_C i ck( ) Bt l A dder . s et A ddend ( A ddend. Text ) A dder . Sum Res ul t . C i on = A apt dder . get Res ul t End Sub Pr i vat e Sub C ear Bt n_C i ck( ) l l A dder . C ear l A ddend. Text = A dder . get A ddend Res ul t . C i on = A apt dder . get Res ul t End Sub 注意,这段代码根本不知道服务器是用 Java 实现的。 664

运行这个程序并调用了 C eat eO ect ( ) 函数以后,就会在 W ndow 注册表里搜索指定的 Pr ogI D r bj i s 。在与 Pr ogI D有关的信息中,最重要的是 Java 类文件的名字。作为一个响应,会启动 Java 虚拟机,而且在 JVM 内 部调用 Java 对象的实例。从那个时候开始,JVM 就会自动接管客户和服务器代码之间的交流。

A. 5 . 4 用 J av a 设计 COM客户
现在,让我们转到另一侧,并用 Java 开发一个 C M客户。这个程序会调用系统已安装的 C M O O 服务器内的服 务。就目前这个例子来说,我们使用的是在前一个例子里为服务器实现的一个客户。尽管代码在 Java 程序员 的眼中看起来比较熟悉,但在幕后发生的一切却并不寻常。本例使用了用 Java 写成的一个服务器,但它可应 用于系统内安装的任何 A i veX控件、A i veX A om i on 服务器或者 A i veX组件——只要我们有一个类 ct ct ut at ct 型库。 首先,我们将 Jact i vex 工具应用于服务器的类型库。Jact i vex 有一系列选项和开关可供选择。但它最基本 的形式是读取一个类型库,并生成 Java 源文件。这个源文件保存于我们的 w ndow /j ava/t r us t l i b目录中。 i s 通过下面这行代码,它应用于为外部 C M A om i on 服务器生成的类型库: O ut at j act i vex /j avat l b JavaA dder . t l b Jact i vex 完成以后,我们再来看看自己的 w ndow /j ava/t r us t l i b目录。此时可在其中看到一个新的子目 i s 录,名为 j avaadder 。这个目录包含了用于新包的源文件。这是在 Java 里与类型库的功能差不多的一个库。 这些文件需要使用 M cr os of t 编译器的专用引导命令:@ 。j act i vex 生成多个文件的原因是 C M i com O 使用多个 实体来描述一个 C M O 服务器(另一个原因是我没有对 M D 文件和 Java/C M工具的使用进行细致的调整)。 I L O 名为 A dder . j ava的文件等价于 M D 文件中的一个 cocl as s 引导命令:它是对一个 C M I L O 类的声明。其他文件 则是由服务器揭示出来的 C M O 接口的 Java 等价物。这些接口(比如 A dder _D s pat chD aul t . j ava i ef )都属于 “遣送”(D s pat ch)接口,属于 A om i on 控制器与 A om i on 服务器之间的沟通机制的一部分。 i ut at ut at Java/C M集成特性也支持双接口的实现与使用。但是,I D s pat ch 和双接口的问题已超出了本附录的范围。 O i 在下面,大家可看到对应的客户代码。第一行只是导入由 j act i vex 生成的包。然后创建并使用 C M O A om i on服务器的一个实例,就象它是一个原始的 Java 类那样。请注意行内的类型模型,其中“例示” ut at 了 C M对象(即生成并调用它的一个实例)。这与 C M O O 对象模型是一致的。在 C M O 中,程序员永远不会得到 对整个对象的一个引用。相反,他们只能拥有对类内实现的一个或多个接口的引用。 “例示”A dder 类的一个 Java 对象以后,就相当于指示 C M O 激活服务器,并创建这个 C M对象的一个实例。 O 但我们随后必须指定自己想使用哪个接口,在由服务器实现的接口中挑选一个。这正是类型模型完成的工 作。这儿使用的是“默认遣送”接口,它是 A om i on 控制器用于同一个 A om i on 服务器通信的标准接 ut at ut at 口。欲了解这方面的细节,请参考由 I bi d 编著的《I ns i de C M O 》。请注意激活服务器并选择一个 C M O 接口是 多么容易! i m t j avaadder . * ; por publ i c cl as s JavaC i ent { l publ i c s t at i c voi d m n( St r i ng [ ] ar gs ) { ai A dder _D s pat chD aul t i A i ef dder = (A dder _D s pat chD aul t ) new A i ef dder ( ) ; iA dder . s et A ddend( 3) ; iA dder . s um ) ; ( iA dder . s um ) ; ( iA dder . s um ) ; ( Sys t em out . pr i nt l n( i A . dder . get Res ul t ( ) ) ; } } 现在,我们可以编译它,并开始运行程序。 1. com m . com包 . s com m . com . s 包为 C M O 的开发定义了数量众多的类。它支持 G D的使用——Var i ant (变体)和 Saf eA r ay UI r 665

A om i on(安全数组自动)类型——能与 A i veX控件在一个较深的层次打交道,并可控制 C M ut at ct O 异常。 由于篇幅有限,这里不可能涉及所有这些主题。但我想着重强调一下 C M异常的问题。根据规范,几乎所有 O CM O 函数都会返回一个 HRESULT 值,它告诉我们函数调用是否成功,以及失败的原因。但若观察服务器和客 户代码中的 Java 方法签名,就会发现没有 HRESULT 。相反,我们用函数返回值从一些函数那里取回数据。 “虚拟机”(VM )会将 Java 风格的函数调用转换成 C M风格的函数调用,甚至包括返回参数。但假若我们在 O 服务器里调用的一个函数在 C M O 这一级失败,又会在虚拟机里出现什么事情呢?在这种情况下,JVM 会认为 HRESULT 值标志着一次失败,并会产生类 com m . com C Fai l Except i on 的一个固有 Java 异常。这样一来, . s . om 我们就可用 Java 异常控制机制来管理 C M O 错误,而不是检查函数的返回值。 如欲深入了解这个包内包含的类,请参考微软公司的产品文档。

A. 5 . 5 Act i v eX/ Beans 集成
Java/C M集成一个有趣的结果就是 A i veX/Beans 的集成。也就是说,Java Bean 可包含到象 V O ct B或任何一种 M cr os of t O f i ce产品那样的 A i veX容器里。而一个 A i veX控件可包含到象 Sun BeanBox 这样的 Be ns i f ct ct a 容器里。M cr os of t JVM会帮助我们考虑到所有的细节。一个 A i veX控件仅仅是一个 C M i ct O 服务器,它展示 了预先定义好的、请求的接口。Bean 只是一个特殊的 Java 类,它遵循特定的编程风格。但在写作本书的时 候,这一集成仍然不能算作完美。例如,虚拟机不能将 JavaBeans 事件映射成为 C M事件模型。若希望从 O A i veX容器内部的一个 Bean 里对事件加以控制,Bean 必须通过低级技术拦截象鼠标行动这类的系统事件, ct 不能采用标准的 JavaBeans 委托事件模型。 抛开这个问题不管,A i veX/Beans 集成仍然是非常有趣的。由于牵涉的概念与工具与上面讨论的完全相 ct 同,所以请参阅您的 M cr os of t 文档,了解进一步的细节。 i

A. 5 . 6 固有方法与程序片的注意事项
固有方法为我们带来了安全问题的一些考虑。若您的 Java 代码发出对一个固有方法的调用,就相当于将控制 权传递到了虚拟机“体系”的外面。固有方法拥有对操作系统的完全访问权限!当然,如果由自己编写固有 方法,这正是我们所希望的。但这对程序片来说却是不可接受的——至少不能默许这样做。我们不想看到从 因特网远程服务器下载回来的一个程序片自由自在地操作文件系统以及机器的其他敏感区域,除非特别允许 它这样做。为了用 J/D r ect ,RN 和 C M i I O 集成防止此类情况的发生,只有受到信任(委托)的 Java 代码才有 权发出对固有方法的调用。根据程序片的具体使用,必须满足不同的条件才可放行。例如,使用 J/D r ect 的 i 一个程序片必须拥有数字化签名,指出自己受到完全信任。在写作本书的时候,并不是所有这些安全机制都 已实现(对于 M cr os of t SD f or Java,bet a 2 版本)。所以当新版本出现以后,请务必留意它的文档说 i K 明。

A. 6 CORBA
在大型的分布式应用中,我们的某些要求并非前面讲述的方法能够满足的。举个例子来说,我们可能想同以 前遗留下来的数据仓库打交道,或者需要从一个服务器对象里获取服务,无论它的物理位置在哪里。在这些 情况下,都要求某种形式的“远程过程调用”(RPC ),而且可能要求与语言无关。此时,C RBA可为我们提 O 供很大的帮助。 C RBA并非一种语言特性,而是一种集成技术。它代表着一种具体的规范,各个开发商通过遵守这一规范, O 可设计出符合 C RBA标准的集成产品。C RBA规范是由 O G开发出来的。这家非赢利性的机构致力于定义一 O O M 个标准框架,从而实现分布式、与语言无关对象的相互操作。 利用 C RBA O ,我们可实现对 Java 对象以及非 Java 对象的远程调用,并可与传统的系统进行沟通——采用一 种“位置透明”的形式。Java 增添了连网支持,是一种优秀的“面向对象”程序设计语言,可构建出图形化 和非图形化的应用(程序)。Java 和 O G对象模型存在着很好的对应关系;例如,无论 Java 还是 C RBA都 M O 实现了“接口”的概念,并且都拥有一个引用(参考)对象模型。

A. 6 . 1 CORBA 基础
由 O G制订的对象相互操作规范通常称为“对象管理体系”(O ect M M bj anagem ent A chi t ect ur e,O A r M)。O A M 定义了两个组件:“核心对象模型”(C e O ect M or bj odel )和“O A参考体系”(O A Ref er ence M M M odel )。O A参考体系定义了一套基层服务结构及机制,实现了对象相互间进行操作的能力。O A参考体系 M M 包括“对象请求代理”(O ect Reques t Br oker ,O bj RB)、“对象服务”(O ect Ser vi ces ,也称作 bj 666

C RBA er vi ces )以及一些通用机制。 O s O 是对象间相互请求的一条通信总线。进行请求时,毋需关心对方的物理位置在哪里。这意味着在客户代 RB 码中看起来象一次方案调用的过程实际是非常复杂的一次操作。首先,必须存在与服务器对象的一条连接途 径。而且为了创建一个连接,O 必须知道具体实现服务器的代码存放在哪里。建好连接后,必须对方法自 RB 变量进行“汇集”。例如,将它们转换到一个二进制流里,以便通过网络传送。必须传递的其他信息包括服 务器的机器名称、服务器进程以及对那个进程内的服务器对象进行标识的信息等等。最后,这些信息通过一 种低级线路协议传递,信息在服务器那一端解码,最后正式执行调用。O 将所有这些复杂的操作都从程序 RB 员眼前隐藏起来了,并使程序员的工作几乎和与调用本地对象的方法一样简单。 并没有硬性规定应如何实现 O 核心,但为了在不同开发商的 O 之间实现一种基本的兼容,O G定义了一 RB RB M 系列服务,它们可通过标准接口访问。 1. C RBA接口定义语言(I D O L) C RBA是面向语言的透明而设计的:一个客户对象可调用属于不同类的服务器对象方法,无论对方是用何种 O 语言实现的。当然,客户对象事先必须知道由服务器对象揭示的方法名称及签名。这时便要用到 I D L。C RBA O I D 是一种与语言无关的设计方法,可用它指定数据类型、属性、操作、接口以及更多的东西。I D 的语法类 L L 似于 C ++或 Java 语法。下面这张表格为大家总结了三种语言一些通用概念,并展示了它们的对应关系。 C RBA I D Java C O L ++ 模块(M odul e 包(Package) 命名空间(N es pace) ) am 接口(I nt er f ace) 接口(I nt er f ace) 纯抽象类(Pur e abs t r act cl as s ) 方法(M hod 方法(M hod) 成员函数(M ber f unct i on) et ) et em 继承概念也获得了支持——就象 C ++那样,同样使用冒号运算符。针对需要由服务器和客户实现和使用的属 性、方法以及接口,程序员要写出一个 I D 描述。随后,I D 会由一个由厂商提供的 I D L L L/Java 编译器进行编 译,后者会读取 I D 源码,并生成相应的 Java 代码。 L I D 编译器是一个相当有用的工具:它不仅生成与 I D 等价的 Java 源码,也会生成用于汇集方法自变量的代 L L 码,并可发出远程调用。我们将这种代码称为“根干”(St ub and Skel et on)代码,它组织成多个 Java 源 文件,而且通常属于同一个 Java 包的一部分。 2. 命名服务 命名服务属于 C RBA基本服务之一。C RBA对象是通过一个引用访问的。尽管引用信息用我们的眼睛来看没 O O 什么意义,但可为引用分配由程序员定义的字串名。这一操作叫作“引用的字串化”。一个叫作“命名服 务”(N i ng Ser vi ce am )的 O A组件专门用于执行“字串到对象”以及“对象到字串”转换及映射。由于命 M 名服务扮演了服务器和客户都能查询和操作的一个电话本的角色,所以它作为一个独立的进程运行。创建 “对象到字串”映射的过程叫作“绑定一个对象”;删除映射关系的过程叫作“取消绑定”;而让对象引用 传递一个字串的过程叫作“解析名称”。 比如在启动的时候,服务器应用可创建一个服务器对象,将对象同命名服务绑定起来,然后等候客户发出请 求。客户首先获得一个服务器引用,解析出字串名,然后通过引用发出对服务器的调用。 同样地,“命名服务”规范也属于 C RBA的一部分,但实现它的应用程序是由 O 厂商(开发商)提供的。 O RB 由于厂商不同,我们访问命名服务的方式也可能有所区别。

A. 6 . 2 一个例子
这儿显示的代码可能并不详尽,因为不同的 O 有不同的方法来访问 C RBA服务,所以无论什么例子都要取 RB O 决于具体的厂商(下例使用了 JavaI D L,这是 Sun公司的一个免费产品。它配套提供了一个简化版本的 O RB、 一个命名服务以及一个“I D L→Java”编译器)。除此之外,由于 Java 仍处在发展初期,所以在不同的 Java/C RBA产品里并不是包含了所有 C RBA特性。 O O 我们希望实现一个服务器,令其在一些机器上运行,其他机器能向它查询正确的时间。我们也希望实现一个 客户,令其请求正确的时间。在这种情况下,我们让两个程序都用 Java 实现。但在实际应用中,往往分别采 用不同的语言。 1. 编写 I D 源码 L 667

第一步是为提供的服务编写一个 I D 描述。这通常是由服务器程序员完成的。随后,程序员就可用任何语言 L 实现服务器,只需那种语言里存在着一个 C RBA I D 编译器。 O L I D 文件已分发给客户端的程序员,并成为两种语言间的桥梁。 L 下面这个例子展示了时间服务器的 I D 描述情况: L m odul e Rem eTi m { ot e i nt er f ace Exact Ti m { e s t r i ng get Ti m ) ; e( }; }; 这是对 Rem eTi m 命名空间内的 Exact Ti m 接口的一个声明。该接口由单独一个方法构成,它以字串格式返 ot e e 回当前时间。 2. 创建根干 第二步是编译 I D L,创建 Java 根干代码。我们将利用这些代码实现客户和服务器。与 JavaI D 产品配套提供 L 的工具是 i dl t oj ava: i dl t oj ava - f s er ver - f cl i ent Rem eTi m i dl ot e. 其中两个标记告诉 i dl t oj ava 同时为根和干生成代码。i dl t oj ava会生成一个 Java 包,它在 I D 模块、 L Rem eTi m ot e以及生成的 Java 文件置入 Rem eTi m ot e子目录后命名。_Exact Ti m m Bas e. j ava代表我们用于 eI pl 实现服务器对象的“干”;而_Exact Ti m ub. j ava将用于客户。在 Exact Ti m j ava中,用 Java 方式表示 eSt e. 了 I D 接口。此外还包含了用到的其他支持文件,例如用于简化访问命名服务的文件。 L 3. 实现服务器和客户 大家在下面看到的是服务器端使用的代码。服务器对象是在 Exact Ti m eSer ver 类里实现的。 Rem eTi m ot eSer ver 这个应用的作用是:创建一个服务器对象,通过 O 为其注册,指定对象引用时采用的名 RB 称,然后“安静”地等候客户发出请求。 i m t Rem eT i m * ; por ot e. i m t or g. om C N i ng. *; por g. os am i m t or g. om C N i ng. N i ngC ext Package. *; por g. os am am ont i m t or g. om C RBA *; por g. O . i m t j ava. ut i l . * ; por i m t j ava. t ext . * ; por // Ser ver obj ect i m em at i on pl ent cl as s Exact Ti m eSer ver ext ends _Exact Ti m m Bas e{ eI pl publ i c St r i ng get Ti m ) { e( r et ur n D eFor m . at at get Ti m ns t ance( D eFor m . FULL) . eI at at f or m ( new D e( at at Sys t em cur r ent Ti m i l l i s ( ) ) ) ; . eM } } // Rem e appl i cat i on i m em at i on ot pl ent publ i c cl as s Rem eTi m ot eSer ver { publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) { ai try { // O cr eat i on and i ni t i al i zat i on: RB 668

O or b = O i ni t ( ar gs , nul l ) ; RB RB. // C eat e t he s er ver obj ect and r egi s t er i t : r Exact Ti m eSer ver t i m eSer ver O Ref = bj new Exact T i m eSer ver ( ) ; or b. connect ( t i m eSer ver O Ref ) ; bj // G t he r oot nam ng cont ext : et i or g. om C RBA O ect obj Ref = g. O . bj or b. r es ol ve_i ni t i al _r ef er ences ( " N eSer vi ce" ) ; am N i ngC ext ncRef = am ont N i ngC ext Hel per . nar r ow obj Ref ) ; am ont ( // A s i gn a s t r i ng nam t o t he s e // obj ect r ef er ence ( bi ndi ng) : N eC ponent nc = am om new N eC ponent ( " Exact T i m , " " ) ; am om e" N eC ponent pat h[ ] = { nc} ; am om ncRef . r ebi nd( pat h, t i m eSer ver O Ref ) ; bj // W t f or cl i ent r eques t s : ai j ava. l ang. O ect s ync = bj new j ava. l ang. O ect ( ) ; bj s ynchr oni zed( s ync) { s ync. w t ( ) ; ai } } cat ch ( Except i on e) { Sys t em out . pr i nt l n( . " Rem e T i m s er ver er r or : " + e) ; ot e e. pr i nt St ackTr ace( Sys t em out ) ; . } } } 正如大家看到的那样,服务器对象的实现是非常简单的;它是一个普通的 Java 类,从 I D 编译器生成的 L “干”代码中继承而来。但在与 O 以及其他 C RBA服务进行联系的时候,情况却变得稍微有些复杂。 RB O 4. 一些 C RBA服务 O 这里要简单介绍一下 JavaI D 相关代码所做的工作(注意暂时忽略了 C RBA代码与不同厂商有关这一事 L O 实)。m n( )的第一行代码用于启动 O ai RB。而且理所当然,这正是服务器对象需要同它进行沟通的原因。就 在 O 初始化以后,紧接着就创建了一个服务器对象。实际上,它正式名称应该是“短期服务对象”:从客 RB 户那里接收请求,“生存时间”与创建它的进程是相同的。创建好短期服务对象后,就会通过 O 对其进行 RB 注册。这意味着 O 已知道它的存在,可将请求转发给它。 RB 到目前为止,我们拥有的全部东西就是一个 t i m eSer ver O Ref ——只有在当前服务器进程里才有效的一个对 bj 象引用。下一步是为这个服务对象分配一个字串形式的名字。客户会根据那个名字寻找服务对象。我们通过 命名服务(N i ng Ser vi ce)完成这一操作。首先,我们需要对命名服务的一个对象引用。通过调用 am r es ol ve_i ni t i al _r ef er ences ( ) ,可获得对命名服务的字串式对象引用(在 JavaI D 中是 L “N eSer vi ce”),并将这个引用返回。这是对采用 nar r ow ) 方法的一个特定 N i ngC ext 引用的模 am ( am ont 型。我们现在可开始使用命名服务了。 为了将服务对象同一个字串形式的对象引用绑定在一起,我们首先创建一个 N eC ponent 对象,用 am om “Exact Ti m e”进行初始化。“Exact Ti m e”是我们想用于绑定服务对象的名称字串。随后使用 r ebi nd( ) 方 法,这是受限于对象引用的字串化引用。我们用 r ebi nd( ) 分配一个引用——即使它已经存在。而假若引用已 经存在,那么 bi nd( ) 会造成一个异常。在 C RBA中,名称由一系列 N eC ext 构成——这便是我们为什么 O am ont 要用一个数组将名称与对象引用绑定起来的原因。 669

服务对象最好准备好由客户使用。此时,服务器进程会进入一种等候状态。同样地,由于它是一种“短期服 务”,所以生存时间要受服务器进程的限制。JavaI D 目前尚未提供对“持久对象”(只要创建它们的进程 L 保持运行状态,对象就会一直存在下去)的支持。 现在,我们已对服务器代码的工作有了一定的认识。接下来看看客户代码: i m or t Rem eTi m *; p ot e. i m t or g. om C N i ng. *; por g. os am i m t or g. om C RBA *; por g. O . publ i c cl as s Rem eTi m l i ent { ot eC publ i c s t at i c voi d m n( St r i ng ar gs [ ] ) { ai try { // O cr eat i on and i ni t i al i zat i on: RB O or b = O i ni t ( ar gs , nul l ) ; RB RB. // G t he r oo nam ng cont ext : et t i or g. om C RBA O ect obj Ref = g. O . bj or b. r es ol ve_i ni t i al _r ef er ences ( " N eSer vi ce" ) ; am N i ngC ext ncRef = am ont N i ngC ext Hel per . nar r ow obj Ref ) ; am ont ( // G ( r es ol ve) t he s t r i ngi f i ed obj ect et // r ef er e nce f or t he t i m s er ver : e N eC ponent nc = am om new N eC ponent ( " Exact T i m , " " ) ; am om e" N eC ponent pat h[ ] = { nc} ; am om Exact Ti m t i m bj Ref = e eO Exact Ti m eHel per . nar r ow ( ncRef . r es ol ve( pat h) ) ; // M ake r eques t s t o t he s er ver obj ect : St r i ng exact Ti m = t i m bj Ref . get Ti m ) ; e eO e( Sys t em out . pr i nt l n( exact Ti m ; . e) } cat ch ( Except i on e) { Sys t em out . pr i nt l n( . " Rem e Ti m s er ver er r or : " + e) ; ot e e. pr i nt St ackTr ace( Sys t em out ) ; . } } } 前几行所做的工作与它们在服务器进程里是一样的:O 获得初始化,并解析出对命名服务的一个引用。 RB 接下来,我们需要用到服务对象的一个对象引用,所以将字串形式的对象引用直接传递给 r es ol ve( ) 方法, 并用 nar r ow ) 方法将结果造型到 Exact Ti m 接口引用里。最后调用 get Ti m ) 。 ( e e( 5. 激活名称服务进程 现在,我们已分别获得了一个服务器和一个客户应用,它们已作好相互间进行沟通的准备。大家知道两者都 需要利用命名服务绑定和解析字串形式的对象引用。在运行服务或者客户之前,我们必须启动命名服务进 程。在 JavaI D 中,命名服务属于一个 Java 应用,是随产品配套提供的。但它可能与其他产品有所不同。 L JavaI D 命名服务在 JVM L 的一个实例里运行,并(默认)监视网络端口 900。 6. 激活服务器与客户 现在,我们已准备好启动服务器和客户应用(之所以按这一顺序,是由于服务器的存在是“短期”的)。若 各个方面都设置无误,那么获得的就是在客户控制台窗口内的一行输出文字,提醒我们当前的时间是多少。 670

当然,这一结果本身并没有什么令人兴奋的。但应注意一个问题:即使都处在同一台机器上,客户和服务器 应用仍然运行于不同的虚拟机内。它们之间的通信是通过一个基本的集成层进行的——即 O 与命名服务的 RB 集成。 这只是一个简单的例子,面向非网络环境设计。但通常将 O 配置成“与位置无关”。若服务器与客户分别 RB 位于不同的机器上,那么 O 可用一个名为“安装库”(I m em at i on Repos i t or y)的组件解析出远程字 RB pl ent 串式引用。尽管“安装库”属于 C RBA的一部分,但它几乎没有具体的规格,所以各厂商的实现方式是不尽 O 相同的。 正如大家看到的那样,C RBA还有许多方面的问题未在这儿进行详细讲述。但通过以上的介绍,应已对其有 O 一个基本的认识。若想获得 C RBA更详细的资料,最传真的起点莫过于 O B W O M eb站点,地址是 ht t p: //w w om or g。这个地方提供了丰富的文档资料、白页、程序以及对其他 C RBA资源和产品的链接。 w . g. O

A. 6 . 3 J av a 程序片和 CORB A
Java 程序片可扮演一名 C RBA客户的角色。这样一来,程序片就可访问由 C RBA对象揭示的远程信息和服 O O 务。但程序片只能同最初下载它的那个服务器连接,所以程序片与它沟通的所有 C RBA对象都必须位于那台 O 服务器上。这与 C RBA的宗旨是相悖的:它许诺可以实现“位置的透明”,或者“与位置无关”。 O 将 Java 程序片作为 C RBA客户使用时,也会带来一些安全方面的问题。如果您在内联网中,一个办法是放宽 O 对浏览器的安全限制。或者设置一道防火墙,以便建立与外部服务器安全连接。 针对这一问题,有些 Java O 产品专门提供了自己的解决方案。例如,有些产品实现了一种名为“HTTP 通 RB 道”(HTTP Tunnel i ng )的技术,另一些则提供了特别的防火墙功能。 作为放到附录中的内容,所有这些主题都显得太复杂了。但它们确实是需要重点注意的问题。

A. 6 . 4 比较 CORB A 与 RM I
我们已经知道,C RBA的一项主要特性就是对 RPC O (远程过程调用)的支持。利用这一技术,我们的本地对象 可调用位置远程对象内的方法。当然,目前已有一项固有的 Java 特性可以做完全相同的事情:RM (参考第 I 15 章)。尽管 RM 使 Java 对象之间进行 RPC调用成为可能,但 C RBA能在用任何语言编制的对象之间进行 I O RPC 。这显然是一项很大的区别。 然而,可通过 RM 调用远程、非 Java 代码的服务。我们需要的全部东西就是位于服务器那一端的、某种形式 I 的封装 Java 对象,它将非 Java 代码“包裹”于其中。封装对象通过 RM 同 Java 客户建立外部连接,并于内 I 部建立与非 Java 代码的连接——采用前面讲到的某种技术,如 JN 或 J/D r ect 。 I i 使用这种方法时,要求我们编写某种类型的“集成层”——这其实正是 C RBA帮我们做的事情。但是这样做 O 以后,就不再需要其他厂商开发的 O 了。 RB

A. 7 总结
我们在这个附录讨论的都是从一个 Java 应用里调用非 Java 代码最基本的技术。每种技术都有自己的优缺 点。但目前最主要的问题是并非所有这些特性都能在所有 JVM 中找到。因此,即使一个 Java 程序能调用位于 特定平台上的固有方法,仍有可能不适用于安装了不同 JVM 的另一种平台。 Sun公司提供的 JN 具有灵活、简单(尽管它要求对 JVM内核进行大量控制)、功能强大以及通用于大多数 I JVM 的优点。到本书完稿时为止,微软仍未提供对 JN 的支持,而是提供了自己的 J/D r ect (调用 W n32 I i i D 函数的一种简便方法)和 RN (特别适合编写高效率的代码,但要求对 JVM内核有很深入的理解)。微软 LL I 也提供了自己的专利 Java/C M集成方案。这一方案具有很强大的功能,且将 Java 变成了编写 C M O O 服务器和 客户的有效语言。只有微软公司的编译器和 JVM 能提供对 J/D r ect 、RN 以及 Java/C M的支持。 i I O 我们最后研究的是 C RBA O ,它使我们的 Java 对象可与其他对象沟通——无论它们的物理位置在哪里,也无论 是用何种语言实现的。C RBA与前面提到的所有技术都不同,因为它并未集成到 Java 语言里,而是采用了其 O 他厂商(第三方)的集成技术,并要求我们购买其他厂商提供的 O RB。C RBA是一种有趣和通用的方案,但如 O 果只是想发出对操作系统的调用,它也许并非一种最佳方案。

671

附录 B 对比 C++和 J ava
“作为一名 C ++程序员,我们早已掌握了面向对象程序设计的基本概念,而且 Java 的语法无疑是非常熟悉 的。事实上,Java 本来就是从 C ++衍生出来的。” 然而,C ++和 Java 之间仍存在一些显著的差异。可以这样说,这些差异代表着技术的极大进步。一旦我们弄 清楚了这些差异,就会理解为什么说 Java 是一种优秀的程序设计语言。本附录将引导大家认识用于区分 Java 和 C ++的一些重要特征。 ( 1) 最大的障碍在于速度:解释过的 Java 要比 C的执行速度慢上约 20 倍。无论什么都不能阻止 Java 语言进 行编译。写作本书的时候,刚刚出现了一些准实时编译器,它们能显著加快速度。当然,我们完全有理由认 为会出现适用于更多流行平台的纯固有编译器,但假若没有那些编译器,由于速度的限制,必须有些问题是 Java 不能解决的。 ( 2) 和 C ++一样,Java 也提供了两种类型的注释。 ( 3) 所有东西都必须置入一个类。不存在全局函数或者全局数据。如果想获得与全局函数等价的功能,可考 虑将 s t at i c方法和 s t at i c 数据置入一个类里。注意没有象结构、枚举或者联合这一类的东西,一切只有 “类”(C as s )! l ( 4) 所有方法都是在类的主体定义的。所以用 C ++的眼光看,似乎所有函数都已嵌入,但实情并非如何(嵌 入的问题在后面讲述)。 ( 5) 在 Java 中,类定义采取几乎和 C ++一样的形式。但没有标志结束的分号。没有 cl as s f oo这种形式的类 声明,只有类定义。 cl as s aType( ) voi d aM hod( ) { /* 方法主体 */} et } ( 6) Java 中没有作用域范围运算符“: : ”。Java 利用点号做所有的事情,但可以不用考虑它,因为只能在一 个类里定义元素。即使那些方法定义,也必须在一个类的内部,所以根本没有必要指定作用域的范围。我们 注意到的一项差异是对 s t at i c方法的调用:使用 C as s N e. m hodN e( ) 。除此以外,package l am et am (包)的名 字是用点号建立的,并能用 i m t 关键字实现 C por ++的“# ncl ude”的一部分功能。例如下面这个语句: i i m t j ava. aw . *; por t (# ncl ude 并不直接映射成 i m t ,但在使用时有类似的感觉。) i por ( 7) 与 C ++类似,Java 含有一系列“主类型”(Pr i m t i ve t ype),以实现更有效率的访问。在 Ja i va中,这 些类型包括 bool ean,char ,byt e,s hor t ,i nt ,l ong,f l oat 以及 doubl e。所有主类型的大小都是固有 的,且与具体的机器无关(考虑到移植的问题)。这肯定会对性能造成一定的影响,具体取决于不同的机 器。对类型的检查和要求在 Java 里变得更苛刻。例如: ■条件表达式只能是 bool ean(布尔)类型,不可使用整数。 ■必须使用象 X+Y 这样的一个表达式的结果;不能仅仅用“X+Y”来实现“副作用”。 ( 8) char (字符)类型使用国际通用的 16 位 Uni code字符集,所以能自动表达大多数国家的字符。 ( 9) 静态引用的字串会自动转换成 St r i ng对象。和 C及 C ++不同,没有独立的静态字符数组字串可供使用。 ( 10) Java增添了三个右移位运算符“>>>”,具有与“逻辑”右移位运算符类似的功用,可在最末尾插入零 值。“>>”则会在移位的同时插入符号位(即“算术”移位)。 ( 11) 尽管表面上类似,但与 C ++相比,Java 数组采用的是一个颇为不同的结构,并具有独特的行为。有一个 只读的 l engt h 成员,通过它可知道数组有多大。而且一旦超过数组边界,运行期检查会自动丢弃一个异常。 所有数组都是在内存“堆”里创建的,我们可将一个数组分配给另一个(只是简单地复制数组句柄)。数组 标识符属于第一级对象,它的所有方法通常都适用于其他所有对象。 ( 12) 对于所有不属于主类型的对象,都只能通过 new命令创建。和 C ++不同,Java 没有相应的命令可以“在 堆栈上”创建不属于主类型的对象。所有主类型都只能在堆栈上创建,同时不使用 new命令。所有主要的类 都有自己的“封装(器)”类,所以能够通过 new创建等价的、以内存“堆”为基础的对象(主类型数组是 一个例外:它们可象 C ++那样通过集合初始化进行分配,或者使用 new )。 ( 13) Java中不必进行提前声明。若想在定义前使用一个类或方法,只需直接使用它即可——编译器会保证 672

使用恰当的定义。所以和在 C ++中不同,我们不会碰到任何涉及提前引用的问题。 ( 14) Java没有预处理机。若想使用另一个库里的类,只需使用 i m t 命令,并指定库名即可。不存在类似 por 于预处理机的宏。 ( 15) Java用包代替了命名空间。由于将所有东西都置入一个类,而且由于采用了一种名为“封装”的机 制,它能针对类名进行类似于命名空间分解的操作,所以命名的问题不再进入我们的考虑之列。数据包也会 在单独一个库名下收集库的组件。我们只需简单地“i m t ”(导入)一个包,剩下的工作会由编译器自动 por 完成。 ( 16) 被定义成类成员的对象句柄会自动初始化成 nul l 。对基本类数据成员的初始化在 Java 里得到了可靠的 保障。若不明确地进行初始化,它们就会得到一个默认值(零或等价的值)。可对它们进行明确的初始化 (显式初始化):要么在类内定义它们,要么在构建器中定义。采用的语法比 C ++的语法更容易理解,而且 对于 s t at i c和非 s t at i c成员来说都是固定不变的。我们不必从外部定义 s t at i c成员的存储方式,这和 C ++ 是不同的。 ( 17) 在 Java 里,没有象 C和 C ++那样的指针。用 new创建一个对象的时候,会获得一个引用(本书一直将 其称作“句柄”)。例如: St r i ng s = new St r i ng( " how ) ; dy" 然而,C ++引用在创建时必须进行初始化,而且不可重定义到一个不同的位置。但 Java 引用并不一定局限于 创建时的位置。它们可根据情况任意定义,这便消除了对指针的部分需求。在 C和 C ++里大量采用指针的另 一个原因是为了能指向任意一个内存位置(这同时会使它们变得不安全,也是 Java 不提供这一支持的原 因)。指针通常被看作在基本变量数组中四处移动的一种有效手段。Java 允许我们以更安全的形式达到相同 的目标。解决指针问题的终极方法是“固有方法”(已在附录 A讨论)。将指针传递给方法时,通常不会带 来太大的问题,因为此时没有全局函数,只有类。而且我们可传递对对象的引用。Java 语言最开始声称自己 “完全不采用指针!”但随着许多程序员都质问没有指针如何工作?于是后来又声明“采用受到限制的指 针”。大家可自行判断它是否“真”的是一个指针。但不管在何种情况下,都不存在指针“算术”。 ( 18) Java提供了与 C ++类似的“构建器”(C t r uct or )。如果不自己定义一个,就会获得一个默认构建 ons 器。而如果定义了一个非默认的构建器,就不会为我们自动定义默认构建器。这和 C ++是一样的。注意没有 复制构建器,因为所有自变量都是按引用传递的。 ( 19) Java中没有“破坏器”(D t r uct or )。变量不存在“作用域”的问题。一个对象的“存在时间”是 es 由对象的存在时间决定的,并非由垃圾收集器决定。有个 f i nal i ze( )方法是每一个类的成员,它在某种程度 上类似于 C ++的“破坏器”。但 f i nal i ze( ) 是由垃圾收集器调用的,而且只负责释放“资源”(如打开的文 件、套接字、端口、URL 等等)。如需在一个特定的地点做某样事情,必须创建一个特殊的方法,并调用 它,不能依赖 f i nal i ze( ) 。而在另一方面,C ++中的所有对象都会(或者说“应该”)破坏,但并非 Java 中 的所有对象都会被当作“垃圾”收集掉。由于 Java 不支持破坏器的概念,所以在必要的时候,必须谨慎地创 建一个清除方法。而且针对类内的基础类以及成员对象,需要明确调用所有清除方法。 ( 20) Java具有方法“过载”机制,它的工作原理与 C ++函数的过载几乎是完全相同的。 ( 21) Java不支持默认自变量。 ( 22) Java 中没有 got o。它采取的无条件跳转机制是“br eak 标签”或者“cont i nue 标准”,用于跳出当前 的多重嵌套循环。 ( 23) Java采用了一种单根式的分级结构,因此所有对象都是从根类 O ect 统一继承的。而在 C bj ++中,我们 可在任何地方启动一个新的继承树,所以最后往往看到包含了大量树的“一片森林”。在 Java 中,我们无论 如何都只有一个分级结构。尽管这表面上看似乎造成了限制,但由于我们知道每个对象肯定至少有一个 O ect 接口,所以往往能获得更强大的能力。C bj ++目前似乎是唯一没有强制单根结构的唯一一种 O O语言。 ( 24) Java没有模板或者参数化类型的其他形式。它提供了一系列集合:Vect or (向量),St ack(堆栈)以 及 Has ht abl e(散列表),用于容纳 O ect 引用。利用这些集合,我们的一系列要求可得到满足。但这些集 bj 合并非是为实现象 C ++“标准模板库”(ST L)那样的快速调用而设计的。Java 1. 2 中的新集合显得更加完 整,但仍不具备正宗模板那样的高效率使用手段。 ( 25) “垃圾收集”意味着在 Java 中出现内存漏洞的情况会少得多,但也并非完全不可能(若调用一个用于 分配存储空间的固有方法,垃圾收集器就不能对其进行跟踪监视)。然而,内存漏洞和资源漏洞多是由于编 写不当的 f i nal i ze( ) 造成的,或是由于在已分配的一个块尾释放一种资源造成的(“破坏器”在此时显得特 别方便)。垃圾收集器是在 C ++基础上的一种极大进步,使许多编程问题消弥于无形之中。但对少数几个垃 圾收集器力有不逮的问题,它却是不大适合的。但垃圾收集器的大量优点也使这一处缺点显得微不足道。 ( 26) Java内建了对多线程的支持。利用一个特殊的 Thr ead类,我们可通过继承创建一个新线程(放弃了 r un( ) 方法)。若将 s ynchr oni z ed(同步)关键字作为方法的一个类型限制符使用,相互排斥现象会在对象 673

这一级发生。在任何给定的时间,只有一个线程能使用一个对象的 s ynchr oni z ed方法。在另一方面,一个 s ynchr oni z ed方法进入以后,它首先会“锁定”对象,防止其他任何 s ynchr oni z ed方法再使用那个对象。 只有退出了这个方法,才会将对象“解锁”。在线程之间,我们仍然要负责实现更复杂的同步机制,方法是 创建自己的“监视器”类。递归的 s ynchr oni z ed方法可以正常运作。若线程的优先等级相同,则时间的“分 片”不能得到保证。 ( 27) 我们不是象 C ++那样控制声明代码块,而是将访问限定符(publ i c,pr i vat e 和 pr ot ect ed)置入每个 类成员的定义里。若未规定一个“显式”(明确的)限定符,就会默认为“友好的”(f r i endl y )。这意味 着同一个包里的其他元素也可以访问它(相当于它们都成为 C ++的“f r i ends”——朋友),但不可由包外的 任何元素访问。类——以及类内的每个方法——都有一个访问限定符,决定它是否能在文件的外部“可 见”。pr i vat e关键字通常很少在 Java 中使用,因为与排斥同一个包内其他类的访问相比,“友好的”访问 通常更加有用。然而,在多线程的环境中,对 pr i vat e的恰当运用是非常重要的。Java 的 pr ot ect ed关键字 意味着“可由继承者访问,亦可由包内其他元素访问”。注意 Java 没有与 C ++的 pr ot ect ed关键字等价的元 素,后者意味着“只能由继承者访问”(以前可用“pr i vat e pr ot ect ed”实现这个目的,但这一对关键字的 组合已被取消了)。 ( 28) 嵌套的类。在 C ++中,对类进行嵌套有助于隐藏名称,并便于代码的组织(但 C ++的“命名空间”已使 名称的隐藏显得多余)。Java 的“封装”或“打包”概念等价于 C ++的命名空间,所以不再是一个问题。 Java 1. 1 引入了“内部类”的概念,它秘密保持指向外部类的一个句柄——创建内部类对象的时候需要用 到。这意味着内部类对象也许能访问外部类对象的成员,毋需任何条件——就好象那些成员直接隶属于内部 类对象一样。这样便为回调问题提供了一个更优秀的方案——C ++是用指向成员的指针解决的。 ( 29) 由于存在前面介绍的那种内部类,所以 Java 里没有指向成员的指针。 ( 30) Java不存在“嵌入”(i nl i ne)方法。Java 编译器也许会自行决定嵌入一个方法,但我们对此没有更 多的控制权力。在 Java 中,可为一个方法使用 f i nal 关键字,从而“建议”进行嵌入操作。然而,嵌入函数 对于 C ++的编译器来说也只是一种建议。 ( 31) Java中的继承具有与 C ++相同的效果,但采用的语法不同。Java 用 ext ends 关键字标志从一个基础类 的继承,并用 s uper 关键字指出准备在基础类中调用的方法,它与我们当前所在的方法具有相同的名字(然 而,Java 中的 s uper 关键字只允许我们访问父类的方法——亦即分级结构的上一级)。通过在 C ++中设定基 础类的作用域,我们可访问位于分级结构较深处的方法。亦可用 s uper 关键字调用基础类构建器。正如早先 指出的那样,所有类最终都会从 O ect 里自动继承。和 C bj ++不同,不存在明确的构建器初始化列表。但编译 器会强迫我们在构建器主体的开头进行全部的基础类初始化,而且不允许我们在主体的后面部分进行这一工 作。通过组合运用自动初始化以及来自未初始化对象句柄的异常,成员的初始化可得到有效的保证。 publ i c cl as s Foo ext ends publ i c Foo( St r i ng m g) s s uper ( m g) ; // C l s s al } publ i c baz( i nt i ) { // s uper . ba i ) ; // C l z( al } } Bar { { bas e cons t r uct or O r i de ver s bas e m hod et

( 32) Java中的继承不会改变基础类成员的保护级别。我们不能在 Java 中指定 publ i c,pr i vat e或者 pr ot ect ed继承,这一点与 C ++是相同的。此外,在衍生类中的优先方法不能减少对基础类方法的访问。例 如,假设一个成员在基础类中属于 publ i c,而我们用另一个方法代替了它,那么用于替换的方法也必须属于 publ i c(编译器会自动检查)。 ( 33) Java提供了一个 i nt er f ace 关键字,它的作用是创建抽象基础类的一个等价物。在其中填充抽象方 法,且没有数据成员。这样一来,对于仅仅设计成一个接口的东西,以及对于用 ext ends 关键字在现有功能 基础上的扩展,两者之间便产生了一个明显的差异。不值得用 abs t r act 关键字产生一种类似的效果,因为我 们不能创建属于那个类的一个对象。一个 abs t r act (抽象)类可包含抽象方法(尽管并不要求在它里面包含 什么东西),但它也能包含用于具体实现的代码。因此,它被限制成一个单一的继承。通过与接口联合使 用,这一方案避免了对类似于 C ++虚拟基础类那样的一些机制的需要。 为创建可进行“例示”(即创建一个实例)的一个 i nt er f ace(接口)的版本,需使用 i m em s 关键字。 pl ent 它的语法类似于继承的语法,如下所示: 674

publ i c i nt er f ace Face { publ i c voi d s m l e( ) ; i } publ i c cl as s Baz ext ends Bar i m em s Face { pl ent publ i c voi d s m l e( ) { i Sys t em out . pr i nt l n( " a w m s m l e" ) ; . ar i } } ( 34) Java中没有 vi r t ual 关键字,因为所有非 s t at i c 方法都肯定会用到动态绑定。在 Java 中,程序员不 必自行决定是否使用动态绑定。C ++之所以采用了 vi r t ual ,是由于我们对性能进行调整的时候,可通过将其 省略,从而获得执行效率的少量提升(或者换句话说:“如果不用,就没必要为它付出代价”)。vi r t ual 经常会造成一定程度的混淆,而且获得令人不快的结果。f i nal 关键字为性能的调整规定了一些范围——它 向编译器指出这种方法不能被取代,所以它的范围可能被静态约束(而且成为嵌入状态,所以使用 C ++非 vi r t ual 调用的等价方式)。这些优化工作是由编译器完成的。 ( 35) Java不提供多重继承机制(M ),至少不象 C I ++那样做。与 pr ot ect ed类似,M 表面上是一个很不错 I 的主意,但只有真正面对一个特定的设计问题时,才知道自己需要它。由于 Java 使用的是“单根”分级结 构,所以只有在极少的场合才需要用到 M 。i nt er f ace 关键字会帮助我们自动完成多个接口的合并工作。 I ( 36) 运行期的类型标识功能与 C ++极为相似。例如,为获得与句柄 X有关的信息,可使用下述代码: X. get C as s ( ) . get N e( ) ; l am 为进行一个“类型安全”的紧缩造型,可使用: der i ved d = ( der i ved) bas e; 这与旧式风格的 C造型是一样的。编译器会自动调用动态造型机制,不要求使用额外的语法。尽管它并不象 C ++的“new cas t s ”那样具有易于定位造型的优点,但 Java 会检查使用情况,并丢弃那些“异常”,所以它 不会象 C ++那样允许坏造型的存在。 ( 37) Java采取了不同的异常控制机制,因为此时已经不存在构建器。可添加一个 f i nal l y 从句,强制执行 特定的语句,以便进行必要的清除工作。Java 中的所有异常都是从基础类 Thr ow e里继承而来的,所以可 abl 确保我们得到的是一个通用接口。 publ i c voi d f ( O b) t hr ow I O bj s Except i on { m es our ce m = b. cr eat eRes our ce( ) ; yr r try { m . Us eRes our ce( ) ; r } cat ch ( M yExcept i on e) { // handl e m except i on y } cat ch ( Thr ow e e) { abl // handl e al l ot her except i ons } f i nal l y { m . di s pos e( ) ; // s peci al cl eanup r } } ( 38) Java的异常规范比 C ++的出色得多。丢弃一个错误的异常后,不是象 C ++那样在运行期间调用一个函 数,Java 异常规范是在编译期间检查并执行的。除此以外,被取代的方法必须遵守那一方法的基础类版本的 异常规范:它们可丢弃指定的异常或者从那些异常衍生出来的其他异常。这样一来,我们最终得到的是更为 “健壮”的异常控制代码。 ( 39) Java 具有方法过载的能力,但不允许运算符过载。St r i ng类不能用+和+=运算符连接不同的字串,而且 St r i ng表达式使用自动的类型转换,但那是一种特殊的内建情况。 ( 40) 通过事先的约定,C ++中经常出现的 cons t 问题在 Java 里已得到了控制。我们只能传递指向对象的句 柄,本地副本永远不会为我们自动生成。若希望使用类似 C ++按值传递那样的技术,可调用 cl one( ) ,生成自 变量的一个本地副本(尽管 cl one( )的设计依然尚显粗糙——参见第 12 章)。根本不存在被自动调用的副本 675

构建器。为创建一个编译期的常数值,可象下面这样编码: s t at i c f i nal i nt SI ZE = 255 s t at i c f i nal i nt BSI ZE = 8 * SI ZE ( 41) 由于安全方面的原因,“应用程序”的编程与“程序片”的编程之间存在着显著的差异。一个最明显的 问题是程序片不允许我们进行磁盘的写操作,因为这样做会造成从远程站点下载的、不明来历的程序可能胡 乱改写我们的磁盘。随着 Java 1. 1 对数字签名技术的引用,这一情况已有所改观。根据数字签名,我们可确 切知道一个程序片的全部作者,并验证他们是否已获得授权。Java 1. 2 会进一步增强程序片的能力。 ( 42) 由于 Java 在某些场合可能显得限制太多,所以有时不愿用它执行象直接访问硬件这样的重要任务。 Java 解决这个问题的方案是“固有方法”,允许我们调用由其他语言写成的函数(目前只支持 C和 C ++)。 这样一来,我们就肯定能够解决与平台有关的问题(采用一种不可移植的形式,但那些代码随后会被隔离起 来)。程序片不能调用固有方法,只有应用程序才可以。 ( 43) Java提供对注释文档的内建支持,所以源码文件也可以包含它们自己的文档。通过一个单独的程序, 这些文档信息可以提取出来,并重新格式化成 HTM 。这无疑是文档管理及应用的极大进步。 L ( 44) Java包含了一些标准库,用于完成特定的任务。C ++则依靠一些非标准的、由其他厂商提供的库。这些 任务包括(或不久就要包括): ■连网 ■数据库连接(通过 JD ) BC ■多线程 ■分布式对象(通过 RM 和 C RBA I O ) ■压缩 ■商贸 由于这些库简单易用,而且非常标准,所以能极大加快应用程序的开发速度。 ( 45) Java 1. 1 包含了 Java Beans 标准,后者可创建在可视编程环境中使用的组件。由于遵守同样的标准, 所以可视组件能够在所有厂商的开发环境中使用。由于我们并不依赖一家厂商的方案进行可视组件的设计, 所以组件的选择余地会加大,并可提高组件的效能。除此之外,Java Beans 的设计非常简单,便于程序员理 解;而那些由不同的厂商开发的专用组件框架则要求进行更深入的学习。 ( 46) 若访问 Java 句柄失败,就会丢弃一次异常。这种丢弃测试并不一定要正好在使用一个句柄之前进行。 根据 Java 的设计规范,只是说异常必须以某种形式丢弃。许多 C ++运行期系统也能丢弃那些由于指针错误造 成的异常。 ( 47) Java通常显得更为健壮,为此采取的手段如下: ■对象句柄初始化成 nul l (一个关键字) ■句柄肯定会得到检查,并在出错时丢弃异常 ■所有数组访问都会得到检查,及时发现边界违例情况 ■自动垃圾收集,防止出现内存漏洞 ■明确、“傻瓜式”的异常控制机制 ■为多线程提供了简单的语言支持 ■对网络程序片进行字节码校验

676

附录 C J ava 编程规则
本附录包含了大量有用的建议,帮助大家进行低级程序设计,并提供了代码编写的一般性指导: ( 1) 类名首字母应该大写。字段、方法以及对象(句柄)的首字母应小写。对于所有标识符,其中包含的所 有单词都应紧靠在一起,而且大写中间单词的首字母。例如: Thi s I s A l as s N e C am t hi s I s M hodO Fi el dN e et r am 若在定义中出现了常数初始化字符,则大写 s t at i c f i nal 基本类型标识符中的所有字母。这样便可标志出它 们属于编译期的常数。 Java 包(Package)属于一种特殊情况:它们全都是小写字母,即便中间的单词亦是如此。对于域名扩展名 称,如 com ,or g,net 或者 edu等,全部都应小写(这也是 Java 1. 1 和 Java 1. 2 的区别之一)。 ( 2) 为了常规用途而创建一个类时,请采取“经典形式”,并包含对下述元素的定义: equal s ( ) has hC ode( ) t oSt r i ng( ) cl one( )(i m em pl ent C oneabl e) l i m em pl ent Ser i al i zabl e ( 3) 对于自己创建的每一个类,都考虑置入一个 m n( ) ,其中包含了用于测试那个类的代码。为使用一个项 ai 目中的类,我们没必要删除测试代码。若进行了任何形式的改动,可方便地返回测试。这些代码也可作为如 何使用类的一个示例使用。 ( 4) 应将方法设计成简要的、功能性单元,用它描述和实现一个不连续的类接口部分。理想情况下,方法应 简明扼要。若长度很大,可考虑通过某种方式将其分割成较短的几个方法。这样做也便于类内代码的重复使 用(有些时候,方法必须非常大,但它们仍应只做同样的一件事情)。 ( 5) 设计一个类时,请设身处地为客户程序员考虑一下(类的使用方法应该是非常明确的)。然后,再设身 处地为管理代码的人考虑一下(预计有可能进行哪些形式的修改,想想用什么方法可把它们变得更简单)。 ( 6) 使类尽可能短小精悍,而且只解决一个特定的问题。下面是对类设计的一些建议: ■一个复杂的开关语句:考虑采用“多形”机制 ■数量众多的方法涉及到类型差别极大的操作:考虑用几个类来分别实现 ■许多成员变量在特征上有很大的差别:考虑使用几个类 ( 7) 让一切东西都尽可能地“私有”——pr i vat e。可使库的某一部分“公共化”(一个方法、类或者一个字 段等等),就永远不能把它拿出。若强行拿出,就可能破坏其他人现有的代码,使他们不得不重新编写和设 计。若只公布自己必须公布的,就可放心大胆地改变其他任何东西。在多线程环境中,隐私是特别重要的一 个因素——只有 pr i vat e字段才能在非同步使用的情况下受到保护。 ( 8) 谨惕“巨大对象综合症”。对一些习惯于顺序编程思维、且初涉 O P 领域的新手,往往喜欢先写一个顺 O 序执行的程序,再把它嵌入一个或两个巨大的对象里。根据编程原理,对象表达的应该是应用程序的概念, 而非应用程序本身。 ( 9) 若不得已进行一些不太雅观的编程,至少应该把那些代码置于一个类的内部。 ( 10) 任何时候只要发现类与类之间结合得非常紧密,就需要考虑是否采用内部类,从而改善编码及维护工作 (参见第 14 章 14. 1. 2 小节的“用内部类改进代码”)。 ( 11) 尽可能细致地加上注释,并用 j avadoc 注释文档语法生成自己的程序文档。 ( 12) 避免使用“魔术数字”,这些数字很难与代码很好地配合。如以后需要修改它,无疑会成为一场噩梦, 因为根本不知道“100”到底是指“数组大小”还是“其他全然不同的东西”。所以,我们应创建一个常数, 并为其使用具有说服力的描述性名称,并在整个程序中都采用常数标识符。这样可使程序更易理解以及更易 维护。 ( 13) 涉及构建器和异常的时候,通常希望重新丢弃在构建器中捕获的任何异常——如果它造成了那个对象的 创建失败。这样一来,调用者就不会以为那个对象已正确地创建,从而盲目地继续。 ( 14) 当客户程序员用完对象以后,若你的类要求进行任何清除工作,可考虑将清除代码置于一个良好定义的 方法里,采用类似于 cl eanup( ) 这样的名字,明确表明自己的用途。除此以外,可在类内放置一个 bool ean (布尔)标记,指出对象是否已被清除。在类的 f i nal i ze( ) 方法里,请确定对象已被清除,并已丢弃了从 677

Runt i m eExcept i on继承的一个类(如果还没有的话),从而指出一个编程错误。在采取象这样的方案之前, 请确定 f i nal i ze( ) 能够在自己的系统中工作(可能需要调用 Sys t em r unFi nal i zer s O . nExi t ( t r ue) ,从而确保 这一行为)。 ( 15) 在一个特定的作用域内,若一个对象必须清除(非由垃圾收集机制处理),请采用下述方法:初始化对 象;若成功,则立即进入一个含有 f i nal l y 从句的 t r y 块,开始清除工作。 ( 16) 若在初始化过程中需要覆盖(取消)f i nal i ze( ) ,请记住调用 s uper . f i nal i ze( ) (若 O ect 属于我们 bj 的直接超类,则无此必要)。在对 f i nal i ze( )进行覆盖的过程中,对 s uper . f i nal i ze( ) 的调用应属于最后一 个行动,而不应是第一个行动,这样可确保在需要基础类组件的时候它们依然有效。 ( 17) 创建大小固定的对象集合时,请将它们传输至一个数组(若准备从一个方法里返回这个集合,更应如此 操作)。这样一来,我们就可享受到数组在编译期进行类型检查的好处。此外,为使用它们,数组的接收者 也许并不需要将对象“造型”到数组里。 ( 18) 尽量使用 i nt er f aces ,不要使用 abs t r act 类。若已知某样东西准备成为一个基础类,那么第一个选择 应是将其变成一个 i nt er f ace(接口)。只有在不得不使用方法定义或者成员变量的时候,才需要将其变成 一个 abs t r act (抽象)类。接口主要描述了客户希望做什么事情,而一个类则致力于(或允许)具体的实施 细节。 ( 19) 在构建器内部,只进行那些将对象设为正确状态所需的工作。尽可能地避免调用其他方法,因为那些方 法可能被其他人覆盖或取消,从而在构建过程中产生不可预知的结果(参见第 7 章的详细说明)。 ( 20) 对象不应只是简单地容纳一些数据;它们的行为也应得到良好的定义。 ( 21) 在现成类的基础上创建新类时,请首先选择“新建”或“创作”。只有自己的设计要求必须继承时,才 应考虑这方面的问题。若在本来允许新建的场合使用了继承,则整个设计会变得没有必要地复杂。 ( 22) 用继承及方法覆盖来表示行为间的差异,而用字段表示状态间的区别。一个非常极端的例子是通过对不 同类的继承来表示颜色,这是绝对应该避免的:应直接使用一个“颜色”字段。 ( 23) 为避免编程时遇到麻烦,请保证在自己类路径指到的任何地方,每个名字都仅对应一个类。否则,编译 器可能先找到同名的另一个类,并报告出错消息。若怀疑自己碰到了类路径问题,请试试在类路径的每一个 起点,搜索一下同名的. cl as s 文件。 ( 24) 在 Java 1. 1 A T 中使用事件“适配器”时,特别容易碰到一个陷阱。若覆盖了某个适配器方法,同时 W 拼写方法没有特别讲究,最后的结果就是新添加一个方法,而不是覆盖现成方法。然而,由于这样做是完全 合法的,所以不会从编译器或运行期系统获得任何出错提示——只不过代码的工作就变得不正常了。 ( 25) 用合理的设计方案消除“伪功能”。也就是说,假若只需要创建类的一个对象,就不要提前限制自己使 用应用程序,并加上一条“只生成其中一个”注释。请考虑将其封装成一个“独生子”的形式。若在主程序 里有大量散乱的代码,用于创建自己的对象,请考虑采纳一种创造性的方案,将些代码封装起来。 ( 26) 警惕“分析瘫痪”。请记住,无论如何都要提前了解整个项目的状况,再去考察其中的细节。由于把握 了全局,可快速认识自己未知的一些因素,防止在考察细节的时候陷入“死逻辑”中。 ( 27) 警惕“过早优化”。首先让它运行起来,再考虑变得更快——但只有在自己必须这样做、而且经证实在 某部分代码中的确存在一个性能瓶颈的时候,才应进行优化。除非用专门的工具分析瓶颈,否则很有可能是 在浪费自己的时间。性能提升的隐含代价是自己的代码变得难于理解,而且难于维护。 ( 28) 请记住,阅读代码的时间比写代码的时间多得多。思路清晰的设计可获得易于理解的程序,但注释、细 致的解释以及一些示例往往具有不可估量的价值。无论对你自己,还是对后来的人,它们都是相当重要的。 如对此仍有怀疑,那么请试想自己试图从联机 Java 文档里找出有用信息时碰到的挫折,这样或许能将你说 服。 ( 29) 如认为自己已进行了良好的分析、设计或者实施,那么请稍微更换一下思维角度。试试邀请一些外来人 士——并不一定是专家,但可以是来自本公司其他部门的人。请他们用完全新鲜的眼光考察你的工作,看看 是否能找出你一度熟视无睹的问题。采取这种方式,往往能在最适合修改的阶段找出一些关键性的问题,避 免产品发行后再解决问题而造成的金钱及精力方面的损失。 ( 30) 良好的设计能带来最大的回报。简言之,对于一个特定的问题,通常会花较长的时间才能找到一种最恰 当的解决方案。但一旦找到了正确的方法,以后的工作就轻松多了,再也不用经历数小时、数天或者数月的 痛苦挣扎。我们的努力工作会带来最大的回报(甚至无可估量)。而且由于自己倾注了大量心血,最终获得 一个出色的设计方案,成功的快感也是令人心动的。坚持抵制草草完工的诱惑——那样做往往得不偿失。 ( 31) 可在 W eb上找到大量的编程参考资源,甚至包括大量新闻组、讨论组、邮寄列表等。下面这个地方提供 了大量有益的链接: ht t p: //w w ul b. ac. be/es p/i p- Li nks /Java/j oodcs /m - W w. m ebBi bl i o. ht m l

678

附录 D 性能
“本附录由 Joe Shar p投稿,并获得他的同意在这儿转载。请联系 Shar pJoe@ . com aol ” Java 语言特别强调准确性,但可靠的行为要以性能作为代价。这一特点反映在自动收集垃圾、严格的运行期 检查、完整的字节码检查以及保守的运行期同步等等方面。对一个解释型的虚拟机来说,由于目前有大量平 台可供挑选,所以进一步阻碍了性能的发挥。 “先做完它,再逐步完善。幸好需要改进的地方通常不会太多。”(St eve M onnel l 的《A cC bout per f or m ance 16] ) 》[ 本附录的宗旨就是指导大家寻找和优化“需要完善的那一部分”。

D. 1 基本方法
只有正确和完整地检测了程序后,再可着手解决性能方面的问题: ( 1) 在现实环境中检测程序的性能。若符合要求,则目标达到。若不符合,则转到下一步。 ( 2) 寻找最致命的性能瓶颈。这也许要求一定的技巧,但所有努力都不会白费。如简单地猜测瓶颈所在,并 试图进行优化,那么可能是白花时间。 ( 3) 运用本附录介绍的提速技术,然后返回步骤 1。 为使努力不至白费,瓶颈的定位是至关重要的一环。D onal d Knut h[ 9] 曾改进过一个程序,那个程序把 50% 的时间都花在约 4%的代码量上。在仅一个工作小时里,他修改了几行代码,使程序的执行速度倍增。此 时,若将时间继续投入到剩余代码的修改上,那么只会得不偿失。Knut h 在编程界有一句名言:“过早的优 化是一切麻烦的根源”(Pr em ur e opt i m zat i on i s t he r oot of al l evi l )。最明智的做法是抑制过早 at i 优化的冲动,因为那样做可能遗漏多种有用的编程技术,造成代码更难理解和操控,并需更大的精力进行维 护。

D. 2 寻找瓶颈
为找出最影响程序性能的瓶颈,可采取下述几种方法:

D. 2 . 1 安插自己的测试代码
插入下述“显式”计时代码,对程序进行评测: l ong s t ar t = Sys t em cur r ent Ti m i l l i s ( ) ; . eM / / 要计时的运算代码放在这儿 l ong t i m = Sys t em cur r ent Ti m i l l i s ( ) - s t ar t ; e . eM 利用 Sys t em out . pr i nt l n( ),让一种不常用到的方法将累积时间打印到控制台窗口。由于一旦出错,编译器 . 会将其忽略,所以可用一个“静态最终布尔值”(St at i c f i nal bool ean)打开或关闭计时,使代码能放心 留在最终发行的程序里,这样任何时候都可以拿来应急。尽管还可以选用更复杂的评测手段,但若仅仅为了 量度一个特定任务的执行时间,这无疑是最简便的方法。 Sys t em cur r ent Ti m i l l i s ( )返回的时间以千分之一秒(1 毫秒)为单位。然而,有些系统的时间精度低于 1 . eM 毫秒(如 W ndow PC i s ),所以需要重复 n次,再将总时间除以 n,获得准确的时间。

D. 2 . 2 J DK 性能评测[ 2]
JD 配套提供了一个内建的评测程序,能跟踪花在每个例程上的时间,并将评测结果写入一个文件。不幸的 K 是,JD 评测器并不稳定。它在 JD 1. 1. 1 中能正常工作,但在后续版本中却非常不稳定。 K K 为运行评测程序,请在调用 Java 解释器的未优化版本时加上- pr of 选项。例如: j ava_g - pr of m l as s yC 或加上一个程序片(A et ): ppl 679

j ava_g - pr of s un. appl et . A et Vi ew appl et . ht m ppl er l 理解评测程序的输出信息并不容易。事实上,在 JD 1. 0 中,它居然将方法名称截短为 30 字符。所以可能无 K 法区分出某些方法。然而,若您用的平台确实能支持- pr of 选项,那么可试试 Vl adi m r Bul at ov 的 i “Hyper Por f ”[ 3] 或者 G eg W t e 的“Pr of i l eVi ew ”来解释一下结果。 r hi er

D. 2 . 3 特殊工具
如果想随时跟上性能优化工具的潮流,最好的方法就是作一些 W eb站点的常客。比如由 Jonat han Har dw ck i 制作的“Tool s f or O i m zi ng Java”(Java 优化工具)网站: pt i ht t p: //w w cs . cm edu/~j ch/j ava/t ool s . ht m w. u. l

D. 2 . 4 性能评测的技巧
由于评测时要用到系统时钟,所以当时不要运行其他任何进程或应用程序,以免影响测试结果。 如对自己的程序进行了修改,并试图(至少在开发平台上)改善它的性能,那么在修改前后应分别测 试一下代码的执行时间。 尽量在完全一致的环境中进行每一次时间测试。 如果可能,应设计一个不依赖任何用户输入的测试,避免用户的不同反应导致结果出现误差。

D. 3 提速方法
现在,关键的性能瓶颈应已隔离出来。接下来,可对其应用两种类型的优化:常规手段以及依赖 Java 语言。

D. 3 . 1 常规手段
通常,一个有效的提速方法是用更现实的方式重新定义程序。例如,在《Pr ogr am i ng Pear l s 》(编程拾 m 贝)一书中[ 14] ,Bent l ey 利用了一段小说数据描写,它可以生成速度非常快、而且非常精简的拼写检查 器,从而介绍了 D oug M l r oy 对英语语言的表述。除此以外,与其他方法相比,更好的算法也许能带来更大 cI 的性能提升——特别是在数据集的尺寸越来越大的时候。欲了解这些常规手段的详情,请参考本附录末尾的 “一般书籍”清单。

D. 3 . 2 依赖语言的方法
为进行客观的分析,最好明确掌握各种运算的执行时间。这样一来,得到的结果可独立于当前使用的计算 机——通过除以花在本地赋值上的时间,最后得到的就是“标准时间”。 运算 示例 标准时间 本地赋值 i =n; 1. 0 实例赋值 t hi s . i =n; 1. 2 i nt 增值 i ++; 1. 5 byt e 增值 b++; 2. 0 s hor t 增值 s ++; 2. 0 f l oat 增值 f ++; 2. 0 doubl e增值 d++; 2. 0 空循环 w l e( t r ue) n++; 2. 0 hi 三元表达式 ( x>2(或 2 的任意次幂) 使用更快的硬件指令

D. 3 . 3 特殊情况
■字串的开销:字串连接运算符+看似简单,但实际需要消耗大量系统资源。编译器可高效地连接字串,但变 量字串却要求可观的处理器时间。例如,假设 s 和 t 是字串变量: Sys t em out . pr i nt l n( " headi ng" + s + " t r ai l er " + t ) ; . 上述语句要求新建一个 St r i ngBuf f er (字串缓冲),追加自变量,然后用 t oSt r i ng( ) 将结果转换回一个字 串。因此,无论磁盘空间还是处理器时间,都会受到严重消耗。若准备追加多个字串,则可考虑直接使用一 个字串缓冲——特别是能在一个循环里重复利用它的时候。通过在每次循环里禁止新建一个字串缓冲,可节 省 980 单位的对象创建时间(如前所述)。利用 s ubs t r i ng( ) 以及其他字串方法,可进一步地改善性能。如 果可行,字符数组的速度甚至能够更快。也要注意由于同步的关系,所以 St r i ngTokeni zer 会造成较大的开 销。 ■同步:在 JD 解释器中,调用同步方法通常会比调用不同步方法慢 10 倍。经 JI T 编译器处理后,这一性能 K 上的差距提升到 50 到 100 倍(注意前表总结的时间显示出要慢 97 倍)。所以要尽可能避免使用同步方法— —若不能避免,方法的同步也要比代码块的同步稍快一些。 ■重复利用对象:要花很长的时间来新建一个对象(根据前表总结的时间,对象的新建时间是赋值时间的 980 倍,而新建一个小数组的时间是赋值时间的 3100 倍)。因此,最明智的做法是保存和更新老对象的字 段,而不是创建一个新对象。例如,不要在自己的 pai nt ( )方法中新建一个 Font 对象。相反,应将其声明成 实例对象,再初始化一次。在这以后,可在 pai nt ( )里需要的时候随时进行更新。参见 Bent l ey 编著的《编 程拾贝》,p. 81[ 15] 。 ■异常:只有在不正常的情况下,才应放弃异常处理模块。什么才叫“不正常”呢?这通常是指程序遇到了 问题,而这一般是不愿见到的,所以性能不再成为优先考虑的目标。进行优化时,将小的“t r y- cat ch”块合 并到一起。由于这些块将代码分割成小的、各自独立的片断,所以会妨碍编译器进行优化。另一方面,若过 份热衷于删除异常处理模块,也可能造成代码健壮程度的下降。 ■散列处理:首先,Java 1. 0 和 1. 1 的标准“散列表”(Has ht abl e)类需要造型以及特别消耗系统资源的 同步处理(570 单位的赋值时间)。其次,早期的 JD 库不能自动决定最佳的表格尺寸。最后,散列函数应 K 针对实际使用项(Key)的特征设计。考虑到所有这些原因,我们可特别设计一个散列类,令其与特定的应用 程序配合,从而改善常规散列表的性能。注意 Java 1. 2 集合库的散列映射(Has hM ap)具有更大的灵活性, 而且不会自动同步。 ■方法内嵌:只有在方法属于 f i nal (最终)、pr i vat e(专用)或 s t at i c(静态)的情况下,Java 编译器 才能内嵌这个方法。而且某些情况下,还要求它绝对不可以有局部变量。若代码花大量时间调用一个不含上 述任何属性的方法,那么请考虑为其编写一个“f i nal ”版本。 ■I / O :应尽可能使用缓冲。否则,最终也许就是一次仅输入/输出一个字节的恶果。注意 JD 1. 0 的 I / O类 K 采用了大量同步措施,所以若使用象 r eadFul l y( ) 这样的一个“大批量”调用,然后由自己解释数据,就可 获得更佳的性能。也要注意 Java 1. 1 的“r eader ”和“w i t er ”类已针对性能进行了优化。 r ■造型和实例:造型会耗去 2 到 200 个单位的赋值时间。开销更大的甚至要求上溯继承(遗传)结构。其他 681

高代价的操作会损失和恢复更低层结构的能力。 ■图形:利用剪切技术,减少在 r epai nt ( ) 中的工作量;倍增缓冲区,提高接收速度;同时利用图形压缩技 术,缩短下载时间。来自 JavaW l d的“Java A et s ”以及来自 Sun的“Per f or m ng A m i on”是两个 or ppl i ni at 很好的教程。请记着使用最贴切的命令。例如,为根据一系列点画一个多边形,和 dr aw ne( ) 相比, Li dr aw ygon( ) 的速度要快得多。如必须画一条单像素粗细的直线,dr aw ne( x, y, x, y)的速度比 Pol Li f i l l Rect ( x, y, 1, 1)快。 ■使用 A 类:尽量使用来自 Java A 的类,因为它们本身已针对机器的性能进行了优化。这是用 Java 难 PI PI 于达到的。比如在复制任意长度的一个数组时,ar r ar yC opy( ) 比使用循环的速度快得多。 ■替换 A 类:有些时候,A 类提供了比我们希望更多的功能,相应的执行时间也会增加。因此,可定做 PI PI 特别的版本,让它做更少的事情,但可更快地运行。例如,假定一个应用程序需要一个容器来保存大量数 组。为加快执行速度,可将原来的 Vect or (矢量)替换成更快的动态对象数组。 1. 其他建议 ■将重复的常数计算移至关键循环之外——比如计算固定长度缓冲区的 buf f er . l engt h。 ■s t at i c f i nal (静态最终)常数有助于编译器优化程序。 ■实现固定长度的循环。 ■使用 j avac 的优化选项:- O 。它通过内嵌 s t at i c,f i nal 以及 pr i vat e方法,从而优化编译过的代码。注 意类的长度可能会增加(只对 JD 1. 1 而言——更早的版本也许不能执行字节查证)。新型的“Jus t - i nK ti m e”(JI T)编译器会动态加速代码。 ■尽可能地将计数减至 0——这使用了一个特殊的 JVM 字节码。

D. 4 参考资源
D. 4 . 1 性能工具
[ 1] 运行于 Pent i umPr o 200,N s cape 3. 0,JD 1. 1. 4 的 M cr oBenchm k(参见下面的参考资源[ 5] ) et K i ar [ 2] Sun的 Java 文档页——JD Java 解释器主题: K ht t p: //j ava. s un. com oduct s /JD ool s /w n32/j ava. ht m /pr K/t i l [ 3] Vl adi m r Bul at ov 的 Hyper Pr of i ht t p: //w w phys i cs . or s t . edu/~bul at ov/Hyper Pr of w. [ 4] G eg W t e 的 Pr of i l eVi ew r hi er ht t p: //w w i net m . com w. i /~gw /Pr of i l eVi ew /Pr of i l eVi ew . ht m hi er er l

D. 4 . 2 W 站点 eb
[ 5] 对于 Java 代码的优化主题,最出色的在线参考资源是 Jonat han Har dw ck 的“Java O i m zat i on”网 i pt i 站: ht t p: //w w cs . cm edu/~j ch/j ava/opt i m zat i on. ht m w. u. i l “Java 优化工具”主页: ht t p: //w w cs . cm edu/~j ch/j ava/t ool s . ht m w. u. l 以及“Java M cr obenchm r ks”(有一个 45 秒钟的评测过程): i a ht t p: //w w cs . cm edu/~j ch/j ava/benchm ks . ht m w. u. ar l

D. 4 . 3 文章
[ 6] “M ake Java f as t : O i m ze! How t o get t he gr eat es t per f or m pt i anceout of your code t hr ough l ow l evel opt i m zat i ons i n Java”(让 Java 更快:优化!如何通过在 Java 中的低级优化,使代码发挥最出色 i 的性能)。作者:D oug Bel l 。网址: ht t p: //w w j avaw l d. com avaw l d/j w 04- 1997/j w 04- opt i m ze. ht m w. or /j or i l (含一个全面的性能评测程序片,有详尽注释) [ 7] “Java O i m zat i on Res our ces ”(Java 优化资源) pt i ht t p: //w w cs . cm edu/~j ch/j ava/r es our ces . ht m w. u. l [ 8] “O i m zi ng Java f or Speed”(优化 Java,提高速度): pt i ht t p: //w w cs . cm edu/~j ch/j ava/s peed. ht m w. u. l 682

[ 9] “A Em r i cal St udy of FO n pi RTRA Pr ogr am ”(FO RA N s RT N程序实战解析)。作者:D onal d Knut h。 1971 年出版。第 1 卷,p. 105- 33,“软件——实践和练习”。 [ 10] “Bui l di ng Hi gh- Per f or m ance A i cat i ons and Ser ver s i n Java: A Exper i ent i al St ud ppl n y”。作 者: Ji m y N m guyen,M chael Fr aenkel ,Ri char dRedpat h,Bi nh Q N i . guyen 以及 Sandeep K. Si nghal 。I BM T. J. W s on Res ear chC er , I BM Sof t w e Sol ut i ons。 at ent ar ht t p: //w w i bm com ava/educat i on/j avahi pr . ht m w. . /j l

D. 4 . 4 J av a 专业书籍
[ 11] 《A dvanced Java,I di om ,Pi t f al l s ,St yl es , and Pr ogr am i ng Ti ps 》。作者:C i s Laf f r a。 s m hr Pr ent i ce Hal l 1997 年出版(Java 1. 0)。第 11 章第 20 小节。

D. 4 . 5 一般书籍
[ 12] 《D a St r uct ur es and C Pr ogr am 》(数据结构和 C程序)。作者:J. Van W at s yk。A s on- W l y ddi es 1998 年出版。 [ 13] 《W i t i ng Ef f i ci ent Pr ogr am 》(编写有效的程序)。作者:Jon Bent l ey。Pr ent i ce H l l 1982 年 r s a 出版。特别参考 p. 110 和 p. 145- 151。 [ 14] 《M e Pr ogr am i ng Pear l s 》(编程拾贝第二版)。作者:JonBent l ey。“A s oci at i on f or or m s C put i ng M om achi ner y”,1998 年 2 月。 [ 15] 《Pr ogr am i ng Pear l s 》(编程拾贝)。作者:Jone Bent l ey。A s on- W l ey 1989 年出版。第2 部 m ddi es 分强调了常规的性能改善问题。 [ 16] 《C ode C pl et e: A Pr act i cal Handbook of Sof t w e om ar C t r uct i on》(完整代码索引:实用软件开发手册)。作者:St eve M onnel l 。M cr os of t 出版社 1993 ons cC i 年出版,第 9 章。 [ 17] 《O ect - O i ent ed Sys t em D bj r evel opm 》(面向对象系统的开发)。作者:C peaux,Lea和 ent ham Faur e。第 25 章。 [ 18] 《T he A t of Pr ogr am i ng》(编程艺术)。作者:D r m onal d Knut h。第 1 卷“基本算法第 3 版”;第3 卷“排序和搜索第 2 版”。A s on- W l ey 出版。这是有关程序算法的一本百科全书。 ddi es [ 19] 《A gor i t hm i n C Fundam ent al s , D a St r uct ur es , Sor t i ng, Sear chi ng》(C算法:基础、数据结 l s : m at 构、排序、搜索)第 3 版。作者:Rober t Sedgew ck。A s on- W l ey 1997 年出版。作者是 Knut h的学生。 i ddi es 这是专门讨论几种语言的七个版本之一。对算法进行了深入浅出的解释。

683

附录 E 关于垃圾收集的一些话
“很难相信 Java 居然能和 C ++一样快,甚至还能更快一些。” 据我自己的实践,这种说法确实成立。然而,我也发现许多关于速度的怀疑都来自一些早期的实现方式。由 于这些方式并非特别有效,所以没有一个模型可供参考,不能解释 Java 速度快的原因。 我之所以想到速度,部分原因是由于 C ++模型。C ++将自己的主要精力放在编译期间“静态”发生的所有事情 上,所以程序的运行期版本非常短小和快速。C ++也直接建立在 C模型的基础上(主要为了向后兼容),但有 时仅仅由于它在 C中能按特定的方式工作,所以也是 C ++中最方便的一种方法。最重要的一种情况是 C和 C ++ 对内存的管理方式,它是某些人觉得 Java 速度肯定慢的重要依据:在 Java 中,所有对象都必须在内存 “堆”里创建。 而在 C ++中,对象是在堆栈中创建的。这样可达到更快的速度,因为当我们进入一个特定的作用域时,堆栈 指针会向下移动一个单位,为那个作用域内创建的、以堆栈为基础的所有对象分配存储空间。而当我们离开 作用域的时候(调用完毕所有局部构建器后),堆栈指针会向上移动一个单位。然而,在 C ++里创建“内存 堆”(Heap)对象通常会慢得多,因为它建立在 C的内存堆基础上。这种内存堆实际是一个大的内存池,要 求必须进行再循环(再生)。在 C ++里调用 del et e 以后,释放的内存会在堆里留下一个洞,所以再调用 new 的时候,存储分配机制必须进行某种形式的搜索,使对象的存储与堆内任何现成的洞相配,否则就会很快用 光堆的存储空间。之所以内存堆的分配会在 C ++里对性能造成如此重大的性能影响,对可用内存的搜索正是 一个重要的原因。所以创建基于堆栈的对象要快得多。 同样地,由于 C ++如此多的工作都在编译期间进行,所以必须考虑这方面的因素。但在 Java的某些地方,事 情的发生却要显得“动态”得多,它会改变模型。创建对象的时候,垃圾收集器的使用对于提高对象创建的 速度产生了显著的影响。从表面上看,这种说法似乎有些奇怪——存储空间的释放会对存储空间的分配造成 影响,但它正是 JVM 采取的重要手段之一,这意味着在 Java 中为堆对象分配存储空间几乎能达到与 C ++中在 堆栈里创建存储空间一样快的速度。 可将 C ++的堆(以及更慢的 Java 堆)想象成一个庭院,每个对象都拥有自己的一块地皮。在以后的某个时 间,这种“不动产”会被抛弃,而且必须再生。但在某些 JVM 里,Java 堆的工作方式却是颇有不同的。它更 象一条传送带:每次分配了一个新对象后,都会朝前移动。这意味着对象存储空间的分配可以达到非常快的 速度。“堆指针”简单地向前移至处女地,所以它与 C ++的堆栈分配方式几乎是完全相同的(当然,在数据 记录上会多花一些开销,但要比搜索存储空间快多了)。 现在,大家可能注意到了堆事实并非一条传送带。如按那种方式对待它,最终就要求进行大量的页交换(这 对性能的发挥会产生巨大干扰),这样终究会用光内存,出现内存分页错误。所以这儿必须采取一个技巧, 那就是著名的“垃圾收集器”。它在收集“垃圾”的同时,也负责压缩堆里的所有对象,将“堆指针”移至 尽可能靠近传送带开头的地方,远离发生(内存)分页错误的地点。垃圾收集器会重新安排所有东西,使其 成为一个高速、无限自由的堆模型,同时游刃有余地分配存储空间。 为真正掌握它的工作原理,我们首先需要理解不同垃圾收集器(G )采取的工作方案。一种简单、但速度较 C 慢的 G C技术是引用计数。这意味着每个对象都包含了一个引用计数器。每当一个句柄同一个对象连接起来 时,引用计数器就会增值。每当一个句柄超出自己的作用域,或者设为 nul l 时,引用计数就会减值。这样一 来,只要程序处于运行状态,就需要连续进行引用计数管理——尽管这种管理本身的开销比较少。垃圾收集 器会在整个对象列表中移动巡视,一旦它发现其中一个引用计数成为 0,就释放它占据的存储空间。但这样 做也有一个缺点:若对象相互之间进行循环引用,那么即使引用计数不是 0,仍有可能属于应收掉的“垃 圾”。为了找出这种自引用的组,要求垃圾收集器进行大量额外的工作。引用计数属于垃圾收集的一种类 型,但它看起来并不适合在所有 JVM 方案中采用。 在速度更快的方案里,垃圾收集并不建立在引用计数的基础上。相反,它们基于这样一个原理:所有非死锁 的对象最终都肯定能回溯至一个句柄,该句柄要么存在于堆栈中,要么存在于静态存储空间。这个回溯链可 能经历了几层对象。所以,如果从堆栈和静态存储区域开始,并经历所有句柄,就能找出所有活动的对象。 对于自己找到的每个句柄,都必须跟踪到它指向的那个对象,然后跟随那个对象中的所有句柄,“跟踪追 击”到它们指向的对象⋯⋯等等,直到遍历了从堆栈或静态存储区域中的句柄发起的整个链接网路为止。中 途移经的每个对象都必须仍处于活动状态。注意对于那些特殊的自引用组,并不会出现前述的问题。由于它 们根本找不到,所以会自动当作垃圾处理。 在这里阐述的方法中,JVM 采用一种“自适应”的垃圾收集方案。对于它找到的那些活动对象,具体采取的 操作取决于当前正在使用的是什么变体。其中一个变体是“停止和复制”。这意味着由于一些不久之后就会 684

非常明显的原因,程序首先会停止运行(并非一种后台收集方案)。随后,已找到的每个活动对象都会从一 个内存堆复制到另一个,留下所有的垃圾。除此以外,随着对象复制到新堆,它们会一个接一个地聚焦在一 起。这样可使新堆显得更加紧凑(并使新的存储区域可以简单地抽离末尾,就象前面讲述的那样)。 当然,将一个对象从一处挪到另一处时,指向那个对象的所有句柄(引用)都必须改变。对于那些通过跟踪 内存堆的对象而获得的句柄,以及那些静态存储区域,都可以立即改变。但在“遍历”过程中,还有可能遇 到指向这个对象的其他句柄。一旦发现这个问题,就当即进行修正(可想象一个散列表将老地址映射成新地 址)。 有两方面的问题使复制收集器显得效率低下。第一个问题是我们拥有两个堆,所有内存都在这两个独立的堆 内来回移动,要求付出的管理量是实际需要的两倍。为解决这个问题,有些 JVM根据需要分配内存堆,并将 一个堆简单地复制到另一个。 第二个问题是复制。随着程序变得越来越“健壮”,它几乎不产生或产生很少的垃圾。尽管如此,一个副本 收集器仍会将所有内存从一处复制到另一处,这显得非常浪费。为避免这个问题,有些 JVM 能侦测是否没有 产生新的垃圾,并随即改换另一种方案(这便是“自适应”的缘由)。另一种方案叫作“标记和清除”,Sun 公司的 JVM一直采用的都是这种方案。对于常规性的应用,标记和清除显得非常慢,但一旦知道自己不产生 垃圾,或者只产生很少的垃圾,它的速度就会非常快。 标记和清除采用相同的逻辑:从堆栈和静态存储区域开始,并跟踪所有句柄,寻找活动对象。然而,每次发 现一个活动对象的时候,就会设置一个标记,为那个对象作上“记号”。但此时尚不收集那个对象。只有在 标记过程结束,清除过程才正式开始。在清除过程中,死锁的对象会被释放然而,不会进行任何形式的复 制,所以假若收集器决定压缩一个断续的内存堆,它通过移动周围的对象来实现。 “停止和复制”向我们表明这种类型的垃圾收集并不是在后台进行的;相反,一旦发生垃圾收集,程序就会 停止运行。在 Sun公司的文档库中,可发现许多地方都将垃圾收集定义成一种低优先级的后台进程,但它只 是一种理论上的实验,实际根本不能工作。在实际应用中,Sun的垃圾收集器会在内存减少时运行。除此以 外,“标记和清除”也要求程序停止运行。 正如早先指出的那样,在这里介绍的 JVM 中,内存是按大块分配的。若分配一个大块头对象,它会获得自己 的内存块。严格的“停止和复制”要求在释放旧堆之前,将每个活动的对象从源堆复制到一个新堆,此时会 涉及大量的内存转换工作。通过内存块,垃圾收集器通常可利用死块复制对象,就象它进行收集时那样。每 个块都有一个生成计数,用于跟踪它是否依然“存活”。通常,只有自上次垃圾收集以来创建的块才会得到 压缩;对于其他所有块,如果已从其他某些地方进行了引用,那么生成计数都会溢出。这是许多短期的、临 时的对象经常遇到的情况。会周期性地进行一次完整清除工作——大块头的对象仍未复制(只是让它们的生 成计数溢出),而那些包含了小对象的块会进行复制和压缩。JVM 会监视垃圾收集器的效率,如果由于所有 对象都属于长期对象,造成垃圾收集成为浪费时间的一个过程,就会切换到“标记和清除”方案。类似地, JVM 会跟踪监视成功的“标记与清除”工作,若内存堆变得越来越“散乱”,就会换回“停止和复制”方 案。“自定义”的说法就是从这种行为来的,我们将其最后总结为:“根据情况,自动转换停止和复制/标 记和清除这两种模式”。 JVM 还采用了其他许多加速方案。其中一个特别重要的涉及装载器以及 JI T 编译器。若必须装载一个类(通 常是我们首次想创建那个类的一个对象时),会找到. cl as s 文件,并将那个类的字节码送入内存。此时,一 个方法是用 JI T 编译所有代码,但这样做有两方面的缺点:它会花更多的时间,若与程序的运行时间综合考 虑,编译时间还有可能更长;而且它增大了执行文件的长度(字节码比扩展过的 JI T 代码精简得多),这有 可能造成内存页交换,从而显著放慢一个程序的执行速度。另一种替代办法是:除非确有必要,否则不经 JI T 编译。这样一来,那些根本不会执行的代码就可能永远得不到 JI T 的编译。 由于 JVM 对浏览器来说是外置的,大家可能希望在使用浏览器的时候从一些 JVM 的速度提高中获得好处。但 非常不幸,JVM 目前不能与不同的浏览器进行沟通。为发挥一种特定 JVM 的潜力,要么使用内建了那种 JVM 的浏览器,要么只有运行独立的 Java 应用程序。

685

附录 F 推荐读物
■《Java i n a N s hel l : A D kt op Q ck Ref er ence ut es ui ,第 2 版》 作者:D d Fl anagan avi 出版社:O Rei l l y & A s oc ' s 出版时间:1997 简介:对 Java 1. 1 联机文档的一个简要总结。就个人来说,我更喜欢在线阅览文档,特别是在它们变化得如 此快的时候。然而,许多人仍然喜欢印刷出来的文档,这样可以省一些上网费。而且这本书也提供了比联机 文档更多的讨论。 ■《The Java C as s Li br ar i es : A A l n nnot at ed Ref er ence》 作者:Pat r i ck C han和 Ros anna Lee 出版社:A s on- W l ey ddi es 出版时间:1997 简介:作为一种联机参考资源,应向读者提供足够多的说明,使其简单易用。《T hi nki ng i n Java 》的一名 技术审定员说道:“如果我只能有一本 Java 书,那么肯定选它。”不过我可没有他那么激动。它太大、太 贵,而且示例的质量并不能令我满意。但在遇到麻烦的时候,该书还是很有参考价值的。而且与《Java i n a N s hel l 》相比,它看起来有更大的深度(当然也有更多的文字)。 ut ■《Java N w k Pr ogr am i ng》 et or m 作者:El l i ot e Rus t y Har ol d D d Fl anagan avi 出版社:O Rei l l y ' 出版时间:1997 简介:在阅读本书前,我可以说根本不理解 Java 有关网络的问题。后来,我也发现他的 W eb站点“C e au af Lai t ”是个令人激动的、很人个性的以及经常更新的去处,涉及大量有价值的 Java 开发资源。由于几乎每天 更新,所以在这里能看到与 Java 有关的大量新闻。站点地址是:ht t p: //s uns i t e. unc. edu/j avaf aq/ 。 ■《C e Java,第 3 版》 or 作者:C nel 和 Hor s t m or ann 出版社:Pr ent i ce- Hal l 出版时间:1997 简介:对于自己碰到的问题,若在《T hi nki ng i n Java》里找不到答案,这就是一个很好的参考地点。注 意:Java 1. 1 的版本是《C e Java 1. 1 Vol um 1- Fundam al s & C e Java 1. 1 Vol um 2 - A or e ent or e dvanced Feat ur es 》 ■《JD D abas e A BC at cces s w t h Java i 》 作者:Ham l t on,C t el l 和 Fi s her i at 出版社:A s on- W l ey ddi es 出版时间:1997 简介:如果对 SQ 和数据库一无所知,这本书就可以作为一个相当好的起点。它也对 A 进行了详尽的解 L PI 释,并提供一个“注释参考。与“Java 系列”(由 JavaSof t 授权的唯一一套丛书)的其他所有书籍一样, 这本书的缺点也是进行了过份的渲染,只说 Java 的好话——在这一系列书籍里找不到任何不利于 Java 的地 方。 ■《Java Pr ogr am i ng w t h C RBA》 m i O 作者:A eas Vogel 和 Kei t h D ndr uddy 出版社:Jonh W l ey & Sons i 出版时间:1997 简介:针对三种主要的 Java O RB(Vi s br oker ,O bi x,Joe),本书分别用大量代码实例进行了详尽的阐 r 686

述。 ■《D i gn Pat t er ns 》 es 作者:G m ,Hel m am a ,Johns on和 Vl i s s i des 出版社:A s on- W l ey ddi es 出版时间:1995 简介:这是一本发起了编程领域方案革命的经典书籍。 ■《UM Tool ki t 》 L 作者:Hans - Er i k Er i ks s on 和 M agnus Penker 出版社:Jonh W l ey & Sons i 出版时间:1997 简介:解释 UM 以及如何使用它,并提供 Java 的实际案例供参考。配套 C - RO 包含了 Java 代码以及 L D M Rat i onal Ros e 的一个删减版本。本书对 UM 进行了非常出色的描述,并解释了如何用它构建实际的系统。 L ■《Pr act i cal A gor i t hm f or Pr ogr am er s 》 l s m 作者:Bi ns t ock 和 Rex 出版社:A s on- W l ey ddi es 出版时间:1995 简介:算法是用 C描述的,所以它们很容易就能转换到 Java 里面。每种算法都有详尽的解释。

687

Similar Documents

Free Essay

Public Policy Problem

...Question: identify public policy problem and propose a policy to address it Introduction Definition: According Wikipedia Public policy as government action is generally the principled guide to action taken by the administrative or executive branches of the state with regard to a class of issues in a manner consistent with law and institutional customs. Other scholars define it as a system of "courses of action, regulatory measures, laws, and funding priorities concerning a given topic promulgated by a governmental entity or its representatives." Public policy is commonly embodied "in constitutions, legislative acts, and judicial decisions." Unemployment as a public policy problem In Kenya one of the public policy problems is unemployment. Unemployment in Kenya is where persons, who are able to work, are actually seeking jobs, but are unable to find work. According to the Ministry of Youth and Sports, there are nearly 2.5 million unemployed youth, and barely 125,000 are absorbed annually into formal employment. Some have referred to the problem as a ‘ticking time bomb’ saying the number of unemployed youth could rise to 14 million over the next seven years the causes of unemployment in Kenya rage from Government corruption to poverty, to lack of proper infrastructure but I think the cause of the high unemployment is Our Education system. Our education system is very academic. Our children go to school on Saturdays as young as standard 2, a standard 7 pupil wakes up...

Words: 760 - Pages: 4

Free Essay

Public Policy

...Public Policy The case study concerning Ocean Policy Change has been an important issue for many years. There have been policy changes over the years on ocean dumping. Starting in the early 1970’s there was much controversy on environmental protection and public policy. The concerns were that there needed to be policies on ocean dumping and what measurements the government was going to take to control this practice. President Nixon stated that public policy should be “to ban unregulated ocean dumping of all material and to place strict limits on ocean disposal of any materials harmful to the environment” (Stewart et al, 2008). Hence, the Marine Protection, Research, and Sanctuaries Act of 1972 (MPRSA) was passed. This policy had the strictest principles regarding ocean dumping. There was a permit system put into place which was monitored by the EPA and Army Corp of Engineers. In 1973 it was determined by the Environmental Protection Agency (EPA) to terminate all dumping even it was by permit. In the early 1980’s the city of New York applied for a permit to continue dumping the sludge from the sewer plants. The city brought a federal law suit asking for continued dumping of sewer sludge. The court in turn ruled that the EPA loosen their constraints on the dumping of sewer sludge. Due to the change of Presidents in 1981, there was even more leniency about the EPA guidelines or permits. There was more flexibility in the issuance of permits. Because of the knowledge and research...

Words: 621 - Pages: 3

Free Essay

Public Policy Problems in the Environment

...Public Policy Problems In The Environment Public policy is characterized by Webster's as "The fundamental strategy or set of arrangements structuring the establishment of open laws, particularly such arrangement not yet formally articulated." The United States Government has numerous approaches in the region of the nature. The Environmental Protection Agency (EPA) was made in 1970 to help distinguish natural issues in our country, and to set approach on the best way to manage those issues. Yet, with so much cash used by the legislature to manage issues with the nature, it must be noted that issues still exist, even inside the organization that was intended to help in any case. Amid the presidential fight of the last decision, an issue emerged concerning the "vitality emergency" that was driving fuel and oil costs up all through our nation. VP Al Gore upheld President Clinton's belief system of sitting tight for the best possible administrative activities to pass through Congress, and when the circumstances justified, give some restricted arrivals of oil from the national oil hold. Applicant George W. Bramble, on the other had, favored boring in the administration ensured grounds of Alaska to discover future oil saves with the goal that America would never again be so subject to remote oil. The issue with Bush's arrangement, as per Gore, was that this could be obliterating to the earth of the barely populated Alaskan wild. Despite the political, legitimate or moral ramifications...

Words: 872 - Pages: 4

Premium Essay

Public Policy

...Public Policy Everybody probably always wonder how rules, procedures, and practices are brought into the daily life of everyone in the United States. It all starts with public policy. Public policy is what governments decide. Policies aren’t the same as laws, but it is more a matter of how the laws are implemented. The public policy is formed in not that many steps or groups. “Policy is formulated with active involvement on the part of interest groups whose members do not seek election.” Also, the groups bring leaders and experts to define and discuss a problem and to reach a consensus on policies that address problems. You got those groups that will lobby on one particular issue and while others will research and train on the issues as that one group concentrates on one issue. There is a process that identifies the problem of the public policy. The four typical and main steps in public policy process is identifying a problem, formulating a policy, implementing the policy change, and evaluating the result. Each step is followed in order to make sure the process is done correctly. The first step is to outline the problem. After identifying and studying the problem, a new public policy may be formulated or developed. The next step would be once the new policy is an effect, then organizations and agencies will be responsible to carry out the policy. The final step is to evaluate, in which it is always ongoing. The person that is responsible for determining solutions for public...

Words: 350 - Pages: 2

Premium Essay

Public Policy

...contained in a known as public policy. In this vain, this essay is an attempt to define the term policy using examples from education. Furthermore, it will explain why it is necessary that each sphere of public life be enshrined in a public policy. The term Policy is not a precise term. Unfortunately, the term policy is something which takes different forms. There is push to designate policy as the ‘outputs’ of the political system, and in a lesser degree to define public policy as more or less like interdependent policies dealing with many different activities. Studies of public policy areas, on the centrally, have tended to focus on the evaluation of policy decisions in terms of specified values, that is, a rational rather than a political analysis. The magnitude of this problem can be recognized from the other definitions, which have been advanced by scholars in this field. To start with, Policy refers to those plans, positions and guidelines of government which influence decisions by government (e.g., policies in support of sustainable economic development or policies to enhance access to government services by persons with disabilities). In other words, policy is an action to achieve a goal (Bell & Stevenson, 2006). A policy establishes guidelines or direction for the City's actions with regard to one or more concerns, problems or opportunities. A policy can also be a general plan or approach to a specific need, problem or issue. This means that a policy is a general directive...

Words: 1466 - Pages: 6

Premium Essay

Public Policy: Beliefs And Influence

...Public Policy: Beliefs and Influence To start off, how do you define public policy? This question is so simple, yet everyone seems to define it differently from one person to another. According to Thomas A Birkland, a political scientist specializing in the study of public policy, he writes “Considerable debate remains over whether there is one coherent set of principles that can govern the study and understanding of what we call public policy. Since, there are many ways to define public policy”. Laws and public policy should be influenced by cultural and religious beliefs to a certain extent. First off, this country was founded off of Catholicism and Christianity. The majority of our population still actively believes in them to this day. Not only does religion have the capability of keeping people in line, but it gives people an incentive to be good to one another. After all, sinning is bad and the way to go to heaven is through moral actions and projecting love to one another. People have deep rooted beliefs and it makes them scared to do bad things, since there is the possibility of going to hell. The 10 Commandments are a good thing to abide by since they encourage upholding moral action and truth. Because of this, there are some cases in which it is better to have laws and public policy influenced by religion. In these cases, religion would...

Words: 653 - Pages: 3

Premium Essay

Public Policy

...MODELS FOR POLICY ANALYSIS INSTITUTIONALISM: POLICY AS INSTITUTIONAL OUTPUT Government institutions have long been a central focus of political science. Public policy is authoritatively determined, implemented, and enforced by these institutions. Therelationship between public policy and government institutions is very close. Strictly speaking, a policy does not become a public policy until it is adopted, implemented, and enforced by some government institution. Government institutions give public policy three distinctive characteristics. •First, government lends legitimacy to policies. Government policies are generally regarded as legal obligations that command the loyalty of citizens. •Second government policies involve universality. Only government policies extend to all people in a society; the policies of other groups or organizations reach only a part of the society. •Finally, government monopolizes coercion in society, only government can legitimately imprison violators of its policies. The impact of institutional arrangements on public policy is an empirical question that deserves investigation. Federalism recognizes that both the national government and the state governments derive independent legal authority from their own citizens. PROCESS: POLICY AS POLITICAL ACTIVITY Today political processes and behaviors are a central focus of political science. Political scientists with an interest in policy have grouped various activities according to their relationship...

Words: 1479 - Pages: 6

Free Essay

Designing Public Policy

...conflict when designing public policy. Efficiency is a comparative idea. It is a way of judging the merits of different ways of doing things. Efficient people are ones who get a lot done in a little time. Efficient allocations of resources are ones that yield the most total value for society from existing resources. Efficient choices are ones that result in the largest benefits for the same cost or the least cost given the benefit (Stone, 2002). According to Stone (2002), advocates of market mechanisms for policy making might argue that an exchange is efficient if it maximizes welfare at the present moment; inhabitants of the polis live over the long run and consider the time dimension a crucial part of most decisions. Another broad set challenges from the polis question, that an individual makes exchanges on the basis of full information about the objective alternatives and their subjective preferences. The challenge is impossible to have the type of information necessary for voluntary exchanges to result in efficiency. The market model requires accurate and complete information. But in the polis, information is always incomplete, interpretive and deliberately controlled. People can never have full information about the alternative available for satisfying their goals. Even if the money cost of information were zero, there would still be enormous time costs to process all the free information. Long term consequences are at issue in many public policy decisions as well-whether...

Words: 494 - Pages: 2

Free Essay

A. the Scope and Nature of the Public Policy Problem.

...University SOC 320 Public Policy & Social Services Instructor: Bernie Colon February 18, 2014 Weekly Journal I’ve started this journal one week into this class, and became aware that sociology is a major undertaking, but so far it has been a great journey. Knowing my audience for this paper allows me to draw from my last 3 years of study, this in turn will allow me to qualify what I may have learned. Cultural Anthropology seems to be at the forefront of my thinking why? I will address this thesis at the end of this course. This journal will be with me for the next 5 weeks and like it or not I will reveal my thoughts and intuition concerning the given subject. Let’s get started. For your journal entry, you will explore aspects of your personal experience and/or beliefs regarding significant public policies and what you have learned in your readings. As you reflect, please address the following questions: What are the current major environmental issues? "The amount of greenhouse gas in the atmosphere is already above the threshold that can potentially cause dangerous climate change. We are already at risk...It's not next year or next decade, it's now." Report from the UN Office for the Coordination of Humanitarian Affairs (OCHA): We are at the mercy of our environment and in my life time I have seen the environment destroyed. Sociology seems to be showing me another aspect in my comprehension of critical thinking. The environment became a legitimate policy issue in...

Words: 496 - Pages: 2

Premium Essay

Race, Urban Poverty, and Public Policy

...The problems of race and urban poverty remain pressing challenges which the United States has yet to address. Changes in the global economy, technology, and race relations during the last 30 years have necessitated new and innovative analyses and policy responses. A common thread which weaves throughout many of the studies reviewed here is the dynamics of migration. In When Work Disappears, immigrants provide comparative data with which to highlight the problems of ghetto poverty affecting blacks. In No Shame in My Game, Puerto Rican and Dominican immigrants are part of the changing demographics in Harlem. In Canarsie, the possible migration of blacks into a working/middle-class neighborhood prompts conservative backlash from a traditionally liberal community. In Streetwise, the migration of yuppies as a result of gentrification, and the movement of nearby-ghetto blacks into these urban renewal sites also invoke fear of crime and neighborhood devaluation among the gentrifying community. Not only is migration a common thread, but the persistence of poverty, despite the current economic boom, is the cornerstone of all these works. Poverty, complicated by the dynamics of race in America, call for universalistic policy strategies, some of which are articulated in Poor Support and The War Against the Poor. In When Work Disappears, William Julius Wilson builds upon many of the insights he introduced in The Truly Disadvantaged, such as the rampant joblessness, social isolation, and...

Words: 2489 - Pages: 10

Premium Essay

Public Policy Evalution

...Policy evaluation can be better defined as a process by which general judgments about quality, goal attainment, program effectiveness, impact, and costs can be determined. It is an assessment of whether a set of activities implemented under a specific policy has achieved a given set of objectives. Once public policy has been operationalized through the formal adoption of laws, rules, or regulations, and the bureaucracy has taken action to implement the policy, some form of evaluation needs to be accomplished to determine if the policy has achieved the desired outcome or impact. Public policy represents the expenditure of limited public resources and or restrictions on certain types of individual or organizational behavior. Consequently, the public has a right to expect that their government officials are accountable for the validity, efficiency, and effectiveness of those policies. Policy evaluation is therefore an absolutely critical stage in the policy process whereby we can determine whether a policy’s effects are intended or unintended and whether the results are positive or negative for the target population and society as a whole. In essence, policy evaluation is the process used to determine what the consequences of public policy are and what has and has not been achieved. Elected officials, policy makers, community leaders, bureaucrats, and the public want to know what policies work and what policies don't, and the purpose of evaluation is to determine whether an implemented...

Words: 2590 - Pages: 11

Free Essay

Public Policy and State Law

...Public policy reaches into all parts of society, and addresses social problems in a purposeful way, that has been decided to be for the good of the people. Any level of government, whether federal, state or local may be in involved in a particular policy effort because of social problems, public demand for action on these problems, and formulate or produce solutions that become formal plans of action (Kraft & Furlong, 2013). Resources are allocated, according to policy guidelines, to citizens within the US. Because we are a representative democracy, public opinion often mirrors public policy (Pawson & Wong, 2013). Public policy effects the public and is driven by social & economic conditions, among other things. Issues are brought forward and contemplated before they are deemed appropriate for being proposed for legislation. A policy often starts with a public problem that has been pressed by the public and/or groups for formal adoption. The issues are brought forward, solutions discussed and if found feasible, then may pushed forward to be adopted by legislatures (Pawson & Wong, 2013). For instance, states have speed limits to reduce the number of accidents on the road ways. In a given area, if there are higher than average deaths, there may be a push to reduce the speed limit on that stretch of road. I commute about an hour and a half each day to work and am often thankful to police officers who enforce the speed limit. Without these policies in effect...

Words: 551 - Pages: 3

Premium Essay

Need for Advertising and Public Policy

...article I found was in the International Journal of Advertising titled, On the Need for Advertising and Public Policy Research by Charles R. Taylor. Charles Taylor is a Professor of Marketing at Villanova University in Villanova, Philadelphia. Charles Taylor’s main point and concern in this article is the need for Advertising scholars to perform impartial research in the public policy area. Taylor begins his editorial with examples of advertising regulation and affects of advertising on society with the topic of the obesity epidemic and advertising’s role in it. Taylor’s first reference is from an article written in The Australian which states, “Restricting television advertisements for junk food aimed at children would be one of the most cost-effective public health measures governments could make, yielding huge savings from preventing fat kids turning into sickly adults.” Another article that supports this claim is referenced is in the Journal of Obesity by authors who are on the Faculty of Health, Medicine, Nursing and Behavioral Sciences at Deakin University. A study done by Anne Magnus and Michelle Haby show that a range of “21 to 57% of all childhood obesity is being caused by junk food advertising in the U.S.” (Taylor, 601). Taylor then gets to his main point and states how all of these articles may be produced by intelligent doctors, researchers for the public health and so forth, but not one of these articles and/or estimates produced were from a “doctor-ally trained...

Words: 578 - Pages: 3

Premium Essay

Leadership in Public Policy Making

...Leadership in Public Policy Making Dawn Miller PPA 601 Foundation of Public Administration Dr. Shavonnie Carthens September 15, 2014 Leadership in Public Policy Making Leadership is a significant role in any organization or agency that determines the conduct of actions as well as the course of action inside of the organization or agency. Leaders help develop clear visions, work multiple constituencies, know as well as understand social and ethical values, create vibrant networks, improves performance, and maintains values (Cropf, 2008, pg. 233). There are different levels of leadership abilities this depends on what each person wants to achieve and what the organization is requiring. The top leadership level is an executive level, the leaders at this level need to be practical, have personal skills as well as political skills. Leaders need to know that it is not only political factors that have a part in decisions or policy making but there are environmental issues too. Public policies that are successful usually have had a leader(s) that have developed, promoted, and executed some sort of public policy. Being able complete this accomplishment the leader(s) have learned or developed certain leadership traits. Traits that are important for a public policy are creativity, enthusiastic, follow through, responsibility, and persuasiveness. Characteristics that are different from a leader and a follower, intellectual, and endurance. Leadership traits are beneficial...

Words: 928 - Pages: 4

Premium Essay

The Impact of Uk Government Policies on the Public Services

...Kane Gibson BTEC level 3 Extended Diploma in Public Services (Uniforms) Unit 1 Government, Policies and the Public Service 14 January 2016 The impact of UK Government Policies on the Public Services Human rights Each UK citizen has certain rights, these include the rights to: Life, Prohibition of torture, Prohibition of slavery and forced labour, Liberty and security, A fair trial, No punishment without law, Respect for private and family life, Freedom of thought, conscience and religion, Freedom of expression, Freedom of assembly and association, Marry, Prohibition of discrimination, Protection of property, Education, Free elections, and Abolition of the death penalty. Environmental Air quality plan for reducing nitrogen dioxide (NO2) in Greater London urban area listing (UK0001) The Air quality plan is setting out what the government will be doing about improving the air quality in London as well as reduce nitrogen dioxide emissions. Another is “River Thames: application for registration of a launch” The government are setting out a registration for a powered boat to be in the river thames. The boats that are not included are Kayaks, canoes, and other non powered boats. The reason for this is to attempt reduction in emissions and other toxic wastes. All boats kept, rented, or let for hire must be registered with the Environment Agency (Inland Waterways) Order 2010 annually to calculate the amount of emissions entering the water. Affecting the military...

Words: 1814 - Pages: 8