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 (