java编程思想第五版.pdf
http://www.100md.com
2020年11月3日
![]() |
| 第1页 |
![]() |
| 第9页 |
![]() |
| 第18页 |
![]() |
| 第25页 |
![]() |
| 第41页 |
![]() |
| 第368页 |
参见附件(2206KB,541页)。
Java编程思想赢得了全球程序员的广泛赞誉,即使是最晦涩的概念,在Bruce Eckel的文字亲和力和小而直接的编程示例面前也会化解于无形。从Java的基础语法到最高级特性(深入的面向对象概念、多线程、自动项目构建、单元测试和调试等),本书都能逐步指导你轻松掌握

读者的心声
比我看过的Java书好多了…非常全面,举例都恰到好处,显得颇具“智慧".和其他许多Java书籍相比,我觉得它更成熟、连贯、更有说服力、更严谨。总之,写得非常好,肯定是一本习Java好书.(Anatoly Vorobey,Technionlniversity,Haifa,以色列).
是我见过的最好的编程指南,对任何语言都不外如是。(Joakim ziegler,FIX系统管理员)
感谢你写出如此优秀的一本Java参考书。(Dr.Gavin Pillay,Registrar,King Edward VIl Hospital,南非)
再次感谢您这本令人震惊的书。我以前真的有点儿不知所从的感觉(因为不是C程序员),但你的书浅显易懂,使我能很快掌握Java-差不多就是阅读的速度吧。能从头掌握基本原理和概念的感觉真好,再也不用通过不断的试验和出错来建立概念模型了。希望不久能有机会参加您的讲座。(Randall R.Hawley,Automation Technician,Eli Lilly&Co)
我迄今为止看过的最好的计算机参考书。(Tom Holland)
这是我读过的关于程序设计的最好的一本书 第16章有关设计方案的内容是我这么久以来看过的最有价值的。(Han Finci,助教,计算机科学学院,耶路撒冷希伯来大学,以色列)
为什么面向对象的编程会在软件开发领域造成如此震憾的影响?
面向对象编程(OOP)具有多方面的吸引力。对管理人员,它实现了更快和更廉价的开发与维护过程。对分析与设计人员,建模处理变得更加简单,能生成清晰、易于维护的设计方案。对程序员,对象模型显得如此高雅和浅显。此外,面向对象工具以及库的巨大威力使编程成为一项更使人愉悦的任务。每个人都可从中获益,至少表面如此。
如果说它有缺点,那就是掌握它需付出的代价。思考对象的时候,需要采用形象思维,而不是程序化的思维。与程序化设计相比,对象的设计过程更具挑战性--特别是在尝试创建可重复使用(可再生)的对象时。过去,那些初涉面向对象编程领域的人都必须进行一项令人痛苦的选择:
(1)选择一种诸如Smallalk的语言,“出师”前必须掌握一个巨型的库。
(2)选择几乎根本没有库的C+(注释0),然后深入学习这种语言,直至能自行编写对象库。
抽象的进步
所有编程语言的最终目的都是提供一种“抽象”方法。一种较有争议的说法是:解决问题的复杂程度直接取决于抽象的种类及质量。这儿的“种类”是指准备对什么进行“抽象”?汇编语言是对基础机器的少量抽象。后来的许多“命令式”语言(如FORTRAN,BASIC和C)是对汇编语言的一种抽象。与汇编语言相比,这些语言已有了长足的进步,但它们的抽象原理依然要求我们着重考虑计算机的结构,而非考虑问题本身的结构。在机器模型(位于“方案空间")与实际解决的问题模型(位于“问题空间”)之间,程序员必须建立起一种联系。这个过程要求人们付出较大的精力,而且由于它脱离了编程语言本身的范围,造成程序代码很难编写,而且要花较大的代价进行维护。由此造成的副作用便是一门完善的“编程方法”学科。
java编程思想第五版截图




《Thinking In Java》中文版
--免费电子书,仅供网上参考,不得私自出版!
——本书(第五版)原始版本为word版(Java编程思想(第五版).rar,各章
节文件独立,含源代码) ,为了便于阅读,现合并成 pdf 版。enjoycodes.com 整
理
作者:Bruce Eckel
主页:http:www.BruceEckel.com
编译:Trans Bot
主页:http:member.netease.com~transbot
致谢
--献给那些直到现在仍在孜孜不倦创造下一代计算机语言的人们!
指导您利用万维网的语言进行面向对象的程序设计
完整的正文、更新内容及程序代码可从 http:www.bruceeckel.com 下载
从 Java 的基本语法到它最高级的特性 (网络编程、 高级面向对象能力、 多线程) ,《Thinking In Java》都能对您有所裨益。Bruce Eckel 优美的行文以及短小、精悍的程序示例有助于您理解含义模糊的概念。
面向初学者和某种程度的专家
教授 Java 语言,而不是与平台有关的理论
覆盖 Java 1.2 的大多数重要方面:Swing 和新集合
系统讲述 Java 的高级理论:网络编程、多线程处理、虚拟机性能以及同非 Java
代码的连接
320 个有用的 Java 程序,15000 行以上代码
解释面向对象基本理论,从继承到设计方案
来自与众不同的获奖作者 Bruce Eckel
可通过万维网免费索取源码和持续更新的本书电子版
从 www.BruceEckel.com 获得配套 CD(含 15 小时以上的合成语音授课)
读者如是说: “最好的 Java 参考书……绝对让人震惊” ; “购买 Java 参考书最明
智的选择” ; “我见过的最棒的编程指南” 。
Bruce Eckel 也 是 《 Thinking in C++ 》 的 作 者 , 该 书 曾 获 1995 年
SoftwareDevelopment Jolt Award 最佳书籍大奖。作为一名有 20 经验的编程专
家,曾教授过世界上许多地区的人进行对象编程。最开始涉及的领域是 C++,现
在也进军 Java。他是 C++标准协会有表决权的成员之一,曾就面向对象程序设计
这一主题写过其他 5 本书,发表过 150 多篇文章,并是多家计算机杂志的专栏作 家,其中包括《Web Techniques》的 Java 专栏。曾出席过 C++和 Java 的“软件
开发者会议” ,并分获“应用物理”与“计算机工程”的学士和硕士学位。
读者的心声
比我看过的 Java 书好多了……非常全面,举例都恰到好处,显得颇具“智慧” 。
和其他许多 Java 书籍相比,我觉得它更成熟、连贯、更有说服力、更严谨。总
之,写得非常好,肯定是一本学习 Java 的好书。 (Anatoly Vorobey,TechnionUniversity,Haifa,以色列) 。
是我见过的最好的编程指南,对任何语言都不外如是。 (Joakim ziegler,FIX
系统管理员)
感谢你写出如此优秀的一本 Java 参考书。 (Dr. Gavin Pillay,Registrar,King
Edward VII Hospital,南非)
再次感谢您这本令人震惊的书。我以前真的有点儿不知所从的感觉(因为不是 C
程序员) ,但你的书浅显易懂,使我能很快掌握 Java——差不多就是阅读的速度
吧。能从头掌握基本原理和概念的感觉真好,再也不用通过不断的试验和出错来
建立概念模型了。希望不久能有机会参加您的讲座。 (Randall R. Hawley,Automation Technician,Eli Lilly Co)
我迄今为止看过的最好的计算机参考书。 (Tom Holland)
这是我读过的关于程序设计的最好的一本书……第 16 章有关设计方案的内容是
我这么久以来看过的最有价值的。 (Han Finci,助教,计算机科学学院,耶路撒
冷希伯来大学,以色列)
有史以来最好的一本 Java 参考书。 (Ravindra Pai,Oracle 公司 SUNOS 产品线)
这是关于 Java 的一本好书。非常不错,你干得太好了!书中涉及的深度真让人
震惊。一旦正式出版,我肯定会买下它。我从 96 年十月就开始学习 Java 了。通
过比较几本书,你的书可以纳入“必读”之列。这几个月来,我一直在搞一个完
全用 Java 写的产品。你的书巩固了我一些薄弱的地方,并大大延伸了我已知的
东西。甚至在会见承包商的时候,我都引用了书中的一些解释,它对我们的开发
小组太有用了。通过询问组内成员我从书中学来的知识(比如数组和矢量的区
别) ,可以判断他们对 Java 的掌握有多深。 (Steve Wilkinson,MCI 通信公司资
深专家)
好书!我见过的最好的一本 Java 教材。 (Jeff Sinclair,软件工程师,Kestral
Computing 公司)
感谢你的《Thinking in Java》 。终于有人能突破传统的计算机参考书模式,进
入一个更全面、更深入的境界。我读过许多书,只有你的和 Patrick Winston的书才在我心目中占据了一个位置。我已向客户郑重推荐这本书。再次感谢。
(Richard Brooks,Java 顾问,Sun 专业服务公司,达拉斯市)
其他书讨论的都是 Java“是什么” (讲述语法和库) , 或者 Java“怎样用” (编程实
例) 。 《Thinking in Java》显然与众不同,是我所知唯一一本解释 Java“为什么”
的书:为什么象这样设计,为什么象这样工作,为什么有时不能工作,为什么比
C++好,为什么没有 C++好,等等。尽管这本书也很好讲述了“是什么”和“怎
样用”的问题,但它的特色并在于此。这本书特别适合那些想追根溯源的人。
(Robert S. Stephenson)
感谢您写出这么一本优秀的书,我对它越来越爱不释手。我的学生也喜欢它。
(Chuck Iverson)
向你在《Thinking in Java》的工作致敬。这本书对因特网的未来进行了最恰当
的揭示, 我只是想对你说声 “谢谢” , 它非常有价值。 (Patrick Barrell, Network
Officer Mamco-QAF Mfg 公司)
市面上大多数 Java 书作为新手指南都是不错的。但它们的立意大多雷同,举的
例子也有过多的重复。 从未没见过象您这样的一本书, 它和那些书完全是两码事。
我认为它是迄今为止最好的一本参考书。请快些出版它!……另外,由于
《Thinking in Java》都这么好了,我也赶快去买了一本《Thinking in C++》 。
(George Laframboise,LightWorx 技术咨询公司)
从前给你写过信,主要是表达对《Thinking in C++》一书的惊叹(那本书在我
的书架上占有突出的位置) 。今天,我很欣慰地看到你投向了 Java 领域,并有幸
拜读了最新的《Thinking in Java》电子版。看过之后,我不得不说: “服了! ”
内容非常精彩,有很强的说服力,不象读那些干巴巴的参考书。你讲到了 Java
开发最重要、也最易忽略的方面:基本原理。 (Sean Brady)
你举的例子都非常浅显,很容易理解。Java 的许多重要细节都照顾到了,而单
薄的 Java 文档根本没有涉及那些方面。另外,这本书没有浪费读者的时间。程
序员已经知道了一些基本的事实,你在这个基础上进行了很好的发挥。 (Kai
Engert,Innovative Software 公司,德国)
我是您的《Thinking in C++》的忠实读者。通读了您的 Java 书的电子版以后,发现您在这两本书上有同样高级别的写作水平。谢谢! (Peter R. Neuwald)
写得非常好的一本 Java 书……我认为您的工作简直可以说“伟大” 。我是芝加哥
地区 Java 特别兴趣组的头儿, 已在最近的几次聚会上推荐了您的书和 Web 站点。
以后每个月开 SIG 会的时候,我都想把《Thinking in Java》作为基本的指导教
材使用。一般来说,我们会每次讨论书中的一章内容。 (Mark Ertes)
衷心感谢你的书,它写得太好了。我已把它推荐给自己的用户和 Ph.D.学生。
( Hugues LeroyIrisa-Inria Rennes France , Head of Scientific Computingand Industrial Tranfert)
我到现在只读了《Thinking in Java》的 40 页内容,但已对它留下了深刻的印
象。这无疑是见过的最精彩的编程专业书……而且我本身就是一个作家,所以这
点儿看法还是有些权威吧。我已订购了《Thinking in C++》 ,已经等得迫不及待
了——我是一名编程新手,最怕的就是散乱无章的学习线索。所以必须在这里向
您的出色工作表示敬意。以前看过的书似乎都有这方面的毛病,经常使我才提起
的兴致消弥于无形。但看了你的书以后,感觉好多了。 (Glenn Becker,EducationalTheatre Association)
谢谢您这本出色的书。在终于认识了 Java 与 C++之间纠缠不清的一些事实后,我真的要非常感谢这本书。对您的书非常满意! (Felix Bizaoui,Twin
OaksIndustries,Louisa,Va)
恭喜你写出这么好的一本书。我是在有了阅读《Thinking in C++》的经历以后,才来看这本《Thinking in Java》的,它确实没让我失望。 (Jaco van derMerwe,软件专家,DataFusion Systems 有限公司,Stellenbosch,南非)
这是我看过的最好的 Java 书之一。 (E.E. Pritchard,资深软件工程师,英国剑
桥动画系统有限公司)
你的东东让其他 Java 参考收黯然失色。看来其他作者都应该向你看齐了。
(Brett g Porter,资深程序员,Art Logic)
我花了一、两个星期的时间来看你的书,并对以前我看过的一些 Java 书进行了
比较。显然,只有你的书才能让我真正“入门” 。现在,我已向我的许多朋友推
荐了这本书,他们都对其作出了很高的评价。请接受我真诚的祝贺,并希望她早
些正式出版。 (Rama Krishna Bhupathi,软件工程师,TCSI 公司,圣琼斯)
这是一本充满智慧的书,与简单的参考书有着截然不同的风格。它现在已成了我
进行 Java 创作一份主要参考。你的目录做得相当不错,让人一目了然,很快就
能找到自己需要的东西。 更高兴的是, 这本书没有写成一本改头换面的 API 字典,也没有把我们这些程序员看作傻瓜。 (Grant Sayer,Java Components Group
Leader,Ceedata Systems Pty 有限公司,澳大利亚)
啧啧,一本可读性强、论据充分的 Java 书。外面有太多用词贫乏的 Java 书(也
有几本好的) ,只有你的书是最好的。那些垃圾在你的书前面不值一提。 (John
Root,Web 开发员,伦敦社会安全部)
我刚刚开始看《Thinking in Java》 。我希望它能有更大的突破,因为《Thinking
in C++》写得实在太好了。我是一名有经验的 C++程序员,事先看那本书对学习
Java 很有帮助。但我在 Java 上的经验不够,希望这本新书能让我满意。您真是
一名“高产高质”作者。 (Kevin K. Lewis,ObjectSpace 公司技术员)
我认为这是本好书。从这本书中,我学到了与 Java 有关的所有知识。谢谢你能
让这本书通过互联网免费发行。如果不那样做,我根本不可能象现在这样有巨大
的进步。 但最令人高兴的是, 你的书并没有成为一本官方 Java 手册, 指出了 Java
一些不当的地方。你真是做了一件大好事。 (Frederik Fix, Belgium)
我现在经常查阅你的书。大约两年前,当我想开始学习 C++的时候,是《C++
InsideOut》指导我游历 C++的世界。它使我在这方面的技能大增,并找到了一
个较好的职位。现在出于工作上的原因需要学习 Java,又是《Thinking in Java》
给我正确的指引。 尽管现在可选择的书更多了, 但我知道自己别无选择。 很奇妙,不是吗?现在看这本书的时候, 我居然有一种重新认识自己的感觉。 衷心感谢你,我现在的理解又比以前深入多了。 (Anand Kumar S.,软件工程师,Computervision 公司,印度)
你的书给人一种“鹤立鸡群”的感觉。 (Peter Robinson,剑桥大学计算机实验
室)
这是我看过的最好的一本 Java 参考书。现在想起来,能找到这样的一本书简直
是幸运。谢谢! (Chuck Peterson,因特网产品线主管,IVIS International 公
司)
这本书太棒了!它已是我看过的第三本 Java 书了,真后悔没有早点儿发现它。
前两本书都没坚持看完,但我已决心看完这一本。不妨告诉你,当时我是想寻找
关于内部类使用的一些资料,是我的朋友告诉我网上能下载这本书。你干得真不
错! (Jerry Nowlin,MTS,Lucent Technologies)
在我看过的6本Java书中,你的《Thinking in Java》是最好和最有用的。
(Michael Van Waas,Ph.D,TMR Associates 公司总裁)
我很想对《Thinking in Java》说声谢谢。这是一本多么出色的书——并不单指
它在网上免费发送!作为一名学生,我认为你的书有不可估量的价值(我有《C++
InsideOut》的拷贝,那是关于 C++的另一本好书) ,因为它不仅教我怎样做,而且解释了为什么。这当然为我用 C++或 Java 这样的语言编程打下了坚实的基
础。我有许多朋友都象我一样热爱编程,在向他们推荐了这本书后,反映都非常
好,他们的看法同我一样。再次感谢您。顺便提一句,我是一个印尼畜牲,整天
都喜欢和 Java 泡在一起! (Ray Frederick Djajadinata,Trisakti 大学学生,Indonesian Pork)
你把这本书放在网上引起了相当程度的轰动, 我对你的做法表示真诚的感谢与支
持! (Shane LeBouthillier,加拿大艾伯特大学计算机工程系学生)
告诉你吧,我是多么热烈地盼望读到你每个月的专栏!作为 OOP 设计的新手,我
要感谢你把即使最基本的概念都讲得那么透彻和全面。我已下载了你的书,但我
保证会在它正式出版后另行购买。感谢你提供的所有帮助! (Dan Cashmer,B.C.Ziegler Co.)
祝贺你完成了一件伟大的作品。我现在下载的是《Thinking in Java》的 PDF
版。这本书还没有读完,便迫不及待地跑到书店去找你的《Thinking in C++》 。
我在计算机界干了8年,是一个顾问,兼软件工程师、教师培训专家,最近辞
职自己开了一间公司。所以见过不少的书。但是,正是这些书使我的女朋友称我
为“书呆子” !并不是我概念掌握得不深入——只是由于现在的发展太快,使我
短期内不能适应新技术。但这两本书都给了我很大的启示,它与以前接触过或买
过的计算机参考书都大不相同。写作风格很棒,每个新概念都讲得很好,书中充
满了“智慧” 。 (Simon Goland,simonsez@smartt.com,Simon Says Consulting
公司)
必须认为你的《Thinking in Java》非常优秀!那正是我一直以来梦想的参考书。
其中印象最深的是有关使用 Java 1.1 作软件设计时的一些优缺点分析。
(DirkDuehr,Lexikon Verlag,Bertelsmann AG,德国)
谢谢您写出两本空前绝后的书( 《Thinking in Java》和《Thinking in C++》 ) 。
它们使我在面向对象的程序设计上跨出了一大步。 (Donald Lawson,DCLEnterprises)
谢谢你花时间写出一本真正有用的 Java 参考书,你现在绝对能为自己的工作感
到骄傲了。 (Dominic Turner,GEAC Support)
这是我见过的最好的一本 Java 书。 (Jean-Yves MENGANT,Chief Software
Architect NAT-SYSTEM,法国巴黎)
《Thinking in Java》无论在覆盖的范围还是讲述方法上都有独到之处。看懂这
本书非常容易,摘录的代码段也很有说服力。 (Ron Chan,Ph.D,Expert Choice
公司,Pittsburgh PA)
你的书太棒了。我看过许多编程书刊,只有你的书给人一种全新的视野。其他作
者都该向你好好学习才是。 (Ningjian Wang,信息系统工程师,The Vangurad
Group)
《Thinking in Java》是一本出色的、可读性极强的书,我已向我的学生推荐阅
读。 (Dr. Pual Gorman,计算机科学系,Otago 大学,Dunedin 市,新西兰)
在我看过的书中,你的书最有品味,不象有的书那样粗制滥造。任何搞软件开发
的人都不应错过。 (Jose Suriol,Scylax 公司)
感谢您免费提供这本书,它是我看过或翻过的最好的一本。 (JeffLapchinsky,Net Results Technologies 公司程序员)
这本书简明扼要,看起来不仅毫不费力,而且象是一种享受。 (Keith Ritchie,Java 研发组,KL Group 公司)
这真的是我看过的最好的一本 Java 书! (Daniel Eng)
我看过的最好的 Java 书! (Rich Hoffarth,Senior Architect,West Group)
感谢你这本出色的书籍,我好久都没有经历让人如此愉悦的阅读过程了。 (Fred
Trimble,Actium 公司)
你的写作能准确把握轻重缓急,并能成功抓住细节。这本书让学习变成了一件有
趣的事情,我感觉满意,非常满意!谢谢你这本出色的学习教程。 (Rajesh Rau,软件顾问)
《Thinking in Java》让整个自由世界都感受到了震憾! (Miko O'Sullivan,Idocs 公司总裁)
关于《Thinking in C++》 :
荣获 1995 年由《软件开发》杂志评选的“最佳书籍”奖!
“这本书可算一个完美的典型。把它放到自己的书架上绝对不会后悔。关于 IO
数据流的那部分内容包含了迄今为止我看过的最全面、 最容易理解的文字。 ” (Al
Stevens, 《道伯博士》杂志投稿编辑)
“Eckel 的书是唯一一本清楚解释了面向对象程序设计基础问题的书。这本书也
是 C++的一本出色教材。 ” (Andrew Binstock, 《Unix Review》编辑) ”
“Bruce 用他对 C++深刻的洞察力震惊了我们, 《Thinking in C++》无疑是各种
伟大思想的出色组合。如果想得到各种困难的 C++问题的答案,请购买这本杰出
的参考书” (Gary Entsminger, 《对象之道》的作者)
“ 《Thinking in C++》非常耐心和有技巧地讲述了关于 C++的各种问题,包括如
何使用内联、索引、运算符过载以及动态对象。另外还包括一些高级主题,比如
模板的正确使用、 违例和多重继承等。 所有这些都精巧地编织在一起, 成为 Eckel
独特的对象和程序设计思想。所有 C++开发者的书架上都应摆上这本书。如果你
正在用 C++搞正式开发,这本书绝对有借鉴价值。 ” (Richard Hale Shaw, 《PC
Magazine》投稿编辑) 。
1
第1 章 对象入门
“为什么面向对象的编程会在软件开发领域造成如此震憾的影响?”
面向对象编程(OOP)具有多方面的吸引力。对管理人员,它实现了更快和
更廉价的开发与维护过程。对分析与设计人员,建模处理变得更加简单,能生成
清晰、易于维护的设计方案。对程序员,对象模型显得如此高雅和浅显。此外,面向对象工具以及库的巨大威力使编程成为一项更使人愉悦的任务。 每个人都可
从中获益,至少表面如此。
如果说它有缺点,那就是掌握它需付出的代价。思考对象的时候,需要采用
形象思维,而不是程序化的思维。与程序化设计相比,对象的设计过程更具挑战
性——特别是在尝试创建可重复使用(可再生)的对象时。过去,那些初涉面向
对象编程领域的人都必须进行一项令人痛苦的选择:
(1) 选择一种诸如Smalltalk的语言, “出师”前必须掌握一个巨型的库。
(2) 选择几乎根本没有库的C++(注释①) ,然后深入学习这种语言,直至能
自行编写对象库。
①:幸运的是,这一情况已有明显改观。现在有第三方库以及标准的C++库
供选用。
事实上,很难很好地设计出对象——从而很难设计好任何东西。因此,只有
数量相当少的“专家”能设计出最好的对象,然后让其他人享用。对于成功的
OOP语言,它们不仅集成了这种语言的语法以及一个编译程序(编译器) ,而且
还有一个成功的开发环境,其中包含设计优良、易于使用的库。所以,大多数程
序员的首要任务就是用现有的对象解决自己的应用问题。 本章的目标就是向大家
揭示出面向对象编程的概念,并证明它有多么简单。
本章将向大家解释 Java 的多项设计思想, 并从概念上解释面向对象的程序设
计。但要注意在阅读完本章后,并不能立即编写出全功能的 Java 程序。所有详
细的说明和示例会在本书的其他章节慢慢道来。
1.1 抽象的进步
所有编程语言的最终目的都是提供一种“抽象”方法。一种较有争议的说法
是:解决问题的复杂程度直接取决于抽象的种类及质量。这儿的“种类”是指准
备对什么进行“抽象”?汇编语言是对基础机器的少量抽象。后来的许多“命令
式”语言(如 FORTRAN,BASIC和 C)是对汇编语言的一种抽象。与汇编语言
相比,这些语言已有了长足的进步,但它们的抽象原理依然要求我们着重考虑计
算机的结构,而非考虑问题本身的结构。在机器模型(位于“方案空间” )与实
际解决的问题模型(位于“问题空间” )之间,程序员必须建立起一种联系。这
个过程要求人们付出较大的精力,而且由于它脱离了编程语言本身的范围,造成
程序代码很难编写,而且要花较大的代价进行维护。由此造成的副作用便是一门
完善的“编程方法”学科。
为机器建模的另一个方法是为要解决的问题制作模型。对一些早期语言来
说,如 LISP和 APL,它们的做法是“从不同的角度观察世界”——“所有问题
都归纳为列表”或“所有问题都归纳为算法” 。PROLOG则将所有问题都归纳为 2
决策链。对于这些语言,我们认为它们一部分是面向基于“强制”的编程,另一
部分则是专为处理图形符号设计的。每种方法都有自己特殊的用途,适合解决某
一类的问题。但只要超出了它们力所能及的范围,就会显得非常笨拙。
面向对象的程序设计在此基础上则跨出了一大步,程序员可利用一些工具表
达问题空间内的元素。 由于这种表达非常普遍, 所以不必受限于特定类型的问题。
我们将问题空间中的元素以及它们在方案空间的表示物称作“对象” (Object) 。
当然,还有一些在问题空间没有对应体的其他对象。通过添加新的对象类型,程
序可进行灵活的调整,以便与特定的问题配合。所以在阅读方案的描述代码时,会读到对问题进行表达的话语。 与我们以前见过的相比, 这无疑是一种更加灵活、更加强大的语言抽象方法。总之,OOP 允许我们根据问题来描述问题,而不是
根据方案。 然而, 仍有一个联系途径回到计算机。 每个对象都类似一台小计算机;
它们有自己的状态,而且可要求它们进行特定的操作。与现实世界的“对象”或
者“物体”相比,编程“对象”与它们也存在共通的地方:它们都有自己的特征
和行为。
Alan Kay 总结了 Smalltalk 的五大基本特征。这是第一种成功的面向对象程
序设计语言,也是 Java 的基础语言。通过这些特征,我们可理解“纯粹”的面
向对象程序设计方法是什么样的:
(1) 所有东西都是对象。可将对象想象成一种新型变量;它保存着数据,但
可要求它对自身进行操作。理论上讲,可从要解决的问题身上提出所有概念性的
组件,然后在程序中将其表达为一个对象。
(2) 程序是一大堆对象的组合;通过消息传递,各对象知道自己该做些什么。
为了向对象发出请求,需向那个对象“发送一条消息” 。更具体地讲,可将消息
想象为一个调用请求,它调用的是从属于目标对象的一个子例程或函数。
(3) 每个对象都有自己的存储空间,可容纳其他对象。或者说,通过封装现
有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可
达到任意高的复杂程度。
(4) 每个对象都有一种类型。根据语法,每个对象都是某个“类”的一个“实
例” 。其中, “类” (Class)是“类型” (Type)的同义词。一个类最重要的特征
就是“能将什么消息发给它?” 。
(5) 同一类所有对象都能接收相同的消息。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆” (Circle)的一个对象也属于类型为“形状”
(Shape)的一个对象,所以一个圆完全能接收形状消息。这意味着可让程序代
码统一指挥“形状” ,令其自动控制所有符合“形状”描述的对象,其中自然包
括“圆” 。这一特性称为对象的“可替换性” ,是 OOP 最重要的概念之一。
一些语言设计者认为面向对象的程序设计本身并不足以方便解决所有形式
的程序问题,提倡将不同的方法组合成“多形程序设计语言” (注释②) 。
②:参见 Timothy Budd 编著的《Multiparadigm Programming in Leda》 ,Addison-Wesley 1995 年出版。
1.2 对象的接口 3
亚里士多德或许是认真研究“类型”概念的第一人,他曾谈及“鱼类和鸟类”
的问题。 在世界首例面向对象语言Simula-67中, 第一次用到了这样的一个概念:
所有对象——尽管各有特色——都属于某一系列对象的一部分,这些对象具
有通用的特征和行为。在 Simula-67 中,首次用到了 class 这个关键字,它为程
序引入了一个全新的类型(clas 和type 通常可互换使用;注释③) 。
③:有些人进行了进一步的区分,他们强调“类型”决定了接口,而“类”
是那个接口的一种特殊实现方式。
Simula 是一个很好的例子。正如这个名字所暗示的,它的作用是“模拟”
(Simulate)象“银行出纳员”这样的经典问题。在这个例子里,我们有一系列
出纳员、客户、帐号以及交易等。每类成员(元素)都具有一些通用的特征:每
个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个
成员都有自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以
在计算机程序中,能用独一无二的实体分别表示出纳员、客户、帐号以及交易。
这个实体便是“对象” ,而且每个对象都隶属一个特定的“类” ,那个类具有自己
的通用特征与行为。
因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数
据“类型” (Type) ,但几乎所有面向对象的程序设计语言都采用了“class”关键
字。当您看到“type”这个字的时候,请同时想到“class”;反之亦然。
建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决
问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临
的最大一项挑战性就是:如何在“问题空间” (问题实际存在的地方)的元素与
“方案空间” (对实际问题进行建模的地方,如计算机)的元素之间建立理想的
“一对一”对应或映射关系。
如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其做一些实际的事情,比如完成一次交易、在屏幕上画一些东西或者打开一个
开关等等。 每个对象仅能接受特定的请求。 我们向对象发出的请求是通过它的 “接
口” (Interface)定义的,对象的“类型”或“类”则规定了它的接口形式。 “类
型”与“接口”的等价或对应关系是面向对象程序设计的基础。
下面让我们以电灯泡为例:
29 页图
Type name
Interface
Light lt = new Light;
lt.on;
在这个例子中,类型类的名称是 Light,可向 Light对象发出的请求包括包
Light
On
Off
Brighten
Dim 4
括打开(on) 、关闭(off) 、变得更明亮(brighten)或者变得更暗淡(dim) 。通
过简单地声明一个名字(lt) ,我们为 Light 对象创建了一个“句柄” 。然后用new
关键字新建类型为Light 的一个对象。再用等号将其赋给句柄。为了向对象发送
一条消息,我们列出句柄名(lt) ,再用一个句点符号(.)把它同消息名称(on)
连接起来。从中可以看出,使用一些预先定义好的类时,我们在程序里采用的代
码是非常简单和直观的。
1.3 实现方案的隐藏
为方便后面的讨论,让我们先对这一领域的从业人员作一下分类。从根本上
说, 大致有两方面的人员涉足面向对象的编程: “类创建者” (创建新数据类型的
人) 以及 “客户程序员” (在自己的应用程序中采用现成数据类型的人; 注释④) 。
对客户程序员来讲,最主要的目标就是收集一个充斥着各种类的编程“工具箱” ,以便快速开发符合自己要求的应用。而对类创建者来说,他们的目标则是从头构
建一个类,只向客户程序员开放有必要开放的东西(接口) ,其他所有细节都隐
藏起来。为什么要这样做?隐藏之后,客户程序员就不能接触和改变那些细节,所以原创者不用担心自己的作品会受到非法修改, 可确保它们不会对其他人造成
影响。
④:感谢我的朋友 Scott Meyers,是他帮我起了这个名字。
“接口” (Interface)规定了可对一个特定的对象发出哪些请求。然而,必须
在某个地方存在着一些代码,以便满足这些请求。这些代码与那些隐藏起来的数
据便叫作“隐藏的实现” 。站在程式化程序编写(Procedural Programming)的角
度,整个问题并不显得复杂。一种类型含有与每种可能的请求关联起来的函数。
一旦向对象发出一个特定的请求,就会调用那个函数。我们通常将这个过程总结
为向对象“发送一条消息” (提出一个请求) 。对象的职责就是决定如何对这条消
息作出反应(执行相应的代码) 。
对于任何关系,重要一点是让牵连到的所有成员都遵守相同的规则。创建一
个库时,相当于同客户程序员建立了一种关系。对方也是程序员,但他们的目标
是组合出一个特定的应用(程序) ,或者用您的库构建一个更大的库。
若任何人都能使用一个类的所有成员,那么客户程序员可对那个类做任何事
情,没有办法强制他们遵守任何约束。即便非常不愿客户程序员直接操作类内包
含的一些成员,但倘若未进行访问控制,就没有办法阻止这一情况的发生——所
有东西都会暴露无遗。
有两方面的原因促使我们控制对成员的访问。第一个原因是防止程序员接触
他们不该接触的东西——通常是内部数据类型的设计思想。 若只是为了解决特定
的问题,用户只需操作接口即可,毋需明白这些信息。我们向用户提供的实际是
一种服务, 因为他们很容易就可看出哪些对自己非常重要, 以及哪些可忽略不计。
进行访问控制的第二个原因是允许库设计人员修改内部结构,不用担心它会
对客户程序员造成什么影响。例如,我们最开始可能设计了一个形式简单的类,以便简化开发。以后又决定进行改写,使其更快地运行。若接口与实现方法早已
隔离开, 并分别受到保护, 就可放心做到这一点, 只要求用户重新链接一下即可。
Java 采用三个显式(明确)关键字以及一个隐式(暗示)关键字来设置类边 5
界:public,private,protected以及暗示性的friendly。若未明确指定其他关键字,则默认为后者。这些关键字的使用和含义都是相当直观的,它们决定了谁能使用
后续的定义内容。 “public”(公共)意味着后续的定义任何人均可使用。而在另
一方面, “private”(私有)意味着除您自己、类型的创建者以及那个类型的内部
函数成员,其他任何人都不能访问后续的定义信息。private 在您与客户程序员之
间竖起了一堵墙。若有人试图访问私有成员,就会得到一个编译期错误。
“friendly”(友好的)涉及“包装”或“封装” (Package)的概念——即Java用
来构建库的方法。若某样东西是“友好的” ,意味着它只能在这个包装的范围内
使用(所以这一访问级别有时也叫作“包装访问” ) 。 “protected”(受保护的)与
“private”相似,只是一个继承的类可访问受保护的成员,但不能访问私有成员。
继承的问题不久就要谈到。
1.4 方案的重复使用
创建并测试好一个类后,它应(从理想的角度)代表一个有用的代码单位。
但并不象许多人希望的那样,这种重复使用的能力并不容易实现;它要求较多的
经验以及洞察力,这样才能设计出一个好的方案,才有可能重复使用。
许多人认为代码或设计方案的重复使用是面向对象的程序设计提供的最伟
大的一种杠杆。
为重复使用一个类,最简单的办法是仅直接使用那个类的对象。但同时也能
将那个类的一个对象置入一个新类。我们把这叫作“创建一个成员对象” 。新类
可由任意数量和类型的其他对象构成。 无论如何, 只要新类达到了设计要求即可。
这个概念叫作“组织”——在现有类的基础上组织一个新类。有时,我们也将组
织称作“包含”关系,比如“一辆车包含了一个变速箱” 。
对象的组织具有极大的灵活性。新类的“成员对象”通常设为“私有”
(Private) ,使用这个类的客户程序员不能访问它们。这样一来,我们可在不干
扰客户代码的前提下,从容地修改那些成员。也可以在“运行期”更改成员,这
进一步增大了灵活性。后面要讲到的“继承”并不具备这种灵活性,因为编译器
必须对通过继承创建的类加以限制。
由于继承的重要性,所以在面向对象的程序设计中,它经常被重点强调。作
为新加入这一领域的程序员,或许早已先入为主地认为“继承应当随处可见” 。
沿这种思路产生的设计将是非常笨拙的,会大大增加程序的复杂程度。相反,新
建类的时候,首先应考虑“组织”对象;这样做显得更加简单和灵活。利用对象
的组织,我们的设计可保持清爽。一旦需要用到继承,就会明显意识到这一点。
1.5 继承:重新使用接口
就其本身来说,对象的概念可为我们带来极大的便利。它在概念上允许我们
将各式各样数据和功能封装到一起。这样便可恰当表达“问题空间”的概念,不
用刻意遵照基础机器的表达方式。在程序设计语言中,这些概念则反映为具体的
数据类型(使用class关键字) 。
我们费尽心思做出一种数据类型后,假如不得不又新建一种类型,令其实现
大致相同的功能, 那会是一件非常令人灰心的事情。 但若能利用现成的数据类型,对其进行“克隆” ,再根据情况进行添加和修改,情况就显得理想多了。 “继承”
正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原
始类(正式名称叫作基础类、超类或父类)发生了变化,修改过的“克隆”类(正 6
式名称叫作继承类或者子类)也会反映出这种变化。在 Java 语言中,继承是通
过 extends关键字实现的
使用继承时,相当于创建了一个新类。这个新类不仅包含了现有类型的所有
成员(尽管private成员被隐藏起来,且不能访问) ,但更重要的是,它复制了基
础类的接口。也就是说,可向基础类的对象发送的所有消息亦可原样发给衍生类
的对象。根据可以发送的消息,我们能知道类的类型。这意味着衍生类具有与基
础类相同的类型!为真正理解面向对象程序设计的含义,首先必须认识到这种类
型的等价关系。
由于基础类和衍生类具有相同的接口,所以那个接口必须进行特殊的设计。
也就是说,对象接收到一条特定的消息后,必须有一个“方法”能够执行。若只
是简单地继承一个类,并不做其他任何事情,来自基础类接口的方法就会直接照
搬到衍生类。这意味着衍生类的对象不仅有相同的类型,也有同样的行为,这一
后果通常是我们不愿见到的。
有两种做法可将新得的衍生类与原来的基础类区分开。第一种做法十分简
单:为衍生类添加新函数(功能) 。这些新函数并非基础类接口的一部分。进行
这种处理时,一般都是意识到基础类不能满足我们的要求,所以需要添加更多的
函数。这是一种最简单、最基本的继承用法,大多数时候都可完美地解决我们的
问题。然而,事先还是要仔细调查自己的基础类是否真的需要这些额外的函数。
1.5.1 改善基础类
尽管 extends 关键字暗示着我们要为接口“扩展”新功能,但实情并非肯定
如此。为区分我们的新类,第二个办法是改变基础类一个现有函数的行为。我们
将其称作“改善”那个函数。
为改善一个函数, 只需为衍生类的函数建立一个新定义即可。 我们的目标是:
“尽管使用的函数接口未变,但它的新版本具有不同的表现” 。
1.5.2 等价与类似关系
针对继承可能会产生这样的一个争论:继承只能改善原基础类的函数吗?若
答案是肯定的,则衍生类型就是与基础类完全相同的类型,因为都拥有完全相同
的接口。这样造成的结果就是:我们完全能够将衍生类的一个对象换成基础类的
一个对象!可将其想象成一种“纯替换” 。在某种意义上,这是进行继承的一种
理想方式。此时,我们通常认为基础类和衍生类之间存在一种“等价”关系——
因为我们可以理直气壮地说: “圆就是一种几何形状” 。为了对继承进行测试,一
个办法就是看看自己是否能把它们套入这种“等价”关系中,看看是否有意义。
但在许多时候,我们必须为衍生类型加入新的接口元素。所以不仅扩展了接
口,也创建了一种新类型。这种新类型仍可替换成基础类型,但这种替换并不是
完美的,因为不可在基础类里访问新函数。我们将其称作“类似”关系;新类型
拥有旧类型的接口,但也包含了其他函数,所以不能说它们是完全等价的。举个
例子来说,让我们考虑一下制冷机的情况。假定我们的房间连好了用于制冷的各
种控制器;也就是说,我们已拥有必要的“接口”来控制制冷。现在假设机器出
了故障,我们把它换成一台新型的冷、热两用空调,冬天和夏天均可使用。冷、热空调“类似”制冷机,但能做更多的事情。由于我们的房间只安装了控制制冷
的设备,所以它们只限于同新机器的制冷部分打交道。新机器的接口已得到了扩
展,但现有的系统并不知道除原始接口以外的任何东西。 7
认识了等价与类似的区别后,再进行替换时就会有把握得多。尽管大多数时
候“纯替换”已经足够,但您会发现在某些情况下,仍然有明显的理由需要在衍
生类的基础上增添新功能。通过前面对这两种情况的讨论,相信大家已心中有数
该如何做。
1.6 多形对象的互换使用
通常, 继承最终会以创建一系列类收场, 所有类都建立在统一的接口基础上。
我们用一幅颠倒的树形图来阐明这一点(注释⑤) :
⑤:这儿采用了“统一记号法” ,本书将主要采用这种方法。
35 页图
对这样的一系列类,我们要进行的一项重要处理就是将衍生类的对象当作基
础类的一个对象对待。这一点是非常重要的,因为它意味着我们只需编写单一的
代码,令其忽略类型的特定细节,只与基础类打交道。这样一来,那些代码就可
与类型信息分开。所以更易编写,也更易理解。此外,若通过继承增添了一种新
类型,如“三角形” ,那么我们为“几何形状”新类型编写的代码会象在旧类型
里一样良好地工作。所以说程序具备了“扩展能力” ,具有“扩展性” 。
以上面的例子为基础,假设我们用Java 写了这样一个函数:
35-36 页程序
void doStuff(Shape s) {
s.erase;
...
s.draw;
}
Circle c = new Circle;
Triangle t = new Triangle;
Line l = new Line;
doStuff(c);
doStuff(t);
Shape
Draw
Clear
SetColor
GetColor
Move
Circle Square Trangle 8
doStuff(l);
这个函数可与任何“几何形状” (Shape)通信,所以完全独立于它要描绘
(draw)和删除(erase)的任何特定类型的对象。如果我们在其他一些程序里使
用 doStuff函数:
36页上程序
void doStuff(Shape s) {
s.erase;
...
s.draw;
}
那么对doStuff的调用会自动良好地工作,无论对象的具体类型是什么。
这实际是一个非常有用的编程技巧。请考虑下面这行代码:
doStuff(c);
此时,一个 Circle(圆)句柄传递给一个本来期待 Shape(形状)句柄的函
数。由于圆是一种几何形状,所以doStuff能正确地进行处理。也就是说,凡是
doStuff能发给一个 Shape 的消息,Circle 也能接收。所以这样做是安全的,不
会造成错误。
我们将这种把衍生类型当作它的基本类型处理的过程叫作“Upcasting”(上
溯造型) 。其中, “cast”(造型)是指根据一个现成的模型创建;而“Up”(向上)
表明继承的方向是从 “上面” 来的——即基础类位于顶部, 而衍生类在下方展开。
所以,根据基础类进行造型就是一个从上面继承的过程,即“Upcasting”。
在面向对象的程序里,通常都要用到上溯造型技术。这是避免去调查准确类
型的一个好办法。请看看 doStuff里的代码:
s.erase;
...
s.draw;
注意它并未这样表达: “如果你是一个 Circle,就这样做;如果你是一个
Square,就那样做;等等” 。若那样编写代码,就需检查一个 Shape 所有可能的
类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的Shape
类型后,都要相应地进行修改。在这儿,我们只需说: “你是一种几何形状,我
知道你能将自己删掉,即erase;请自己采取那个行动,并自己去控制所有的细
节吧。 ”
1.6.1 动态绑定
在doStuff的代码里,最让人吃惊的是尽管我们没作出任何特殊指示,采取
的操作也是完全正确和恰当的。我们知道,为Circle调用 draw时执行的代码与
为一个 Square 或 Line 调用 draw时执行的代码是不同的。但在将 draw消息发
给一个匿名Shape 时,根据 Shape 句柄当时连接的实际类型,会相应地采取正确
的操作。这当然令人惊讶,因为当 Java 编译器为 doStuff编译代码时,它并不
知道自己要操作的准确类型是什么。 尽管我们确实可以保证最终会为 Shape 调用 9
erase,为 Shape 调用 draw,但并不能保证为特定的 Circle,Square 或者 Line
调用什么。然而最后采取的操作同样是正确的,这是怎么做到的呢?
将一条消息发给对象时,如果并不知道对方的具体类型是什么,但采取的行
动同样是正确的,这种情况就叫作“多形性” (Polymorphism) 。对面向对象的程
序设计语言来说,它们用以实现多形性的方法叫作“动态绑定” 。编译器和运行
期系统会负责对所有细节的控制;我们只需知道会发生什么事情,而且更重要的
是,如何利用它帮助自己设计程序。
有些语言要求我们用一个特殊的关键字来允许动态绑定。在 C++中,这个关
键字是 virtual。在 Java 中,我们则完全不必记住添加一个关键字,因为函数的
动态绑定是自动进行的。所以在将一条消息发给对象时,我们完全可以肯定对象
会采取正确的行动,即使其中涉及上溯造型之类的处理。
1.6.2 抽象的基础类和接口
设计程序时,我们经常都希望基础类只为自己的衍生类提供一个接口。也就
是说,我们不想其他任何人实际创建基础类的一个对象,只对上溯造型成它,以
便使用它们的接口。为达到这个目的,需要把那个类变成“抽象”的——使用
abstract 关键字。若有人试图创建抽象类的一个对象,编译器就会阻止他们。这
种工具可有效强制实行一种特殊的设计。
亦可用 abstract 关键字描述一个尚未实现的方法——作为一个“根”使用,指出: “这是适用于从这个类继承的所有类型的一个接口函数,但目前尚没有对
它进行任何形式的实现。 ”抽象方法也许只能在一个抽象类里创建。继承了一个
类后,那个方法就必须实现,否则继承的类也会变成“抽象”类。通过创建一个
抽象方法,我们可以将一个方法置入接口中,不必再为那个方法提供可能毫无意
义的主体代码。
interface(接口)关键字将抽象类的概念更延伸了一步,它完全禁止了所有
的函数定义。 “接口”是一种相当有效和常用的工具。另外如果自己愿意,亦可
将多个接口都合并到一起(不能从多个普通 class 或 abstract class中继承) 。
1.7 对象的创建和存在时间
从技术角度说,OOP(面向对象程序设计)只是涉及抽象的数据类型、继承
以及多形性,但另一些问题也可能显得非常重要。本节将就这些问题进行探讨。
最重要的问题之一是对象的创建及破坏方式。对象需要的数据位于哪儿,如
何控制对象的“存在时间”呢?针对这个问题,解决的方案是各异其趣的。C++
认为程序的执行效率是最重要的一个问题,所以它允许程序员作出选择。为获得
最快的运行速度,存储以及存在时间可在编写程序时决定,只需将对象放置在堆
栈(有时也叫作自动或定域变量)或者静态存储区域即可。这样便为存储空间的
分配和释放提供了一个优先级。 某些情况下, 这种优先级的控制是非常有价值的。
然而,我们同时也牺牲了灵活性,因为在编写程序时,必须知道对象的准确的数
量、存在时间、以及类型。如果要解决的是一个较常规的问题,如计算机辅助设
计、仓储管理或者空中交通控制,这一方法就显得太局限了。
第二个方法是在一个内存池中动态创建对象,该内存池亦叫“堆”或者“内
存堆” 。若采用这种方式,除非进入运行期,否则根本不知道到底需要多少个对
象,也不知道它们的存在时间有多长,以及准确的类型是什么。这些参数都在程
序正式运行时才决定的。若需一个新对象,只需在需要它的时候在内存堆里简单 10
地创建它即可。由于存储空间的管理是运行期间动态进行的,所以在内存堆里分
配存储空间的时间比在堆栈里创建的时间长得多 (在堆栈里创建存储空间一般只
需要一个简单的指令,将堆栈指针向下或向下移动即可) 。由于动态创建方法使
对象本来就倾向于复杂, 所以查找存储空间以及释放它所需的额外开销不会为对
象的创建造成明显的影响。除此以外,更大的灵活性对于常规编程问题的解决是
至关重要的。
C++允许我们决定是在写程序时创建对象,还是在运行期间创建,这种控制
方法更加灵活。大家或许认为既然它如此灵活,那么无论如何都应在内存堆里创
建对象,而不是在堆栈中创建。但还要考虑另外一个问题,亦即对象的“存在时
间”或者“生存时间” (Lifetime) 。若在堆栈或者静态存储空间里创建一个对象,编译器会判断对象的持续时间有多长,到时会自动“破坏”或者“清除”它。程
序员可用两种方法来破坏一个对象:用程序化的方式决定何时破坏对象,或者利
用由运行环境提供的一种“垃圾收集器”特性,自动寻找那些不再使用的对象,并将其清除。当然,垃圾收集器显得方便得多,但要求所有应用程序都必须容忍
垃圾收集器的存在,并能默许随垃圾收集带来的额外开销。但这并不符合 C++
语言的设计宗旨,所以未能包括到 C++里。但 Java 确实提供了一个垃圾收集器
(Smalltalk 也有这样的设计;尽管 Delphi 默认为没有垃圾收集器,但可选择安
装;而C++亦可使用一些由其他公司开发的垃圾收集产品) 。
本节剩下的部分将讨论操纵对象时要考虑的另一些因素。
1.7.1 集合与继承器
针对一个特定问题的解决,如果事先不知道需要多少个对象,或者它们的持
续时间有多长,那么也不知道如何保存那些对象。既然如此,怎样才能知道那些
对象要求多少空间呢?事先上根本无法提前知道,除非进入运行期。
在面向对象的设计中,大多数问题的解决办法似乎都有些轻率——只是简单
地创建另一种类型的对象。 用于解决特定问题的新型对象容纳了指向其他对象的
句柄。 当然, 也可以用数组来做同样的事情, 那是大多数语言都具有的一种功能。
但不能只看到这一点。这种新对象通常叫作“集合” (亦叫作一个“容器” ,但
AWT 在不同的场合应用了这个术语,所以本书将一直沿用“集合”的称呼。在
需要的时候,集合会自动扩充自己,以便适应我们在其中置入的任何东西。所以
我们事先不必知道要在一个集合里容下多少东西。只需创建一个集合,以后的工
作让它自己负责好了。
幸运的是,设计优良的 OOP 语言都配套提供了一系列集合。在 C++中,它
们是以“标准模板库” (STL)的形式提供的。Object Pascal 用自己的“可视组件
库” (VCL)提供集合。Smalltalk提供了一套非常完整的集合。而Java 也用自己
的标准库提供了集合。在某些库中,一个常规集合便可满足人们的大多数要求;
而在另一些库中(特别是C++的库) ,则面向不同的需求提供了不同类型的集合。
例如,可以用一个矢量统一对所有元素的访问方式;一个链接列表则用于保证所
有元素的插入统一。所以我们能根据自己的需要选择适当的类型。其中包括集、队列、散列表、树、堆栈等等。
所有集合都提供了相应的读写功能。将某样东西置入集合时,采用的方式是
十分明显的。有一个叫作“推” (Push) 、 “添加” (Add)或其他类似名字的函数
用于做这件事情。但将数据从集合中取出的时候,方式却并不总是那么明显。如
果是一个数组形式的实体,比如一个矢量(Vector) ,那么也许能用索引运算符 11
或函数。但在许多情况下,这样做往往会无功而返。此外,单选定函数的功能是
非常有限的。如果想对集合中的一系列元素进行操纵或比较,而不是仅仅面向一
个,这时又该怎么办呢?
办法就是使用一个“继续器” (Iterator) ,它属于一种对象,负责选择集合内
的元素,并把它们提供给继承器的用户。作为一个类,它也提供了一级抽象。利
用这一级抽象,可将集合细节与用于访问那个集合的代码隔离开。通过继承器的
作用,集合被抽象成一个简单的序列。继承器允许我们遍历那个序列,同时毋需
关心基础结构是什么——换言之, 不管它是一个矢量、 一个链接列表、 一个堆栈,还是其他什么东西。这样一来,我们就可以灵活地改变基础数据,不会对程序里
的代码造成干扰。Java 最开始(在 1.0 和 1.1 版中)提供的是一个标准继承器,名为 Enumeration(枚举) ,为它的所有集合类提供服务。Java 1.2新增一个更复
杂的集合库, 其中包含了一个名为Iterator的继承器, 可以做比老式的Enumeration
更多的事情。
从设计角度出发,我们需要的是一个全功能的序列。通过对它的操纵,应该
能解决自己的问题。如果一种类型的序列即可满足我们的所有要求,那么完全没
有必要再换用不同的类型。 有两方面的原因促使我们需要对集合作出选择。 首先,集合提供了不同的接口类型以及外部行为。堆栈的接口与行为与队列的不同,而
队列的接口与行为又与一个集(Set)或列表的不同。利用这个特征,我们解决
问题时便有更大的灵活性。
其次,不同的集合在进行特定操作时往往有不同的效率。最好的例子便是矢
量(Vector)和列表(List)的区别。它们都属于简单的序列,拥有完全一致的
接口和外部行为。但在执行一些特定的任务时,需要的开销却是完全不同的。对
矢量内的元素进行的随机访问(存取)是一种常时操作;无论我们选择的选择是
什么,需要的时间量都是相同的。但在一个链接列表中,若想到处移动,并随机
挑选一个元素,就需付出“惨重”的代价。而且假设某个元素位于列表较远的地
方,找到它所需的时间也会长许多。但在另一方面,如果想在序列中部插入一个
元素,用列表就比用矢量划算得多。这些以及其他操作都有不同的执行效率,具
体取决于序列的基础结构是什么。在设计阶段,我们可以先从一个列表开始。最
后调整性能的时候,再根据情况把它换成矢量。由于抽象是通过继承器进行的,所以能在两者方便地切换,对代码的影响则显得微不足道。
最后,记住集合只是一个用来放置对象的储藏所。如果那个储藏所能满足我
们的所有需要,就完全没必要关心它具体是如何实现的(这是大多数类型对象的
一个基本概念) 。 如果在一个编程环境中工作, 它由于其他因素 (比如在 Windows
下运行,或者由垃圾收集器带来了开销)产生了内在的开销,那么矢量和链接列
表之间在系统开销上的差异就或许不是一个大问题。 我们可能只需要一种类型的
序列。甚至可以想象有一个“完美”的集合抽象,它能根据自己的使用方式自动
改变基层的实现方式。
1.7.2 单根结构
在面向对象的程序设计中,由于C++的引入而显得尤为突出的一个问题是:
所有类最终是否都应从单独一个基础类继承。在 Java 中(与其他几乎所有 OOP
语言一样) ,对这个问题的答案都是肯定的,而且这个终级基础类的名字很简单,就是一个“Object”。这种“单根结构”具有许多方面的优点。
单根结构中的所有对象都有一个通用接口,所以它们最终都属于相同的类 12
型。另一种方案(就象 C++那样)是我们不能保证所有东西都属于相同的基本类
型。从向后兼容的角度看,这一方案可与 C 模型更好地配合,而且可以认为它
的限制更少一些。但假期我们想进行纯粹的面向对象编程,那么必须构建自己的
结构,以期获得与内建到其他 OOP 语言里的同样的便利。需添加我们要用到的
各种新类库,还要使用另一些不兼容的接口。理所当然地,这也需要付出额外的
精力使新接口与自己的设计方案配合(可能还需要多重继承) 。为得到 C++额外
的“灵活性” ,付出这样的代价值得吗?当然,如果真的需要——如果早已是 C
专家,如果对 C 有难舍的情结——那么就真的很值得。但假如你是一名新手,首次接触这类设计,象 Java 那样的替换方案也许会更省事一些。
单根结构中的所有对象(比如所有Java 对象)都可以保证拥有一些特定的功
能。在自己的系统中,我们知道对每个对象都能进行一些基本操作。一个单根结
构,加上所有对象都在内存堆中创建,可以极大简化参数的传递(这在 C++里是
一个复杂的概念) 。
利用单根结构,我们可以更方便地实现一个垃圾收集器。与此有关的必要支
持可安装于基础类中,而垃圾收集器可将适当的消息发给系统内的任何对象。如
果没有这种单根结构,而且系统通过一个句柄来操纵对象,那么实现垃圾收集器
的途径会有很大的不同,而且会面临许多障碍。
由于运行期的类型信息肯定存在于所有对象中,所以永远不会遇到判断不出
一个对象的类型的情况。这对系统级的操作来说显得特别重要,比如违例控制;
而且也能在程序设计时获得更大的灵活性。
但大家也可能产生疑问,既然你把好处说得这么天花乱坠,为什么 C++没有
采用单根结构呢?事实上,这是早期在效率与控制上权衡的一种结果。单根结构
会带来程序设计上的一些限制。而且更重要的是,它加大了新程序与原有 C 代
码兼容的难度。尽管这些限制仅在特定的场合会真的造成问题,但为了获得最大
的灵活程度,C++最终决定放弃采用单根结构这一做法。而 Java 不存在上述的问
题,它是全新设计的一种语言,不必与现有的语言保持所谓的“向后兼容” 。所
以很自然地,与其他大多数面向对象的程序设计语言一样,单根结构在 Java 的
设计方案中很快就落实下来。
1.7.3 集合库与方便使用集合
由于集合是我们经常都要用到的一种工具,所以一个集合库是十分必要的,它应该可以方便地重复使用。这样一来,我们就可以方便地取用各种集合,将其
插入自己的程序。Java提供了这样的一个库,尽管它在 Java 1.0 和 1.1 中都显得
非常有限(Java 1.2 的集合库则无疑是一个杰作) 。
1. 下溯造型与模板通用性
为了使这些集合能够重复使用,或者“再生” ,Java 提供了一种通用类型,以前曾把它叫作“Object”。单根结构意味着、所有东西归根结底都是一个对象” !
所以容纳了 Object 的一个集合实际可以容纳任何东西。这使我们对它的重复使
用变得非常简便。
为使用这样的一个集合,只需添加指向它的对象句柄即可,以后可以通过句
柄重新使用对象。但由于集合只能容纳Object,所以在我们向集合里添加对象句
柄时,它会上溯造型成 Object,这样便丢失了它的身份或者标识信息。再次使用
它的时候,会得到一个Object句柄,而非指向我们早先置入的那个类型的句柄。 13
所以怎样才能归还它的本来面貌, 调用早先置入集合的那个对象的有用接口呢?
在这里,我们再次用到了造型(Cast) 。但这一次不是在分级结构中上溯造型
成一种更“通用”的类型。而是下溯造型成一种更“特殊”的类型。这种造型方
法叫作“下溯造型” (Downcasting) 。举个例子来说,我们知道在上溯造型的时
候,Circle(圆)属于Shape(几何形状)的一种类型,所以上溯造型是安全的。
但我们不知道一个Object 到底是Circle还是Shape,所以很难保证下溯造型的安
全进行,除非确切地知道自己要操作的是什么。
但这也不是绝对危险的,因为假如下溯造型成错误的东西,会得到我们称为
“违例” (Exception)的一种运行期错误。我们稍后即会对此进行解释。但在从
一个集合提取对象句柄时,必须用某种方式准确地记住它们是什么,以保证下溯
造型的正确进行。
下溯造型和运行期检查都要求花额外的时间来运行程序,而且程序员必须付
出额外的精力。既然如此,我们能不能创建一个“智能”集合,令其知道自己容
纳的类型呢?这样做可消除下溯造型的必要以及潜在的错误。答案是肯定的,我
们可以采用“参数化类型” ,它们是编译器能自动定制的类,可与特定的类型配
合。例如,通过使用一个参数化集合,编译器可对那个集合进行定制,使其只接
受 Shape,而且只提取 Shape。
参数化类型是 C++一个重要的组成部分,这部分是 C++没有单根结构的缘
故。在 C++中,用于实现参数化类型的关键字是 template(模板) 。Java 目前尚
未提供参数化类型,因为由于使用的是单根结构,所以使用它显得有些笨拙。但
这并不能保证以后的版本不会实现,因为“generic”这个词已被Java“保留到将来
实现” (在Ada 语言中, “generic”被用来实现它的模板) 。 Java 采取的这种关键字
保留机制其实经常让人摸不着头脑,很难断定以后会发生什么事情。
1.7.4 清除时的困境:由谁负责清除?
每个对象都要求资源才能“生存” ,其中最令人注目的资源是内存。如果不
再需要使用一个对象, 就必须将其清除, 以便释放这些资源, 以便其他对象使用。
如果要解决的是非常简单的问题,如何清除对象这个问题并不显得很突出:我们
创建对象,在需要的时候调用它,然后将其清除或者“破坏” 。但在另一方面,我们平时遇到的问题往往要比这复杂得多。
举个例子来说, 假设我们要设计一套系统, 用它管理一个机场的空中交通 (同
样的模型也可能适于管理一个仓库的货柜、或者一套影带出租系统、或者宠物店
的宠物房。这初看似乎十分简单:构造一个集合用来容纳飞机,然后创建一架新
飞机, 将其置入集合。 对进入空中交通管制区的所有飞机都如此处理。 至于清除,在一架飞机离开这个区域的时候把它简单地删去即可。
但事情并没有这么简单,可能还需要另一套系统来记录与飞机有关的数据。
当然,和控制器的主要功能不同,这些数据的重要性可能一开始并不显露出来。
例如,这条记录反映的可能是离开机场的所有小飞机的飞行计划。所以我们得到
了由小飞机组成的另一个集合。 一旦创建了一个飞机对象, 如果它是一架小飞机,那么也必须把它置入这个集合。然后在系统空闲时期,需对这个集合中的对象进
行一些后台处理。
问题现在显得更复杂了:如何才能知道什么时间删除对象呢?用完对象后,系统的其他某些部分可能仍然要发挥作用。同样的问题也会在其他大量场合出
现,而且在程序设计系统中(如 C++) ,在用完一个对象之后必须明确地将其删 14
除,所以问题会变得异常复杂(注释⑥) 。
⑥:注意这一点只对内存堆里创建的对象成立(用 new 命令创建的) 。但在
另一方面,对这儿描述的问题以及其他所有常见的编程问题来说,都要求对象在
内存堆里创建。
在 Java 中,垃圾收集器在设计时已考虑到了内存的释放问题(尽管这并不包
括清除一个对象涉及到的其他方面) 。垃圾收集器“知道”一个对象在什么时候
不再使用,然后会自动释放那个对象占据的内存空间。采用这种方式,另外加上
所有对象都从单个根类 Object 继承的事实,而且由于我们只能在内存堆中以一
种方式创建对象,所以 Java 的编程要比 C++的编程简单得多。我们只需要作出
少量的抉择,即可克服原先存在的大量障碍。
1. 垃圾收集器对效率及灵活性的影响
既然这是如此好的一种手段,为什么在C++里没有得到充分的发挥呢?我们
当然要为这种编程的方便性付出一定的代价,代价就是运行期的开销。正如早先
提到的那样,在 C++中,我们可在堆栈中创建对象。在这种情况下,对象会得以
自动清除(但不具有在运行期间随心所欲创建对象的灵活性) 。在堆栈中创建对
象是为对象分配存储空间最有效的一种方式, 也是释放那些空间最有效的一种方
式。在内存堆(Heap)中创建对象可能要付出昂贵得多的代价。如果总是从同
一个基础类继承,并使所有函数调用都具有“同质多形”特征,那么也不可避免
地需要付出一定的代价。但垃圾收集器是一种特殊的问题,因为我们永远不能确
定它什么时候启动或者要花多长的时间。这意味着在 Java 程序执行期间,存在
着一种不连贯的因素。所以在某些特殊的场合,我们必须避免用它——比如在一
个程序的执行必须保持稳定、连贯的时候(通常把它们叫作“实时程序” ,尽管
并不是所有实时编程问题都要这方面的要求——注释⑦) 。
⑦:根据本书一些技术性读者的反馈,有一个现成的实时 Java 系统
(www.newmonics.com)确实能够保证垃圾收集器的效能。
C++语言的设计者曾经向 C 程序员发出请求(而且做得非常成功) ,不要希
望在可以使用 C 的任何地方,向语言里加入可能对 C++的速度或使用造成影响
的任何特性。 这个目的达到了, 但代价就是C++的编程不可避免地复杂起来。 Java
比 C++简单,但付出的代价是效率以及一定程度的灵活性。但对大多数程序设计
问题来说,Java 无疑都应是我们的首选。
1.8 违例控制:解决错误
从最古老的程序设计语言开始,错误控制一直都是设计者们需要解决的一个
大问题。由于很难设计出一套完美的错误控制方案,许多语言干脆将问题简单地
忽略掉,将其转嫁给库设计人员。对大多数错误控制方案来说,最主要的一个问
题是它们严重依赖程序员的警觉性,而不是依赖语言本身的强制标准。如果程序
员不够警惕——若比较匆忙, 这几乎是肯定会发生的——程序所依赖的错误控制
方案便会失效。
“违例控制”将错误控制方案内置到程序设计语言中,有时甚至内建到操作 15
系统内。这里的“违例” (Exception)属于一个特殊的对象,它会从产生错误的
地方“扔”或“掷”出来。随后,这个违例会被设计用于控制特定类型错误的“违
例控制器”捕获。在情况变得不对劲的时候,可能有几个违例控制器并行捕获对
应的违例对象。由于采用的是独立的执行路径,所以不会干扰我们的常规执行代
码。这样便使代码的编写变得更加简单,因为不必经常性强制检查代码。除此以
外, “掷”出的一个违例不同于从函数返回的错误值,也不同于由函数设置的一
个标志。那些错误值或标志的作用是指示一个错误状态,是可以忽略的。但违例
不能被忽略,所以肯定能在某个地方得到处置。最后,利用违例能够可靠地从一
个糟糕的环境中恢复。此时一般不需要退出,我们可以采取某些处理,恢复程序
的正常执行。显然,这样编制出来的程序显得更加可靠。
Java 的违例控制机制与大多数程序设计语言都有所不同。因为在Java 中,违
例控制模块是从一开始就封装好的,所以必须使用它!如果没有自己写一些代码
来正确地控制违例,就会得到一条编译期出错提示。这样可保证程序的连贯性,使错误控制变得更加容易。
注意违例控制并不属于一种面向对象的特性,尽管在面向对象的程序设计语
言中,违例通常是用一个对象表示的。早在面向对象语言问世以前,违例控制就
已经存在了。
1.9 多线程
在计算机编程中,一个基本的概念就是同时对多个任务加以控制。许多程序
设计问题都要求程序能够停下手头的工作,改为处理其他一些问题,再返回主进
程。可以通过多种途径达到这个目的。最开始的时候,那些拥有机器低级知识的
程序员编写一些“中断服务例程” ,主进程的暂停是通过硬件级的中断实现的。
尽管这是一种有用的方法,但编出的程序很难移植,由此造成了另一类的代价高
昂问题。
有些时候,中断对那些实时性很强的任务来说是很有必要的。但还存在其他
许多问题,它们只要求将问题划分进入独立运行的程序片断中,使整个程序能更
迅速地响应用户的请求。在一个程序中,这些独立运行的片断叫作“线程”
(Thread) ,利用它编程的概念就叫作“多线程处理” 。多线程处理一个常见的例
子就是用户界面。利用线程,用户可按下一个按钮,然后程序会立即作出响应,而不是让用户等待程序完成了当前任务以后才开始响应。
最开始,线程只是用于分配单个处理器的处理时间的一种工具。但假如操作
系统本身支持多个处理器,那么每个线程都可分配给一个不同的处理器,真正进
入“并行运算”状态。从程序设计语言的角度看,多线程操作最有价值的特性之
一就是程序员不必关心到底使用了多少个处理器。 程序在逻辑意义上被分割为数
个线程;假如机器本身安装了多个处理器,那么程序会运行得更快,毋需作出任
何特殊的调校。
根据前面的论述,大家可能感觉线程处理非常简单。但必须注意一个问题:
共享资源!如果有多个线程同时运行,而且它们试图访问相同的资源,就会遇到
一个问题。举个例子来说,两个进程不能将信息同时发送给一台打印机。为解决
这个问题,对那些可共享的资源来说(比如打印机) ,它们在使用期间必须进入
锁定状态。所以一个线程可将资源锁定,在完成了它的任务后,再解开(释放)
这个锁,使其他线程可以接着使用同样的资源。
Java 的多线程机制已内建到语言中,这使一个可能较复杂的问题变得简单起 16
来。对多线程处理的支持是在对象这一级支持的,所以一个执行线程可表达为一
个对象。Java 也提供了有限的资源锁定方案。它能锁定任何对象占用的内存(内
存实际是多种共享资源的一种) ,所以同一时间只能有一个线程使用特定的内存
空间。为达到这个目的,需要使用 synchronized关键字。其他类型的资源必须由
程序员明确锁定,这通常要求程序员创建一个对象,用它代表一把锁,所有线程
在访问那个资源时都必须检查这把锁。
1.10 永久性
创建一个对象后,只要我们需要,它就会一直存在下去。但在程序结束运行
时,对象的“生存期”也会宣告结束。尽管这一现象表面上非常合理,但深入追
究就会发现,假如在程序停止运行以后,对象也能继续存在,并能保留它的全部
信息,那么在某些情况下将是一件非常有价值的事情。下次启动程序时,对象仍
然在那里,里面保留的信息仍然是程序上一次运行时的那些信息。当然,可以将
信息写入一个文件或者数据库,从而达到相同的效果。但尽管可将所有东西都看
作一个对象,如果能将对象声明成“永久性” ,并令其为我们照看其他所有细节,无疑也是一件相当方便的事情。
Java 1.1 提供了对“有限永久性”的支持,这意味着我们可将对象简单地保
存到磁盘上,以后任何时间都可取回。之所以称它为“有限”的,是由于我们仍
然需要明确发出调用,进行对象的保存和取回工作。这些工作不能自动进行。在
Java 未来的版本中,对“永久性”的支持有望更加全面。
1.11 Java 和因特网
既然 Java不过另一种类型的程序设计语言, 大家可能会奇怪它为什么值得如
此重视, 为什么还有这么多的人认为它是计算机程序设计的一个里程碑呢?如果
您来自一个传统的程序设计背景,那么答案在刚开始的时候并不是很明显。Java
除了可解决传统的程序设计问题以外,还能解决 World Wide Web(万维网)上
的编程问题。
1.11.1 什么是 Web?
Web这个词刚开始显得有些泛泛,似乎“冲浪” 、 “网上存在”以及“主页”
等等都和它拉上了一些关系。甚至还有一种“Internet 综合症”的说法,对许多
人狂热的上网行为提出了质疑。我们在这里有必要作一些深入的探讨,但在这之
前,必须理解客户机服务器系统的概念,这是充斥着许多令人迷惑的问题的又
一个计算领域。
1. 客户机服务器计算
客户机服务器系统的基本思想是我们能在一个统一的地方集中存放信息
资源。一般将数据集中保存在某个数据库中,根据其他人或者机器的请求将信息
投递给对方。客户机服务器概述的一个关键在于信息是“集中存放”的。所以
我们能方便地更改信息,然后将修改过的信息发放给信息的消费者。将各种元素
集中到一起,信息仓库、用于投递信息的软件以及信息及软件所在的那台机器,它们联合起来便叫作“服务器” (Server) 。而对那些驻留在远程机器上的软件,它们需要与服务器通信,取回信息,进行适当的处理,然后在远程机器上显示出
来,这些就叫作“客户”(Client) 。 17
这样看来,客户机服务器的基本概念并不复杂。这里要注意的一个主要问
题是单个服务器需要同时向多个客户提供服务。在这一机制中,通常少不了一套
数据库管理系统,使设计人员能将数据布局封装到表格中,以获得最优的使用。
除此以外,系统经常允许客户将新信息插入一个服务器。这意味着必须确保客户
的新数据不会与其他客户的新数据冲突, 或者说需要保证那些数据在加入数据库
的时候不会丢失(用数据库的术语来说,这叫作“事务处理” ) 。客户软件发生了
改变之后,它们必须在客户机器上构建、调试以及安装。所有这些会使问题变得
比我们一般想象的复杂得多。另外,对多种类型的计算机和操作系统的支持也是
一个大问题。最后,性能的问题显得尤为重要:可能会有数百个客户同时向服务
器发出请求。所以任何微小的延误都是不能忽视的。为尽可能缓解潜伏的问题,程序员需要谨慎地分散任务的处理负担。 一般可以考虑让客户机负担部分处理任
务, 但有时亦可分派给服务器所在地的其他机器, 那些机器亦叫作 “中间件” (中
间件也用于改进对系统的维护) 。
所以在具体实现的时候,其他人发布信息这样一个简单的概念可能变得异常
复杂。有时甚至会使人产生完全无从着手的感觉。客户机服务器的概念在这时
就可以大显身手了。事实上,大约有一半的程序设计活动都可以采用客户机服
务器的结构。这种系统可负责从处理订单及信用卡交易,一直到发布各类数据的
方方面面的任务——股票市场、科学研究、政府运作等等。在过去,我们一般为
单独的问题采取单独的解决方案;每次都要设计一套新方案。这些方案无论创建
还是使用都比较困难,用户每次都要学习和适应新界面。客户机服务器问题需
要从根本上加以变革!
2. Web是一个巨大的服务器
Web 实际就是一套规模巨大的客户机服务器系统。但它的情况要复杂一
些,因为所有服务器和客户都同时存在于单个网络上面。但我们没必要了解更进
一步的细节,因为唯一要关心的就是一次建立同一个服务器的连接,并同它打交
道(即使可能要在全世界的范围内搜索正确的服务器) 。
最开始的时候, 这是一个简单的单向操作过程。 我们向一个服务器发出请求,它向我们回传一个文件,由于本机的浏览器软件(亦即“客户”或“客户程序” )
负责解释和格式化,并在我们面前的屏幕上正确地显示出来。但人们不久就不满
足于只从一个服务器传递网页。他们希望获得完全的客户机服务器能力,使客
户 (程序) 也能反馈一些信息到服务器。 比如希望对服务器上的数据库进行检索,向服务器添加新信息,或者下一份订单等等(这也提供了比以前的系统更高的安
全要求) 。在 Web的发展过程中,我们可以很清晰地看出这些令人心喜的变化。
Web浏览器的发展终于迈出了重要的一步:某个信息可在任何类型的计算机
上显示出来,毋需任何改动。然而,浏览器仍然显得很原始,在用户迅速增多的
要求面前显得有些力不从心。它们的交互能力不够强,而且对服务器和因特网都
造成了一定程度的干扰。这是由于每次采取一些要求编程的操作时,必须将信息
反馈回服务器,在服务器那一端进行处理。所以完全可能需要等待数秒乃至数分
钟的时间才会发现自己刚才拼错了一个单词。 由于浏览器只是一个纯粹的查看程
序, 所以连最简单的计算任务都不能进行 (当然在另一方面, 它也显得非常安全,因为不能在本机上面执行任何程序,避开了程序错误或者病毒的骚扰) 。
为解决这个问题,人们采取了许多不同的方法。最开始的时候,人们对图形
标准进行了改进,使浏览器能显示更好的动画和视频。为解决剩下的问题,唯一 18
的办法就是在客户端(浏览器)内运行程序。这就叫作“客户端编程” ,它是对
传统的“服务器端编程”的一个非常重要的拓展。
1.11.2 客户端编程(注释⑧)
Web最初采用的“服务器-浏览器”方案可提供交互式内容,但这种交互能
力完全由服务器提供,为服务器和因特网带来了不小的负担。服务器一般为客户
浏览器产生静态网页,由后者简单地解释并显示出来。基本 HTML 语言提供了
简单的数据收集机制:文字输入框、复选框、单选钮、列表以及下拉列表等,另
外还有一个按钮,只能由程序规定重新设置表单中的数据,以便回传给服务器。
用户提交的信息通过所有 Web 服务器均能支持的“通用网关接口” (CGI)回传
到服务器。包含在提交数据中的文字指示 CGI 该如何操作。最常见的行动是运
行位于服务器的一个程序。那个程序一般保存在一个名为“cgi-bin”的目录中(按
下 Web 页内的一个按钮时,请注意一下浏览器顶部的地址窗,经常都能发现
“cgi-bin”的字样) 。 大多数语言都可用来编制这些程序, 但其中最常见的是 Perl。
这是由于 Perl 是专为文字的处理及解释而设计的,所以能在任何服务器上安装
和使用,无论采用的处理器或操作系统是什么。
⑧:本节内容改编自某位作者的一篇文章。那篇文章最早出现在位于
www.mainspring.com的 Mainspring上。本节的采用已征得了对方的同意。
今天的许多 Web 站点都严格地建立在 CGI 的基础上,事实上几乎所有事情
都可用 CGI 做到。唯一的问题就是响应时间。CGI 程序的响应取决于需要传送
多少数据,以及服务器和因特网两方面的负担有多重(而且 CGI 程序的启动比
较慢) 。Web 的早期设计者并未预料到当初绰绰有余的带宽很快就变得不够用,这正是大量应用充斥网上造成的结果。例如,此时任何形式的动态图形显示都几
乎不能连贯地显示,因为此时必须创建一个 GIF 文件,再将图形的每种变化从
服务器传递给客户。而且大家应该对输入表单上的数据校验有着深刻的体会。原
来的方法是我们按下网页上的提交按钮(Submit) ;数据回传给服务器;服务器
启动一个 CGI 程序,检查用户输入是否有错;格式化一个 HTML 页,通知可能
遇到的错误,并将这个页回传给我们;随后必须回到原先那个表单页,再输入一
遍。这种方法不仅速度非常慢,也显得非常繁琐。
解决的办法就是客户端的程序设计。 运行 Web浏览器的大多数机器都拥有足
够强的能力,可进行其他大量工作。与此同时,原始的静态 HTML 方法仍然可
以采用,它会一直等到服务器送回下一个页。客户端编程意味着 Web 浏览器可
获得更充分的利用,并可有效改善 Web服务器的交互(互动)能力。
对客户端编程的讨论与常规编程问题的讨论并没有太大的区别。采用的参数
肯定是相同的,只是运行的平台不同:Web浏览器就象一个有限的操作系统。无
论如何,我们仍然需要编程,仍然会在客户端编程中遇到大量问题,同时也有很
多解决的方案。在本节剩下的部分里,我们将对这些问题进行一番概括,并介绍
在客户端编程中采取的对策。
1. 插件
朝客户端编程迈进的时候,最重要的一个问题就是插件的设计。利用插件,程序员可以方便地为浏览器添加新功能, 用户只需下载一些代码, 把它们 “插入” 19
浏览器的适当位置即可。这些代码的作用是告诉浏览器“从现在开始,你可以进
行这些新活动了” (仅需下载这些插入一次) 。 有些快速和功能强大的行为是通过
插件添加到浏览器的。但插件的编写并不是一件简单的任务。在我们构建一个特
定的站点时,可能并不希望涉及这方面的工作。对客户端程序设计来说,插件的
价值在于它允许专业程序员设计出一种新的语言,并将那种语言添加到浏览器,同时不必经过浏览器原创者的许可。 由此可以看出, 插件实际是浏览器的一个 “后
门” ,允许创建新的客户端程序设计语言(尽管并非所有语言都是作为插件实现
的) 。
2. 脚本编制语言
插件造成了脚本编制语言的爆炸性增长。通过这种脚本语言,可将用于自己
客户端程序的源码直接插入HTML页, 而对那种语言进行解释的插件会在HTML
页显示的时候自动激活。脚本语言一般都倾向于尽量简化,易于理解。而且由于
它们是从属于 HTML 页的一些简单正文,所以只需向服务器发出对那个页的一
次请求, 即可非常快地载入。 缺点是我们的代码全部暴露在人们面前。 另一方面,由于通常不用脚本编制语言做过份复杂的事情,所以这个问题暂且可以放在一
边。
脚本语言真正面向的是特定类型问题的解决,其中主要涉及如何创建更丰
富、更具有互动能力的图形用户界面(GUI) 。然而,脚本语言也许能解决客户
端编程中 80%的问题。你碰到的问题可能完全就在那 80%里面。而且由于脚本
编制语言的宗旨是尽可能地简化与快速,所以在考虑其他更复杂的方案之前(如
Java 及ActiveX) ,首先应想一下脚本语言是否可行。
目前讨论得最多的脚本编制语言包括 JavaScript(它与 Java 没有任何关系;
之所以叫那个名字,完全是一种市场策略) 、VBScript(同 Visual Basic很相似)
以及 TclTk(来源于流行的跨平台GUI 构造语言) 。当然还有其他许多语言,也
有许多正在开发中。
JavaScript也许是目常用的, 它得到的支持也最全面。 无论 NetscapeNavigator,Microsoft Internet Explorer,还是Opera,目前都提供了对 JavaScript 的支持。除
此以外,市面上讲述JavaScript 的书籍也要比讲述其他语言的书多得多。有些工
具还能利用JavaScript自动产生网页。 当然, 如果你已经有Visual Basic或者Tcl Tk
的深厚功底,当然用它们要简单得多,起码可以避免学习新语言的烦恼(解决
Web方面的问题就已经够让人头痛了) 。
3. Java
如果说一种脚本编制语言能解决 80%的客户端程序设计问题,那么剩下的
20%又该怎么办呢?它们属于一些高难度的问题吗?目前最流行的方案就是
Java。它不仅是一种功能强大、高度安全、可以跨平台使用以及国际通用的程序
设计语言,也是一种具有旺盛生命力的语言。对 Java 的扩展是不断进行的,提
供的语言特性和库能够很好地解决传统语言不能解决的问题,比如多线程操作、数据库访问、连网程序设计以及分布式计算等等。Java 通过“程序片” (Applet)
巧妙地解决了客户端编程的问题。
程序片(或“小应用程序” )是一种非常小的程序,只能在 Web 浏览器中运
行。作为 Web 页的一部分,程序片代码会自动下载回来(这和网页中的图片差
不多) 。激活程序片后,它会执行一个程序。程序片的一个优点体现在:通过程 20
序片,一旦用户需要客户软件,软件就可从服务器自动下载回来。它们能自动取
得客户软件的最新版本,不会出错,也没有重新安装的麻烦。由于 Java 的设计
原理,程序员只需要创建程序的一个版本,那个程序能在几乎所有计算机以及安
装了 Java 解释器的浏览器中运行。由于 Java 是一种全功能的编程语言,所以在
向服务器发出一个请求之前,我们能先在客户端做完尽可能多的工作。例如,再
也不必通过因特网传送一个请求表单, 再由服务器确定其中是否存在一个拼写或
者其他参数错误。大多数数据校验工作均可在客户端完成,没有必要坐在计算机
前面焦急地等待服务器的响应。这样一来,不仅速度和响应的灵敏度得到了极大
的提高,对网络和服务器造成的负担也可以明显减轻,这对保障因特网的畅通是
至关重要的。
与脚本程序相比,Java程序片的另一个优点是它采用编译好的形式,所以客
户端看不到源码。当然在另一方面,反编译 Java 程序片也并不是件难事,而且
代码的隐藏一般并不是个重要的问题。大家要注意另外两个重要的问题。正如本
书以前会讲到的那样,编译好的 Java 程序片可能包含了许多模块,所以要多次
“命中” (访问)服务器以便下载(在 Java 1.1 中,这个问题得到了有效的改善
——利用 Java 压缩档,即 JAR 文件——它允许设计者将所有必要的模块都封装
到一起,供用户统一下载) 。在另一方面,脚本程序是作为 Web页正文的一部分
集成到 Web 页内的。这种程序一般都非常小,可有效减少对服务器的点击数。
另一个因素是学习方面的问题。不管你平时听别人怎么说,Java 都不是一种十分
容易便可学会的语言。 如果你以前是一名 Visual Basic程序员, 那么转向VBScript
会是一种最快捷的方案。由于 VBScript 可以解决大多数典型的客户机服务器
问题,所以一旦上手,就很难下定决心再去学习Java。如果对脚本编制语言比较
熟,那么在转向Java之前,建议先熟悉一下JavaScript 或者 VBScript,因为它们
可能已经能够满足你的需要,不必经历学习 Java 的艰苦过程。
4. ActiveX
在某种程度上,Java 的一个有力竞争对手应该是微软的 ActiveX,尽管它采
用的是完全不同的一套实现机制。ActiveX最早是一种纯 Windows 的方案。经过
一家独立的专业协会的努力, ActiveX现在已具备了跨平台使用的能力。 实际上,ActiveX的意思是 “假如你的程序同它的工作环境正常连接, 它就能进入 Web 页,并在支持 ActiveX 的浏览器中运行” (IE 固化了对 ActiveX 的支持,而 Netscape
需要一个插件) 。所以,ActiveX 并没有限制我们使用一种特定的语言。比如,假设我们已经是一名有经验的 Windows 程序员,能熟练地使用象 C++、Visual
Basic 或者 BorlandDelphi 那样的语言,就能几乎不加任何学习地创建出 ActiveX
组件。事实上,ActiveX是在我们的 Web页中使用“历史遗留”代码的最佳途径。
5. 安全
自动下载和通过因特网运行程序听起来就象是一个病毒制造者的梦想。在客
户端的编程中,ActiveX带来了最让人头痛的安全问题。点击一个 Web站点的时
候,可能会随同 HTML 网页传回任何数量的东西:GIF 文件、脚本代码、编译
好的 Java代码以及 ActiveX组件。有些是无害的;GIF文件不会对我们造成任何
危害,而脚本编制语言通常在自己可做的事情上有着很大的限制。Java 也设计成
在一个安全“沙箱”里在它的程序片中运行,这样可防止操作位于沙箱以外的磁
盘或者内存区域。 21
ActiveX 是所有这些里面最让人担心的。用 ActiveX 编写程序就象编制
Windows应用程序——可以做自己想做的任何事情。 下载回一个ActiveX组件后,它完全可能对我们磁盘上的文件造成破坏。当然,对那些下载回来并不限于在
Web 浏览器内部运行的程序,它们同样也可能破坏我们的系统。从 BBS 下载回
来的病毒一直是个大问题,但因特网的速度使得这个问题变得更加复杂。
目前解决的办法是“数字签名” ,代码会得到权威机构的验证,显示出它的
作者是谁。这一机制的基础是认为病毒之所以会传播,是由于它的编制者匿名的
缘故。所以假如去掉了匿名的因素,所有设计者都不得不为它们的行为负责。这
似乎是一个很好的主意,因为它使程序显得更加正规。但我对它能消除恶意因素
持怀疑态度,因为假如一个程序便含有Bug,那么同样会造成问题。
Java 通过“沙箱”来防止这些问题的发生。Java 解释器内嵌于我们本地的
Web浏览器中,在程序片装载时会检查所有有嫌疑的指令。特别地,程序片根本
没有权力将文件写进磁盘,或者删除文件(这是病毒最喜欢做的事情之一) 。我
们通常认为程序片是安全的。 而且由于安全对于营建一套可靠的客户机服务器
系统至关重要,所以会给病毒留下漏洞的所有错误都能很快得到修复(浏览器软
件实际需要强行遵守这些安全规则; 而有些浏览器则允许我们选择不同的安全级
别,防止对系统不同程度的访问) 。
大家或许会怀疑这种限制是否会妨碍我们将文件写到本地磁盘。比如,我们
有时需要构建一个本地数据库,或将数据保存下来,以便日后离线使用。最早的
版本似乎每个人都能在线做任何敏感的事情,但这很快就变得非常不现实(尽管
低价“互联网工具”有一天可能会满足大多数用户的需要) 。解决的方案是“签
了名的程序片” ,它用公共密钥加密算法验证程序片确实来自它所声称的地方。
当然在通过验证后,签了名的一个程序片仍然可以开始清除你的磁盘。但从理论
上说,既然现在能够找到创建人“算帐” ,他们一般不会干这种蠢事。Java 1.1
为数字签名提供了一个框架,在必要时,可让一个程序片“走”到沙箱的外面来。
数字签名遗漏了一个重要的问题,那就是人们在因特网上移动的速度。如下
载回一个错误百出的程序,而它很不幸地真的干了某些蠢事,需要多久的时间才
能发觉这一点呢?这也许是几天,也可能几周之后。发现了之后,又如何追踪当
初肇事的程序呢(以及它当时的责任有多大)?
6. 因特网和内联网
Web是解决客户机服务器问题的一种常用方案,所以最好能用相同的技术
解决此类问题的一些“子集” ,特别是公司内部的传统客户机服务器问题。对
于传统的客户机服务器模式, 我们面临的问题是拥有多种不同类型的客户计算
机,而且很难安装新的客户软件。但通过 Web 浏览器和客户端编程,这两类问
题都可得到很好的解决。若一个信息网络局限于一家特定的公司,那么在将 Web
技术应用于它之后,即可称其为“内联网” (Intranet) ,以示与国际性的“因特
网” (Internet)有别。内联网提供了比因特网更大的安全级别,因为可以物理性
地控制对公司内部服务器的使用。说到培训,一般只要人们理解了浏览器的常规
概念,就可以非常轻松地掌握网页和程序片之间的差异,所以学习新型系统的开
销会大幅度减少。
安全问题将我们引入客户端编程领域一个似乎是自动形成的分支。若程序是
在因特网上运行, 由于无从知晓它会在什么平台上运行, 所以编程时要特别留意,防范可能出现的编程错误。需作一些跨平台处理,以及适当的安全防范,比如采 22
用某种脚本语言或者 Java。
但假如在内联网中运行,面临的一些制约因素就会发生变化。全部机器均为
IntelWindows 平台是件很平常的事情。在内联网中,需要对自己代码的质量负
责。而且一旦发现错误,就可以马上改正。除此以外,可能已经有了一些“历史
遗留”的代码,并用较传统的客户机服务器方式使用那些代码。但在进行升级
时,每次都要物理性地安装一道客户程序。浪费在升级安装上的时间是转移到浏
览器的一项重要原因。使用了浏览器后,升级就变得易如反掌,而且整个过程是
透明和自动进行的。如果真的是牵涉到这样的一个内联网中,最明智的方法是采
用 ActiveX,而非试图采用一种新的语言来改写程序代码。
面临客户端编程问题令人困惑的一系列解决方案时,最好的方案是先做一次
投资回报分析。请总结出问题的全部制约因素,以及什么才是最快的方案。由
于客户端程序设计仍然要编程, 所以无论如何都该针对自己的特定情况采取最好
的开发途径。这是准备面对程序开发中一些不可避免的问题时,我们可以作出的
最佳姿态。
1.11.3 服务器端编程
我们的整个讨论都忽略了服务器端编程的问题。如果向服务器发出一个请
求,会发生什么事情?大多数时候的请求都是很简单的一个“把这个文件发给
我” 。浏览器随后会按适当的形式解释这个文件:作为HTML页、一幅图、一个
Java 程序片、一个脚本程序等等。向服务器发出的较复杂的请求通常涉及到对一
个数据库进行操作(事务处理) 。其中最常见的就是发出一个数据库检索命令,得到结果后,服务器会把它格式化成 HTML 页,并作为结果传回来(当然,假
如客户通过 Java 或者某种脚本语言具有了更高的智能,那么原始数据就能在客
户端发送和格式化;这样做速度可以更快,也能减轻服务器的负担) 。另外,有
时需要在数据库中注册自己的名字(比如加入一个组时) ,或者向服务器发出一
份订单,这就涉及到对那个数据库的修改。这类服务器请求必须通过服务器端的
一些代码进行,我们称其为“服务器端的编程” 。在传统意义上,服务器端编程
是用 Perl和 CGI 脚本进行的,但更复杂的系统已经出现。其中包括基于Java 的
Web服务器,它允许我们用 Java进行所有服务器端编程,写出的程序就叫作“小
服务程序” (Servlet) 。
1.11.4 一个独立的领域:应用程序
与 Java 有关的大多数争论都是与程序片有关的。 Java 实际是一种常规用途的
程序设计语言, 可解决任何类型的问题, 至少理论上如此。 而且正如前面指出的,可以用更有效的方式来解决大多数客户机服务器问题。 如果将视线从程序片身
上转开(同时放宽一些限制,比如禁止写盘等) ,就进入了常规用途的应用程序
的广阔领域。 这种应用程序可独立运行, 毋需浏览器, 就象普通的执行程序那样。
在这儿,Java 的特色并不仅仅反应在它的移植能力,也反映在编程本身上。就象
贯穿全书都会讲到的那样,Java 提供了许多有用的特性,使我们能在较短的时间
里创建出比用从前的程序设计语言更健壮的程序。
但要注意任何东西都不是十全十美的,我们为此也要付出一些代价。其中最
明显的是执行速度放慢了(尽管可对此进行多方面的调整) 。和任何语言一样,Java 本身也存在一些限制,使得它不十分适合解决某些特殊的编程问题。但不管
怎样,Java都是一种正在快速发展的语言。随着每个新版本的发布,它变得越来 23
越可爱,能充分解决的问题也变得越来越多。
1.12 分析和设计
面向对象的范式是思考程序设计时一种新的、而且全然不同的方式,许多人
最开始都会在如何构造一个项目上皱起了眉头。 事实上, 我们可以作出一个 “好”
的设计,它能充分利用 OOP提供的所有优点。
有关 OOP 分析与设计的书籍大多数都不尽如人意。其中的大多数书都充斥
着莫名其妙的话语、笨拙的笔调以及许多听起来似乎很重要的声明(注释⑨) 。
我认为这种书最好压缩到一章左右的空间,至多写成一本非常薄的书。具有讽剌
意味的是,那些特别专注于复杂事物管理的人往往在写一些浅显、明白的书上面
大费周章! 如果不能说得简单和直接, 一定没多少人喜欢看这方面的内容。 毕竟,OOP 的全部宗旨就是让软件开发的过程变得更加容易。尽管这可能影响了那些
喜欢解决复杂问题的人的生计, 但为什么不从一开始就把事情弄得简单些呢?因
此,希望我能从开始就为大家打下一个良好的基础,尽可能用几个段落来说清楚
分析与设计的问题。
⑨:最好的入门书仍然是 Grady Booch 的《Object-Oriented Design
withApplications,第 2版本》 ,Wiely Sons于 1996 年出版。这本书讲得很有深
度,而且通俗易懂,尽管他的记号方法对大多数设计来说都显得不必要地复杂。
1.12.1 不要迷失
在整个开发过程中,最重要的事情就是:不要将自己迷失!但事实上这种事
情很容易发生。大多数方法都设计用来解决最大范围内的问题。当然,也存在一
些特别困难的项目, 需要作者付出更为艰辛的努力, 或者付出更大的代价。 但是,大多数项目都是比较“常规”的,所以一般都能作出成功的分析与设计,而且只
需用到推荐的一小部分方法。但无论多么有限,某些形式的处理总是有益的,这
可使整个项目的开发更加容易,总比直接了当开始编码好!
也就是说,假如你正在考察一种特殊的方法,其中包含了大量细节,并推荐
了许多步骤和文档,那么仍然很难正确判断自己该在何时停止。时刻提醒自己注
意以下几个问题:
(1) 对象是什么?(怎样将自己的项目分割成一系列单独的组件?)
(2) 它们的接口是什么?(需要将什么消息发给每一个对象?)
在确定了对象和它们的接口后,便可着手编写一个程序。出于对多方面原因
的考虑,可能还需要比这更多的说明及文档,但要求掌握的资料绝对不能比这还
少。
整个过程可划分为四个阶段,阶段 0 刚刚开始采用某些形式的结构。
1.12.2 阶段 0:拟出一个计划
第一步是决定在后面的过程中采取哪些步骤。 这听起来似乎很简单 (事实上,我们这儿说的一切都似乎很简单) ,但很常见的一种情况是:有些人甚至没有进
入阶段 1,便忙忙慌慌地开始编写代码。如果你的计划本来就是“直接开始开始
编码” ,那样做当然也无可非议(若对自己要解决的问题已有很透彻的理解,便
可考虑那样做) 。但最低程度也应同意自己该有个计划。
在这个阶段,可能要决定一些必要的附加处理结构。但非常不幸,有些程序 24
员写程序时喜欢随心所欲,他们认为“该完成的时候自然会完成” 。这样做刚开
始可能不会有什么问题,但我觉得假如能在整个过程中设置几个标志,或者“路
标” ,将更有益于你集中注意力。这恐怕比单纯地为了“完成工作”而工作好得
多。至少,在达到了一个又一个的目标,经过了一个接一个的路标以后,可对自
己的进度有清晰的把握,干劲也会相应地提高,不会产生“路遥漫漫无期”的感
觉。
座我刚开始学习故事结构起(我想有一天能写本小说出来) ,就一直坚持这
种做法,感觉就象简单地让文字“流”到纸上。在我写与计算机有关的东西时,发现结构要比小说简单得多,所以不需要考虑太多这方面的问题。但我仍然制订
了整个写作的结构,使自己对要写什么做到心中有数。因此,即使你的计划就是
直接开始写程序,仍然需要经历以下的阶段,同时向自己提出一些特定的问题。
1.12.3 阶段 1:要制作什么?
在上一代程序设计中(即“过程化或程序化设计” ) ,这个阶段称为“建立需
求分析和系统规格” 。当然,那些操作今天已经不再需要了,或者至少改换了形
式。大量令人头痛的文档资料已成为历史。但当时的初衷是好的。需求分析的意
思是 “建立一系列规则, 根据它判断任务什么时候完成, 以及客户怎样才能满意” 。
系统规格则表示“这里是一些具体的说明,让你知道程序需要做什么(而不是怎
样做)才能满足要求” 。需求分析实际就是你和客户之间的一份合约(即使客户
就在本公司内部工作,或者是其他对象及系统) 。系统规格是对所面临问题的最
高级别的一种揭示,我们依据它判断任务是否完成,以及需要花多长的时间。由
于这些都需要取得参与者的一致同意, 所以我建议尽可能地简化它们——最好采
用列表和基本图表的形式——以节省时间。可能还会面临另一些限制,需要把它
们扩充成为更大的文档。
我们特别要注意将重点放在这一阶段的核心问题上,不要纠缠于细枝末节。
这个核心问题就是:决定采用什么系统。对这个问题,最有价值的工具就是一个
名为“使用条件”的集合。对那些采用“假如……,系统该怎样做?”形式的问
题,这便是最有说服力的回答。例如, “假如客户需要提取一张现金支票,但当
时又没有这么多的现金储备,那么自动取款机该怎样反应?”对这个问题, “使
用条件”可以指示自动取款机在那种“条件”下的正确操作。
应尽可能总结出自己系统的一套完整的“使用条件”或者“应用场合” 。一
旦完成这个工作, 就相当于摸清了想让系统完成的核心任务。 由于将重点放在 “使
用条件”上,一个很好的效果就是它们总能让你放精力放在最关键的东西上,并
防止自己分心于对完成任务关系不大的其他事情上面。也就是说,只要掌握了一
套完整的“使用条件” ,就可以对自己的系统作出清晰的描述,并转移到下一个
阶段。在这一阶段,也有可能无法完全掌握系统日后的各种应用场合,但这也没
有关系。只要肯花时间,所有问题都会自然而然暴露出来。不要过份在意系统规
格的“完美” ,否则也容易产生挫败感和焦燥情绪。
在这一阶段,最好用几个简单的段落对自己的系统作出描述,然后围绕它们
再进行扩充,添加一些“名词”和“动词” 。 “名词”自然成为对象,而“动词”
自然成为要整合到对象接口中的“方法” 。只要亲自试着做一做,就会发现这是
多么有用的一个工具;有些时候,它能帮助你完成绝大多数的工作。
尽管仍处在初级阶段,但这时的一些日程安排也可能会非常管用。我们现在
对自己要构建的东西应该有了一个较全面的认识, 所以可能已经感觉到了它大概 25
会花多长的时间来完成。 此时要考虑多方面的因素: 如果估计出一个较长的日程,那么公司也许决定不再继续下去; 或者一名主管已经估算出了这个项目要花多长
的时间,并会试着影响你的估计。但无论如何,最好从一开始就草拟出一份“诚
实”的时间表,以后再进行一些暂时难以作出的决策。目前有许多技术可帮助我
们计算出准确的日程安排(就象那些预测股票市场起落的技术) ,但通常最好的
方法还是依赖自己的经验和直觉(不要忘记,直觉也要建立在经验上) 。感觉一
下大概需要花多长的时间,然后将这个时间加倍,再加上 10%。你的感觉可能
是正确的; “也许”能在那个时间里完成。但“加倍”使那个时间更加充裕, “10
%”的时间则用于进行最后的推敲和深化。但同时也要对此向上级主管作出适当
的解释,无论对方有什么抱怨和修改,只要明确地告诉他们:这样的一个日程安
排,只是我的一个估计!
1.12.4 阶段 2:如何构建?
在这一阶段,必须拿出一套设计方案,并解释其中包含的各类对象在外观上
是什么样子,以及相互间是如何沟通的。此时可考虑采用一种特殊的图表工具:
“统一建模语言” (UML) 。请到 http:www.rational.com 去下载一份 UML 规格
书。作为第 1 阶段中的描述工具,UML 也是很有帮助的。此外,还可用它在第
2 阶段中处理一些图表(如流程图) 。当然并非一定要使用 UML,但它对你会很
有帮助,特别是在希望描绘一张详尽的图表,让许多人在一起研究的时候。除
UML外,还可选择对对象以及它们的接口进行文字化描述(就象我在《Thinking
in C++》里说的那样,但这种方法非常原始,发挥的作用亦较有限。
我曾有一次非常成功的咨询经历,那时涉及到一小组人的初始设计。他们以
前还没有构建过 OOP(面向对象程序设计)项目,将对象画在白板上面。我们
谈到各对象相互间该如何沟通(通信) ,并删除了其中的一部分,以及替换了另
一部分对象。这个小组(他们知道这个项目的目的是什么)实际上已经制订出了
设计方案;他们自己“拥有”了设计,而不是让设计自然而然地显露出来。我在
那里做的事情就是对设计进行指导,提出一些适当的问题,尝试作出一些假设,并从小组中得到反馈,以便修改那些假设。这个过程中最美妙的事情就是整个小
组并不是通过学习一些抽象的例子来进行面向对象的设计, 而是通过实践一个真
正的设计来掌握 OOP 的窍门,而那个设计正是他们当时手上的工作!
作出了对对象以及它们的接口的说明后,就完成了第2阶段的工作。当然,这些工作可能并不完全。有些工作可能要等到进入阶段3 才能得知。但这已经足
够了。我们真正需要关心的是最终找出所有的对象。能早些发现当然好,但 OOP
提供了足够完美的结构,以后再找出它们也不迟。
1.12.5 阶段 3:开始创建
读这本书的可能是程序员,现在进入的正是你可能最感兴趣的阶段。由于手
头上有一个计划——无论它有多么简要, 而且在正式编码前掌握了正确的设计结
构,所以会发现接下去的工作比一开始就埋头写程序要简单得多。而这正是我们
想达到的目的。让代码做到我们想做的事情,这是所有程序项目最终的目标。但
切不要急功冒进,否则只有得不偿失。根据我的经验,最后先拿出一套较为全面
的方案,使其尽可能设想周全,能满足尽可能多的要求。给我的感觉,编程更象
一门艺术,不能只是作为技术活来看待。所有付出最终都会得到回报。作为真正
的程序员,这并非可有可无的一种素质。全面的思考、周密的准备、良好的构造 26
不仅使程序更易构建与调试,也使其更易理解和维护,而那正是一套软件赢利的
必要条件。
构建好系统,并令其运行起来后,必须进行实际检验,以前做的那些需求分
析和系统规格便可派上用场了。全面地考察自己的程序,确定提出的所有要求均
已满足。现在一切似乎都该结束了?是吗?
1.12.6 阶段 4:校订
事实上,整个开发周期还没有结束,现在进入的是传统意义上称为“维护”
的一个阶段。 “维护”是一个比较暧昧的称呼,可用它表示从“保持它按设想的
轨道运行” 、 “加入客户从前忘了声明的功能”或者更传统的“除掉暴露出来的一
切臭虫”等等意思。所以大家对“维护”这个词产生了许多误解,有的人认为:
凡是需要“维护”的东西,必定不是好的,或者是有缺陷的!因为这个词说明你
实际构建的是一个非常“原始”的程序,以后需要频繁地作出改动、添加新的代
码或者防止它的落后、退化等。因此,我们需要用一个更合理的词语来称呼以后
需要继续的工作。
这个词便是“校订” 。换言之, “你第一次做的东西并不完善,所以需为自己
留下一个深入学习、认知的空间,再回过头去作一些改变” 。对于要解决的问题,随着对它的学习和了解愈加深入,可能需要作出大量改动。进行这些工作的一个
动力是随着不断的改革优化,终于能够从自己的努力中得到回报,无论这需要经
历一个较短还是较长的时期。
什么时候才叫“达到理想的状态”呢?这并不仅仅意味着程序必须按要求的
那样工作,并能适应各种指定的“使用条件” ,它也意味着代码的内部结构应当
尽善尽美。至少,我们应能感觉出整个结构都能良好地协调运作。没有笨拙的语
法,没有臃肿的对象,也没有一些华而不实的东西。除此以外,必须保证程序结
构有很强的生命力。由于多方面的原因,以后对程序的改动是必不可少。但必须
确定改动能够方便和清楚地进行。这里没有花巧可言。不仅需要理解自己构建的
是什么,也要理解程序如何不断地进化。幸运的是,面向对象的程序设计语言特
别适合进行这类连续作出的修改——由对象建立起来的边界可有效保证结构的
整体性,并能防范对无关对象进行的无谓干扰、破坏。也可以对自己的程序作一
些看似激烈的大变动,同时不会破坏程序的整体性,不会波及到其他代码。事实
上,对“校订”的支持是 OOP非常重要的一个特点。
通过校订,可创建出至少接近自己设想的东西。然后从整体上观察自己的作
品,把它与自己的要求比较,看看还短缺什么。然后就可以从容地回过头去,对
程序中不恰当的部分进行重新设计和重新实现(注释⑩) 。在最终得到一套恰当
的方案之前, 可能需要解决一些不能回避的问题, 或者至少解决问题的一个方面。
而且一般要多“校订”几次才行( “设计范式”在这里可起到很大的帮助作用。
有关它的讨论,请参考本书第 16 章) 。
构建一套系统时, “校订”几乎是不可避免的。我们需要不断地对比自己的
需求,了解系统是否自己实际所需要的。有时只有实际看到系统,才能意识到自
己需要解决一个不同的问题。若认为这种形式的校订必然会发生,那么最好尽快
拿出自己的第一个版本,检查它是否自己希望的,使自己的思想不断趋向成熟。
反复的“校订”同“递增开发”有关密不可分的关系。递增开发意味着先从
系统的核心入手,将其作为一个框架实现,以后要在这个框架的基础上逐渐建立
起系统剩余的部分。随后,将准备提供的各种功能(特性)一个接一个地加入其 27
中。这里最考验技巧的是架设起一个能方便扩充所有目标特性的一个框架(对这
个问题,大家可参考第 16 章的论述) 。这样做的好处在于一旦令核心框架运作起
来,要加入的每一项特性就象它自身内的一个小项目,而非大项目的一部分。此
外,开发或维护阶段合成的新特性可以更方便地加入。OOP 之所以提供了对递
增开发的支持,是由于假如程序设计得好,每一次递增都可以成为完善的对象或
者对象组。
⑩:这有点类似“快速造型” 。此时应着眼于建立一个简单、明了的版本,使自己能对系统有个清楚的把握。再把这个原型扔掉,并正式地构建一个。快速
造型最麻烦的一种情况就是人们不将原型扔掉,而是直接在它的基础上建造。如
果再加上程序化设计中“结构”的缺乏,就会导致一个混乱的系统,致使维护成
本增加。
1.12.7 计划的回报
如果没有仔细拟定的设计图,当然不可能建起一所房子。如建立的是一所狗
舍,尽管设计图可以不必那么详尽,但仍然需要一些草图,以做到心中有数。软
件开发则完全不同,它的“设计图” (计划)必须详尽而完备。在很长的一段时
间里,人们在他们的开发过程中并没有太多的结构,但那些大型项目很容易就会
遭致失败。通过不断的摸索,人们掌握了数量众多的结构和详细资料。但它们的
使用却使人提心吊胆在意——似乎需要把自己的大多数时间花在编写文档上, 而
没有多少时间来编程(经常如此) 。我希望这里为大家讲述的一切能提供一条折
衷的道路。需要采取一种最适合自己需要(以及习惯)的方法。不管制订出的计
划有多么小,但与完全没有计划相比,一些形式的计划会极大改善你的项目。请
记住:根据估计,没有计划的 50%以上的项目都会失败!
1.13 Java 还是 C++?
Java 特别象 C++;由此很自然地会得出一个结论:C++似乎会被 Java 取代。
但我对这个逻辑存有一些疑问。无论如何,C++仍有一些特性是 Java 没有的。而
且尽管已有大量保证,声称 Java 有一天会达到或超过 C++的速度。但这个突破
迄今仍未实现(尽管 Java 的速度确实在稳步提高,但仍未达到 C++的速度) 。此
外,许多领域都存在为数众多的C++爱好者,所以我并不认为那种语言很快就会
被另一种语言替代(爱好者的力量是容忽视的。比如在我主持的一次“中高级
Java 研讨会”上,Allen Holub声称两种最常用的语言是 Rexx和 COBOL) 。
我感觉 Java 强大之处反映在与C++稍有不同的领域。 C++是一种绝对不会试
图迎合某个模子的语言。 特别是它的形式可以变化多端, 以解决不同类型的问题。
这主要反映在象 Microsoft Visual C++和 Borland C++ Builder(我最喜欢这个)那
样的工具身上。它们将库、组件模型以及代码生成工具等合成到一起,以开发视
窗化的末端用户应用(用于 Microsoft Windows 操作系统) 。但在另一方面,Windows 开发人员最常用的是什么呢?是微软的 Visual Basic(VB) 。当然,我
们在这儿暂且不提 VB 的语法极易使人迷惑的事实——即使一个只有几页长度
的程序,产生的代码也十分难于管理。从语言设计的角度看,尽管VB是那样成
功和流行,但仍然存在不少的缺点。最好能够同时拥有VB那样的强大功能和易
用性,同时不要产生难于管理的代码。而这正是Java 最吸引人的地方:作为“下
一代的 VB”。无论你听到这种主张后有什么感觉,请无论如何都仔细想一想:人 28
们对 Java 做了大量的工作,使它能方便程序员解决应用级问题(如连网和跨平
台 UI 等) ,所以它在本质上允许人们创建非常大型和灵活的代码主体。同时,考
虑到 Java 还拥有我迄今为止尚未在其他任何一种语言里见到的最“健壮”的类
型检查及错误控制系统,所以 Java 确实能大大提高我们的编程效率。这一点是
勿庸置疑的!
但对于自己某个特定的项目, 真的可以不假思索地将 C++换成 Java吗?除了
Web程序片,还有两个问题需要考虑。首先,假如要使用大量现有的库(这样肯
定可以提高不少的效率) ,或者已经有了一个坚实的 C或 C++代码库,那么换成
Java 后,反映会阻碍开发进度,而不是加快它的速度。但若想从头开始构建自己
的所有代码,那么 Java 的简单易用就能有效地缩短开发时间。
最大的问题是速度。在原始的Java 解释器中,解释过的 Java 会比 C慢上 20
到 50 倍。尽管经过长时间的发展,这个速度有一定程度的提高,但和 C比起来
仍然很悬殊。计算机最注重的就是速度;假如在一台计算机上不能明显较快地干
活,那么还不如用手做(有人建议在开发期间使用Java,以缩短开发时间。然后
用一个工具和支撑库将代码转换成 C++,这样可获得更快的执行速度) 。
为使 Java 适用于大多数 Web 开发项目,关键在于速度上的改善。此时要用
到人们称为“刚好及时” (Just-In Time,或 JIT)的编译器,甚至考虑更低级的
代码编译器(写作本书时,也有两款问世) 。当然,低级代码编译器会使编译好
的程序不能跨平台执行,但同时也带来了速度上的提升。这个速度甚至接近 C
和 C++。而且 Java 中的程序交叉编译应当比 C 和 C++中简单得多(理论上只需
重编译即可,但实际仍较难实现;其他语言也曾作出类似的保证) 。
在本书附录,大家可找到与JavaC++比较.对 Java 现状的观察以及编码规
则有关的内容。 1
第2章 一切都是对象
“尽管以 C++为基础,但 Java是一种更纯粹的面向对象程序设计语言” 。
无论 C++还是 Java 都属于杂合语言。但在 Java 中,设计者觉得这种杂合
并不象在 C++里那么重要。杂合语言允许采用多种编程风格;之所以说 C++是
一种杂合语言,是因为它支持与 C 语言的向后兼容能力。由于 C++是 C 的一个
超集,所以包含的许多特性都是后者不具备的,这些特性使 C++在某些地方显
得过于复杂。
Java 语言首先便假定了我们只希望进行面向对象的程序设计。也就是说,正式用它设计之前,必须先将自己的思想转入一个面向对象的世界(除非早已习
惯了这个世界的思维方式) 。只有做好这个准备工作,与其他 OOP 语言相比,才能体会到 Java 的易学易用。在本章,我们将探讨 Java 程序的基本组件,并
体会为什么说 Java乃至 Java程序内的一切都是对象。
2.1 用句柄操纵对象
每种编程语言都有自己的数据处理方式。有些时候,程序员必须时刻留意准
备处理的是什么类型。您曾利用一些特殊语法直接操作过对象,或处理过一些间
接表示的对象吗(C或C++里的指针)?
所有这些在 Java 里都得到了简化,任何东西都可看作对象。因此,我们可
采用一种统一的语法,任何地方均可照搬不误。但要注意,尽管将一切都“看作”
对象,但操纵的标识符实际是指向一个对象的“句柄” (Handle) 。在其他 Java
参考书里,还可看到有的人将其称作一个“引用” ,甚至一个“指针” 。可将这一
情形想象成用遥控板(句柄)操纵电视机(对象) 。只要握住这个遥控板,就相
当于掌握了与电视机连接的通道。但一旦需要“换频道”或者“关小声音” ,我
们实际操纵的是遥控板(句柄) ,再由遥控板自己操纵电视机(对象) 。如果要在
房间里四处走走,并想保持对电视机的控制,那么手上拿着的是遥控板,而非电
视机。
此外,即使没有电视机,遥控板亦可独立存在。也就是说,只是由于拥有一
个句柄,并不表示必须有一个对象同它连接。所以如果想容纳一个词或句子,可
创建一个String句柄:
String s;
但这里创建的只是句柄,并不是对象。若此时向 s 发送一条消息,就会获得
一个错误(运行期) 。这是由于 s 实际并未与任何东西连接(即“没有电视机” ) 。
因此,一种更安全的做法是:创建一个句柄时,记住无论如何都进行初始化:
String s = asdf;
然而,这里采用的是一种特殊类型:字串可用加引号的文字初始化。通常,必须为对象使用一种更通用的初始化类型。
2.2 所有对象都必须创建
创建句柄时,我们希望它同一个新对象连接。通常用 new关键字达到这一目
的。new的意思是: “把我变成这些对象的一种新类型” 。所以在上面的例子中,可以说:
String s = new String(asdf);
它不仅指出“将我变成一个新字串” ,也通过提供一个初始字串,指出了“如 2
何生成这个新字串” 。
当然,字串(String)并非唯一的类型。Java配套提供了数量众多的现成类
型。对我们来讲,最重要的就是记住能自行创建类型。事实上,这应是 Java程
序设计的一项基本操作,是继续本书后余部分学习的基础。
2.2.1 保存到什么地方
程序运行时,我们最好对数据保存到什么地方做到心中有数。特别要注意的
是内存的分配。有六个地方都可以保存数据:
(1) 寄存器。这是最快的保存区域,因为它位于和其他所有保存方式不同的
地方:处理器内部。然而,寄存器的数量十分有限,所以寄存器是根据需要由编
译器分配。我们对此没有直接的控制权,也不可能在自己的程序里找到寄存器存
在的任何踪迹。
(2) 堆栈。驻留于常规RAM(随机访问存储器)区域,但可通过它的“堆栈
指针”获得处理的直接支持。堆栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。 这是一种特别快、 特别有效的数据保存方式, 仅次于寄存器。
创建程序时,Java 编译器必须准确地知道堆栈内保存的所有数据的“长度”以
及“存在时间” 。这是由于它必须生成相应的代码,以便向上和向下移动指针。
这一限制无疑影响了程序的灵活性,所以尽管有些Java数据要保存在堆栈里—
—特别是对象句柄,但 Java对象并不放到其中。
(3) 堆。一种常规用途的内存池(也在RAM 区域) ,其中保存了 Java对象。
和堆栈不同, “内存堆”或“堆” (Heap)最吸引人的地方在于编译器不必知道
要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。
因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new
命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当
然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉
更长的时间!
(4) 静态存储。这儿的“静态” (Static)是指“位于固定位置” (尽管也在
RAM 里) 。程序运行期间,静态存储的数据将随时等候调用。可用static 关键字
指出一个对象的特定元素是静态的。但Java 对象本身永远都不会置入静态存储
空间。
(5) 常数存储。常数值通常直接置于程序代码内部。这样做是安全的,因为
它们永远都不会改变。有的常数需要严格地保护,所以可考虑将它们置入只读存
储器(ROM) 。
(6) 非 RAM 存储。若数据完全独立于一个程序之外,则程序不运行时仍可
存在, 并在程序的控制范围之外。 其中两个最主要的例子便是 “流式对象” 和 “固
定对象” 。对于流式对象,对象会变成字节流,通常会发给另一台机器。而对于
固定对象,对象保存在磁盘中。即使程序中止运行,它们仍可保持自己的状态不
变。对于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体
中。一旦需要,甚至能将它们恢复成普通的、基于RAM 的对象。Java 1.1提供
了对 Lightweight persistence 的支持。未来的版本甚至可能提供更完整的方案。
2.2.2 特殊情况:主要类型
有一系列类需特别对待;可将它们想象成“基本” 、 “主要”或者“主”
(Primitive)类型,进行程序设计时要频繁用到它们。之所以要特别对待,是由 3
于用 new 创建对象(特别是小的、简单的变量)并不是非常有效,因为new 将
对象置于“堆”里。对于这些类型,Java采纳了与 C和 C++相同的方法。也就
是说,不是用 new 创建变量,而是创建一个并非句柄的“自动”变量。这个变
量容纳了具体的值,并置于堆栈中,能够更高效地存取。
Java 决定了每种主要类型的大小。就象在大多数语言里那样,这些大小并
不随着机器结构的变化而变化。这种大小的不可更改正是Java程序具有很强移
植能力的原因之一。
主类型 大小 最小值 最大值 封装器类型
boolean 1 位 - - Boolean
char 16位 Unicode 0 Unicode 2的 16次方-1 Character
byte 8位 -128 +127 Byte(注释①)
short 16位 -2的 15次方 +2的 15次方-1 Short(注释①)
int 32位 -2的31次方 +2的31次方-1 Integer
long 64 位 -2的 63次方 +2的63次方-1 Long
float 32位 IEEE754 IEEE754 Float
double 64 位 IEEE754 IEEE754 Double
Void - - - Void(注释①)
①:到Java 1.1才有,1.0版没有。
数值类型全都是有符号(正负号)的,所以不必费劲寻找没有符号的类型。
主数据类型也拥有自己的“封装器” (wrapper)类。这意味着假如想让堆内
一个非主要对象表示那个主类型,就要使用对应的封装器。例如:
char c = 'x';
Character C = new Character('c');
也可以直接使用:
Character C = new Character('x');
这样做的原因将在以后的章节里解释。
1. 高精度数字
Java 1.1增加了两个类, 用于进行高精度的计算: BigInteger 和 BigDecimal。
尽管它们大致可以划分为“封装器”类型,但两者都没有对应的“主类型” 。
这两个类都有自己特殊的“方法” ,对应于我们针对主类型执行的操作。也
就是说,能对 int 或 float 做的事情,对 BigInteger 和 BigDecimal 一样可以做。
只是必须使用方法调用,不能使用运算符。此外,由于牵涉更多,所以运算速度
会慢一些。我们牺牲了速度,但换来了精度。
BigInteger 支持任意精度的整数。也就是说,我们可精确表示任意大小的整
数值,同时在运算过程中不会丢失任何信息。
BigDecimal支持任意精度的定点数字。例如,可用它进行精确的币值计算。
至于调用这两个类时可选用的构建器和方法,请自行参考联机帮助文档。
2.2.3 Java的数组 4
几乎所有程序设计语言都支持数组。在 C和 C++里使用数组是非常危险的,因为那些数组只是内存块。若程序访问自己内存块以外的数组,或者在初始化之
前使用内存(属于常规编程错误) ,会产生不可预测的后果(注释②) 。
②:在 C++里,应尽量不要使用数组,换用标准模板库(Standard
TemplateLibrary)里更安全的容器。
Java 的一项主要设计目标就是安全性。所以在 C 和 C++里困扰程序员的许
多问题都未在 Java 里重复。一个 Java 可以保证被初始化,而且不可在它的范
围之外访问。由于系统自动进行范围检查,所以必然要付出一些代价:针对每个
数组,以及在运行期间对索引的校验,都会造成少量的内存开销。但由此换回的
是更高的安全性,以及更高的工作效率。为此付出少许代价是值得的。
创建对象数组时,实际创建的是一个句柄数组。而且每个句柄都会自动初始
化成一个特殊值,并带有自己的关键字:null(空) 。一旦 Java 看到 null,就知
道该句柄并未指向一个对象。正式使用前,必须为每个句柄都分配一个对象。若
试图使用依然为 null的一个句柄,就会在运行期报告问题。因此,典型的数组错
误在 Java里就得到了避免。
也可以创建主类型数组。同样地,编译器能够担保对它的初始化,因为会将
那个数组的内存划分成零。
数组问题将在以后的章节里详细讨论。
2.3 绝对不要清除对象
在大多数程序设计语言中,变量的“存在时间” (Lifetime)一直是程序员需
要着重考虑的问题。变量应持续多长的时间?如果想清除它,那么何时进行?在
变量存在时间上纠缠不清会造成大量的程序错误。 在下面的小节里, 将阐示 Java
如何帮助我们完成所有清除工作,从而极大了简化了这个问题。
2.3.1 作用域
大多数程序设计语言都提供了“作用域” (Scope)的概念。对于在作用域里
定义的名字,作用域同时决定了它的“可见性”以及“存在时间” 。在 C,C++
和 Java里,作用域是由花括号的位置决定的。参考下面这个例子:
{
int x = 12;
only x available
{
int q = 96;
both x q available
}
only x available
q “out of scope”
}
73页程序
5
作为在作用域里定义的一个变量,它只有在那个作用域结束之前才可使用。
在上面的例子中,缩进排版使Java代码更易辨读。由于 Java是一种形式自
由的语言,所以额外的空格、制表位以及回车都不会对结果程序造成影响。
注意尽管在 C和 C++里是合法的,但在 Java里不能象下面这样书写代码:
{
int x = 12;
{
int x = 96; illegal
}
}
74页上程序
编译器会认为变量 x 已被定义。所以 C 和 C++能将一个变量“隐藏”在一
个更大的作用域里。但这种做法在 Java 里是不允许的,因为 Java 的设计者认
为这样做使程序产生了混淆。
2.3.2 对象的作用域
Java 对象不具备与主类型一样的存在时间。用 new 关键字创建一个 Java
对象的时候,它会超出作用域的范围之外。所以假若使用下面这段代码:
{
String s = new String(a string);
} 作用域的终点
那么句柄 s 会在作用域的终点处消失。然而,s 指向的 String 对象依然占据
着内存空间。在上面这段代码里,我们没有办法访问对象,因为指向它的唯一一
个句柄已超出了作用域的边界。在后面的章节里,大家还会继续学习如何在程序
运行期间传递和复制对象句柄。
这样造成的结果便是:对于用 new创建的对象,只要我们愿意,它们就会一
直保留下去。这个编程问题在 C 和 C++里特别突出。看来在 C++里遇到的麻烦
最大:由于不能从语言获得任何帮助,所以在需要对象的时候,根本无法确定它
们是否可用。而且更麻烦的是,在 C++里,一旦工作完成,必须保证将对象清
除。
这样便带来了一个有趣的问题。假如 Java 让对象依然故我,怎样才能防止
它们大量充斥内存,并最终造成程序的“凝固”呢。在 C++里,这个问题最令
程序员头痛。但Java以后,情况却发生了改观。Java有一个特别的“垃圾收集
器” ,它会查找用 new创建的所有对象,并辨别其中哪些不再被引用。随后,它
会自动释放由那些闲置对象占据的内存,以便能由新对象使用。这意味着我们根
本不必操心内存的回收问题。只需简单地创建对象,一旦不再需要它们,它们就
会自动离去。这样做可防止在 C++里很常见的一个编程问题:由于程序员忘记
释放内存造成的“内存溢出” 。
2.4 新建数据类型:类 6
如果说一切东西都是对象,那么用什么决定一个“类” (Class)的外观与行
为呢?换句话说,是什么建立起了一个对象的“类型” (Type)呢?大家可能猜
想有一个名为“type”的关键字。但从历史看来,大多数面向对象的语言都用关
键字“class”表达这样一个意思: “我准备告诉你对象一种新类型的外观” 。class
关键字太常用了,以至于本书许多地方并没有用粗体字或双引号加以强调。在这
个关键字的后面,应该跟随新数据类型的名称。例如:
class ATypeName {类主体置于这里}
这样就引入了一种新类型,接下来便可用new创建这种类型的一个新对象:
ATypeName a = new ATypeName;
在 ATypeName里, 类主体只由一条注释构成 (星号和斜杠以及其中的内容,本章后面还会详细讲述) ,所以并不能对它做太多的事情。事实上,除非为其定
义了某些方法,否则根本不能指示它做任何事情。
2.4.1 字段和方法
定义一个类时(我们在 Java 里的全部工作就是定义类、制作那些类的对象
以及将消息发给那些对象) , 可在自己的类里设置两种类型的元素: 数据成员 (有
时也叫“字段” )以及成员函数(通常叫“方法” ) 。其中,数据成员是一种对象
(通过它的句柄与其通信) ,可以为任何类型。它也可以是主类型(并不是句柄)
之一。如果是指向对象的一个句柄,则必须初始化那个句柄,用一种名为“构建
器” (第 4 章会对此详述)的特殊函数将其与一个实际对象连接起来(就象早先
看到的那样,使用 new关键字) 。但若是一种主类型,则可在类定义位置直接初
始化(正如后面会看到的那样,句柄亦可在定义位置初始化) 。
每个对象都为自己的数据成员保有存储空间;数据成员不会在对象之间共
享。下面是定义了一些数据成员的类示例:
class DataOnly {
int i;
float f;
boolean b;
}
76页上程序
这个类并没有做任何实质性的事情,但我们可创建一个对象:
DataOnly d = new DataOnly;
可将值赋给数据成员,但首先必须知道如何引用一个对象的成员。为达到引
用对象成员的目的,首先要写上对象句柄的名字,再跟随一个点号(句点) ,再
跟随对象内部成员的名字。即“对象句柄.成员” 。例如:
d.i = 47;
d.f = 1.1f;
d.b = false;
一个对象也可能包含了另一个对象,而另一个对象里则包含了我们想修改的
数据。对于这个问题,只需保持“连接句点”即可。例如:
myPlane.leftTank.capacity = 100;
除容纳数据之外,DataOnly 类再也不能做更多的事情,因为它没有成员函 7
数(方法) 。为正确理解工作原理,首先必须知道“自变量”和“返回值”的概
念。我们马上就会详加解释。
1. 主成员的默认值
若某个主数据类型属于一个类成员,那么即使不明确(显式)进行初始化,也可以保证它们获得一个默认值。
主类型 默认值
Boolean false
Char '\u0000'(null)
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d
一旦将变量作为类成员使用,就要特别注意由 Java 分配的默认值。这样做
可保证主类型的成员变量肯定得到了初始化(C++不具备这一功能) ,可有效遏
止多种相关的编程错误。
然而,这种保证却并不适用于“局部”变量——那些变量并非一个类的字段。
所以,假若在一个函数定义中写入下述代码:
int x;
那么 x会得到一些随机值 (这与 C和 C++是一样的) , 不会自动初始化成零。
我们责任是在正式使用 x前分配一个适当的值。如果忘记,就会得到一条编译期
错误, 告诉我们变量可能尚未初始化。 这种处理正是 Java优于C++的表现之一。
许多 C++编译器会对变量未初始化发出警告,但在Java里却是错误。
2.5 方法、自变量和返回值
迄今为止, 我们一直用 “函数” (Function) 这个词指代一个已命名的子例程。
但在 Java里, 更常用的一个词却是 “方法” (Method) , 代表 “完成某事的途径” 。
尽管它们表达的实际是同一个意思,但从现在开始,本书将一直使用“方法” ,而不是“函数” 。
Java 的“方法”决定了一个对象能够接收的消息。通过本节的学习,大家
会知道方法的定义有多么简单!
方法的基本组成部分包括名字、自变量、返回类型以及主体。下面便是它最
基本的形式:
返回类型 方法名( 自变量列表 ) { 方法主体 }
返回类型是指调用方法之后返回的数值类型。显然,方法名的作用是对具体
的方法进行标识和引用。自变量列表列出了想传递给方法的信息类型和名称。
Java的方法只能作为类的一部分创建。 只能针对某个对象调用一个方法 (注 8
释③) ,而且那个对象必须能够执行那个方法调用。若试图为一个对象调用错误
的方法,就会在编译期得到一条出错消息。为一个对象调用方法时,需要先列出
对象的名字,在后面跟上一个句点,再跟上方法名以及它的参数列表。亦即“对
象名.方法名(自变量 1,自变量 2,自变量 3...)。举个例子来说,假设我们有一
个方法名叫f,它没有自变量,返回的是类型为int 的一个值。那么,假设有一
个名为a的对象,可为其调用方法 f,则代码如下:
int x = a.f;
返回值的类型必须兼容 x的类型。
象这样调用一个方法的行动通常叫作“向对象发送一条消息” 。在上面的例
子中,消息是 f,而对象是 a。面向对象的程序设计通常简单地归纳为“向对象
发送消息” 。
③:正如马上就要学到的那样, “静态”方法可针对类调用,毋需一个对象。
2.5.1 自变量列表
自变量列表规定了我们传送给方法的是什么信息。正如大家或许已猜到的那
样,这些信息——如同Java内其他任何东西——采用的都是对象的形式。因此,我们必须在自变量列表里指定要传递的对象类型,以及每个对象的名字。正如在
Java 其他地方处理对象时一样,我们实际传递的是“句柄” (注释④) 。然而,句柄的类型必须正确。倘若希望自变量是一个“字串” ,那么传递的必须是一个
字串。
④:对于前面提及的“特殊”数据类型boolean,char,byte,short,int,long, ,float 以及 double 来说是一个例外。但在传递对象时,通常都是指传递指
向对象的句柄。
下面让我们考虑将一个字串作为自变量使用的方法。下面列出的是定义代
码,必须将它置于一个类定义里,否则无法编译:
int storage(String s) {
return s.length 2;
}
这个方法告诉我们需要多少字节才能容纳一个特定字串里的信息(字串里的
每个字符都是 16位,或者说 2个字节、长整数,以便提供对 Unicode字符的支
持) 。自变量的类型为String,而且叫作 s。一旦将 s 传递给方法,就可将它当作
其他对象一样处理(可向其发送消息) 。在这里,我们调用的是 length方法,它是 String 的方法之一。该方法返回的是一个字串里的字符数。
通过上面的例子,也可以了解 return关键字的运用。它主要做两件事情。首
先,它意味着“离开方法,我已完工了” 。其次,假设方法生成了一个值,则那
个值紧接在 return 语句的后面。在这种情况下,返回值是通过计算表达式
“s.length2”而产生的。
可按自己的愿望返回任意类型,但倘若不想返回任何东西,就可指示方法返
回 void(空) 。下面列出一些例子。 9
boolean flag { return true; }
float naturalLogBase { return 2.718; }
void nothing { return; }
void nothing2 {}
若返回类型为void,则return关键字唯一的作用就是退出方法。所以一旦抵
达方法末尾,该关键字便不需要了。可在任何地方从一个方法返回。但假设已指
定了一种非 void 的返回类型,那么无论从何地返回,编译器都会确保我们返回
的是正确的类型。
到此为止,大家或许已得到了这样的一个印象:一个程序只是一系列对象的
集合, 它们的方法将其他对象作为自己的自变量使用, 而且将消息发给那些对象。
这种说法大体正确,但通过以后的学习,大家还会知道如何在一个方法里作出决
策,做一些更细致的基层工作。至于这一章,只需理解消息传送就足够了。
2.6 构建 Java程序
正式构建自己的第一个Java程序前,还有几个问题需要注意。
2.6.1 名字的可见性
在所有程序设计语言里,一个不可避免的问题是对名字或名称的控制。假设
您在程序的某个模块里使用了一个名字, 而另一名程序员在另一个模块里使用了
相同的名字。此时,如何区分两个名字,并防止两个名字互相冲突呢?这个问题
在 C语言里特别突出。 因为程序未提供很好的名字管理方法。 C++的类 (即 Java
类的基础)嵌套使用类里的函数,使其不至于同其他类里的嵌套函数名冲突。然
而,C++仍然允许使用全局数据以及全局函数,所以仍然难以避免冲突。为解决
这个问题,C++用额外的关键字引入了“命名空间”的概念。
由于采用全新的机制,所以 Java 能完全避免这些问题。为了给一个库生成
明确的名字,采用了与 Internet 域名类似的名字。事实上,Java 的设计者鼓励
程序员反转使用自己的Internet域名,因为它们肯定是独一无二的。由于我的域
名是 BruceEckel.com ,所以我的实用工具库就可命名为
com.bruceeckel.utility.foibles。反转了域名后,可将点号想象成子目录。
在 Java 1.0 和 Java 1.1中,域扩展名com,edu,org,net 等都约定为大
写形式。 所以库的样子就变成: COM.bruceeckel.utility.foibles。 然而, 在 Java 1.2
的开发过程中,设计者发现这样做会造成一些问题。所以目前的整个软件包都以
小写字母为标准。
Java 的这种特殊机制意味着所有文件都自动存在于自己的命名空间里。而
且一个文件里的每个类都自动获得一个独一无二的标识符(当然,一个文件里的
类名必须是唯一的) 。所以不必学习特殊的语言知识来解决这个问题——语言本
身已帮我们照顾到这一点。
2.6.2 使用其他组件
一旦要在自己的程序里使用一个预先定义好的类,编译器就必须知道如何找
到它。当然,这个类可能就在发出调用的那个相同的源码文件里。如果是那种情
况,只需简单地使用这个类即可——即使它直到文件的后面仍未得到定义。Java 10
消除了“向前引用”的问题,所以不要关心这些事情。
但假若那个类位于其他文件里呢?您或许认为编译器应该足够“联盟” ,可
以自行发现它。但实情并非如此。假设我们想使用一个具有特定名称的类,但那
个类的定义位于多个文件里。或者更糟,假设我们准备写一个程序,但在创建它
的时候,却向自己的库加入了一个新类,它与现有某个类的名字发生了冲突。
为解决这个问题,必须消除所有潜在的、纠缠不清的情况。为达到这个目的,要用 import 关键字准确告诉 Java编译器我们希望的类是什么。 import 的作用是
指示编译器导入一个“包”——或者说一个“类库” (在其他语言里,可将“库”
想象成一系列函数、数据以及类的集合。但请记住,Java 的所有代码都必须写
入一个类中) 。
大多数时候,我们直接采用来自标准 Java 库的组件(部件)即可,它们是
与编译器配套提供的。使用这些组件时,没有必要关心冗长的保留域名;举个例
子来说,只需象下面这样写一行代码即可:
import java.util.Vector;
它的作用是告诉编译器我们想使用 Java 的 Vector 类。然而,util 包含了数
量众多的类,我们有时希望使用其中的几个,同时不想全部明确地声明它们。为
达到这个目的,可使用“”通配符。如下所示:
import java.util.;
需导入一系列类时, 采用的通常是这个办法。 应尽量避免一个一个地导入类。
2.6.3 static 关键字
通常,我们创建类时会指出那个类的对象的外观与行为。除非用new创建那
个类的一个对象,否则实际上并未得到任何东西。只有执行了 new 后,才会正
式生成数据存储空间,并可使用相应的方法。
但在两种特殊的情形下,上述方法并不堪用。一种情形是只想用一个存储区
域来保存一个特定的数据——无论要创建多少个对象,甚至根本不创建对象。另
一种情形是我们需要一个特殊的方法,它没有与这个类的任何对 ......
--免费电子书,仅供网上参考,不得私自出版!
——本书(第五版)原始版本为word版(Java编程思想(第五版).rar,各章
节文件独立,含源代码) ,为了便于阅读,现合并成 pdf 版。enjoycodes.com 整
理
作者:Bruce Eckel
主页:http:www.BruceEckel.com
编译:Trans Bot
主页:http:member.netease.com~transbot
致谢
--献给那些直到现在仍在孜孜不倦创造下一代计算机语言的人们!
指导您利用万维网的语言进行面向对象的程序设计
完整的正文、更新内容及程序代码可从 http:www.bruceeckel.com 下载
从 Java 的基本语法到它最高级的特性 (网络编程、 高级面向对象能力、 多线程) ,《Thinking In Java》都能对您有所裨益。Bruce Eckel 优美的行文以及短小、精悍的程序示例有助于您理解含义模糊的概念。
面向初学者和某种程度的专家
教授 Java 语言,而不是与平台有关的理论
覆盖 Java 1.2 的大多数重要方面:Swing 和新集合
系统讲述 Java 的高级理论:网络编程、多线程处理、虚拟机性能以及同非 Java
代码的连接
320 个有用的 Java 程序,15000 行以上代码
解释面向对象基本理论,从继承到设计方案
来自与众不同的获奖作者 Bruce Eckel
可通过万维网免费索取源码和持续更新的本书电子版
从 www.BruceEckel.com 获得配套 CD(含 15 小时以上的合成语音授课)
读者如是说: “最好的 Java 参考书……绝对让人震惊” ; “购买 Java 参考书最明
智的选择” ; “我见过的最棒的编程指南” 。
Bruce Eckel 也 是 《 Thinking in C++ 》 的 作 者 , 该 书 曾 获 1995 年
SoftwareDevelopment Jolt Award 最佳书籍大奖。作为一名有 20 经验的编程专
家,曾教授过世界上许多地区的人进行对象编程。最开始涉及的领域是 C++,现
在也进军 Java。他是 C++标准协会有表决权的成员之一,曾就面向对象程序设计
这一主题写过其他 5 本书,发表过 150 多篇文章,并是多家计算机杂志的专栏作 家,其中包括《Web Techniques》的 Java 专栏。曾出席过 C++和 Java 的“软件
开发者会议” ,并分获“应用物理”与“计算机工程”的学士和硕士学位。
读者的心声
比我看过的 Java 书好多了……非常全面,举例都恰到好处,显得颇具“智慧” 。
和其他许多 Java 书籍相比,我觉得它更成熟、连贯、更有说服力、更严谨。总
之,写得非常好,肯定是一本学习 Java 的好书。 (Anatoly Vorobey,TechnionUniversity,Haifa,以色列) 。
是我见过的最好的编程指南,对任何语言都不外如是。 (Joakim ziegler,FIX
系统管理员)
感谢你写出如此优秀的一本 Java 参考书。 (Dr. Gavin Pillay,Registrar,King
Edward VII Hospital,南非)
再次感谢您这本令人震惊的书。我以前真的有点儿不知所从的感觉(因为不是 C
程序员) ,但你的书浅显易懂,使我能很快掌握 Java——差不多就是阅读的速度
吧。能从头掌握基本原理和概念的感觉真好,再也不用通过不断的试验和出错来
建立概念模型了。希望不久能有机会参加您的讲座。 (Randall R. Hawley,Automation Technician,Eli Lilly Co)
我迄今为止看过的最好的计算机参考书。 (Tom Holland)
这是我读过的关于程序设计的最好的一本书……第 16 章有关设计方案的内容是
我这么久以来看过的最有价值的。 (Han Finci,助教,计算机科学学院,耶路撒
冷希伯来大学,以色列)
有史以来最好的一本 Java 参考书。 (Ravindra Pai,Oracle 公司 SUNOS 产品线)
这是关于 Java 的一本好书。非常不错,你干得太好了!书中涉及的深度真让人
震惊。一旦正式出版,我肯定会买下它。我从 96 年十月就开始学习 Java 了。通
过比较几本书,你的书可以纳入“必读”之列。这几个月来,我一直在搞一个完
全用 Java 写的产品。你的书巩固了我一些薄弱的地方,并大大延伸了我已知的
东西。甚至在会见承包商的时候,我都引用了书中的一些解释,它对我们的开发
小组太有用了。通过询问组内成员我从书中学来的知识(比如数组和矢量的区
别) ,可以判断他们对 Java 的掌握有多深。 (Steve Wilkinson,MCI 通信公司资
深专家)
好书!我见过的最好的一本 Java 教材。 (Jeff Sinclair,软件工程师,Kestral
Computing 公司)
感谢你的《Thinking in Java》 。终于有人能突破传统的计算机参考书模式,进
入一个更全面、更深入的境界。我读过许多书,只有你的和 Patrick Winston的书才在我心目中占据了一个位置。我已向客户郑重推荐这本书。再次感谢。
(Richard Brooks,Java 顾问,Sun 专业服务公司,达拉斯市)
其他书讨论的都是 Java“是什么” (讲述语法和库) , 或者 Java“怎样用” (编程实
例) 。 《Thinking in Java》显然与众不同,是我所知唯一一本解释 Java“为什么”
的书:为什么象这样设计,为什么象这样工作,为什么有时不能工作,为什么比
C++好,为什么没有 C++好,等等。尽管这本书也很好讲述了“是什么”和“怎
样用”的问题,但它的特色并在于此。这本书特别适合那些想追根溯源的人。
(Robert S. Stephenson)
感谢您写出这么一本优秀的书,我对它越来越爱不释手。我的学生也喜欢它。
(Chuck Iverson)
向你在《Thinking in Java》的工作致敬。这本书对因特网的未来进行了最恰当
的揭示, 我只是想对你说声 “谢谢” , 它非常有价值。 (Patrick Barrell, Network
Officer Mamco-QAF Mfg 公司)
市面上大多数 Java 书作为新手指南都是不错的。但它们的立意大多雷同,举的
例子也有过多的重复。 从未没见过象您这样的一本书, 它和那些书完全是两码事。
我认为它是迄今为止最好的一本参考书。请快些出版它!……另外,由于
《Thinking in Java》都这么好了,我也赶快去买了一本《Thinking in C++》 。
(George Laframboise,LightWorx 技术咨询公司)
从前给你写过信,主要是表达对《Thinking in C++》一书的惊叹(那本书在我
的书架上占有突出的位置) 。今天,我很欣慰地看到你投向了 Java 领域,并有幸
拜读了最新的《Thinking in Java》电子版。看过之后,我不得不说: “服了! ”
内容非常精彩,有很强的说服力,不象读那些干巴巴的参考书。你讲到了 Java
开发最重要、也最易忽略的方面:基本原理。 (Sean Brady)
你举的例子都非常浅显,很容易理解。Java 的许多重要细节都照顾到了,而单
薄的 Java 文档根本没有涉及那些方面。另外,这本书没有浪费读者的时间。程
序员已经知道了一些基本的事实,你在这个基础上进行了很好的发挥。 (Kai
Engert,Innovative Software 公司,德国)
我是您的《Thinking in C++》的忠实读者。通读了您的 Java 书的电子版以后,发现您在这两本书上有同样高级别的写作水平。谢谢! (Peter R. Neuwald)
写得非常好的一本 Java 书……我认为您的工作简直可以说“伟大” 。我是芝加哥
地区 Java 特别兴趣组的头儿, 已在最近的几次聚会上推荐了您的书和 Web 站点。
以后每个月开 SIG 会的时候,我都想把《Thinking in Java》作为基本的指导教
材使用。一般来说,我们会每次讨论书中的一章内容。 (Mark Ertes)
衷心感谢你的书,它写得太好了。我已把它推荐给自己的用户和 Ph.D.学生。
( Hugues LeroyIrisa-Inria Rennes France , Head of Scientific Computingand Industrial Tranfert)
我到现在只读了《Thinking in Java》的 40 页内容,但已对它留下了深刻的印
象。这无疑是见过的最精彩的编程专业书……而且我本身就是一个作家,所以这
点儿看法还是有些权威吧。我已订购了《Thinking in C++》 ,已经等得迫不及待
了——我是一名编程新手,最怕的就是散乱无章的学习线索。所以必须在这里向
您的出色工作表示敬意。以前看过的书似乎都有这方面的毛病,经常使我才提起
的兴致消弥于无形。但看了你的书以后,感觉好多了。 (Glenn Becker,EducationalTheatre Association)
谢谢您这本出色的书。在终于认识了 Java 与 C++之间纠缠不清的一些事实后,我真的要非常感谢这本书。对您的书非常满意! (Felix Bizaoui,Twin
OaksIndustries,Louisa,Va)
恭喜你写出这么好的一本书。我是在有了阅读《Thinking in C++》的经历以后,才来看这本《Thinking in Java》的,它确实没让我失望。 (Jaco van derMerwe,软件专家,DataFusion Systems 有限公司,Stellenbosch,南非)
这是我看过的最好的 Java 书之一。 (E.E. Pritchard,资深软件工程师,英国剑
桥动画系统有限公司)
你的东东让其他 Java 参考收黯然失色。看来其他作者都应该向你看齐了。
(Brett g Porter,资深程序员,Art Logic)
我花了一、两个星期的时间来看你的书,并对以前我看过的一些 Java 书进行了
比较。显然,只有你的书才能让我真正“入门” 。现在,我已向我的许多朋友推
荐了这本书,他们都对其作出了很高的评价。请接受我真诚的祝贺,并希望她早
些正式出版。 (Rama Krishna Bhupathi,软件工程师,TCSI 公司,圣琼斯)
这是一本充满智慧的书,与简单的参考书有着截然不同的风格。它现在已成了我
进行 Java 创作一份主要参考。你的目录做得相当不错,让人一目了然,很快就
能找到自己需要的东西。 更高兴的是, 这本书没有写成一本改头换面的 API 字典,也没有把我们这些程序员看作傻瓜。 (Grant Sayer,Java Components Group
Leader,Ceedata Systems Pty 有限公司,澳大利亚)
啧啧,一本可读性强、论据充分的 Java 书。外面有太多用词贫乏的 Java 书(也
有几本好的) ,只有你的书是最好的。那些垃圾在你的书前面不值一提。 (John
Root,Web 开发员,伦敦社会安全部)
我刚刚开始看《Thinking in Java》 。我希望它能有更大的突破,因为《Thinking
in C++》写得实在太好了。我是一名有经验的 C++程序员,事先看那本书对学习
Java 很有帮助。但我在 Java 上的经验不够,希望这本新书能让我满意。您真是
一名“高产高质”作者。 (Kevin K. Lewis,ObjectSpace 公司技术员)
我认为这是本好书。从这本书中,我学到了与 Java 有关的所有知识。谢谢你能
让这本书通过互联网免费发行。如果不那样做,我根本不可能象现在这样有巨大
的进步。 但最令人高兴的是, 你的书并没有成为一本官方 Java 手册, 指出了 Java
一些不当的地方。你真是做了一件大好事。 (Frederik Fix, Belgium)
我现在经常查阅你的书。大约两年前,当我想开始学习 C++的时候,是《C++
InsideOut》指导我游历 C++的世界。它使我在这方面的技能大增,并找到了一
个较好的职位。现在出于工作上的原因需要学习 Java,又是《Thinking in Java》
给我正确的指引。 尽管现在可选择的书更多了, 但我知道自己别无选择。 很奇妙,不是吗?现在看这本书的时候, 我居然有一种重新认识自己的感觉。 衷心感谢你,我现在的理解又比以前深入多了。 (Anand Kumar S.,软件工程师,Computervision 公司,印度)
你的书给人一种“鹤立鸡群”的感觉。 (Peter Robinson,剑桥大学计算机实验
室)
这是我看过的最好的一本 Java 参考书。现在想起来,能找到这样的一本书简直
是幸运。谢谢! (Chuck Peterson,因特网产品线主管,IVIS International 公
司)
这本书太棒了!它已是我看过的第三本 Java 书了,真后悔没有早点儿发现它。
前两本书都没坚持看完,但我已决心看完这一本。不妨告诉你,当时我是想寻找
关于内部类使用的一些资料,是我的朋友告诉我网上能下载这本书。你干得真不
错! (Jerry Nowlin,MTS,Lucent Technologies)
在我看过的6本Java书中,你的《Thinking in Java》是最好和最有用的。
(Michael Van Waas,Ph.D,TMR Associates 公司总裁)
我很想对《Thinking in Java》说声谢谢。这是一本多么出色的书——并不单指
它在网上免费发送!作为一名学生,我认为你的书有不可估量的价值(我有《C++
InsideOut》的拷贝,那是关于 C++的另一本好书) ,因为它不仅教我怎样做,而且解释了为什么。这当然为我用 C++或 Java 这样的语言编程打下了坚实的基
础。我有许多朋友都象我一样热爱编程,在向他们推荐了这本书后,反映都非常
好,他们的看法同我一样。再次感谢您。顺便提一句,我是一个印尼畜牲,整天
都喜欢和 Java 泡在一起! (Ray Frederick Djajadinata,Trisakti 大学学生,Indonesian Pork)
你把这本书放在网上引起了相当程度的轰动, 我对你的做法表示真诚的感谢与支
持! (Shane LeBouthillier,加拿大艾伯特大学计算机工程系学生)
告诉你吧,我是多么热烈地盼望读到你每个月的专栏!作为 OOP 设计的新手,我
要感谢你把即使最基本的概念都讲得那么透彻和全面。我已下载了你的书,但我
保证会在它正式出版后另行购买。感谢你提供的所有帮助! (Dan Cashmer,B.C.Ziegler Co.)
祝贺你完成了一件伟大的作品。我现在下载的是《Thinking in Java》的 PDF
版。这本书还没有读完,便迫不及待地跑到书店去找你的《Thinking in C++》 。
我在计算机界干了8年,是一个顾问,兼软件工程师、教师培训专家,最近辞
职自己开了一间公司。所以见过不少的书。但是,正是这些书使我的女朋友称我
为“书呆子” !并不是我概念掌握得不深入——只是由于现在的发展太快,使我
短期内不能适应新技术。但这两本书都给了我很大的启示,它与以前接触过或买
过的计算机参考书都大不相同。写作风格很棒,每个新概念都讲得很好,书中充
满了“智慧” 。 (Simon Goland,simonsez@smartt.com,Simon Says Consulting
公司)
必须认为你的《Thinking in Java》非常优秀!那正是我一直以来梦想的参考书。
其中印象最深的是有关使用 Java 1.1 作软件设计时的一些优缺点分析。
(DirkDuehr,Lexikon Verlag,Bertelsmann AG,德国)
谢谢您写出两本空前绝后的书( 《Thinking in Java》和《Thinking in C++》 ) 。
它们使我在面向对象的程序设计上跨出了一大步。 (Donald Lawson,DCLEnterprises)
谢谢你花时间写出一本真正有用的 Java 参考书,你现在绝对能为自己的工作感
到骄傲了。 (Dominic Turner,GEAC Support)
这是我见过的最好的一本 Java 书。 (Jean-Yves MENGANT,Chief Software
Architect NAT-SYSTEM,法国巴黎)
《Thinking in Java》无论在覆盖的范围还是讲述方法上都有独到之处。看懂这
本书非常容易,摘录的代码段也很有说服力。 (Ron Chan,Ph.D,Expert Choice
公司,Pittsburgh PA)
你的书太棒了。我看过许多编程书刊,只有你的书给人一种全新的视野。其他作
者都该向你好好学习才是。 (Ningjian Wang,信息系统工程师,The Vangurad
Group)
《Thinking in Java》是一本出色的、可读性极强的书,我已向我的学生推荐阅
读。 (Dr. Pual Gorman,计算机科学系,Otago 大学,Dunedin 市,新西兰)
在我看过的书中,你的书最有品味,不象有的书那样粗制滥造。任何搞软件开发
的人都不应错过。 (Jose Suriol,Scylax 公司)
感谢您免费提供这本书,它是我看过或翻过的最好的一本。 (JeffLapchinsky,Net Results Technologies 公司程序员)
这本书简明扼要,看起来不仅毫不费力,而且象是一种享受。 (Keith Ritchie,Java 研发组,KL Group 公司)
这真的是我看过的最好的一本 Java 书! (Daniel Eng)
我看过的最好的 Java 书! (Rich Hoffarth,Senior Architect,West Group)
感谢你这本出色的书籍,我好久都没有经历让人如此愉悦的阅读过程了。 (Fred
Trimble,Actium 公司)
你的写作能准确把握轻重缓急,并能成功抓住细节。这本书让学习变成了一件有
趣的事情,我感觉满意,非常满意!谢谢你这本出色的学习教程。 (Rajesh Rau,软件顾问)
《Thinking in Java》让整个自由世界都感受到了震憾! (Miko O'Sullivan,Idocs 公司总裁)
关于《Thinking in C++》 :
荣获 1995 年由《软件开发》杂志评选的“最佳书籍”奖!
“这本书可算一个完美的典型。把它放到自己的书架上绝对不会后悔。关于 IO
数据流的那部分内容包含了迄今为止我看过的最全面、 最容易理解的文字。 ” (Al
Stevens, 《道伯博士》杂志投稿编辑)
“Eckel 的书是唯一一本清楚解释了面向对象程序设计基础问题的书。这本书也
是 C++的一本出色教材。 ” (Andrew Binstock, 《Unix Review》编辑) ”
“Bruce 用他对 C++深刻的洞察力震惊了我们, 《Thinking in C++》无疑是各种
伟大思想的出色组合。如果想得到各种困难的 C++问题的答案,请购买这本杰出
的参考书” (Gary Entsminger, 《对象之道》的作者)
“ 《Thinking in C++》非常耐心和有技巧地讲述了关于 C++的各种问题,包括如
何使用内联、索引、运算符过载以及动态对象。另外还包括一些高级主题,比如
模板的正确使用、 违例和多重继承等。 所有这些都精巧地编织在一起, 成为 Eckel
独特的对象和程序设计思想。所有 C++开发者的书架上都应摆上这本书。如果你
正在用 C++搞正式开发,这本书绝对有借鉴价值。 ” (Richard Hale Shaw, 《PC
Magazine》投稿编辑) 。
1
第1 章 对象入门
“为什么面向对象的编程会在软件开发领域造成如此震憾的影响?”
面向对象编程(OOP)具有多方面的吸引力。对管理人员,它实现了更快和
更廉价的开发与维护过程。对分析与设计人员,建模处理变得更加简单,能生成
清晰、易于维护的设计方案。对程序员,对象模型显得如此高雅和浅显。此外,面向对象工具以及库的巨大威力使编程成为一项更使人愉悦的任务。 每个人都可
从中获益,至少表面如此。
如果说它有缺点,那就是掌握它需付出的代价。思考对象的时候,需要采用
形象思维,而不是程序化的思维。与程序化设计相比,对象的设计过程更具挑战
性——特别是在尝试创建可重复使用(可再生)的对象时。过去,那些初涉面向
对象编程领域的人都必须进行一项令人痛苦的选择:
(1) 选择一种诸如Smalltalk的语言, “出师”前必须掌握一个巨型的库。
(2) 选择几乎根本没有库的C++(注释①) ,然后深入学习这种语言,直至能
自行编写对象库。
①:幸运的是,这一情况已有明显改观。现在有第三方库以及标准的C++库
供选用。
事实上,很难很好地设计出对象——从而很难设计好任何东西。因此,只有
数量相当少的“专家”能设计出最好的对象,然后让其他人享用。对于成功的
OOP语言,它们不仅集成了这种语言的语法以及一个编译程序(编译器) ,而且
还有一个成功的开发环境,其中包含设计优良、易于使用的库。所以,大多数程
序员的首要任务就是用现有的对象解决自己的应用问题。 本章的目标就是向大家
揭示出面向对象编程的概念,并证明它有多么简单。
本章将向大家解释 Java 的多项设计思想, 并从概念上解释面向对象的程序设
计。但要注意在阅读完本章后,并不能立即编写出全功能的 Java 程序。所有详
细的说明和示例会在本书的其他章节慢慢道来。
1.1 抽象的进步
所有编程语言的最终目的都是提供一种“抽象”方法。一种较有争议的说法
是:解决问题的复杂程度直接取决于抽象的种类及质量。这儿的“种类”是指准
备对什么进行“抽象”?汇编语言是对基础机器的少量抽象。后来的许多“命令
式”语言(如 FORTRAN,BASIC和 C)是对汇编语言的一种抽象。与汇编语言
相比,这些语言已有了长足的进步,但它们的抽象原理依然要求我们着重考虑计
算机的结构,而非考虑问题本身的结构。在机器模型(位于“方案空间” )与实
际解决的问题模型(位于“问题空间” )之间,程序员必须建立起一种联系。这
个过程要求人们付出较大的精力,而且由于它脱离了编程语言本身的范围,造成
程序代码很难编写,而且要花较大的代价进行维护。由此造成的副作用便是一门
完善的“编程方法”学科。
为机器建模的另一个方法是为要解决的问题制作模型。对一些早期语言来
说,如 LISP和 APL,它们的做法是“从不同的角度观察世界”——“所有问题
都归纳为列表”或“所有问题都归纳为算法” 。PROLOG则将所有问题都归纳为 2
决策链。对于这些语言,我们认为它们一部分是面向基于“强制”的编程,另一
部分则是专为处理图形符号设计的。每种方法都有自己特殊的用途,适合解决某
一类的问题。但只要超出了它们力所能及的范围,就会显得非常笨拙。
面向对象的程序设计在此基础上则跨出了一大步,程序员可利用一些工具表
达问题空间内的元素。 由于这种表达非常普遍, 所以不必受限于特定类型的问题。
我们将问题空间中的元素以及它们在方案空间的表示物称作“对象” (Object) 。
当然,还有一些在问题空间没有对应体的其他对象。通过添加新的对象类型,程
序可进行灵活的调整,以便与特定的问题配合。所以在阅读方案的描述代码时,会读到对问题进行表达的话语。 与我们以前见过的相比, 这无疑是一种更加灵活、更加强大的语言抽象方法。总之,OOP 允许我们根据问题来描述问题,而不是
根据方案。 然而, 仍有一个联系途径回到计算机。 每个对象都类似一台小计算机;
它们有自己的状态,而且可要求它们进行特定的操作。与现实世界的“对象”或
者“物体”相比,编程“对象”与它们也存在共通的地方:它们都有自己的特征
和行为。
Alan Kay 总结了 Smalltalk 的五大基本特征。这是第一种成功的面向对象程
序设计语言,也是 Java 的基础语言。通过这些特征,我们可理解“纯粹”的面
向对象程序设计方法是什么样的:
(1) 所有东西都是对象。可将对象想象成一种新型变量;它保存着数据,但
可要求它对自身进行操作。理论上讲,可从要解决的问题身上提出所有概念性的
组件,然后在程序中将其表达为一个对象。
(2) 程序是一大堆对象的组合;通过消息传递,各对象知道自己该做些什么。
为了向对象发出请求,需向那个对象“发送一条消息” 。更具体地讲,可将消息
想象为一个调用请求,它调用的是从属于目标对象的一个子例程或函数。
(3) 每个对象都有自己的存储空间,可容纳其他对象。或者说,通过封装现
有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可
达到任意高的复杂程度。
(4) 每个对象都有一种类型。根据语法,每个对象都是某个“类”的一个“实
例” 。其中, “类” (Class)是“类型” (Type)的同义词。一个类最重要的特征
就是“能将什么消息发给它?” 。
(5) 同一类所有对象都能接收相同的消息。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆” (Circle)的一个对象也属于类型为“形状”
(Shape)的一个对象,所以一个圆完全能接收形状消息。这意味着可让程序代
码统一指挥“形状” ,令其自动控制所有符合“形状”描述的对象,其中自然包
括“圆” 。这一特性称为对象的“可替换性” ,是 OOP 最重要的概念之一。
一些语言设计者认为面向对象的程序设计本身并不足以方便解决所有形式
的程序问题,提倡将不同的方法组合成“多形程序设计语言” (注释②) 。
②:参见 Timothy Budd 编著的《Multiparadigm Programming in Leda》 ,Addison-Wesley 1995 年出版。
1.2 对象的接口 3
亚里士多德或许是认真研究“类型”概念的第一人,他曾谈及“鱼类和鸟类”
的问题。 在世界首例面向对象语言Simula-67中, 第一次用到了这样的一个概念:
所有对象——尽管各有特色——都属于某一系列对象的一部分,这些对象具
有通用的特征和行为。在 Simula-67 中,首次用到了 class 这个关键字,它为程
序引入了一个全新的类型(clas 和type 通常可互换使用;注释③) 。
③:有些人进行了进一步的区分,他们强调“类型”决定了接口,而“类”
是那个接口的一种特殊实现方式。
Simula 是一个很好的例子。正如这个名字所暗示的,它的作用是“模拟”
(Simulate)象“银行出纳员”这样的经典问题。在这个例子里,我们有一系列
出纳员、客户、帐号以及交易等。每类成员(元素)都具有一些通用的特征:每
个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个
成员都有自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以
在计算机程序中,能用独一无二的实体分别表示出纳员、客户、帐号以及交易。
这个实体便是“对象” ,而且每个对象都隶属一个特定的“类” ,那个类具有自己
的通用特征与行为。
因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数
据“类型” (Type) ,但几乎所有面向对象的程序设计语言都采用了“class”关键
字。当您看到“type”这个字的时候,请同时想到“class”;反之亦然。
建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决
问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临
的最大一项挑战性就是:如何在“问题空间” (问题实际存在的地方)的元素与
“方案空间” (对实际问题进行建模的地方,如计算机)的元素之间建立理想的
“一对一”对应或映射关系。
如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其做一些实际的事情,比如完成一次交易、在屏幕上画一些东西或者打开一个
开关等等。 每个对象仅能接受特定的请求。 我们向对象发出的请求是通过它的 “接
口” (Interface)定义的,对象的“类型”或“类”则规定了它的接口形式。 “类
型”与“接口”的等价或对应关系是面向对象程序设计的基础。
下面让我们以电灯泡为例:
29 页图
Type name
Interface
Light lt = new Light;
lt.on;
在这个例子中,类型类的名称是 Light,可向 Light对象发出的请求包括包
Light
On
Off
Brighten
Dim 4
括打开(on) 、关闭(off) 、变得更明亮(brighten)或者变得更暗淡(dim) 。通
过简单地声明一个名字(lt) ,我们为 Light 对象创建了一个“句柄” 。然后用new
关键字新建类型为Light 的一个对象。再用等号将其赋给句柄。为了向对象发送
一条消息,我们列出句柄名(lt) ,再用一个句点符号(.)把它同消息名称(on)
连接起来。从中可以看出,使用一些预先定义好的类时,我们在程序里采用的代
码是非常简单和直观的。
1.3 实现方案的隐藏
为方便后面的讨论,让我们先对这一领域的从业人员作一下分类。从根本上
说, 大致有两方面的人员涉足面向对象的编程: “类创建者” (创建新数据类型的
人) 以及 “客户程序员” (在自己的应用程序中采用现成数据类型的人; 注释④) 。
对客户程序员来讲,最主要的目标就是收集一个充斥着各种类的编程“工具箱” ,以便快速开发符合自己要求的应用。而对类创建者来说,他们的目标则是从头构
建一个类,只向客户程序员开放有必要开放的东西(接口) ,其他所有细节都隐
藏起来。为什么要这样做?隐藏之后,客户程序员就不能接触和改变那些细节,所以原创者不用担心自己的作品会受到非法修改, 可确保它们不会对其他人造成
影响。
④:感谢我的朋友 Scott Meyers,是他帮我起了这个名字。
“接口” (Interface)规定了可对一个特定的对象发出哪些请求。然而,必须
在某个地方存在着一些代码,以便满足这些请求。这些代码与那些隐藏起来的数
据便叫作“隐藏的实现” 。站在程式化程序编写(Procedural Programming)的角
度,整个问题并不显得复杂。一种类型含有与每种可能的请求关联起来的函数。
一旦向对象发出一个特定的请求,就会调用那个函数。我们通常将这个过程总结
为向对象“发送一条消息” (提出一个请求) 。对象的职责就是决定如何对这条消
息作出反应(执行相应的代码) 。
对于任何关系,重要一点是让牵连到的所有成员都遵守相同的规则。创建一
个库时,相当于同客户程序员建立了一种关系。对方也是程序员,但他们的目标
是组合出一个特定的应用(程序) ,或者用您的库构建一个更大的库。
若任何人都能使用一个类的所有成员,那么客户程序员可对那个类做任何事
情,没有办法强制他们遵守任何约束。即便非常不愿客户程序员直接操作类内包
含的一些成员,但倘若未进行访问控制,就没有办法阻止这一情况的发生——所
有东西都会暴露无遗。
有两方面的原因促使我们控制对成员的访问。第一个原因是防止程序员接触
他们不该接触的东西——通常是内部数据类型的设计思想。 若只是为了解决特定
的问题,用户只需操作接口即可,毋需明白这些信息。我们向用户提供的实际是
一种服务, 因为他们很容易就可看出哪些对自己非常重要, 以及哪些可忽略不计。
进行访问控制的第二个原因是允许库设计人员修改内部结构,不用担心它会
对客户程序员造成什么影响。例如,我们最开始可能设计了一个形式简单的类,以便简化开发。以后又决定进行改写,使其更快地运行。若接口与实现方法早已
隔离开, 并分别受到保护, 就可放心做到这一点, 只要求用户重新链接一下即可。
Java 采用三个显式(明确)关键字以及一个隐式(暗示)关键字来设置类边 5
界:public,private,protected以及暗示性的friendly。若未明确指定其他关键字,则默认为后者。这些关键字的使用和含义都是相当直观的,它们决定了谁能使用
后续的定义内容。 “public”(公共)意味着后续的定义任何人均可使用。而在另
一方面, “private”(私有)意味着除您自己、类型的创建者以及那个类型的内部
函数成员,其他任何人都不能访问后续的定义信息。private 在您与客户程序员之
间竖起了一堵墙。若有人试图访问私有成员,就会得到一个编译期错误。
“friendly”(友好的)涉及“包装”或“封装” (Package)的概念——即Java用
来构建库的方法。若某样东西是“友好的” ,意味着它只能在这个包装的范围内
使用(所以这一访问级别有时也叫作“包装访问” ) 。 “protected”(受保护的)与
“private”相似,只是一个继承的类可访问受保护的成员,但不能访问私有成员。
继承的问题不久就要谈到。
1.4 方案的重复使用
创建并测试好一个类后,它应(从理想的角度)代表一个有用的代码单位。
但并不象许多人希望的那样,这种重复使用的能力并不容易实现;它要求较多的
经验以及洞察力,这样才能设计出一个好的方案,才有可能重复使用。
许多人认为代码或设计方案的重复使用是面向对象的程序设计提供的最伟
大的一种杠杆。
为重复使用一个类,最简单的办法是仅直接使用那个类的对象。但同时也能
将那个类的一个对象置入一个新类。我们把这叫作“创建一个成员对象” 。新类
可由任意数量和类型的其他对象构成。 无论如何, 只要新类达到了设计要求即可。
这个概念叫作“组织”——在现有类的基础上组织一个新类。有时,我们也将组
织称作“包含”关系,比如“一辆车包含了一个变速箱” 。
对象的组织具有极大的灵活性。新类的“成员对象”通常设为“私有”
(Private) ,使用这个类的客户程序员不能访问它们。这样一来,我们可在不干
扰客户代码的前提下,从容地修改那些成员。也可以在“运行期”更改成员,这
进一步增大了灵活性。后面要讲到的“继承”并不具备这种灵活性,因为编译器
必须对通过继承创建的类加以限制。
由于继承的重要性,所以在面向对象的程序设计中,它经常被重点强调。作
为新加入这一领域的程序员,或许早已先入为主地认为“继承应当随处可见” 。
沿这种思路产生的设计将是非常笨拙的,会大大增加程序的复杂程度。相反,新
建类的时候,首先应考虑“组织”对象;这样做显得更加简单和灵活。利用对象
的组织,我们的设计可保持清爽。一旦需要用到继承,就会明显意识到这一点。
1.5 继承:重新使用接口
就其本身来说,对象的概念可为我们带来极大的便利。它在概念上允许我们
将各式各样数据和功能封装到一起。这样便可恰当表达“问题空间”的概念,不
用刻意遵照基础机器的表达方式。在程序设计语言中,这些概念则反映为具体的
数据类型(使用class关键字) 。
我们费尽心思做出一种数据类型后,假如不得不又新建一种类型,令其实现
大致相同的功能, 那会是一件非常令人灰心的事情。 但若能利用现成的数据类型,对其进行“克隆” ,再根据情况进行添加和修改,情况就显得理想多了。 “继承”
正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原
始类(正式名称叫作基础类、超类或父类)发生了变化,修改过的“克隆”类(正 6
式名称叫作继承类或者子类)也会反映出这种变化。在 Java 语言中,继承是通
过 extends关键字实现的
使用继承时,相当于创建了一个新类。这个新类不仅包含了现有类型的所有
成员(尽管private成员被隐藏起来,且不能访问) ,但更重要的是,它复制了基
础类的接口。也就是说,可向基础类的对象发送的所有消息亦可原样发给衍生类
的对象。根据可以发送的消息,我们能知道类的类型。这意味着衍生类具有与基
础类相同的类型!为真正理解面向对象程序设计的含义,首先必须认识到这种类
型的等价关系。
由于基础类和衍生类具有相同的接口,所以那个接口必须进行特殊的设计。
也就是说,对象接收到一条特定的消息后,必须有一个“方法”能够执行。若只
是简单地继承一个类,并不做其他任何事情,来自基础类接口的方法就会直接照
搬到衍生类。这意味着衍生类的对象不仅有相同的类型,也有同样的行为,这一
后果通常是我们不愿见到的。
有两种做法可将新得的衍生类与原来的基础类区分开。第一种做法十分简
单:为衍生类添加新函数(功能) 。这些新函数并非基础类接口的一部分。进行
这种处理时,一般都是意识到基础类不能满足我们的要求,所以需要添加更多的
函数。这是一种最简单、最基本的继承用法,大多数时候都可完美地解决我们的
问题。然而,事先还是要仔细调查自己的基础类是否真的需要这些额外的函数。
1.5.1 改善基础类
尽管 extends 关键字暗示着我们要为接口“扩展”新功能,但实情并非肯定
如此。为区分我们的新类,第二个办法是改变基础类一个现有函数的行为。我们
将其称作“改善”那个函数。
为改善一个函数, 只需为衍生类的函数建立一个新定义即可。 我们的目标是:
“尽管使用的函数接口未变,但它的新版本具有不同的表现” 。
1.5.2 等价与类似关系
针对继承可能会产生这样的一个争论:继承只能改善原基础类的函数吗?若
答案是肯定的,则衍生类型就是与基础类完全相同的类型,因为都拥有完全相同
的接口。这样造成的结果就是:我们完全能够将衍生类的一个对象换成基础类的
一个对象!可将其想象成一种“纯替换” 。在某种意义上,这是进行继承的一种
理想方式。此时,我们通常认为基础类和衍生类之间存在一种“等价”关系——
因为我们可以理直气壮地说: “圆就是一种几何形状” 。为了对继承进行测试,一
个办法就是看看自己是否能把它们套入这种“等价”关系中,看看是否有意义。
但在许多时候,我们必须为衍生类型加入新的接口元素。所以不仅扩展了接
口,也创建了一种新类型。这种新类型仍可替换成基础类型,但这种替换并不是
完美的,因为不可在基础类里访问新函数。我们将其称作“类似”关系;新类型
拥有旧类型的接口,但也包含了其他函数,所以不能说它们是完全等价的。举个
例子来说,让我们考虑一下制冷机的情况。假定我们的房间连好了用于制冷的各
种控制器;也就是说,我们已拥有必要的“接口”来控制制冷。现在假设机器出
了故障,我们把它换成一台新型的冷、热两用空调,冬天和夏天均可使用。冷、热空调“类似”制冷机,但能做更多的事情。由于我们的房间只安装了控制制冷
的设备,所以它们只限于同新机器的制冷部分打交道。新机器的接口已得到了扩
展,但现有的系统并不知道除原始接口以外的任何东西。 7
认识了等价与类似的区别后,再进行替换时就会有把握得多。尽管大多数时
候“纯替换”已经足够,但您会发现在某些情况下,仍然有明显的理由需要在衍
生类的基础上增添新功能。通过前面对这两种情况的讨论,相信大家已心中有数
该如何做。
1.6 多形对象的互换使用
通常, 继承最终会以创建一系列类收场, 所有类都建立在统一的接口基础上。
我们用一幅颠倒的树形图来阐明这一点(注释⑤) :
⑤:这儿采用了“统一记号法” ,本书将主要采用这种方法。
35 页图
对这样的一系列类,我们要进行的一项重要处理就是将衍生类的对象当作基
础类的一个对象对待。这一点是非常重要的,因为它意味着我们只需编写单一的
代码,令其忽略类型的特定细节,只与基础类打交道。这样一来,那些代码就可
与类型信息分开。所以更易编写,也更易理解。此外,若通过继承增添了一种新
类型,如“三角形” ,那么我们为“几何形状”新类型编写的代码会象在旧类型
里一样良好地工作。所以说程序具备了“扩展能力” ,具有“扩展性” 。
以上面的例子为基础,假设我们用Java 写了这样一个函数:
35-36 页程序
void doStuff(Shape s) {
s.erase;
...
s.draw;
}
Circle c = new Circle;
Triangle t = new Triangle;
Line l = new Line;
doStuff(c);
doStuff(t);
Shape
Draw
Clear
SetColor
GetColor
Move
Circle Square Trangle 8
doStuff(l);
这个函数可与任何“几何形状” (Shape)通信,所以完全独立于它要描绘
(draw)和删除(erase)的任何特定类型的对象。如果我们在其他一些程序里使
用 doStuff函数:
36页上程序
void doStuff(Shape s) {
s.erase;
...
s.draw;
}
那么对doStuff的调用会自动良好地工作,无论对象的具体类型是什么。
这实际是一个非常有用的编程技巧。请考虑下面这行代码:
doStuff(c);
此时,一个 Circle(圆)句柄传递给一个本来期待 Shape(形状)句柄的函
数。由于圆是一种几何形状,所以doStuff能正确地进行处理。也就是说,凡是
doStuff能发给一个 Shape 的消息,Circle 也能接收。所以这样做是安全的,不
会造成错误。
我们将这种把衍生类型当作它的基本类型处理的过程叫作“Upcasting”(上
溯造型) 。其中, “cast”(造型)是指根据一个现成的模型创建;而“Up”(向上)
表明继承的方向是从 “上面” 来的——即基础类位于顶部, 而衍生类在下方展开。
所以,根据基础类进行造型就是一个从上面继承的过程,即“Upcasting”。
在面向对象的程序里,通常都要用到上溯造型技术。这是避免去调查准确类
型的一个好办法。请看看 doStuff里的代码:
s.erase;
...
s.draw;
注意它并未这样表达: “如果你是一个 Circle,就这样做;如果你是一个
Square,就那样做;等等” 。若那样编写代码,就需检查一个 Shape 所有可能的
类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的Shape
类型后,都要相应地进行修改。在这儿,我们只需说: “你是一种几何形状,我
知道你能将自己删掉,即erase;请自己采取那个行动,并自己去控制所有的细
节吧。 ”
1.6.1 动态绑定
在doStuff的代码里,最让人吃惊的是尽管我们没作出任何特殊指示,采取
的操作也是完全正确和恰当的。我们知道,为Circle调用 draw时执行的代码与
为一个 Square 或 Line 调用 draw时执行的代码是不同的。但在将 draw消息发
给一个匿名Shape 时,根据 Shape 句柄当时连接的实际类型,会相应地采取正确
的操作。这当然令人惊讶,因为当 Java 编译器为 doStuff编译代码时,它并不
知道自己要操作的准确类型是什么。 尽管我们确实可以保证最终会为 Shape 调用 9
erase,为 Shape 调用 draw,但并不能保证为特定的 Circle,Square 或者 Line
调用什么。然而最后采取的操作同样是正确的,这是怎么做到的呢?
将一条消息发给对象时,如果并不知道对方的具体类型是什么,但采取的行
动同样是正确的,这种情况就叫作“多形性” (Polymorphism) 。对面向对象的程
序设计语言来说,它们用以实现多形性的方法叫作“动态绑定” 。编译器和运行
期系统会负责对所有细节的控制;我们只需知道会发生什么事情,而且更重要的
是,如何利用它帮助自己设计程序。
有些语言要求我们用一个特殊的关键字来允许动态绑定。在 C++中,这个关
键字是 virtual。在 Java 中,我们则完全不必记住添加一个关键字,因为函数的
动态绑定是自动进行的。所以在将一条消息发给对象时,我们完全可以肯定对象
会采取正确的行动,即使其中涉及上溯造型之类的处理。
1.6.2 抽象的基础类和接口
设计程序时,我们经常都希望基础类只为自己的衍生类提供一个接口。也就
是说,我们不想其他任何人实际创建基础类的一个对象,只对上溯造型成它,以
便使用它们的接口。为达到这个目的,需要把那个类变成“抽象”的——使用
abstract 关键字。若有人试图创建抽象类的一个对象,编译器就会阻止他们。这
种工具可有效强制实行一种特殊的设计。
亦可用 abstract 关键字描述一个尚未实现的方法——作为一个“根”使用,指出: “这是适用于从这个类继承的所有类型的一个接口函数,但目前尚没有对
它进行任何形式的实现。 ”抽象方法也许只能在一个抽象类里创建。继承了一个
类后,那个方法就必须实现,否则继承的类也会变成“抽象”类。通过创建一个
抽象方法,我们可以将一个方法置入接口中,不必再为那个方法提供可能毫无意
义的主体代码。
interface(接口)关键字将抽象类的概念更延伸了一步,它完全禁止了所有
的函数定义。 “接口”是一种相当有效和常用的工具。另外如果自己愿意,亦可
将多个接口都合并到一起(不能从多个普通 class 或 abstract class中继承) 。
1.7 对象的创建和存在时间
从技术角度说,OOP(面向对象程序设计)只是涉及抽象的数据类型、继承
以及多形性,但另一些问题也可能显得非常重要。本节将就这些问题进行探讨。
最重要的问题之一是对象的创建及破坏方式。对象需要的数据位于哪儿,如
何控制对象的“存在时间”呢?针对这个问题,解决的方案是各异其趣的。C++
认为程序的执行效率是最重要的一个问题,所以它允许程序员作出选择。为获得
最快的运行速度,存储以及存在时间可在编写程序时决定,只需将对象放置在堆
栈(有时也叫作自动或定域变量)或者静态存储区域即可。这样便为存储空间的
分配和释放提供了一个优先级。 某些情况下, 这种优先级的控制是非常有价值的。
然而,我们同时也牺牲了灵活性,因为在编写程序时,必须知道对象的准确的数
量、存在时间、以及类型。如果要解决的是一个较常规的问题,如计算机辅助设
计、仓储管理或者空中交通控制,这一方法就显得太局限了。
第二个方法是在一个内存池中动态创建对象,该内存池亦叫“堆”或者“内
存堆” 。若采用这种方式,除非进入运行期,否则根本不知道到底需要多少个对
象,也不知道它们的存在时间有多长,以及准确的类型是什么。这些参数都在程
序正式运行时才决定的。若需一个新对象,只需在需要它的时候在内存堆里简单 10
地创建它即可。由于存储空间的管理是运行期间动态进行的,所以在内存堆里分
配存储空间的时间比在堆栈里创建的时间长得多 (在堆栈里创建存储空间一般只
需要一个简单的指令,将堆栈指针向下或向下移动即可) 。由于动态创建方法使
对象本来就倾向于复杂, 所以查找存储空间以及释放它所需的额外开销不会为对
象的创建造成明显的影响。除此以外,更大的灵活性对于常规编程问题的解决是
至关重要的。
C++允许我们决定是在写程序时创建对象,还是在运行期间创建,这种控制
方法更加灵活。大家或许认为既然它如此灵活,那么无论如何都应在内存堆里创
建对象,而不是在堆栈中创建。但还要考虑另外一个问题,亦即对象的“存在时
间”或者“生存时间” (Lifetime) 。若在堆栈或者静态存储空间里创建一个对象,编译器会判断对象的持续时间有多长,到时会自动“破坏”或者“清除”它。程
序员可用两种方法来破坏一个对象:用程序化的方式决定何时破坏对象,或者利
用由运行环境提供的一种“垃圾收集器”特性,自动寻找那些不再使用的对象,并将其清除。当然,垃圾收集器显得方便得多,但要求所有应用程序都必须容忍
垃圾收集器的存在,并能默许随垃圾收集带来的额外开销。但这并不符合 C++
语言的设计宗旨,所以未能包括到 C++里。但 Java 确实提供了一个垃圾收集器
(Smalltalk 也有这样的设计;尽管 Delphi 默认为没有垃圾收集器,但可选择安
装;而C++亦可使用一些由其他公司开发的垃圾收集产品) 。
本节剩下的部分将讨论操纵对象时要考虑的另一些因素。
1.7.1 集合与继承器
针对一个特定问题的解决,如果事先不知道需要多少个对象,或者它们的持
续时间有多长,那么也不知道如何保存那些对象。既然如此,怎样才能知道那些
对象要求多少空间呢?事先上根本无法提前知道,除非进入运行期。
在面向对象的设计中,大多数问题的解决办法似乎都有些轻率——只是简单
地创建另一种类型的对象。 用于解决特定问题的新型对象容纳了指向其他对象的
句柄。 当然, 也可以用数组来做同样的事情, 那是大多数语言都具有的一种功能。
但不能只看到这一点。这种新对象通常叫作“集合” (亦叫作一个“容器” ,但
AWT 在不同的场合应用了这个术语,所以本书将一直沿用“集合”的称呼。在
需要的时候,集合会自动扩充自己,以便适应我们在其中置入的任何东西。所以
我们事先不必知道要在一个集合里容下多少东西。只需创建一个集合,以后的工
作让它自己负责好了。
幸运的是,设计优良的 OOP 语言都配套提供了一系列集合。在 C++中,它
们是以“标准模板库” (STL)的形式提供的。Object Pascal 用自己的“可视组件
库” (VCL)提供集合。Smalltalk提供了一套非常完整的集合。而Java 也用自己
的标准库提供了集合。在某些库中,一个常规集合便可满足人们的大多数要求;
而在另一些库中(特别是C++的库) ,则面向不同的需求提供了不同类型的集合。
例如,可以用一个矢量统一对所有元素的访问方式;一个链接列表则用于保证所
有元素的插入统一。所以我们能根据自己的需要选择适当的类型。其中包括集、队列、散列表、树、堆栈等等。
所有集合都提供了相应的读写功能。将某样东西置入集合时,采用的方式是
十分明显的。有一个叫作“推” (Push) 、 “添加” (Add)或其他类似名字的函数
用于做这件事情。但将数据从集合中取出的时候,方式却并不总是那么明显。如
果是一个数组形式的实体,比如一个矢量(Vector) ,那么也许能用索引运算符 11
或函数。但在许多情况下,这样做往往会无功而返。此外,单选定函数的功能是
非常有限的。如果想对集合中的一系列元素进行操纵或比较,而不是仅仅面向一
个,这时又该怎么办呢?
办法就是使用一个“继续器” (Iterator) ,它属于一种对象,负责选择集合内
的元素,并把它们提供给继承器的用户。作为一个类,它也提供了一级抽象。利
用这一级抽象,可将集合细节与用于访问那个集合的代码隔离开。通过继承器的
作用,集合被抽象成一个简单的序列。继承器允许我们遍历那个序列,同时毋需
关心基础结构是什么——换言之, 不管它是一个矢量、 一个链接列表、 一个堆栈,还是其他什么东西。这样一来,我们就可以灵活地改变基础数据,不会对程序里
的代码造成干扰。Java 最开始(在 1.0 和 1.1 版中)提供的是一个标准继承器,名为 Enumeration(枚举) ,为它的所有集合类提供服务。Java 1.2新增一个更复
杂的集合库, 其中包含了一个名为Iterator的继承器, 可以做比老式的Enumeration
更多的事情。
从设计角度出发,我们需要的是一个全功能的序列。通过对它的操纵,应该
能解决自己的问题。如果一种类型的序列即可满足我们的所有要求,那么完全没
有必要再换用不同的类型。 有两方面的原因促使我们需要对集合作出选择。 首先,集合提供了不同的接口类型以及外部行为。堆栈的接口与行为与队列的不同,而
队列的接口与行为又与一个集(Set)或列表的不同。利用这个特征,我们解决
问题时便有更大的灵活性。
其次,不同的集合在进行特定操作时往往有不同的效率。最好的例子便是矢
量(Vector)和列表(List)的区别。它们都属于简单的序列,拥有完全一致的
接口和外部行为。但在执行一些特定的任务时,需要的开销却是完全不同的。对
矢量内的元素进行的随机访问(存取)是一种常时操作;无论我们选择的选择是
什么,需要的时间量都是相同的。但在一个链接列表中,若想到处移动,并随机
挑选一个元素,就需付出“惨重”的代价。而且假设某个元素位于列表较远的地
方,找到它所需的时间也会长许多。但在另一方面,如果想在序列中部插入一个
元素,用列表就比用矢量划算得多。这些以及其他操作都有不同的执行效率,具
体取决于序列的基础结构是什么。在设计阶段,我们可以先从一个列表开始。最
后调整性能的时候,再根据情况把它换成矢量。由于抽象是通过继承器进行的,所以能在两者方便地切换,对代码的影响则显得微不足道。
最后,记住集合只是一个用来放置对象的储藏所。如果那个储藏所能满足我
们的所有需要,就完全没必要关心它具体是如何实现的(这是大多数类型对象的
一个基本概念) 。 如果在一个编程环境中工作, 它由于其他因素 (比如在 Windows
下运行,或者由垃圾收集器带来了开销)产生了内在的开销,那么矢量和链接列
表之间在系统开销上的差异就或许不是一个大问题。 我们可能只需要一种类型的
序列。甚至可以想象有一个“完美”的集合抽象,它能根据自己的使用方式自动
改变基层的实现方式。
1.7.2 单根结构
在面向对象的程序设计中,由于C++的引入而显得尤为突出的一个问题是:
所有类最终是否都应从单独一个基础类继承。在 Java 中(与其他几乎所有 OOP
语言一样) ,对这个问题的答案都是肯定的,而且这个终级基础类的名字很简单,就是一个“Object”。这种“单根结构”具有许多方面的优点。
单根结构中的所有对象都有一个通用接口,所以它们最终都属于相同的类 12
型。另一种方案(就象 C++那样)是我们不能保证所有东西都属于相同的基本类
型。从向后兼容的角度看,这一方案可与 C 模型更好地配合,而且可以认为它
的限制更少一些。但假期我们想进行纯粹的面向对象编程,那么必须构建自己的
结构,以期获得与内建到其他 OOP 语言里的同样的便利。需添加我们要用到的
各种新类库,还要使用另一些不兼容的接口。理所当然地,这也需要付出额外的
精力使新接口与自己的设计方案配合(可能还需要多重继承) 。为得到 C++额外
的“灵活性” ,付出这样的代价值得吗?当然,如果真的需要——如果早已是 C
专家,如果对 C 有难舍的情结——那么就真的很值得。但假如你是一名新手,首次接触这类设计,象 Java 那样的替换方案也许会更省事一些。
单根结构中的所有对象(比如所有Java 对象)都可以保证拥有一些特定的功
能。在自己的系统中,我们知道对每个对象都能进行一些基本操作。一个单根结
构,加上所有对象都在内存堆中创建,可以极大简化参数的传递(这在 C++里是
一个复杂的概念) 。
利用单根结构,我们可以更方便地实现一个垃圾收集器。与此有关的必要支
持可安装于基础类中,而垃圾收集器可将适当的消息发给系统内的任何对象。如
果没有这种单根结构,而且系统通过一个句柄来操纵对象,那么实现垃圾收集器
的途径会有很大的不同,而且会面临许多障碍。
由于运行期的类型信息肯定存在于所有对象中,所以永远不会遇到判断不出
一个对象的类型的情况。这对系统级的操作来说显得特别重要,比如违例控制;
而且也能在程序设计时获得更大的灵活性。
但大家也可能产生疑问,既然你把好处说得这么天花乱坠,为什么 C++没有
采用单根结构呢?事实上,这是早期在效率与控制上权衡的一种结果。单根结构
会带来程序设计上的一些限制。而且更重要的是,它加大了新程序与原有 C 代
码兼容的难度。尽管这些限制仅在特定的场合会真的造成问题,但为了获得最大
的灵活程度,C++最终决定放弃采用单根结构这一做法。而 Java 不存在上述的问
题,它是全新设计的一种语言,不必与现有的语言保持所谓的“向后兼容” 。所
以很自然地,与其他大多数面向对象的程序设计语言一样,单根结构在 Java 的
设计方案中很快就落实下来。
1.7.3 集合库与方便使用集合
由于集合是我们经常都要用到的一种工具,所以一个集合库是十分必要的,它应该可以方便地重复使用。这样一来,我们就可以方便地取用各种集合,将其
插入自己的程序。Java提供了这样的一个库,尽管它在 Java 1.0 和 1.1 中都显得
非常有限(Java 1.2 的集合库则无疑是一个杰作) 。
1. 下溯造型与模板通用性
为了使这些集合能够重复使用,或者“再生” ,Java 提供了一种通用类型,以前曾把它叫作“Object”。单根结构意味着、所有东西归根结底都是一个对象” !
所以容纳了 Object 的一个集合实际可以容纳任何东西。这使我们对它的重复使
用变得非常简便。
为使用这样的一个集合,只需添加指向它的对象句柄即可,以后可以通过句
柄重新使用对象。但由于集合只能容纳Object,所以在我们向集合里添加对象句
柄时,它会上溯造型成 Object,这样便丢失了它的身份或者标识信息。再次使用
它的时候,会得到一个Object句柄,而非指向我们早先置入的那个类型的句柄。 13
所以怎样才能归还它的本来面貌, 调用早先置入集合的那个对象的有用接口呢?
在这里,我们再次用到了造型(Cast) 。但这一次不是在分级结构中上溯造型
成一种更“通用”的类型。而是下溯造型成一种更“特殊”的类型。这种造型方
法叫作“下溯造型” (Downcasting) 。举个例子来说,我们知道在上溯造型的时
候,Circle(圆)属于Shape(几何形状)的一种类型,所以上溯造型是安全的。
但我们不知道一个Object 到底是Circle还是Shape,所以很难保证下溯造型的安
全进行,除非确切地知道自己要操作的是什么。
但这也不是绝对危险的,因为假如下溯造型成错误的东西,会得到我们称为
“违例” (Exception)的一种运行期错误。我们稍后即会对此进行解释。但在从
一个集合提取对象句柄时,必须用某种方式准确地记住它们是什么,以保证下溯
造型的正确进行。
下溯造型和运行期检查都要求花额外的时间来运行程序,而且程序员必须付
出额外的精力。既然如此,我们能不能创建一个“智能”集合,令其知道自己容
纳的类型呢?这样做可消除下溯造型的必要以及潜在的错误。答案是肯定的,我
们可以采用“参数化类型” ,它们是编译器能自动定制的类,可与特定的类型配
合。例如,通过使用一个参数化集合,编译器可对那个集合进行定制,使其只接
受 Shape,而且只提取 Shape。
参数化类型是 C++一个重要的组成部分,这部分是 C++没有单根结构的缘
故。在 C++中,用于实现参数化类型的关键字是 template(模板) 。Java 目前尚
未提供参数化类型,因为由于使用的是单根结构,所以使用它显得有些笨拙。但
这并不能保证以后的版本不会实现,因为“generic”这个词已被Java“保留到将来
实现” (在Ada 语言中, “generic”被用来实现它的模板) 。 Java 采取的这种关键字
保留机制其实经常让人摸不着头脑,很难断定以后会发生什么事情。
1.7.4 清除时的困境:由谁负责清除?
每个对象都要求资源才能“生存” ,其中最令人注目的资源是内存。如果不
再需要使用一个对象, 就必须将其清除, 以便释放这些资源, 以便其他对象使用。
如果要解决的是非常简单的问题,如何清除对象这个问题并不显得很突出:我们
创建对象,在需要的时候调用它,然后将其清除或者“破坏” 。但在另一方面,我们平时遇到的问题往往要比这复杂得多。
举个例子来说, 假设我们要设计一套系统, 用它管理一个机场的空中交通 (同
样的模型也可能适于管理一个仓库的货柜、或者一套影带出租系统、或者宠物店
的宠物房。这初看似乎十分简单:构造一个集合用来容纳飞机,然后创建一架新
飞机, 将其置入集合。 对进入空中交通管制区的所有飞机都如此处理。 至于清除,在一架飞机离开这个区域的时候把它简单地删去即可。
但事情并没有这么简单,可能还需要另一套系统来记录与飞机有关的数据。
当然,和控制器的主要功能不同,这些数据的重要性可能一开始并不显露出来。
例如,这条记录反映的可能是离开机场的所有小飞机的飞行计划。所以我们得到
了由小飞机组成的另一个集合。 一旦创建了一个飞机对象, 如果它是一架小飞机,那么也必须把它置入这个集合。然后在系统空闲时期,需对这个集合中的对象进
行一些后台处理。
问题现在显得更复杂了:如何才能知道什么时间删除对象呢?用完对象后,系统的其他某些部分可能仍然要发挥作用。同样的问题也会在其他大量场合出
现,而且在程序设计系统中(如 C++) ,在用完一个对象之后必须明确地将其删 14
除,所以问题会变得异常复杂(注释⑥) 。
⑥:注意这一点只对内存堆里创建的对象成立(用 new 命令创建的) 。但在
另一方面,对这儿描述的问题以及其他所有常见的编程问题来说,都要求对象在
内存堆里创建。
在 Java 中,垃圾收集器在设计时已考虑到了内存的释放问题(尽管这并不包
括清除一个对象涉及到的其他方面) 。垃圾收集器“知道”一个对象在什么时候
不再使用,然后会自动释放那个对象占据的内存空间。采用这种方式,另外加上
所有对象都从单个根类 Object 继承的事实,而且由于我们只能在内存堆中以一
种方式创建对象,所以 Java 的编程要比 C++的编程简单得多。我们只需要作出
少量的抉择,即可克服原先存在的大量障碍。
1. 垃圾收集器对效率及灵活性的影响
既然这是如此好的一种手段,为什么在C++里没有得到充分的发挥呢?我们
当然要为这种编程的方便性付出一定的代价,代价就是运行期的开销。正如早先
提到的那样,在 C++中,我们可在堆栈中创建对象。在这种情况下,对象会得以
自动清除(但不具有在运行期间随心所欲创建对象的灵活性) 。在堆栈中创建对
象是为对象分配存储空间最有效的一种方式, 也是释放那些空间最有效的一种方
式。在内存堆(Heap)中创建对象可能要付出昂贵得多的代价。如果总是从同
一个基础类继承,并使所有函数调用都具有“同质多形”特征,那么也不可避免
地需要付出一定的代价。但垃圾收集器是一种特殊的问题,因为我们永远不能确
定它什么时候启动或者要花多长的时间。这意味着在 Java 程序执行期间,存在
着一种不连贯的因素。所以在某些特殊的场合,我们必须避免用它——比如在一
个程序的执行必须保持稳定、连贯的时候(通常把它们叫作“实时程序” ,尽管
并不是所有实时编程问题都要这方面的要求——注释⑦) 。
⑦:根据本书一些技术性读者的反馈,有一个现成的实时 Java 系统
(www.newmonics.com)确实能够保证垃圾收集器的效能。
C++语言的设计者曾经向 C 程序员发出请求(而且做得非常成功) ,不要希
望在可以使用 C 的任何地方,向语言里加入可能对 C++的速度或使用造成影响
的任何特性。 这个目的达到了, 但代价就是C++的编程不可避免地复杂起来。 Java
比 C++简单,但付出的代价是效率以及一定程度的灵活性。但对大多数程序设计
问题来说,Java 无疑都应是我们的首选。
1.8 违例控制:解决错误
从最古老的程序设计语言开始,错误控制一直都是设计者们需要解决的一个
大问题。由于很难设计出一套完美的错误控制方案,许多语言干脆将问题简单地
忽略掉,将其转嫁给库设计人员。对大多数错误控制方案来说,最主要的一个问
题是它们严重依赖程序员的警觉性,而不是依赖语言本身的强制标准。如果程序
员不够警惕——若比较匆忙, 这几乎是肯定会发生的——程序所依赖的错误控制
方案便会失效。
“违例控制”将错误控制方案内置到程序设计语言中,有时甚至内建到操作 15
系统内。这里的“违例” (Exception)属于一个特殊的对象,它会从产生错误的
地方“扔”或“掷”出来。随后,这个违例会被设计用于控制特定类型错误的“违
例控制器”捕获。在情况变得不对劲的时候,可能有几个违例控制器并行捕获对
应的违例对象。由于采用的是独立的执行路径,所以不会干扰我们的常规执行代
码。这样便使代码的编写变得更加简单,因为不必经常性强制检查代码。除此以
外, “掷”出的一个违例不同于从函数返回的错误值,也不同于由函数设置的一
个标志。那些错误值或标志的作用是指示一个错误状态,是可以忽略的。但违例
不能被忽略,所以肯定能在某个地方得到处置。最后,利用违例能够可靠地从一
个糟糕的环境中恢复。此时一般不需要退出,我们可以采取某些处理,恢复程序
的正常执行。显然,这样编制出来的程序显得更加可靠。
Java 的违例控制机制与大多数程序设计语言都有所不同。因为在Java 中,违
例控制模块是从一开始就封装好的,所以必须使用它!如果没有自己写一些代码
来正确地控制违例,就会得到一条编译期出错提示。这样可保证程序的连贯性,使错误控制变得更加容易。
注意违例控制并不属于一种面向对象的特性,尽管在面向对象的程序设计语
言中,违例通常是用一个对象表示的。早在面向对象语言问世以前,违例控制就
已经存在了。
1.9 多线程
在计算机编程中,一个基本的概念就是同时对多个任务加以控制。许多程序
设计问题都要求程序能够停下手头的工作,改为处理其他一些问题,再返回主进
程。可以通过多种途径达到这个目的。最开始的时候,那些拥有机器低级知识的
程序员编写一些“中断服务例程” ,主进程的暂停是通过硬件级的中断实现的。
尽管这是一种有用的方法,但编出的程序很难移植,由此造成了另一类的代价高
昂问题。
有些时候,中断对那些实时性很强的任务来说是很有必要的。但还存在其他
许多问题,它们只要求将问题划分进入独立运行的程序片断中,使整个程序能更
迅速地响应用户的请求。在一个程序中,这些独立运行的片断叫作“线程”
(Thread) ,利用它编程的概念就叫作“多线程处理” 。多线程处理一个常见的例
子就是用户界面。利用线程,用户可按下一个按钮,然后程序会立即作出响应,而不是让用户等待程序完成了当前任务以后才开始响应。
最开始,线程只是用于分配单个处理器的处理时间的一种工具。但假如操作
系统本身支持多个处理器,那么每个线程都可分配给一个不同的处理器,真正进
入“并行运算”状态。从程序设计语言的角度看,多线程操作最有价值的特性之
一就是程序员不必关心到底使用了多少个处理器。 程序在逻辑意义上被分割为数
个线程;假如机器本身安装了多个处理器,那么程序会运行得更快,毋需作出任
何特殊的调校。
根据前面的论述,大家可能感觉线程处理非常简单。但必须注意一个问题:
共享资源!如果有多个线程同时运行,而且它们试图访问相同的资源,就会遇到
一个问题。举个例子来说,两个进程不能将信息同时发送给一台打印机。为解决
这个问题,对那些可共享的资源来说(比如打印机) ,它们在使用期间必须进入
锁定状态。所以一个线程可将资源锁定,在完成了它的任务后,再解开(释放)
这个锁,使其他线程可以接着使用同样的资源。
Java 的多线程机制已内建到语言中,这使一个可能较复杂的问题变得简单起 16
来。对多线程处理的支持是在对象这一级支持的,所以一个执行线程可表达为一
个对象。Java 也提供了有限的资源锁定方案。它能锁定任何对象占用的内存(内
存实际是多种共享资源的一种) ,所以同一时间只能有一个线程使用特定的内存
空间。为达到这个目的,需要使用 synchronized关键字。其他类型的资源必须由
程序员明确锁定,这通常要求程序员创建一个对象,用它代表一把锁,所有线程
在访问那个资源时都必须检查这把锁。
1.10 永久性
创建一个对象后,只要我们需要,它就会一直存在下去。但在程序结束运行
时,对象的“生存期”也会宣告结束。尽管这一现象表面上非常合理,但深入追
究就会发现,假如在程序停止运行以后,对象也能继续存在,并能保留它的全部
信息,那么在某些情况下将是一件非常有价值的事情。下次启动程序时,对象仍
然在那里,里面保留的信息仍然是程序上一次运行时的那些信息。当然,可以将
信息写入一个文件或者数据库,从而达到相同的效果。但尽管可将所有东西都看
作一个对象,如果能将对象声明成“永久性” ,并令其为我们照看其他所有细节,无疑也是一件相当方便的事情。
Java 1.1 提供了对“有限永久性”的支持,这意味着我们可将对象简单地保
存到磁盘上,以后任何时间都可取回。之所以称它为“有限”的,是由于我们仍
然需要明确发出调用,进行对象的保存和取回工作。这些工作不能自动进行。在
Java 未来的版本中,对“永久性”的支持有望更加全面。
1.11 Java 和因特网
既然 Java不过另一种类型的程序设计语言, 大家可能会奇怪它为什么值得如
此重视, 为什么还有这么多的人认为它是计算机程序设计的一个里程碑呢?如果
您来自一个传统的程序设计背景,那么答案在刚开始的时候并不是很明显。Java
除了可解决传统的程序设计问题以外,还能解决 World Wide Web(万维网)上
的编程问题。
1.11.1 什么是 Web?
Web这个词刚开始显得有些泛泛,似乎“冲浪” 、 “网上存在”以及“主页”
等等都和它拉上了一些关系。甚至还有一种“Internet 综合症”的说法,对许多
人狂热的上网行为提出了质疑。我们在这里有必要作一些深入的探讨,但在这之
前,必须理解客户机服务器系统的概念,这是充斥着许多令人迷惑的问题的又
一个计算领域。
1. 客户机服务器计算
客户机服务器系统的基本思想是我们能在一个统一的地方集中存放信息
资源。一般将数据集中保存在某个数据库中,根据其他人或者机器的请求将信息
投递给对方。客户机服务器概述的一个关键在于信息是“集中存放”的。所以
我们能方便地更改信息,然后将修改过的信息发放给信息的消费者。将各种元素
集中到一起,信息仓库、用于投递信息的软件以及信息及软件所在的那台机器,它们联合起来便叫作“服务器” (Server) 。而对那些驻留在远程机器上的软件,它们需要与服务器通信,取回信息,进行适当的处理,然后在远程机器上显示出
来,这些就叫作“客户”(Client) 。 17
这样看来,客户机服务器的基本概念并不复杂。这里要注意的一个主要问
题是单个服务器需要同时向多个客户提供服务。在这一机制中,通常少不了一套
数据库管理系统,使设计人员能将数据布局封装到表格中,以获得最优的使用。
除此以外,系统经常允许客户将新信息插入一个服务器。这意味着必须确保客户
的新数据不会与其他客户的新数据冲突, 或者说需要保证那些数据在加入数据库
的时候不会丢失(用数据库的术语来说,这叫作“事务处理” ) 。客户软件发生了
改变之后,它们必须在客户机器上构建、调试以及安装。所有这些会使问题变得
比我们一般想象的复杂得多。另外,对多种类型的计算机和操作系统的支持也是
一个大问题。最后,性能的问题显得尤为重要:可能会有数百个客户同时向服务
器发出请求。所以任何微小的延误都是不能忽视的。为尽可能缓解潜伏的问题,程序员需要谨慎地分散任务的处理负担。 一般可以考虑让客户机负担部分处理任
务, 但有时亦可分派给服务器所在地的其他机器, 那些机器亦叫作 “中间件” (中
间件也用于改进对系统的维护) 。
所以在具体实现的时候,其他人发布信息这样一个简单的概念可能变得异常
复杂。有时甚至会使人产生完全无从着手的感觉。客户机服务器的概念在这时
就可以大显身手了。事实上,大约有一半的程序设计活动都可以采用客户机服
务器的结构。这种系统可负责从处理订单及信用卡交易,一直到发布各类数据的
方方面面的任务——股票市场、科学研究、政府运作等等。在过去,我们一般为
单独的问题采取单独的解决方案;每次都要设计一套新方案。这些方案无论创建
还是使用都比较困难,用户每次都要学习和适应新界面。客户机服务器问题需
要从根本上加以变革!
2. Web是一个巨大的服务器
Web 实际就是一套规模巨大的客户机服务器系统。但它的情况要复杂一
些,因为所有服务器和客户都同时存在于单个网络上面。但我们没必要了解更进
一步的细节,因为唯一要关心的就是一次建立同一个服务器的连接,并同它打交
道(即使可能要在全世界的范围内搜索正确的服务器) 。
最开始的时候, 这是一个简单的单向操作过程。 我们向一个服务器发出请求,它向我们回传一个文件,由于本机的浏览器软件(亦即“客户”或“客户程序” )
负责解释和格式化,并在我们面前的屏幕上正确地显示出来。但人们不久就不满
足于只从一个服务器传递网页。他们希望获得完全的客户机服务器能力,使客
户 (程序) 也能反馈一些信息到服务器。 比如希望对服务器上的数据库进行检索,向服务器添加新信息,或者下一份订单等等(这也提供了比以前的系统更高的安
全要求) 。在 Web的发展过程中,我们可以很清晰地看出这些令人心喜的变化。
Web浏览器的发展终于迈出了重要的一步:某个信息可在任何类型的计算机
上显示出来,毋需任何改动。然而,浏览器仍然显得很原始,在用户迅速增多的
要求面前显得有些力不从心。它们的交互能力不够强,而且对服务器和因特网都
造成了一定程度的干扰。这是由于每次采取一些要求编程的操作时,必须将信息
反馈回服务器,在服务器那一端进行处理。所以完全可能需要等待数秒乃至数分
钟的时间才会发现自己刚才拼错了一个单词。 由于浏览器只是一个纯粹的查看程
序, 所以连最简单的计算任务都不能进行 (当然在另一方面, 它也显得非常安全,因为不能在本机上面执行任何程序,避开了程序错误或者病毒的骚扰) 。
为解决这个问题,人们采取了许多不同的方法。最开始的时候,人们对图形
标准进行了改进,使浏览器能显示更好的动画和视频。为解决剩下的问题,唯一 18
的办法就是在客户端(浏览器)内运行程序。这就叫作“客户端编程” ,它是对
传统的“服务器端编程”的一个非常重要的拓展。
1.11.2 客户端编程(注释⑧)
Web最初采用的“服务器-浏览器”方案可提供交互式内容,但这种交互能
力完全由服务器提供,为服务器和因特网带来了不小的负担。服务器一般为客户
浏览器产生静态网页,由后者简单地解释并显示出来。基本 HTML 语言提供了
简单的数据收集机制:文字输入框、复选框、单选钮、列表以及下拉列表等,另
外还有一个按钮,只能由程序规定重新设置表单中的数据,以便回传给服务器。
用户提交的信息通过所有 Web 服务器均能支持的“通用网关接口” (CGI)回传
到服务器。包含在提交数据中的文字指示 CGI 该如何操作。最常见的行动是运
行位于服务器的一个程序。那个程序一般保存在一个名为“cgi-bin”的目录中(按
下 Web 页内的一个按钮时,请注意一下浏览器顶部的地址窗,经常都能发现
“cgi-bin”的字样) 。 大多数语言都可用来编制这些程序, 但其中最常见的是 Perl。
这是由于 Perl 是专为文字的处理及解释而设计的,所以能在任何服务器上安装
和使用,无论采用的处理器或操作系统是什么。
⑧:本节内容改编自某位作者的一篇文章。那篇文章最早出现在位于
www.mainspring.com的 Mainspring上。本节的采用已征得了对方的同意。
今天的许多 Web 站点都严格地建立在 CGI 的基础上,事实上几乎所有事情
都可用 CGI 做到。唯一的问题就是响应时间。CGI 程序的响应取决于需要传送
多少数据,以及服务器和因特网两方面的负担有多重(而且 CGI 程序的启动比
较慢) 。Web 的早期设计者并未预料到当初绰绰有余的带宽很快就变得不够用,这正是大量应用充斥网上造成的结果。例如,此时任何形式的动态图形显示都几
乎不能连贯地显示,因为此时必须创建一个 GIF 文件,再将图形的每种变化从
服务器传递给客户。而且大家应该对输入表单上的数据校验有着深刻的体会。原
来的方法是我们按下网页上的提交按钮(Submit) ;数据回传给服务器;服务器
启动一个 CGI 程序,检查用户输入是否有错;格式化一个 HTML 页,通知可能
遇到的错误,并将这个页回传给我们;随后必须回到原先那个表单页,再输入一
遍。这种方法不仅速度非常慢,也显得非常繁琐。
解决的办法就是客户端的程序设计。 运行 Web浏览器的大多数机器都拥有足
够强的能力,可进行其他大量工作。与此同时,原始的静态 HTML 方法仍然可
以采用,它会一直等到服务器送回下一个页。客户端编程意味着 Web 浏览器可
获得更充分的利用,并可有效改善 Web服务器的交互(互动)能力。
对客户端编程的讨论与常规编程问题的讨论并没有太大的区别。采用的参数
肯定是相同的,只是运行的平台不同:Web浏览器就象一个有限的操作系统。无
论如何,我们仍然需要编程,仍然会在客户端编程中遇到大量问题,同时也有很
多解决的方案。在本节剩下的部分里,我们将对这些问题进行一番概括,并介绍
在客户端编程中采取的对策。
1. 插件
朝客户端编程迈进的时候,最重要的一个问题就是插件的设计。利用插件,程序员可以方便地为浏览器添加新功能, 用户只需下载一些代码, 把它们 “插入” 19
浏览器的适当位置即可。这些代码的作用是告诉浏览器“从现在开始,你可以进
行这些新活动了” (仅需下载这些插入一次) 。 有些快速和功能强大的行为是通过
插件添加到浏览器的。但插件的编写并不是一件简单的任务。在我们构建一个特
定的站点时,可能并不希望涉及这方面的工作。对客户端程序设计来说,插件的
价值在于它允许专业程序员设计出一种新的语言,并将那种语言添加到浏览器,同时不必经过浏览器原创者的许可。 由此可以看出, 插件实际是浏览器的一个 “后
门” ,允许创建新的客户端程序设计语言(尽管并非所有语言都是作为插件实现
的) 。
2. 脚本编制语言
插件造成了脚本编制语言的爆炸性增长。通过这种脚本语言,可将用于自己
客户端程序的源码直接插入HTML页, 而对那种语言进行解释的插件会在HTML
页显示的时候自动激活。脚本语言一般都倾向于尽量简化,易于理解。而且由于
它们是从属于 HTML 页的一些简单正文,所以只需向服务器发出对那个页的一
次请求, 即可非常快地载入。 缺点是我们的代码全部暴露在人们面前。 另一方面,由于通常不用脚本编制语言做过份复杂的事情,所以这个问题暂且可以放在一
边。
脚本语言真正面向的是特定类型问题的解决,其中主要涉及如何创建更丰
富、更具有互动能力的图形用户界面(GUI) 。然而,脚本语言也许能解决客户
端编程中 80%的问题。你碰到的问题可能完全就在那 80%里面。而且由于脚本
编制语言的宗旨是尽可能地简化与快速,所以在考虑其他更复杂的方案之前(如
Java 及ActiveX) ,首先应想一下脚本语言是否可行。
目前讨论得最多的脚本编制语言包括 JavaScript(它与 Java 没有任何关系;
之所以叫那个名字,完全是一种市场策略) 、VBScript(同 Visual Basic很相似)
以及 TclTk(来源于流行的跨平台GUI 构造语言) 。当然还有其他许多语言,也
有许多正在开发中。
JavaScript也许是目常用的, 它得到的支持也最全面。 无论 NetscapeNavigator,Microsoft Internet Explorer,还是Opera,目前都提供了对 JavaScript 的支持。除
此以外,市面上讲述JavaScript 的书籍也要比讲述其他语言的书多得多。有些工
具还能利用JavaScript自动产生网页。 当然, 如果你已经有Visual Basic或者Tcl Tk
的深厚功底,当然用它们要简单得多,起码可以避免学习新语言的烦恼(解决
Web方面的问题就已经够让人头痛了) 。
3. Java
如果说一种脚本编制语言能解决 80%的客户端程序设计问题,那么剩下的
20%又该怎么办呢?它们属于一些高难度的问题吗?目前最流行的方案就是
Java。它不仅是一种功能强大、高度安全、可以跨平台使用以及国际通用的程序
设计语言,也是一种具有旺盛生命力的语言。对 Java 的扩展是不断进行的,提
供的语言特性和库能够很好地解决传统语言不能解决的问题,比如多线程操作、数据库访问、连网程序设计以及分布式计算等等。Java 通过“程序片” (Applet)
巧妙地解决了客户端编程的问题。
程序片(或“小应用程序” )是一种非常小的程序,只能在 Web 浏览器中运
行。作为 Web 页的一部分,程序片代码会自动下载回来(这和网页中的图片差
不多) 。激活程序片后,它会执行一个程序。程序片的一个优点体现在:通过程 20
序片,一旦用户需要客户软件,软件就可从服务器自动下载回来。它们能自动取
得客户软件的最新版本,不会出错,也没有重新安装的麻烦。由于 Java 的设计
原理,程序员只需要创建程序的一个版本,那个程序能在几乎所有计算机以及安
装了 Java 解释器的浏览器中运行。由于 Java 是一种全功能的编程语言,所以在
向服务器发出一个请求之前,我们能先在客户端做完尽可能多的工作。例如,再
也不必通过因特网传送一个请求表单, 再由服务器确定其中是否存在一个拼写或
者其他参数错误。大多数数据校验工作均可在客户端完成,没有必要坐在计算机
前面焦急地等待服务器的响应。这样一来,不仅速度和响应的灵敏度得到了极大
的提高,对网络和服务器造成的负担也可以明显减轻,这对保障因特网的畅通是
至关重要的。
与脚本程序相比,Java程序片的另一个优点是它采用编译好的形式,所以客
户端看不到源码。当然在另一方面,反编译 Java 程序片也并不是件难事,而且
代码的隐藏一般并不是个重要的问题。大家要注意另外两个重要的问题。正如本
书以前会讲到的那样,编译好的 Java 程序片可能包含了许多模块,所以要多次
“命中” (访问)服务器以便下载(在 Java 1.1 中,这个问题得到了有效的改善
——利用 Java 压缩档,即 JAR 文件——它允许设计者将所有必要的模块都封装
到一起,供用户统一下载) 。在另一方面,脚本程序是作为 Web页正文的一部分
集成到 Web 页内的。这种程序一般都非常小,可有效减少对服务器的点击数。
另一个因素是学习方面的问题。不管你平时听别人怎么说,Java 都不是一种十分
容易便可学会的语言。 如果你以前是一名 Visual Basic程序员, 那么转向VBScript
会是一种最快捷的方案。由于 VBScript 可以解决大多数典型的客户机服务器
问题,所以一旦上手,就很难下定决心再去学习Java。如果对脚本编制语言比较
熟,那么在转向Java之前,建议先熟悉一下JavaScript 或者 VBScript,因为它们
可能已经能够满足你的需要,不必经历学习 Java 的艰苦过程。
4. ActiveX
在某种程度上,Java 的一个有力竞争对手应该是微软的 ActiveX,尽管它采
用的是完全不同的一套实现机制。ActiveX最早是一种纯 Windows 的方案。经过
一家独立的专业协会的努力, ActiveX现在已具备了跨平台使用的能力。 实际上,ActiveX的意思是 “假如你的程序同它的工作环境正常连接, 它就能进入 Web 页,并在支持 ActiveX 的浏览器中运行” (IE 固化了对 ActiveX 的支持,而 Netscape
需要一个插件) 。所以,ActiveX 并没有限制我们使用一种特定的语言。比如,假设我们已经是一名有经验的 Windows 程序员,能熟练地使用象 C++、Visual
Basic 或者 BorlandDelphi 那样的语言,就能几乎不加任何学习地创建出 ActiveX
组件。事实上,ActiveX是在我们的 Web页中使用“历史遗留”代码的最佳途径。
5. 安全
自动下载和通过因特网运行程序听起来就象是一个病毒制造者的梦想。在客
户端的编程中,ActiveX带来了最让人头痛的安全问题。点击一个 Web站点的时
候,可能会随同 HTML 网页传回任何数量的东西:GIF 文件、脚本代码、编译
好的 Java代码以及 ActiveX组件。有些是无害的;GIF文件不会对我们造成任何
危害,而脚本编制语言通常在自己可做的事情上有着很大的限制。Java 也设计成
在一个安全“沙箱”里在它的程序片中运行,这样可防止操作位于沙箱以外的磁
盘或者内存区域。 21
ActiveX 是所有这些里面最让人担心的。用 ActiveX 编写程序就象编制
Windows应用程序——可以做自己想做的任何事情。 下载回一个ActiveX组件后,它完全可能对我们磁盘上的文件造成破坏。当然,对那些下载回来并不限于在
Web 浏览器内部运行的程序,它们同样也可能破坏我们的系统。从 BBS 下载回
来的病毒一直是个大问题,但因特网的速度使得这个问题变得更加复杂。
目前解决的办法是“数字签名” ,代码会得到权威机构的验证,显示出它的
作者是谁。这一机制的基础是认为病毒之所以会传播,是由于它的编制者匿名的
缘故。所以假如去掉了匿名的因素,所有设计者都不得不为它们的行为负责。这
似乎是一个很好的主意,因为它使程序显得更加正规。但我对它能消除恶意因素
持怀疑态度,因为假如一个程序便含有Bug,那么同样会造成问题。
Java 通过“沙箱”来防止这些问题的发生。Java 解释器内嵌于我们本地的
Web浏览器中,在程序片装载时会检查所有有嫌疑的指令。特别地,程序片根本
没有权力将文件写进磁盘,或者删除文件(这是病毒最喜欢做的事情之一) 。我
们通常认为程序片是安全的。 而且由于安全对于营建一套可靠的客户机服务器
系统至关重要,所以会给病毒留下漏洞的所有错误都能很快得到修复(浏览器软
件实际需要强行遵守这些安全规则; 而有些浏览器则允许我们选择不同的安全级
别,防止对系统不同程度的访问) 。
大家或许会怀疑这种限制是否会妨碍我们将文件写到本地磁盘。比如,我们
有时需要构建一个本地数据库,或将数据保存下来,以便日后离线使用。最早的
版本似乎每个人都能在线做任何敏感的事情,但这很快就变得非常不现实(尽管
低价“互联网工具”有一天可能会满足大多数用户的需要) 。解决的方案是“签
了名的程序片” ,它用公共密钥加密算法验证程序片确实来自它所声称的地方。
当然在通过验证后,签了名的一个程序片仍然可以开始清除你的磁盘。但从理论
上说,既然现在能够找到创建人“算帐” ,他们一般不会干这种蠢事。Java 1.1
为数字签名提供了一个框架,在必要时,可让一个程序片“走”到沙箱的外面来。
数字签名遗漏了一个重要的问题,那就是人们在因特网上移动的速度。如下
载回一个错误百出的程序,而它很不幸地真的干了某些蠢事,需要多久的时间才
能发觉这一点呢?这也许是几天,也可能几周之后。发现了之后,又如何追踪当
初肇事的程序呢(以及它当时的责任有多大)?
6. 因特网和内联网
Web是解决客户机服务器问题的一种常用方案,所以最好能用相同的技术
解决此类问题的一些“子集” ,特别是公司内部的传统客户机服务器问题。对
于传统的客户机服务器模式, 我们面临的问题是拥有多种不同类型的客户计算
机,而且很难安装新的客户软件。但通过 Web 浏览器和客户端编程,这两类问
题都可得到很好的解决。若一个信息网络局限于一家特定的公司,那么在将 Web
技术应用于它之后,即可称其为“内联网” (Intranet) ,以示与国际性的“因特
网” (Internet)有别。内联网提供了比因特网更大的安全级别,因为可以物理性
地控制对公司内部服务器的使用。说到培训,一般只要人们理解了浏览器的常规
概念,就可以非常轻松地掌握网页和程序片之间的差异,所以学习新型系统的开
销会大幅度减少。
安全问题将我们引入客户端编程领域一个似乎是自动形成的分支。若程序是
在因特网上运行, 由于无从知晓它会在什么平台上运行, 所以编程时要特别留意,防范可能出现的编程错误。需作一些跨平台处理,以及适当的安全防范,比如采 22
用某种脚本语言或者 Java。
但假如在内联网中运行,面临的一些制约因素就会发生变化。全部机器均为
IntelWindows 平台是件很平常的事情。在内联网中,需要对自己代码的质量负
责。而且一旦发现错误,就可以马上改正。除此以外,可能已经有了一些“历史
遗留”的代码,并用较传统的客户机服务器方式使用那些代码。但在进行升级
时,每次都要物理性地安装一道客户程序。浪费在升级安装上的时间是转移到浏
览器的一项重要原因。使用了浏览器后,升级就变得易如反掌,而且整个过程是
透明和自动进行的。如果真的是牵涉到这样的一个内联网中,最明智的方法是采
用 ActiveX,而非试图采用一种新的语言来改写程序代码。
面临客户端编程问题令人困惑的一系列解决方案时,最好的方案是先做一次
投资回报分析。请总结出问题的全部制约因素,以及什么才是最快的方案。由
于客户端程序设计仍然要编程, 所以无论如何都该针对自己的特定情况采取最好
的开发途径。这是准备面对程序开发中一些不可避免的问题时,我们可以作出的
最佳姿态。
1.11.3 服务器端编程
我们的整个讨论都忽略了服务器端编程的问题。如果向服务器发出一个请
求,会发生什么事情?大多数时候的请求都是很简单的一个“把这个文件发给
我” 。浏览器随后会按适当的形式解释这个文件:作为HTML页、一幅图、一个
Java 程序片、一个脚本程序等等。向服务器发出的较复杂的请求通常涉及到对一
个数据库进行操作(事务处理) 。其中最常见的就是发出一个数据库检索命令,得到结果后,服务器会把它格式化成 HTML 页,并作为结果传回来(当然,假
如客户通过 Java 或者某种脚本语言具有了更高的智能,那么原始数据就能在客
户端发送和格式化;这样做速度可以更快,也能减轻服务器的负担) 。另外,有
时需要在数据库中注册自己的名字(比如加入一个组时) ,或者向服务器发出一
份订单,这就涉及到对那个数据库的修改。这类服务器请求必须通过服务器端的
一些代码进行,我们称其为“服务器端的编程” 。在传统意义上,服务器端编程
是用 Perl和 CGI 脚本进行的,但更复杂的系统已经出现。其中包括基于Java 的
Web服务器,它允许我们用 Java进行所有服务器端编程,写出的程序就叫作“小
服务程序” (Servlet) 。
1.11.4 一个独立的领域:应用程序
与 Java 有关的大多数争论都是与程序片有关的。 Java 实际是一种常规用途的
程序设计语言, 可解决任何类型的问题, 至少理论上如此。 而且正如前面指出的,可以用更有效的方式来解决大多数客户机服务器问题。 如果将视线从程序片身
上转开(同时放宽一些限制,比如禁止写盘等) ,就进入了常规用途的应用程序
的广阔领域。 这种应用程序可独立运行, 毋需浏览器, 就象普通的执行程序那样。
在这儿,Java 的特色并不仅仅反应在它的移植能力,也反映在编程本身上。就象
贯穿全书都会讲到的那样,Java 提供了许多有用的特性,使我们能在较短的时间
里创建出比用从前的程序设计语言更健壮的程序。
但要注意任何东西都不是十全十美的,我们为此也要付出一些代价。其中最
明显的是执行速度放慢了(尽管可对此进行多方面的调整) 。和任何语言一样,Java 本身也存在一些限制,使得它不十分适合解决某些特殊的编程问题。但不管
怎样,Java都是一种正在快速发展的语言。随着每个新版本的发布,它变得越来 23
越可爱,能充分解决的问题也变得越来越多。
1.12 分析和设计
面向对象的范式是思考程序设计时一种新的、而且全然不同的方式,许多人
最开始都会在如何构造一个项目上皱起了眉头。 事实上, 我们可以作出一个 “好”
的设计,它能充分利用 OOP提供的所有优点。
有关 OOP 分析与设计的书籍大多数都不尽如人意。其中的大多数书都充斥
着莫名其妙的话语、笨拙的笔调以及许多听起来似乎很重要的声明(注释⑨) 。
我认为这种书最好压缩到一章左右的空间,至多写成一本非常薄的书。具有讽剌
意味的是,那些特别专注于复杂事物管理的人往往在写一些浅显、明白的书上面
大费周章! 如果不能说得简单和直接, 一定没多少人喜欢看这方面的内容。 毕竟,OOP 的全部宗旨就是让软件开发的过程变得更加容易。尽管这可能影响了那些
喜欢解决复杂问题的人的生计, 但为什么不从一开始就把事情弄得简单些呢?因
此,希望我能从开始就为大家打下一个良好的基础,尽可能用几个段落来说清楚
分析与设计的问题。
⑨:最好的入门书仍然是 Grady Booch 的《Object-Oriented Design
withApplications,第 2版本》 ,Wiely Sons于 1996 年出版。这本书讲得很有深
度,而且通俗易懂,尽管他的记号方法对大多数设计来说都显得不必要地复杂。
1.12.1 不要迷失
在整个开发过程中,最重要的事情就是:不要将自己迷失!但事实上这种事
情很容易发生。大多数方法都设计用来解决最大范围内的问题。当然,也存在一
些特别困难的项目, 需要作者付出更为艰辛的努力, 或者付出更大的代价。 但是,大多数项目都是比较“常规”的,所以一般都能作出成功的分析与设计,而且只
需用到推荐的一小部分方法。但无论多么有限,某些形式的处理总是有益的,这
可使整个项目的开发更加容易,总比直接了当开始编码好!
也就是说,假如你正在考察一种特殊的方法,其中包含了大量细节,并推荐
了许多步骤和文档,那么仍然很难正确判断自己该在何时停止。时刻提醒自己注
意以下几个问题:
(1) 对象是什么?(怎样将自己的项目分割成一系列单独的组件?)
(2) 它们的接口是什么?(需要将什么消息发给每一个对象?)
在确定了对象和它们的接口后,便可着手编写一个程序。出于对多方面原因
的考虑,可能还需要比这更多的说明及文档,但要求掌握的资料绝对不能比这还
少。
整个过程可划分为四个阶段,阶段 0 刚刚开始采用某些形式的结构。
1.12.2 阶段 0:拟出一个计划
第一步是决定在后面的过程中采取哪些步骤。 这听起来似乎很简单 (事实上,我们这儿说的一切都似乎很简单) ,但很常见的一种情况是:有些人甚至没有进
入阶段 1,便忙忙慌慌地开始编写代码。如果你的计划本来就是“直接开始开始
编码” ,那样做当然也无可非议(若对自己要解决的问题已有很透彻的理解,便
可考虑那样做) 。但最低程度也应同意自己该有个计划。
在这个阶段,可能要决定一些必要的附加处理结构。但非常不幸,有些程序 24
员写程序时喜欢随心所欲,他们认为“该完成的时候自然会完成” 。这样做刚开
始可能不会有什么问题,但我觉得假如能在整个过程中设置几个标志,或者“路
标” ,将更有益于你集中注意力。这恐怕比单纯地为了“完成工作”而工作好得
多。至少,在达到了一个又一个的目标,经过了一个接一个的路标以后,可对自
己的进度有清晰的把握,干劲也会相应地提高,不会产生“路遥漫漫无期”的感
觉。
座我刚开始学习故事结构起(我想有一天能写本小说出来) ,就一直坚持这
种做法,感觉就象简单地让文字“流”到纸上。在我写与计算机有关的东西时,发现结构要比小说简单得多,所以不需要考虑太多这方面的问题。但我仍然制订
了整个写作的结构,使自己对要写什么做到心中有数。因此,即使你的计划就是
直接开始写程序,仍然需要经历以下的阶段,同时向自己提出一些特定的问题。
1.12.3 阶段 1:要制作什么?
在上一代程序设计中(即“过程化或程序化设计” ) ,这个阶段称为“建立需
求分析和系统规格” 。当然,那些操作今天已经不再需要了,或者至少改换了形
式。大量令人头痛的文档资料已成为历史。但当时的初衷是好的。需求分析的意
思是 “建立一系列规则, 根据它判断任务什么时候完成, 以及客户怎样才能满意” 。
系统规格则表示“这里是一些具体的说明,让你知道程序需要做什么(而不是怎
样做)才能满足要求” 。需求分析实际就是你和客户之间的一份合约(即使客户
就在本公司内部工作,或者是其他对象及系统) 。系统规格是对所面临问题的最
高级别的一种揭示,我们依据它判断任务是否完成,以及需要花多长的时间。由
于这些都需要取得参与者的一致同意, 所以我建议尽可能地简化它们——最好采
用列表和基本图表的形式——以节省时间。可能还会面临另一些限制,需要把它
们扩充成为更大的文档。
我们特别要注意将重点放在这一阶段的核心问题上,不要纠缠于细枝末节。
这个核心问题就是:决定采用什么系统。对这个问题,最有价值的工具就是一个
名为“使用条件”的集合。对那些采用“假如……,系统该怎样做?”形式的问
题,这便是最有说服力的回答。例如, “假如客户需要提取一张现金支票,但当
时又没有这么多的现金储备,那么自动取款机该怎样反应?”对这个问题, “使
用条件”可以指示自动取款机在那种“条件”下的正确操作。
应尽可能总结出自己系统的一套完整的“使用条件”或者“应用场合” 。一
旦完成这个工作, 就相当于摸清了想让系统完成的核心任务。 由于将重点放在 “使
用条件”上,一个很好的效果就是它们总能让你放精力放在最关键的东西上,并
防止自己分心于对完成任务关系不大的其他事情上面。也就是说,只要掌握了一
套完整的“使用条件” ,就可以对自己的系统作出清晰的描述,并转移到下一个
阶段。在这一阶段,也有可能无法完全掌握系统日后的各种应用场合,但这也没
有关系。只要肯花时间,所有问题都会自然而然暴露出来。不要过份在意系统规
格的“完美” ,否则也容易产生挫败感和焦燥情绪。
在这一阶段,最好用几个简单的段落对自己的系统作出描述,然后围绕它们
再进行扩充,添加一些“名词”和“动词” 。 “名词”自然成为对象,而“动词”
自然成为要整合到对象接口中的“方法” 。只要亲自试着做一做,就会发现这是
多么有用的一个工具;有些时候,它能帮助你完成绝大多数的工作。
尽管仍处在初级阶段,但这时的一些日程安排也可能会非常管用。我们现在
对自己要构建的东西应该有了一个较全面的认识, 所以可能已经感觉到了它大概 25
会花多长的时间来完成。 此时要考虑多方面的因素: 如果估计出一个较长的日程,那么公司也许决定不再继续下去; 或者一名主管已经估算出了这个项目要花多长
的时间,并会试着影响你的估计。但无论如何,最好从一开始就草拟出一份“诚
实”的时间表,以后再进行一些暂时难以作出的决策。目前有许多技术可帮助我
们计算出准确的日程安排(就象那些预测股票市场起落的技术) ,但通常最好的
方法还是依赖自己的经验和直觉(不要忘记,直觉也要建立在经验上) 。感觉一
下大概需要花多长的时间,然后将这个时间加倍,再加上 10%。你的感觉可能
是正确的; “也许”能在那个时间里完成。但“加倍”使那个时间更加充裕, “10
%”的时间则用于进行最后的推敲和深化。但同时也要对此向上级主管作出适当
的解释,无论对方有什么抱怨和修改,只要明确地告诉他们:这样的一个日程安
排,只是我的一个估计!
1.12.4 阶段 2:如何构建?
在这一阶段,必须拿出一套设计方案,并解释其中包含的各类对象在外观上
是什么样子,以及相互间是如何沟通的。此时可考虑采用一种特殊的图表工具:
“统一建模语言” (UML) 。请到 http:www.rational.com 去下载一份 UML 规格
书。作为第 1 阶段中的描述工具,UML 也是很有帮助的。此外,还可用它在第
2 阶段中处理一些图表(如流程图) 。当然并非一定要使用 UML,但它对你会很
有帮助,特别是在希望描绘一张详尽的图表,让许多人在一起研究的时候。除
UML外,还可选择对对象以及它们的接口进行文字化描述(就象我在《Thinking
in C++》里说的那样,但这种方法非常原始,发挥的作用亦较有限。
我曾有一次非常成功的咨询经历,那时涉及到一小组人的初始设计。他们以
前还没有构建过 OOP(面向对象程序设计)项目,将对象画在白板上面。我们
谈到各对象相互间该如何沟通(通信) ,并删除了其中的一部分,以及替换了另
一部分对象。这个小组(他们知道这个项目的目的是什么)实际上已经制订出了
设计方案;他们自己“拥有”了设计,而不是让设计自然而然地显露出来。我在
那里做的事情就是对设计进行指导,提出一些适当的问题,尝试作出一些假设,并从小组中得到反馈,以便修改那些假设。这个过程中最美妙的事情就是整个小
组并不是通过学习一些抽象的例子来进行面向对象的设计, 而是通过实践一个真
正的设计来掌握 OOP 的窍门,而那个设计正是他们当时手上的工作!
作出了对对象以及它们的接口的说明后,就完成了第2阶段的工作。当然,这些工作可能并不完全。有些工作可能要等到进入阶段3 才能得知。但这已经足
够了。我们真正需要关心的是最终找出所有的对象。能早些发现当然好,但 OOP
提供了足够完美的结构,以后再找出它们也不迟。
1.12.5 阶段 3:开始创建
读这本书的可能是程序员,现在进入的正是你可能最感兴趣的阶段。由于手
头上有一个计划——无论它有多么简要, 而且在正式编码前掌握了正确的设计结
构,所以会发现接下去的工作比一开始就埋头写程序要简单得多。而这正是我们
想达到的目的。让代码做到我们想做的事情,这是所有程序项目最终的目标。但
切不要急功冒进,否则只有得不偿失。根据我的经验,最后先拿出一套较为全面
的方案,使其尽可能设想周全,能满足尽可能多的要求。给我的感觉,编程更象
一门艺术,不能只是作为技术活来看待。所有付出最终都会得到回报。作为真正
的程序员,这并非可有可无的一种素质。全面的思考、周密的准备、良好的构造 26
不仅使程序更易构建与调试,也使其更易理解和维护,而那正是一套软件赢利的
必要条件。
构建好系统,并令其运行起来后,必须进行实际检验,以前做的那些需求分
析和系统规格便可派上用场了。全面地考察自己的程序,确定提出的所有要求均
已满足。现在一切似乎都该结束了?是吗?
1.12.6 阶段 4:校订
事实上,整个开发周期还没有结束,现在进入的是传统意义上称为“维护”
的一个阶段。 “维护”是一个比较暧昧的称呼,可用它表示从“保持它按设想的
轨道运行” 、 “加入客户从前忘了声明的功能”或者更传统的“除掉暴露出来的一
切臭虫”等等意思。所以大家对“维护”这个词产生了许多误解,有的人认为:
凡是需要“维护”的东西,必定不是好的,或者是有缺陷的!因为这个词说明你
实际构建的是一个非常“原始”的程序,以后需要频繁地作出改动、添加新的代
码或者防止它的落后、退化等。因此,我们需要用一个更合理的词语来称呼以后
需要继续的工作。
这个词便是“校订” 。换言之, “你第一次做的东西并不完善,所以需为自己
留下一个深入学习、认知的空间,再回过头去作一些改变” 。对于要解决的问题,随着对它的学习和了解愈加深入,可能需要作出大量改动。进行这些工作的一个
动力是随着不断的改革优化,终于能够从自己的努力中得到回报,无论这需要经
历一个较短还是较长的时期。
什么时候才叫“达到理想的状态”呢?这并不仅仅意味着程序必须按要求的
那样工作,并能适应各种指定的“使用条件” ,它也意味着代码的内部结构应当
尽善尽美。至少,我们应能感觉出整个结构都能良好地协调运作。没有笨拙的语
法,没有臃肿的对象,也没有一些华而不实的东西。除此以外,必须保证程序结
构有很强的生命力。由于多方面的原因,以后对程序的改动是必不可少。但必须
确定改动能够方便和清楚地进行。这里没有花巧可言。不仅需要理解自己构建的
是什么,也要理解程序如何不断地进化。幸运的是,面向对象的程序设计语言特
别适合进行这类连续作出的修改——由对象建立起来的边界可有效保证结构的
整体性,并能防范对无关对象进行的无谓干扰、破坏。也可以对自己的程序作一
些看似激烈的大变动,同时不会破坏程序的整体性,不会波及到其他代码。事实
上,对“校订”的支持是 OOP非常重要的一个特点。
通过校订,可创建出至少接近自己设想的东西。然后从整体上观察自己的作
品,把它与自己的要求比较,看看还短缺什么。然后就可以从容地回过头去,对
程序中不恰当的部分进行重新设计和重新实现(注释⑩) 。在最终得到一套恰当
的方案之前, 可能需要解决一些不能回避的问题, 或者至少解决问题的一个方面。
而且一般要多“校订”几次才行( “设计范式”在这里可起到很大的帮助作用。
有关它的讨论,请参考本书第 16 章) 。
构建一套系统时, “校订”几乎是不可避免的。我们需要不断地对比自己的
需求,了解系统是否自己实际所需要的。有时只有实际看到系统,才能意识到自
己需要解决一个不同的问题。若认为这种形式的校订必然会发生,那么最好尽快
拿出自己的第一个版本,检查它是否自己希望的,使自己的思想不断趋向成熟。
反复的“校订”同“递增开发”有关密不可分的关系。递增开发意味着先从
系统的核心入手,将其作为一个框架实现,以后要在这个框架的基础上逐渐建立
起系统剩余的部分。随后,将准备提供的各种功能(特性)一个接一个地加入其 27
中。这里最考验技巧的是架设起一个能方便扩充所有目标特性的一个框架(对这
个问题,大家可参考第 16 章的论述) 。这样做的好处在于一旦令核心框架运作起
来,要加入的每一项特性就象它自身内的一个小项目,而非大项目的一部分。此
外,开发或维护阶段合成的新特性可以更方便地加入。OOP 之所以提供了对递
增开发的支持,是由于假如程序设计得好,每一次递增都可以成为完善的对象或
者对象组。
⑩:这有点类似“快速造型” 。此时应着眼于建立一个简单、明了的版本,使自己能对系统有个清楚的把握。再把这个原型扔掉,并正式地构建一个。快速
造型最麻烦的一种情况就是人们不将原型扔掉,而是直接在它的基础上建造。如
果再加上程序化设计中“结构”的缺乏,就会导致一个混乱的系统,致使维护成
本增加。
1.12.7 计划的回报
如果没有仔细拟定的设计图,当然不可能建起一所房子。如建立的是一所狗
舍,尽管设计图可以不必那么详尽,但仍然需要一些草图,以做到心中有数。软
件开发则完全不同,它的“设计图” (计划)必须详尽而完备。在很长的一段时
间里,人们在他们的开发过程中并没有太多的结构,但那些大型项目很容易就会
遭致失败。通过不断的摸索,人们掌握了数量众多的结构和详细资料。但它们的
使用却使人提心吊胆在意——似乎需要把自己的大多数时间花在编写文档上, 而
没有多少时间来编程(经常如此) 。我希望这里为大家讲述的一切能提供一条折
衷的道路。需要采取一种最适合自己需要(以及习惯)的方法。不管制订出的计
划有多么小,但与完全没有计划相比,一些形式的计划会极大改善你的项目。请
记住:根据估计,没有计划的 50%以上的项目都会失败!
1.13 Java 还是 C++?
Java 特别象 C++;由此很自然地会得出一个结论:C++似乎会被 Java 取代。
但我对这个逻辑存有一些疑问。无论如何,C++仍有一些特性是 Java 没有的。而
且尽管已有大量保证,声称 Java 有一天会达到或超过 C++的速度。但这个突破
迄今仍未实现(尽管 Java 的速度确实在稳步提高,但仍未达到 C++的速度) 。此
外,许多领域都存在为数众多的C++爱好者,所以我并不认为那种语言很快就会
被另一种语言替代(爱好者的力量是容忽视的。比如在我主持的一次“中高级
Java 研讨会”上,Allen Holub声称两种最常用的语言是 Rexx和 COBOL) 。
我感觉 Java 强大之处反映在与C++稍有不同的领域。 C++是一种绝对不会试
图迎合某个模子的语言。 特别是它的形式可以变化多端, 以解决不同类型的问题。
这主要反映在象 Microsoft Visual C++和 Borland C++ Builder(我最喜欢这个)那
样的工具身上。它们将库、组件模型以及代码生成工具等合成到一起,以开发视
窗化的末端用户应用(用于 Microsoft Windows 操作系统) 。但在另一方面,Windows 开发人员最常用的是什么呢?是微软的 Visual Basic(VB) 。当然,我
们在这儿暂且不提 VB 的语法极易使人迷惑的事实——即使一个只有几页长度
的程序,产生的代码也十分难于管理。从语言设计的角度看,尽管VB是那样成
功和流行,但仍然存在不少的缺点。最好能够同时拥有VB那样的强大功能和易
用性,同时不要产生难于管理的代码。而这正是Java 最吸引人的地方:作为“下
一代的 VB”。无论你听到这种主张后有什么感觉,请无论如何都仔细想一想:人 28
们对 Java 做了大量的工作,使它能方便程序员解决应用级问题(如连网和跨平
台 UI 等) ,所以它在本质上允许人们创建非常大型和灵活的代码主体。同时,考
虑到 Java 还拥有我迄今为止尚未在其他任何一种语言里见到的最“健壮”的类
型检查及错误控制系统,所以 Java 确实能大大提高我们的编程效率。这一点是
勿庸置疑的!
但对于自己某个特定的项目, 真的可以不假思索地将 C++换成 Java吗?除了
Web程序片,还有两个问题需要考虑。首先,假如要使用大量现有的库(这样肯
定可以提高不少的效率) ,或者已经有了一个坚实的 C或 C++代码库,那么换成
Java 后,反映会阻碍开发进度,而不是加快它的速度。但若想从头开始构建自己
的所有代码,那么 Java 的简单易用就能有效地缩短开发时间。
最大的问题是速度。在原始的Java 解释器中,解释过的 Java 会比 C慢上 20
到 50 倍。尽管经过长时间的发展,这个速度有一定程度的提高,但和 C比起来
仍然很悬殊。计算机最注重的就是速度;假如在一台计算机上不能明显较快地干
活,那么还不如用手做(有人建议在开发期间使用Java,以缩短开发时间。然后
用一个工具和支撑库将代码转换成 C++,这样可获得更快的执行速度) 。
为使 Java 适用于大多数 Web 开发项目,关键在于速度上的改善。此时要用
到人们称为“刚好及时” (Just-In Time,或 JIT)的编译器,甚至考虑更低级的
代码编译器(写作本书时,也有两款问世) 。当然,低级代码编译器会使编译好
的程序不能跨平台执行,但同时也带来了速度上的提升。这个速度甚至接近 C
和 C++。而且 Java 中的程序交叉编译应当比 C 和 C++中简单得多(理论上只需
重编译即可,但实际仍较难实现;其他语言也曾作出类似的保证) 。
在本书附录,大家可找到与JavaC++比较.对 Java 现状的观察以及编码规
则有关的内容。 1
第2章 一切都是对象
“尽管以 C++为基础,但 Java是一种更纯粹的面向对象程序设计语言” 。
无论 C++还是 Java 都属于杂合语言。但在 Java 中,设计者觉得这种杂合
并不象在 C++里那么重要。杂合语言允许采用多种编程风格;之所以说 C++是
一种杂合语言,是因为它支持与 C 语言的向后兼容能力。由于 C++是 C 的一个
超集,所以包含的许多特性都是后者不具备的,这些特性使 C++在某些地方显
得过于复杂。
Java 语言首先便假定了我们只希望进行面向对象的程序设计。也就是说,正式用它设计之前,必须先将自己的思想转入一个面向对象的世界(除非早已习
惯了这个世界的思维方式) 。只有做好这个准备工作,与其他 OOP 语言相比,才能体会到 Java 的易学易用。在本章,我们将探讨 Java 程序的基本组件,并
体会为什么说 Java乃至 Java程序内的一切都是对象。
2.1 用句柄操纵对象
每种编程语言都有自己的数据处理方式。有些时候,程序员必须时刻留意准
备处理的是什么类型。您曾利用一些特殊语法直接操作过对象,或处理过一些间
接表示的对象吗(C或C++里的指针)?
所有这些在 Java 里都得到了简化,任何东西都可看作对象。因此,我们可
采用一种统一的语法,任何地方均可照搬不误。但要注意,尽管将一切都“看作”
对象,但操纵的标识符实际是指向一个对象的“句柄” (Handle) 。在其他 Java
参考书里,还可看到有的人将其称作一个“引用” ,甚至一个“指针” 。可将这一
情形想象成用遥控板(句柄)操纵电视机(对象) 。只要握住这个遥控板,就相
当于掌握了与电视机连接的通道。但一旦需要“换频道”或者“关小声音” ,我
们实际操纵的是遥控板(句柄) ,再由遥控板自己操纵电视机(对象) 。如果要在
房间里四处走走,并想保持对电视机的控制,那么手上拿着的是遥控板,而非电
视机。
此外,即使没有电视机,遥控板亦可独立存在。也就是说,只是由于拥有一
个句柄,并不表示必须有一个对象同它连接。所以如果想容纳一个词或句子,可
创建一个String句柄:
String s;
但这里创建的只是句柄,并不是对象。若此时向 s 发送一条消息,就会获得
一个错误(运行期) 。这是由于 s 实际并未与任何东西连接(即“没有电视机” ) 。
因此,一种更安全的做法是:创建一个句柄时,记住无论如何都进行初始化:
String s = asdf;
然而,这里采用的是一种特殊类型:字串可用加引号的文字初始化。通常,必须为对象使用一种更通用的初始化类型。
2.2 所有对象都必须创建
创建句柄时,我们希望它同一个新对象连接。通常用 new关键字达到这一目
的。new的意思是: “把我变成这些对象的一种新类型” 。所以在上面的例子中,可以说:
String s = new String(asdf);
它不仅指出“将我变成一个新字串” ,也通过提供一个初始字串,指出了“如 2
何生成这个新字串” 。
当然,字串(String)并非唯一的类型。Java配套提供了数量众多的现成类
型。对我们来讲,最重要的就是记住能自行创建类型。事实上,这应是 Java程
序设计的一项基本操作,是继续本书后余部分学习的基础。
2.2.1 保存到什么地方
程序运行时,我们最好对数据保存到什么地方做到心中有数。特别要注意的
是内存的分配。有六个地方都可以保存数据:
(1) 寄存器。这是最快的保存区域,因为它位于和其他所有保存方式不同的
地方:处理器内部。然而,寄存器的数量十分有限,所以寄存器是根据需要由编
译器分配。我们对此没有直接的控制权,也不可能在自己的程序里找到寄存器存
在的任何踪迹。
(2) 堆栈。驻留于常规RAM(随机访问存储器)区域,但可通过它的“堆栈
指针”获得处理的直接支持。堆栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。 这是一种特别快、 特别有效的数据保存方式, 仅次于寄存器。
创建程序时,Java 编译器必须准确地知道堆栈内保存的所有数据的“长度”以
及“存在时间” 。这是由于它必须生成相应的代码,以便向上和向下移动指针。
这一限制无疑影响了程序的灵活性,所以尽管有些Java数据要保存在堆栈里—
—特别是对象句柄,但 Java对象并不放到其中。
(3) 堆。一种常规用途的内存池(也在RAM 区域) ,其中保存了 Java对象。
和堆栈不同, “内存堆”或“堆” (Heap)最吸引人的地方在于编译器不必知道
要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。
因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new
命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当
然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉
更长的时间!
(4) 静态存储。这儿的“静态” (Static)是指“位于固定位置” (尽管也在
RAM 里) 。程序运行期间,静态存储的数据将随时等候调用。可用static 关键字
指出一个对象的特定元素是静态的。但Java 对象本身永远都不会置入静态存储
空间。
(5) 常数存储。常数值通常直接置于程序代码内部。这样做是安全的,因为
它们永远都不会改变。有的常数需要严格地保护,所以可考虑将它们置入只读存
储器(ROM) 。
(6) 非 RAM 存储。若数据完全独立于一个程序之外,则程序不运行时仍可
存在, 并在程序的控制范围之外。 其中两个最主要的例子便是 “流式对象” 和 “固
定对象” 。对于流式对象,对象会变成字节流,通常会发给另一台机器。而对于
固定对象,对象保存在磁盘中。即使程序中止运行,它们仍可保持自己的状态不
变。对于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体
中。一旦需要,甚至能将它们恢复成普通的、基于RAM 的对象。Java 1.1提供
了对 Lightweight persistence 的支持。未来的版本甚至可能提供更完整的方案。
2.2.2 特殊情况:主要类型
有一系列类需特别对待;可将它们想象成“基本” 、 “主要”或者“主”
(Primitive)类型,进行程序设计时要频繁用到它们。之所以要特别对待,是由 3
于用 new 创建对象(特别是小的、简单的变量)并不是非常有效,因为new 将
对象置于“堆”里。对于这些类型,Java采纳了与 C和 C++相同的方法。也就
是说,不是用 new 创建变量,而是创建一个并非句柄的“自动”变量。这个变
量容纳了具体的值,并置于堆栈中,能够更高效地存取。
Java 决定了每种主要类型的大小。就象在大多数语言里那样,这些大小并
不随着机器结构的变化而变化。这种大小的不可更改正是Java程序具有很强移
植能力的原因之一。
主类型 大小 最小值 最大值 封装器类型
boolean 1 位 - - Boolean
char 16位 Unicode 0 Unicode 2的 16次方-1 Character
byte 8位 -128 +127 Byte(注释①)
short 16位 -2的 15次方 +2的 15次方-1 Short(注释①)
int 32位 -2的31次方 +2的31次方-1 Integer
long 64 位 -2的 63次方 +2的63次方-1 Long
float 32位 IEEE754 IEEE754 Float
double 64 位 IEEE754 IEEE754 Double
Void - - - Void(注释①)
①:到Java 1.1才有,1.0版没有。
数值类型全都是有符号(正负号)的,所以不必费劲寻找没有符号的类型。
主数据类型也拥有自己的“封装器” (wrapper)类。这意味着假如想让堆内
一个非主要对象表示那个主类型,就要使用对应的封装器。例如:
char c = 'x';
Character C = new Character('c');
也可以直接使用:
Character C = new Character('x');
这样做的原因将在以后的章节里解释。
1. 高精度数字
Java 1.1增加了两个类, 用于进行高精度的计算: BigInteger 和 BigDecimal。
尽管它们大致可以划分为“封装器”类型,但两者都没有对应的“主类型” 。
这两个类都有自己特殊的“方法” ,对应于我们针对主类型执行的操作。也
就是说,能对 int 或 float 做的事情,对 BigInteger 和 BigDecimal 一样可以做。
只是必须使用方法调用,不能使用运算符。此外,由于牵涉更多,所以运算速度
会慢一些。我们牺牲了速度,但换来了精度。
BigInteger 支持任意精度的整数。也就是说,我们可精确表示任意大小的整
数值,同时在运算过程中不会丢失任何信息。
BigDecimal支持任意精度的定点数字。例如,可用它进行精确的币值计算。
至于调用这两个类时可选用的构建器和方法,请自行参考联机帮助文档。
2.2.3 Java的数组 4
几乎所有程序设计语言都支持数组。在 C和 C++里使用数组是非常危险的,因为那些数组只是内存块。若程序访问自己内存块以外的数组,或者在初始化之
前使用内存(属于常规编程错误) ,会产生不可预测的后果(注释②) 。
②:在 C++里,应尽量不要使用数组,换用标准模板库(Standard
TemplateLibrary)里更安全的容器。
Java 的一项主要设计目标就是安全性。所以在 C 和 C++里困扰程序员的许
多问题都未在 Java 里重复。一个 Java 可以保证被初始化,而且不可在它的范
围之外访问。由于系统自动进行范围检查,所以必然要付出一些代价:针对每个
数组,以及在运行期间对索引的校验,都会造成少量的内存开销。但由此换回的
是更高的安全性,以及更高的工作效率。为此付出少许代价是值得的。
创建对象数组时,实际创建的是一个句柄数组。而且每个句柄都会自动初始
化成一个特殊值,并带有自己的关键字:null(空) 。一旦 Java 看到 null,就知
道该句柄并未指向一个对象。正式使用前,必须为每个句柄都分配一个对象。若
试图使用依然为 null的一个句柄,就会在运行期报告问题。因此,典型的数组错
误在 Java里就得到了避免。
也可以创建主类型数组。同样地,编译器能够担保对它的初始化,因为会将
那个数组的内存划分成零。
数组问题将在以后的章节里详细讨论。
2.3 绝对不要清除对象
在大多数程序设计语言中,变量的“存在时间” (Lifetime)一直是程序员需
要着重考虑的问题。变量应持续多长的时间?如果想清除它,那么何时进行?在
变量存在时间上纠缠不清会造成大量的程序错误。 在下面的小节里, 将阐示 Java
如何帮助我们完成所有清除工作,从而极大了简化了这个问题。
2.3.1 作用域
大多数程序设计语言都提供了“作用域” (Scope)的概念。对于在作用域里
定义的名字,作用域同时决定了它的“可见性”以及“存在时间” 。在 C,C++
和 Java里,作用域是由花括号的位置决定的。参考下面这个例子:
{
int x = 12;
only x available
{
int q = 96;
both x q available
}
only x available
q “out of scope”
}
73页程序
5
作为在作用域里定义的一个变量,它只有在那个作用域结束之前才可使用。
在上面的例子中,缩进排版使Java代码更易辨读。由于 Java是一种形式自
由的语言,所以额外的空格、制表位以及回车都不会对结果程序造成影响。
注意尽管在 C和 C++里是合法的,但在 Java里不能象下面这样书写代码:
{
int x = 12;
{
int x = 96; illegal
}
}
74页上程序
编译器会认为变量 x 已被定义。所以 C 和 C++能将一个变量“隐藏”在一
个更大的作用域里。但这种做法在 Java 里是不允许的,因为 Java 的设计者认
为这样做使程序产生了混淆。
2.3.2 对象的作用域
Java 对象不具备与主类型一样的存在时间。用 new 关键字创建一个 Java
对象的时候,它会超出作用域的范围之外。所以假若使用下面这段代码:
{
String s = new String(a string);
} 作用域的终点
那么句柄 s 会在作用域的终点处消失。然而,s 指向的 String 对象依然占据
着内存空间。在上面这段代码里,我们没有办法访问对象,因为指向它的唯一一
个句柄已超出了作用域的边界。在后面的章节里,大家还会继续学习如何在程序
运行期间传递和复制对象句柄。
这样造成的结果便是:对于用 new创建的对象,只要我们愿意,它们就会一
直保留下去。这个编程问题在 C 和 C++里特别突出。看来在 C++里遇到的麻烦
最大:由于不能从语言获得任何帮助,所以在需要对象的时候,根本无法确定它
们是否可用。而且更麻烦的是,在 C++里,一旦工作完成,必须保证将对象清
除。
这样便带来了一个有趣的问题。假如 Java 让对象依然故我,怎样才能防止
它们大量充斥内存,并最终造成程序的“凝固”呢。在 C++里,这个问题最令
程序员头痛。但Java以后,情况却发生了改观。Java有一个特别的“垃圾收集
器” ,它会查找用 new创建的所有对象,并辨别其中哪些不再被引用。随后,它
会自动释放由那些闲置对象占据的内存,以便能由新对象使用。这意味着我们根
本不必操心内存的回收问题。只需简单地创建对象,一旦不再需要它们,它们就
会自动离去。这样做可防止在 C++里很常见的一个编程问题:由于程序员忘记
释放内存造成的“内存溢出” 。
2.4 新建数据类型:类 6
如果说一切东西都是对象,那么用什么决定一个“类” (Class)的外观与行
为呢?换句话说,是什么建立起了一个对象的“类型” (Type)呢?大家可能猜
想有一个名为“type”的关键字。但从历史看来,大多数面向对象的语言都用关
键字“class”表达这样一个意思: “我准备告诉你对象一种新类型的外观” 。class
关键字太常用了,以至于本书许多地方并没有用粗体字或双引号加以强调。在这
个关键字的后面,应该跟随新数据类型的名称。例如:
class ATypeName {类主体置于这里}
这样就引入了一种新类型,接下来便可用new创建这种类型的一个新对象:
ATypeName a = new ATypeName;
在 ATypeName里, 类主体只由一条注释构成 (星号和斜杠以及其中的内容,本章后面还会详细讲述) ,所以并不能对它做太多的事情。事实上,除非为其定
义了某些方法,否则根本不能指示它做任何事情。
2.4.1 字段和方法
定义一个类时(我们在 Java 里的全部工作就是定义类、制作那些类的对象
以及将消息发给那些对象) , 可在自己的类里设置两种类型的元素: 数据成员 (有
时也叫“字段” )以及成员函数(通常叫“方法” ) 。其中,数据成员是一种对象
(通过它的句柄与其通信) ,可以为任何类型。它也可以是主类型(并不是句柄)
之一。如果是指向对象的一个句柄,则必须初始化那个句柄,用一种名为“构建
器” (第 4 章会对此详述)的特殊函数将其与一个实际对象连接起来(就象早先
看到的那样,使用 new关键字) 。但若是一种主类型,则可在类定义位置直接初
始化(正如后面会看到的那样,句柄亦可在定义位置初始化) 。
每个对象都为自己的数据成员保有存储空间;数据成员不会在对象之间共
享。下面是定义了一些数据成员的类示例:
class DataOnly {
int i;
float f;
boolean b;
}
76页上程序
这个类并没有做任何实质性的事情,但我们可创建一个对象:
DataOnly d = new DataOnly;
可将值赋给数据成员,但首先必须知道如何引用一个对象的成员。为达到引
用对象成员的目的,首先要写上对象句柄的名字,再跟随一个点号(句点) ,再
跟随对象内部成员的名字。即“对象句柄.成员” 。例如:
d.i = 47;
d.f = 1.1f;
d.b = false;
一个对象也可能包含了另一个对象,而另一个对象里则包含了我们想修改的
数据。对于这个问题,只需保持“连接句点”即可。例如:
myPlane.leftTank.capacity = 100;
除容纳数据之外,DataOnly 类再也不能做更多的事情,因为它没有成员函 7
数(方法) 。为正确理解工作原理,首先必须知道“自变量”和“返回值”的概
念。我们马上就会详加解释。
1. 主成员的默认值
若某个主数据类型属于一个类成员,那么即使不明确(显式)进行初始化,也可以保证它们获得一个默认值。
主类型 默认值
Boolean false
Char '\u0000'(null)
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d
一旦将变量作为类成员使用,就要特别注意由 Java 分配的默认值。这样做
可保证主类型的成员变量肯定得到了初始化(C++不具备这一功能) ,可有效遏
止多种相关的编程错误。
然而,这种保证却并不适用于“局部”变量——那些变量并非一个类的字段。
所以,假若在一个函数定义中写入下述代码:
int x;
那么 x会得到一些随机值 (这与 C和 C++是一样的) , 不会自动初始化成零。
我们责任是在正式使用 x前分配一个适当的值。如果忘记,就会得到一条编译期
错误, 告诉我们变量可能尚未初始化。 这种处理正是 Java优于C++的表现之一。
许多 C++编译器会对变量未初始化发出警告,但在Java里却是错误。
2.5 方法、自变量和返回值
迄今为止, 我们一直用 “函数” (Function) 这个词指代一个已命名的子例程。
但在 Java里, 更常用的一个词却是 “方法” (Method) , 代表 “完成某事的途径” 。
尽管它们表达的实际是同一个意思,但从现在开始,本书将一直使用“方法” ,而不是“函数” 。
Java 的“方法”决定了一个对象能够接收的消息。通过本节的学习,大家
会知道方法的定义有多么简单!
方法的基本组成部分包括名字、自变量、返回类型以及主体。下面便是它最
基本的形式:
返回类型 方法名( 自变量列表 ) { 方法主体 }
返回类型是指调用方法之后返回的数值类型。显然,方法名的作用是对具体
的方法进行标识和引用。自变量列表列出了想传递给方法的信息类型和名称。
Java的方法只能作为类的一部分创建。 只能针对某个对象调用一个方法 (注 8
释③) ,而且那个对象必须能够执行那个方法调用。若试图为一个对象调用错误
的方法,就会在编译期得到一条出错消息。为一个对象调用方法时,需要先列出
对象的名字,在后面跟上一个句点,再跟上方法名以及它的参数列表。亦即“对
象名.方法名(自变量 1,自变量 2,自变量 3...)。举个例子来说,假设我们有一
个方法名叫f,它没有自变量,返回的是类型为int 的一个值。那么,假设有一
个名为a的对象,可为其调用方法 f,则代码如下:
int x = a.f;
返回值的类型必须兼容 x的类型。
象这样调用一个方法的行动通常叫作“向对象发送一条消息” 。在上面的例
子中,消息是 f,而对象是 a。面向对象的程序设计通常简单地归纳为“向对象
发送消息” 。
③:正如马上就要学到的那样, “静态”方法可针对类调用,毋需一个对象。
2.5.1 自变量列表
自变量列表规定了我们传送给方法的是什么信息。正如大家或许已猜到的那
样,这些信息——如同Java内其他任何东西——采用的都是对象的形式。因此,我们必须在自变量列表里指定要传递的对象类型,以及每个对象的名字。正如在
Java 其他地方处理对象时一样,我们实际传递的是“句柄” (注释④) 。然而,句柄的类型必须正确。倘若希望自变量是一个“字串” ,那么传递的必须是一个
字串。
④:对于前面提及的“特殊”数据类型boolean,char,byte,short,int,long, ,float 以及 double 来说是一个例外。但在传递对象时,通常都是指传递指
向对象的句柄。
下面让我们考虑将一个字串作为自变量使用的方法。下面列出的是定义代
码,必须将它置于一个类定义里,否则无法编译:
int storage(String s) {
return s.length 2;
}
这个方法告诉我们需要多少字节才能容纳一个特定字串里的信息(字串里的
每个字符都是 16位,或者说 2个字节、长整数,以便提供对 Unicode字符的支
持) 。自变量的类型为String,而且叫作 s。一旦将 s 传递给方法,就可将它当作
其他对象一样处理(可向其发送消息) 。在这里,我们调用的是 length方法,它是 String 的方法之一。该方法返回的是一个字串里的字符数。
通过上面的例子,也可以了解 return关键字的运用。它主要做两件事情。首
先,它意味着“离开方法,我已完工了” 。其次,假设方法生成了一个值,则那
个值紧接在 return 语句的后面。在这种情况下,返回值是通过计算表达式
“s.length2”而产生的。
可按自己的愿望返回任意类型,但倘若不想返回任何东西,就可指示方法返
回 void(空) 。下面列出一些例子。 9
boolean flag { return true; }
float naturalLogBase { return 2.718; }
void nothing { return; }
void nothing2 {}
若返回类型为void,则return关键字唯一的作用就是退出方法。所以一旦抵
达方法末尾,该关键字便不需要了。可在任何地方从一个方法返回。但假设已指
定了一种非 void 的返回类型,那么无论从何地返回,编译器都会确保我们返回
的是正确的类型。
到此为止,大家或许已得到了这样的一个印象:一个程序只是一系列对象的
集合, 它们的方法将其他对象作为自己的自变量使用, 而且将消息发给那些对象。
这种说法大体正确,但通过以后的学习,大家还会知道如何在一个方法里作出决
策,做一些更细致的基层工作。至于这一章,只需理解消息传送就足够了。
2.6 构建 Java程序
正式构建自己的第一个Java程序前,还有几个问题需要注意。
2.6.1 名字的可见性
在所有程序设计语言里,一个不可避免的问题是对名字或名称的控制。假设
您在程序的某个模块里使用了一个名字, 而另一名程序员在另一个模块里使用了
相同的名字。此时,如何区分两个名字,并防止两个名字互相冲突呢?这个问题
在 C语言里特别突出。 因为程序未提供很好的名字管理方法。 C++的类 (即 Java
类的基础)嵌套使用类里的函数,使其不至于同其他类里的嵌套函数名冲突。然
而,C++仍然允许使用全局数据以及全局函数,所以仍然难以避免冲突。为解决
这个问题,C++用额外的关键字引入了“命名空间”的概念。
由于采用全新的机制,所以 Java 能完全避免这些问题。为了给一个库生成
明确的名字,采用了与 Internet 域名类似的名字。事实上,Java 的设计者鼓励
程序员反转使用自己的Internet域名,因为它们肯定是独一无二的。由于我的域
名是 BruceEckel.com ,所以我的实用工具库就可命名为
com.bruceeckel.utility.foibles。反转了域名后,可将点号想象成子目录。
在 Java 1.0 和 Java 1.1中,域扩展名com,edu,org,net 等都约定为大
写形式。 所以库的样子就变成: COM.bruceeckel.utility.foibles。 然而, 在 Java 1.2
的开发过程中,设计者发现这样做会造成一些问题。所以目前的整个软件包都以
小写字母为标准。
Java 的这种特殊机制意味着所有文件都自动存在于自己的命名空间里。而
且一个文件里的每个类都自动获得一个独一无二的标识符(当然,一个文件里的
类名必须是唯一的) 。所以不必学习特殊的语言知识来解决这个问题——语言本
身已帮我们照顾到这一点。
2.6.2 使用其他组件
一旦要在自己的程序里使用一个预先定义好的类,编译器就必须知道如何找
到它。当然,这个类可能就在发出调用的那个相同的源码文件里。如果是那种情
况,只需简单地使用这个类即可——即使它直到文件的后面仍未得到定义。Java 10
消除了“向前引用”的问题,所以不要关心这些事情。
但假若那个类位于其他文件里呢?您或许认为编译器应该足够“联盟” ,可
以自行发现它。但实情并非如此。假设我们想使用一个具有特定名称的类,但那
个类的定义位于多个文件里。或者更糟,假设我们准备写一个程序,但在创建它
的时候,却向自己的库加入了一个新类,它与现有某个类的名字发生了冲突。
为解决这个问题,必须消除所有潜在的、纠缠不清的情况。为达到这个目的,要用 import 关键字准确告诉 Java编译器我们希望的类是什么。 import 的作用是
指示编译器导入一个“包”——或者说一个“类库” (在其他语言里,可将“库”
想象成一系列函数、数据以及类的集合。但请记住,Java 的所有代码都必须写
入一个类中) 。
大多数时候,我们直接采用来自标准 Java 库的组件(部件)即可,它们是
与编译器配套提供的。使用这些组件时,没有必要关心冗长的保留域名;举个例
子来说,只需象下面这样写一行代码即可:
import java.util.Vector;
它的作用是告诉编译器我们想使用 Java 的 Vector 类。然而,util 包含了数
量众多的类,我们有时希望使用其中的几个,同时不想全部明确地声明它们。为
达到这个目的,可使用“”通配符。如下所示:
import java.util.;
需导入一系列类时, 采用的通常是这个办法。 应尽量避免一个一个地导入类。
2.6.3 static 关键字
通常,我们创建类时会指出那个类的对象的外观与行为。除非用new创建那
个类的一个对象,否则实际上并未得到任何东西。只有执行了 new 后,才会正
式生成数据存储空间,并可使用相应的方法。
但在两种特殊的情形下,上述方法并不堪用。一种情形是只想用一个存储区
域来保存一个特定的数据——无论要创建多少个对象,甚至根本不创建对象。另
一种情形是我们需要一个特殊的方法,它没有与这个类的任何对 ......
您现在查看是摘要介绍页, 详见PDF附件(2206KB,541页)。





