├── .gitignore ├── 0.1.md ├── 0.2.md ├── 1-1.gif ├── 1-2.gif ├── 1.1.md ├── 1.10.md ├── 1.11.md ├── 1.12.md ├── 1.13.md ├── 1.2.md ├── 1.3.md ├── 1.4.md ├── 1.5.md ├── 1.6.md ├── 1.7.md ├── 1.8.md ├── 1.9.md ├── 1.md ├── 10.1.md ├── 10.10.md ├── 10.11.md ├── 10.2.md ├── 10.3.md ├── 10.4.md ├── 10.5.md ├── 10.6.md ├── 10.7.md ├── 10.8.md ├── 10.9.md ├── 10.md ├── 11-1.gif ├── 11.1.md ├── 11.2.md ├── 11.3.md ├── 11.4.md ├── 11.5.md ├── 11.md ├── 12.1.md ├── 12.2.md ├── 12.3.md ├── 12.4.md ├── 12.5.md ├── 12.6.md ├── 12.md ├── 13.1.md ├── 13.10.md ├── 13.11.md ├── 13.12.md ├── 13.13.md ├── 13.14.md ├── 13.15.md ├── 13.16.md ├── 13.17.md ├── 13.18.md ├── 13.19.md ├── 13.2.md ├── 13.20.md ├── 13.21.md ├── 13.3.md ├── 13.4.md ├── 13.5.md ├── 13.6.md ├── 13.7.md ├── 13.8.md ├── 13.9.md ├── 13.md ├── 14.1.md ├── 14.2.md ├── 14.3.md ├── 14.4.md ├── 14.5.md ├── 14.6.md ├── 14.7.md ├── 14.md ├── 15.1.md ├── 15.10.md ├── 15.2.md ├── 15.3.md ├── 15.4.md ├── 15.5.md ├── 15.6.md ├── 15.7.md ├── 15.8.md ├── 15.9.md ├── 15.md ├── 16-1.gif ├── 16-2.gif ├── 16-3.gif ├── 16-4.gif ├── 16.1.md ├── 16.10.md ├── 16.2.md ├── 16.3.md ├── 16.4.md ├── 16.5.md ├── 16.6.md ├── 16.7.md ├── 16.8.md ├── 16.9.md ├── 16.md ├── 17.1.md ├── 17.2.md ├── 17.3.md ├── 17.4.md ├── 17.5.md ├── 17.md ├── 2.1.md ├── 2.10.md ├── 2.11.md ├── 2.2.md ├── 2.3.md ├── 2.4.md ├── 2.5.md ├── 2.6.md ├── 2.7.md ├── 2.8.md ├── 2.9.md ├── 2.md ├── 3.1.md ├── 3.2.md ├── 3.3.md ├── 3.4.md ├── 3.md ├── 4.1.md ├── 4.2.md ├── 4.3.md ├── 4.4.md ├── 4.5.md ├── 4.6.md ├── 4.7.md ├── 4.md ├── 5.1.md ├── 5.2.md ├── 5.3.md ├── 5.4.md ├── 5.5.md ├── 5.6.md ├── 5.md ├── 6.1.md ├── 6.10.md ├── 6.11.md ├── 6.2.md ├── 6.3.md ├── 6.4.md ├── 6.5.md ├── 6.6.md ├── 6.7.md ├── 6.8.md ├── 6.9.md ├── 6.md ├── 7-1.gif ├── 7-10.gif ├── 7-2.gif ├── 7-3.gif ├── 7-4.gif ├── 7-5.gif ├── 7-6.gif ├── 7-7.gif ├── 7-8.gif ├── 7-9.gif ├── 7.1.md ├── 7.10.md ├── 7.2.md ├── 7.3.md ├── 7.4.md ├── 7.5.md ├── 7.6.md ├── 7.7.md ├── 7.8.md ├── 7.9.md ├── 7.md ├── 8-1.gif ├── 8.1.md ├── 8.2.md ├── 8.3.md ├── 8.4.md ├── 8.5.md ├── 8.6.md ├── 8.7.md ├── 8.8.md ├── 8.9.md ├── 8.md ├── 9.1.md ├── 9.10.md ├── 9.2.md ├── 9.3.md ├── 9.4.md ├── 9.5.md ├── 9.6.md ├── 9.7.md ├── 9.8.md ├── 9.9.md ├── 9.md ├── LICENSE ├── README.md ├── SUMMARY.md ├── a.md ├── b.md ├── c.md ├── cover.jpg ├── d.md ├── e.md ├── f.md ├── styles └── ebook.css └── update.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Node rules: 2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 3 | .grunt 4 | 5 | ## Dependency directory 6 | ## Commenting this out is preferred by some people, see 7 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 8 | node_modules 9 | 10 | # Book build output 11 | _book 12 | 13 | # eBook build output 14 | *.epub 15 | *.mobi 16 | *.pdf 17 | 18 | Thumbs.db -------------------------------------------------------------------------------- /0.1.md: -------------------------------------------------------------------------------- 1 | # 写在前面的话 2 | 3 | 我的兄弟Todd目前正在进行从硬件到编程领域的工作转变。我曾提醒他下一次大革命的重点将是遗传工程。 4 | 我们的微生物技术将能制造食品、燃油和塑料;它们都是清洁的,不会造成污染,而且能使人类进一步透视物理世界的奥秘。我认为相比之下电脑的进步会显得微不足道。 5 | 6 | 但随后,我又意识到自己正在犯一些科幻作家常犯的错误:在技术中迷失了(这种事情在科幻小说里常有发生)!如果是一名有经验的作家,就知道绝对不能就事论事,必须以人为中心。遗传对我们的生命有非常大的影响,但不能十分确定它能抹淡计算机革命——或至少信息革命——的影响。信息涉及人相互间的沟通:的确,汽车和轮子的发明都非常重要,但它们最终亦如此而已。真正重要的还是我们与世界的关系,而其中最关键的就是通信。 7 | 8 | 这本书或许能说明一些问题。许多人认为我有点儿大胆或者稍微有些狂妄,居然把所有家当都摆到了Web上。“这样做还有谁来买它呢?”他们问。假如我是一个十分守旧的人,那么绝对不这样干。但我确实不想再沿原来的老路再写一本计算机参考书了。我不知道最终会发生什么事情,但的确认为这是我对一本书作出的最明智的一个决定。 9 | 10 | 至少有一件事是可以肯定的,人们开始向我发送纠错反馈。这是一个令人震惊的体验,因为读者会看到书中的每一个角落,并揪出那些藏匿得很深的技术及语法错误。这样一来,和其他以传统方式发行的书不同,我就能及时改正已知的所有类别的错误,而不是让它们最终印成铅字,堂而皇之地出现在各位的面前。俗话说,“当局者迷,旁观者清”。人们对书中的错误是非常敏感的,往往毫不客气地指出:“我想这样说是错误的,我的看法是……”。在我仔细研究后,往往发现自己确实有不当之处,而这是当初写作时根本没有意识到的(检查多少遍也不行)。我意识到这是群体力量的一个可喜的反映,它使这本书显得的确与众不同。 11 | 12 | 但我随之又听到了另一个声音:“好吧,你在那儿放的电子版的确很有创意,但我想要的是从真正的出版社那里印刷的一个版本!”事实上,我作出了许多努力,让它用普通打印机机就能得到很好的阅读效果,但仍然不象真正印刷的书那样正规。许多人不想在屏幕上看完整本书,也不喜欢拿着一叠纸阅读。无论打印格式有多么好,这些人喜欢是仍然是真正的“书”(激光打印机的墨盒也太贵了一点)。现在看来,计算机的革命仍未使出版界完全走出传统的模式。但是,有一个学生向我推荐了未来出版的一种模式:书籍将首先在互联网上出版,然后只有在绝对必要的前提下,才会印刷到纸张上。目前,为数众多的书籍销售都不十分理想,许多出版社都在亏本。但如采用这种方式出版,就显得灵活得多,也更容易保证赢利。 13 | 14 | 这本书也从另一个角度也给了我深刻的启迪。我刚开始的时候以为Java“只是另一种程序设计语言”。这个想法在许多情况下都是成立的。但随着时间的推移,我对它的学习也愈加深入,开始意识到它的基本宗旨与我见过的其他所有语言都有所区别。 15 | 16 | 程序设计与对复杂性的操控有很大的关系:对一个准备解决的问题,它的复杂程度取决用于解决它的机器的复杂程度。正是由于这一复杂性的存在,我们的程序设计项目屡屡失败。对于我以前接触过的所有编程语言,它们都没能跳过这一框框,由此决定了它们的主要设计目标就是克服程序开发与维护中的复杂性。当然,许多语言在设计时就已考虑到了复杂性的问题。但从另一角度看,实际设计时肯定会有另一些问题浮现出来,需把它们考虑到这个复杂性的问题里。不可避免地,其他那些问题最后会变成最让程序员头痛的。例如,C++必须同C保持向后兼容(使C程序员能尽快地适应新环境),同时又要保证编程的效率。C++在这两个方面都设计得很好,为其赢得了不少的声誉。但它们同时也暴露出了额外的复杂性,阻碍了某些项目的成功实现(当然,你可以责备程序员和管理层,但假如一种语言能通过捕获你的错误而提供帮助,它为什么不那样做呢?)。作为另一个例子,Visual Basic(VB)同当初的BASIC有关的紧密的联系。而BASIC并没有打算设计成一种能全面解决问题的语言,所以堆加到VB身上的所有扩展都造成了令人头痛和难于管理和维护的语法。另一方面,C++、VB和其他如Smalltalk之类的语言均在复杂性的问题上下了一番功夫。由此得到的结果便是,它们在解决特定类型的问题时是非常成功的。 17 | 18 | 在理解到Java最终的目标是减轻程序员的负担时,我才真正感受到了震憾,尽管它的潜台词好象是说:“除了缩短时间和减小产生健壮代码的难度以外,我们不关心其他任何事情。”在目前这个初级阶段,达到那个目标的后果便是代码不能特别快地运行(尽管有许多保证都说Java终究有一天会运行得多么快),但它确实将开发时间缩短到令人惊讶的地步——几乎只有创建一个等效C++程序一半甚至更短的时间。这段节省下来的时间可以产生更大的效益,但Java并不仅止于此。它甚至更上一层楼,将重要性越来越明显的一切复杂任务都封装在内,比如网络程序和多线程处理等等。Java的各种语言特性和库在任何时候都能使那些任务轻而易举完成。而且最后,它解决了一些真正有些难度的复杂问题:跨平台程序、动态代码改换以及安全保护等等。换在从前,其中任何每一个都能使你头大如斗。所以不管我们见到了什么性能问题,Java的保证仍然是非常有效的:它使程序员显著提高了程序设计的效率! 19 | 20 | 在我看来,编程效率提升后影响最大的就是Web。网络程序设计以前非常困难,而Java使这个问题迎刃而解(而且Java也在不断地进步,使解决这类问题变得越来越容易)。网络程序的设计要求我们相互间更有效率地沟通,而且至少要比电话通信来得便宜(仅仅电子函件就为许多公司带来了好处)。随着我们网上通信越来越频繁,令人震惊的事情会慢慢发生,而且它们令人吃惊的程度绝不亚于当初工业革命给人带来的震憾。 21 | 22 | 在各个方面:创建程序;按计划编制程序;构造用户界面,使程序能与用户沟通;在不同类型的机器上运行程序;以及方便地编写程序,使其能通过因特网通信——Java提高了人与人之间的“通信带宽”。而且我认为通信革命的结果可能并不单单是数量庞大的比特到处传来传去那么简单。我们认为认清真正的革命发生在哪里,因为人和人之间的交流变得更方便了——个体与个体之间,个体与组之间,组与组之间,甚至在星球之间。有人预言下一次大革命的发生就是由于足够多的人和足够多的相互连接造成的,而这种革命是以整个世界为基础发生的。Java可能是、也可能不是促成那次革命的直接因素,但我在这里至少感觉自己在做一些有意义的工作——尝试教会大家一种重要的语言! 23 | -------------------------------------------------------------------------------- /1-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/1-1.gif -------------------------------------------------------------------------------- /1-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/1-2.gif -------------------------------------------------------------------------------- /1.1.md: -------------------------------------------------------------------------------- 1 | # 1.1 抽象的进步 2 | 3 | 4 | 所有编程语言的最终目的都是提供一种“抽象”方法。一种较有争议的说法是:解决问题的复杂程度直接取决于抽象的种类及质量。这儿的“种类”是指准备对什么进行“抽象”?汇编语言是对基础机器的少量抽象。后来的许多“命令式”语言(如FORTRAN,BASIC和C)是对汇编语言的一种抽象。与汇编语言相比,这些语言已有了长足的进步,但它们的抽象原理依然要求我们着重考虑计算机的结构,而非考虑问题本身的结构。在机器模型(位于“方案空间”)与实际解决的问题模型(位于“问题空间”)之间,程序员必须建立起一种联系。这个过程要求人们付出较大的精力,而且由于它脱离了编程语言本身的范围,造成程序代码很难编写,而且要花较大的代价进行维护。由此造成的副作用便是一门完善的“编程方法”学科。 5 | 6 | 为机器建模的另一个方法是为要解决的问题制作模型。对一些早期语言来说,如LISP和APL,它们的做法是“从不同的角度观察世界”——“所有问题都归纳为列表”或“所有问题都归纳为算法”。PROLOG则将所有问题都归纳为决策链。对于这些语言,我们认为它们一部分是面向基于“强制”的编程,另一部分则是专为处理图形符号设计的。每种方法都有自己特殊的用途,适合解决某一类的问题。但只要超出了它们力所能及的范围,就会显得非常笨拙。 7 | 8 | 面向对象的程序设计在此基础上则跨出了一大步,程序员可利用一些工具表达问题空间内的元素。由于这种表达非常普遍,所以不必受限于特定类型的问题。我们将问题空间中的元素以及它们在方案空间的表示物称作“对象”(`Object`)。当然,还有一些在问题空间没有对应体的其他对象。通过添加新的对象类型,程序可进行灵活的调整,以便与特定的问题配合。所以在阅读方案的描述代码时,会读到对问题进行表达的话语。与我们以前见过的相比,这无疑是一种更加灵活、更加强大的语言抽象方法。总之,OOP允许我们根据问题来描述问题,而不是根据方案。然而,仍有一个联系途径回到计算机。每个对象都类似一台小计算机;它们有自己的状态,而且可要求它们进行特定的操作。与现实世界的“对象”或者“物体”相比,编程“对象”与它们也存在共通的地方:它们都有自己的特征和行为。 9 | 10 | Alan Kay总结了Smalltalk的五大基本特征。这是第一种成功的面向对象程序设计语言,也是Java的基础语言。通过这些特征,我们可理解“纯粹”的面向对象程序设计方法是什么样的: 11 | 12 | (1) 所有东西都是对象。可将对象想象成一种新型变量;它保存着数据,但可要求它对自身进行操作。理论上讲,可从要解决的问题身上提出所有概念性的组件,然后在程序中将其表达为一个对象。 13 | 14 | (2) 程序是一大堆对象的组合;通过消息传递,各对象知道自己该做些什么。为了向对象发出请求,需向那个对象“发送一条消息”。更具体地讲,可将消息想象为一个调用请求,它调用的是从属于目标对象的一个子例程或函数。 15 | 16 | (3) 每个对象都有自己的存储空间,可容纳其他对象。或者说,通过封装现有对象,可制作出新型对象。所以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。 17 | 18 | (4) 每个对象都有一种类型。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(`Class`)是“类型”(`Type`)的同义词。一个类最重要的特征就是“能将什么消息发给它?”。 19 | 20 | (5) 同一类所有对象都能接收相同的消息。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(`Circle`)的一个对象也属于类型为“形状”(`Shape`)的一个对象,所以一个圆完全能接收形状消息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是OOP最重要的概念之一。 21 | 22 | 一些语言设计者认为面向对象的程序设计本身并不足以方便解决所有形式的程序问题,提倡将不同的方法组合成“多态程序设计语言”(注释②)。 23 | 24 | ②:参见Timothy Budd编著的《Multiparadigm Programming in Leda》,Addison-Wesley 1995年出版。 25 | -------------------------------------------------------------------------------- /1.10.md: -------------------------------------------------------------------------------- 1 | # 1.10 永久性 2 | 3 | 4 | 创建一个对象后,只要我们需要,它就会一直存在下去。但在程序结束运行时,对象的“生存期”也会宣告结束。尽管这一现象表面上非常合理,但深入追究就会发现,假如在程序停止运行以后,对象也能继续存在,并能保留它的全部信息,那么在某些情况下将是一件非常有价值的事情。下次启动程序时,对象仍然在那里,里面保留的信息仍然是程序上一次运行时的那些信息。当然,可以将信息写入一个文件或者数据库,从而达到相同的效果。但尽管可将所有东西都看作一个对象,如果能将对象声明成“永久性”,并令其为我们照看其他所有细节,无疑也是一件相当方便的事情。 5 | 6 | Java 1.1提供了对“有限永久性”的支持,这意味着我们可将对象简单地保存到磁盘上,以后任何时间都可取回。之所以称它为“有限”的,是由于我们仍然需要明确发出调用,进行对象的保存和取回工作。这些工作不能自动进行。在Java未来的版本中,对“永久性”的支持有望更加全面。 7 | -------------------------------------------------------------------------------- /1.13.md: -------------------------------------------------------------------------------- 1 | # 1.13 Java还是C++ 2 | 3 | Java特别象C++;由此很自然地会得出一个结论:C++似乎会被Java取代。但我对这个逻辑存有一些疑问。无论如何,C++仍有一些特性是Java没有的。而且尽管已有大量保证,声称Java有一天会达到或超过C++的速度。但这个突破迄今仍未实现(尽管Java的速度确实在稳步提高,但仍未达到C++的速度)。此外,许多领域都存在为数众多的C++爱好者,所以我并不认为那种语言很快就会被另一种语言替代(爱好者的力量是容忽视的。比如在我主持的一次“中/高级Java研讨会”上,Allen Holub声称两种最常用的语言是Rexx和COBOL)。 4 | 5 | 我感觉Java强大之处反映在与C++稍有不同的领域。C++是一种绝对不会试图迎合某个模子的语言。特别是它的形式可以变化多端,以解决不同类型的问题。这主要反映在象Microsoft Visual C++和Borland C++ Builder(我最喜欢这个)那样的工具身上。它们将库、组件模型以及代码生成工具等组合到一起,以开发视窗化的末端用户应用(用于Microsoft Windows操作系统)。但在另一方面,Windows开发人员最常用的是什么呢?是微软的Visual Basic(VB)。当然,我们在这儿暂且不提VB的语法极易使人迷惑的事实——即使一个只有几页长度的程序,产生的代码也十分难于管理。从语言设计的角度看,尽管VB是那样成功和流行,但仍然存在不少的缺点。最好能够同时拥有VB那样的强大功能和易用性,同时不要产生难于管理的代码。而这正是Java最吸引人的地方:作为“下一代的VB”。无论你听到这种主张后有什么感觉,请无论如何都仔细想一想:人们对Java做了大量的工作,使它能方便程序员解决应用级问题(如连网和跨平台UI等),所以它在本质上允许人们创建非常大型和灵活的代码主体。同时,考虑到Java还拥有我迄今为止尚未在其他任何一种语言里见到的最“健壮”的类型检查及错误控制系统,所以Java确实能大大提高我们的编程效率。这一点是勿庸置疑的! 6 | 7 | 但对于自己某个特定的项目,真的可以不假思索地将C++换成Java吗?除了Web程序片,还有两个问题需要考虑。首先,假如要使用大量现有的库(这样肯定可以提高不少的效率),或者已经有了一个坚实的C或C++代码库,那么换成Java后,反映会阻碍开发进度,而不是加快它的速度。但若想从头开始构建自己的所有代码,那么Java的简单易用就能有效地缩短开发时间。 8 | 最大的问题是速度。在原始的Java解释器中,解释过的Java会比C慢上20到50倍。尽管经过长时间的发展,这个速度有一定程度的提高,但和C比起来仍然很悬殊。计算机最注重的就是速度;假如在一台计算机上不能明显较快地干活,那么还不如用手做(有人建议在开发期间使用Java,以缩短开发时间。然后用一个工具和支撑库将代码转换成C++,这样可获得更快的执行速度)。 9 | 为使Java适用于大多数Web开发项目,关键在于速度上的改善。此时要用到人们称为“刚好及时”(Just-In Time,或JIT)的编译器,甚至考虑更低级的代码编译器(写作本书时,也有两款问世)。当然,低级代码编译器会使编译好的程序不能跨平台执行,但同时也带来了速度上的提升。这个速度甚至接近C和C++。而且Java中的程序交叉编译应当比C和C++中简单得多(理论上只需重编译即可,但实际仍较难实现;其他语言也曾作出类似的保证)。 10 | 11 | 在本书附录,大家可找到与Java/C++比较.对Java现状的观察以及编码规则有关的内容。 12 | -------------------------------------------------------------------------------- /1.2.md: -------------------------------------------------------------------------------- 1 | # 1.2 对象的接口 2 | 3 | 亚里士多德或许是认真研究“类型”概念的第一人,他曾谈及“鱼类和鸟类”的问题。在世界首例面向对象语言Simula-67中,第一次用到了这样的一个概念: 4 | 5 | 所有对象——尽管各有特色——都属于某一系列对象的一部分,这些对象具有通用的特征和行为。在Simula-67中,首次用到了`class`这个关键字,它为程序引入了一个全新的类型(`class`和`type`通常可互换使用;注释③)。 6 | 7 | ③:有些人进行了进一步的区分,他们强调“类型”决定了接口,而“类”是那个接口的一种特殊实现方式。 8 | 9 | Simula是一个很好的例子。正如这个名字所暗示的,它的作用是“模拟”(Simulate)象“银行出纳员”这样的经典问题。在这个例子里,我们有一系列出纳员、客户、帐号以及交易等。每类成员(元素)都具有一些通用的特征:每个帐号都有一定的余额;每名出纳都能接收客户的存款;等等。与此同时,每个成员都有自己的状态;每个帐号都有不同的余额;每名出纳都有一个名字。所以在计算机程序中,能用独一无二的实体分别表示出纳员、客户、帐号以及交易。这个实体便是“对象”,而且每个对象都隶属一个特定的“类”,那个类具有自己的通用特征与行为。 10 | 11 | 因此,在面向对象的程序设计中,尽管我们真正要做的是新建各种各样的数据“类型”(`Type`),但几乎所有面向对象的程序设计语言都采用了`class`关键字。当您看到`type`这个字的时候,请同时想到`class`;反之亦然。 12 | 13 | 建好一个类后,可根据情况生成许多对象。随后,可将那些对象作为要解决问题中存在的元素进行处理。事实上,当我们进行面向对象的程序设计时,面临的最大一项挑战性就是:如何在“问题空间”(问题实际存在的地方)的元素与“方案空间”(对实际问题进行建模的地方,如计算机)的元素之间建立理想的“一对一”对应或映射关系。 14 | 15 | 如何利用对象完成真正有用的工作呢?必须有一种办法能向对象发出请求,令其做一些实际的事情,比如完成一次交易、在屏幕上画一些东西或者打开一个开关等等。每个对象仅能接受特定的请求。我们向对象发出的请求是通过它的“接口”(`Interface`)定义的,对象的“类型”或“类”则规定了它的接口形式。“类型”与“接口”的等价或对应关系是面向对象程序设计的基础。 16 | 下面让我们以电灯泡为例: 17 | 18 |  19 | 20 | ``` 21 | Light lt = new Light(); 22 | lt.on(); 23 | ``` 24 | 25 | 在这个例子中,类型/类的名称是`Light`,可向`Light`对象发出的请求包括包括打开(`on`)、关闭(`off`)、变得更明亮(`brighten`)或者变得更暗淡(`dim`)。通过简单地声明一个名字(`lt`),我们为`Light`对象创建了一个“引用”。然后用`new`关键字新建类型为`Light`的一个对象。再用等号将其赋给引用。为了向对象发送一条消息,我们列出引用名(`lt`),再用一个句点符号(`.`)把它同消息名称(`on`)连接起来。从中可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单和直观的。 26 | -------------------------------------------------------------------------------- /1.3.md: -------------------------------------------------------------------------------- 1 | # 1.3 实现方案的隐藏 2 | 3 | 4 | 为方便后面的讨论,让我们先对这一领域的从业人员作一下分类。从根本上说,大致有两方面的人员涉足面向对象的编程:“类创建者”(创建新数据类型的人)以及“客户程序员”(在自己的应用程序中采用现成数据类型的人;注释④)。对客户程序员来讲,最主要的目标就是收集一个充斥着各种类的编程“工具箱”,以便快速开发符合自己要求的应用。而对类创建者来说,他们的目标则是从头构建一个类,只向客户程序员开放有必要开放的东西(接口),其他所有细节都隐藏起来。为什么要这样做?隐藏之后,客户程序员就不能接触和改变那些细节,所以原创者不用担心自己的作品会受到非法修改,可确保它们不会对其他人造成影响。 5 | 6 | ④:感谢我的朋友Scott Meyers,是他帮我起了这个名字。 7 | 8 | “接口”(`Interface`)规定了可对一个特定的对象发出哪些请求。然而,必须在某个地方存在着一些代码,以便满足这些请求。这些代码与那些隐藏起来的数据便叫作“隐藏的实现”。站在程式化程序编写(Procedural Programming)的角度,整个问题并不显得复杂。一种类型含有与每种可能的请求关联起来的函数。一旦向对象发出一个特定的请求,就会调用那个函数。我们通常将这个过程总结为向对象“发送一条消息”(提出一个请求)。对象的职责就是决定如何对这条消息作出反应(执行相应的代码)。 9 | 10 | 对于任何关系,重要一点是让牵连到的所有成员都遵守相同的规则。创建一个库时,相当于同客户程序员建立了一种关系。对方也是程序员,但他们的目标是组合出一个特定的应用(程序),或者用您的库构建一个更大的库。 11 | 12 | 若任何人都能使用一个类的所有成员,那么客户程序员可对那个类做任何事情,没有办法强制他们遵守任何约束。即便非常不愿客户程序员直接操作类内包含的一些成员,但倘若未进行访问控制,就没有办法阻止这一情况的发生——所有东西都会暴露无遗。 13 | 14 | 有两方面的原因促使我们控制对成员的访问。第一个原因是防止程序员接触他们不该接触的东西——通常是内部数据类型的设计思想。若只是为了解决特定的问题,用户只需操作接口即可,毋需明白这些信息。我们向用户提供的实际是一种服务,因为他们很容易就可看出哪些对自己非常重要,以及哪些可忽略不计。 15 | 16 | 进行访问控制的第二个原因是允许库设计人员修改内部结构,不用担心它会对客户程序员造成什么影响。例如,我们最开始可能设计了一个形式简单的类,以便简化开发。以后又决定进行改写,使其更快地运行。若接口与实现方法早已隔离开,并分别受到保护,就可放心做到这一点,只要求用户重新链接一下即可。 17 | 18 | Java采用三个显式(明确)关键字以及一个隐式(暗示)关键字来设置类边界:`public`,`private`,`protected`以及暗示性的`friendly`。若未明确指定其他关键字,则默认为后者。这些关键字的使用和含义都是相当直观的,它们决定了谁能使用后续的定义内容。`public`(公共)意味着后续的定义任何人均可使用。而在另一方面,`private`(私有)意味着除您自己、类型的创建者以及那个类型的内部函数成员,其他任何人都不能访问后续的定义信息。`private`在您与客户程序员之间竖起了一堵墙。若有人试图访问私有成员,就会得到一个编译期错误。`friendly`(友好的)涉及“包装”或“封装”(Package)的概念——即Java用来构建库的方法。若某样东西是“友好的”,意味着它只能在这个包装的范围内使用(所以这一访问级别有时也叫作“包装访问”)。`protected`(受保护的)与`private`相似,只是一个继承的类可访问受保护的成员,但不能访问私有成员。继承的问题不久就要谈到。 19 | -------------------------------------------------------------------------------- /1.4.md: -------------------------------------------------------------------------------- 1 | # 1.4 方案的重复使用 2 | 3 | 创建并测试好一个类后,它应(从理想的角度)代表一个有用的代码单位。但并不象许多人希望的那样,这种重复使用的能力并不容易实现;它要求较多的经验以及洞察力,这样才能设计出一个好的方案,才有可能重复使用。 4 | 5 | 许多人认为代码或设计模式的重复使用是面向对象的程序设计提供的最伟大的一种杠杆。 6 | 7 | 为重复使用一个类,最简单的办法是仅直接使用那个类的对象。但同时也能将那个类的一个对象置入一个新类。我们把这叫作“创建一个成员对象”。新类可由任意数量和类型的其他对象构成。无论如何,只要新类达到了设计要求即可。这个概念叫作“组织”——在现有类的基础上组织一个新类。有时,我们也将组织称作“包含”关系,比如“一辆车包含了一个变速箱”。 8 | 9 | 对象的组织具有极大的灵活性。新类的“成员对象”通常设为“私有”(`Private`),使用这个类的客户程序员不能访问它们。这样一来,我们可在不干扰客户代码的前提下,从容地修改那些成员。也可以在“运行期”更改成员,这进一步增大了灵活性。后面要讲到的“继承”并不具备这种灵活性,因为编译器必须对通过继承创建的类加以限制。 10 | 11 | 由于继承的重要性,所以在面向对象的程序设计中,它经常被重点强调。作为新加入这一领域的程序员,或许早已先入为主地认为“继承应当随处可见”。沿这种思路产生的设计将是非常笨拙的,会大大增加程序的复杂程度。相反,新建类的时候,首先应考虑“组织”对象;这样做显得更加简单和灵活。利用对象的组织,我们的设计可保持清爽。一旦需要用到继承,就会明显意识到这一点。 12 | -------------------------------------------------------------------------------- /1.5.md: -------------------------------------------------------------------------------- 1 | # 1.5 继承:重新使用接口 2 | 3 | 4 | 就其本身来说,对象的概念可为我们带来极大的便利。它在概念上允许我们将各式各样数据和功能封装到一起。这样便可恰当表达“问题空间”的概念,不用刻意遵照基础机器的表达方式。在程序设计语言中,这些概念则反映为具体的数据类型(使用`class`关键字)。 5 | 6 | 我们费尽心思做出一种数据类型后,假如不得不又新建一种类型,令其实现大致相同的功能,那会是一件非常令人灰心的事情。但若能利用现成的数据类型,对其进行“克隆”,再根据情况进行添加和修改,情况就显得理想多了。“继承”正是针对这个目标而设计的。但继承并不完全等价于克隆。在继承过程中,若原始类(正式名称叫作基类、超类或父类)发生了变化,修改过的“克隆”类(正式名称叫作继承类或者子类)也会反映出这种变化。在Java语言中,继承是通过`extends`关键字实现的 7 | 8 | 使用继承时,相当于创建了一个新类。这个新类不仅包含了现有类型的所有成员(尽管`private`成员被隐藏起来,且不能访问),但更重要的是,它复制了基类的接口。也就是说,可向基类的对象发送的所有消息亦可原样发给派生类的对象。根据可以发送的消息,我们能知道类的类型。这意味着派生类具有与基类相同的类型!为真正理解面向对象程序设计的含义,首先必须认识到这种类型的等价关系。 9 | 10 | 由于基类和派生类具有相同的接口,所以那个接口必须进行特殊的设计。也就是说,对象接收到一条特定的消息后,必须有一个“方法”能够执行。若只是简单地继承一个类,并不做其他任何事情,来自基类接口的方法就会直接照搬到派生类。这意味着派生类的对象不仅有相同的类型,也有同样的行为,这一后果通常是我们不愿见到的。 11 | 12 | 有两种做法可将新得的派生类与原来的基类区分开。第一种做法十分简单:为派生类添加新函数(功能)。这些新函数并非基类接口的一部分。进行这种处理时,一般都是意识到基类不能满足我们的要求,所以需要添加更多的函数。这是一种最简单、最基本的继承用法,大多数时候都可完美地解决我们的问题。然而,事先还是要仔细调查自己的基类是否真的需要这些额外的函数。 13 | 14 | ## 1.5.1 改善基类 15 | 16 | 尽管`extends`关键字暗示着我们要为接口“扩展”新功能,但实情并非肯定如此。为区分我们的新类,第二个办法是改变基类一个现有函数的行为。我们将其称作“改善”那个函数。 17 | 18 | 为改善一个函数,只需为派生类的函数建立一个新定义即可。我们的目标是:“尽管使用的函数接口未变,但它的新版本具有不同的表现”。 19 | 20 | ## 1.5.2 等价与类似关系 21 | 22 | 针对继承可能会产生这样的一个争论:继承只能改善原基类的函数吗?若答案是肯定的,则派生类型就是与基类完全相同的类型,因为都拥有完全相同的接口。这样造成的结果就是:我们完全能够将派生类的一个对象换成基类的一个对象!可将其想象成一种“纯替换”。在某种意义上,这是进行继承的一种理想方式。此时,我们通常认为基类和派生类之间存在一种“等价”关系——因为我们可以理直气壮地说:“圆就是一种几何形状”。为了对继承进行测试,一个办法就是看看自己是否能把它们套入这种“等价”关系中,看看是否有意义。 23 | 24 | 但在许多时候,我们必须为派生类型加入新的接口元素。所以不仅扩展了接口,也创建了一种新类型。这种新类型仍可替换成基类型,但这种替换并不是完美的,因为不可在基类里访问新函数。我们将其称作“类似”关系;新类型拥有旧类型的接口,但也包含了其他函数,所以不能说它们是完全等价的。举个例子来说,让我们考虑一下制冷机的情况。假定我们的房间连好了用于制冷的各种控制器;也就是说,我们已拥有必要的“接口”来控制制冷。现在假设机器出了故障,我们把它换成一台新型的冷、热两用空调,冬天和夏天均可使用。冷、热空调“类似”制冷机,但能做更多的事情。由于我们的房间只安装了控制制冷的设备,所以它们只限于同新机器的制冷部分打交道。新机器的接口已得到了扩展,但现有的系统并不知道除原始接口以外的任何东西。 25 | 26 | 认识了等价与类似的区别后,再进行替换时就会有把握得多。尽管大多数时候“纯替换”已经足够,但您会发现在某些情况下,仍然有明显的理由需要在派生类的基础上增添新功能。通过前面对这两种情况的讨论,相信大家已心中有数该如何做。 27 | -------------------------------------------------------------------------------- /1.6.md: -------------------------------------------------------------------------------- 1 | # 1.6 多态对象的互换使用 2 | 3 | 4 | 通常,继承最终会以创建一系列类收场,所有类都建立在统一的接口基础上。我们用一幅颠倒的树形图来阐明这一点(注释⑤): 5 | 6 | ⑤:这儿采用了“统一记号法”,本书将主要采用这种方法。 7 | 8 |  9 | 10 | 对这样的一系列类,我们要进行的一项重要处理就是将派生类的对象当作基类的一个对象对待。这一点是非常重要的,因为它意味着我们只需编写单一的代码,令其忽略类型的特定细节,只与基类打交道。这样一来,那些代码就可与类型信息分开。所以更易编写,也更易理解。此外,若通过继承增添了一种新类型,如“三角形”,那么我们为“几何形状”新类型编写的代码会象在旧类型里一样良好地工作。所以说程序具备了“扩展能力”,具有“扩展性”。 11 | 以上面的例子为基础,假设我们用Java写了这样一个函数: 12 | 13 | ``` 14 | void doStuff(Shape s) { 15 | s.erase(); 16 | // ... 17 | s.draw(); 18 | } 19 | ``` 20 | 21 | 这个函数可与任何“几何形状”(`Shape`)通信,所以完全独立于它要描绘(`draw`)和删除(`erase`)的任何特定类型的对象。如果我们在其他一些程序里使用`doStuff()`函数: 22 | 23 | ``` 24 | Circle c = new Circle(); 25 | Triangle t = new Triangle(); 26 | Line l = new Line(); 27 | doStuff(c); 28 | doStuff(t); 29 | doStuff(l); 30 | ``` 31 | 32 | 那么对`doStuff()`的调用会自动良好地工作,无论对象的具体类型是什么。 33 | 这实际是一个非常有用的编程技巧。请考虑下面这行代码: 34 | 35 | ``` 36 | doStuff(c); 37 | ``` 38 | 39 | 此时,一个`Circle`(圆)引用传递给一个本来期待`Shape`(形状)引用的函数。由于圆是一种几何形状,所以`doStuff()`能正确地进行处理。也就是说,凡是`doStuff()`能发给一个`Shape`的消息,`Circle`也能接收。所以这样做是安全的,不会造成错误。 40 | 41 | 我们将这种把派生类型当作它的基本类型处理的过程叫作“Upcasting”(向上转换)。其中,“cast”(转换)是指根据一个现成的模型创建;而“Up”(向上)表明继承的方向是从“上面”来的——即基类位于顶部,而派生类在下方展开。所以,根据基类进行转换就是一个从上面继承的过程,即“Upcasting”。 42 | 43 | 在面向对象的程序里,通常都要用到向上转换技术。这是避免去调查准确类型的一个好办法。请看看`doStuff()`里的代码: 44 | 45 | ``` 46 | s.erase(); 47 | // ... 48 | s.draw(); 49 | ``` 50 | 51 | 注意它并未这样表达:“如果你是一个`Circle`,就这样做;如果你是一个`Square`,就那样做;等等”。若那样编写代码,就需检查一个`Shape`所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的`Shape`类型后,都要相应地进行修改。在这儿,我们只需说:“你是一种几何形状,我知道你能将自己删掉,即`erase()`;请自己采取那个行动,并自己去控制所有的细节吧。” 52 | 53 | ## 1.6.1 动态绑定 54 | 55 | 在`doStuff()`的代码里,最让人吃惊的是尽管我们没作出任何特殊指示,采取的操作也是完全正确和恰当的。我们知道,为`Circle`调用`draw()`时执行的代码与为一个`Square`或`Line`调用`draw()`时执行的代码是不同的。但在将`draw()`消息发给一个匿名`Shape`时,根据`Shape`引用当时连接的实际类型,会相应地采取正确的操作。这当然令人惊讶,因为当Java编译器为`doStuff()`编译代码时,它并不知道自己要操作的准确类型是什么。尽管我们确实可以保证最终会为`Shape`调用`erase()`,为`Shape`调用`draw()`,但并不能保证为特定的`Circle`,`Square`或者`Line`调用什么。然而最后采取的操作同样是正确的,这是怎么做到的呢? 56 | 57 | 将一条消息发给对象时,如果并不知道对方的具体类型是什么,但采取的行动同样是正确的,这种情况就叫作“多态性”(Polymorphism)。对面向对象的程序设计语言来说,它们用以实现多态性的方法叫作“动态绑定”。编译器和运行期系统会负责对所有细节的控制;我们只需知道会发生什么事情,而且更重要的是,如何利用它帮助自己设计程序。 58 | 59 | 有些语言要求我们用一个特殊的关键字来允许动态绑定。在C++中,这个关键字是`virtual`。在Java中,我们则完全不必记住添加一个关键字,因为函数的动态绑定是自动进行的。所以在将一条消息发给对象时,我们完全可以肯定对象会采取正确的行动,即使其中涉及向上转换之类的处理。 60 | 61 | ## 1.6.2 抽象的基类和接口 62 | 63 | 设计程序时,我们经常都希望基类只为自己的派生类提供一个接口。也就是说,我们不想其他任何人实际创建基类的一个对象,只对向上转换成它,以便使用它们的接口。为达到这个目的,需要把那个类变成“抽象”的——使用`abstract`关键字。若有人试图创建抽象类的一个对象,编译器就会阻止他们。这种工具可有效强制实行一种特殊的设计。 64 | 65 | 亦可用`abstract`关键字描述一个尚未实现的方法——作为一个“根”使用,指出:“这是适用于从这个类继承的所有类型的一个接口函数,但目前尚没有对它进行任何形式的实现。”抽象方法也许只能在一个抽象类里创建。继承了一个类后,那个方法就必须实现,否则继承的类也会变成“抽象”类。通过创建一个抽象方法,我们可以将一个方法置入接口中,不必再为那个方法提供可能毫无意义的主体代码。 66 | 67 | `interface`(接口)关键字将抽象类的概念更延伸了一步,它完全禁止了所有的函数定义。“接口”是一种相当有效和常用的工具。另外如果自己愿意,亦可将多个接口都合并到一起(不能从多个普通`class`或`abstract class`中继承)。 68 | -------------------------------------------------------------------------------- /1.8.md: -------------------------------------------------------------------------------- 1 | # 1.8 异常控制:解决错误 2 | 3 | 4 | 从最古老的程序设计语言开始,错误控制一直都是设计者们需要解决的一个大问题。由于很难设计出一套完美的错误控制方案,许多语言干脆将问题简单地忽略掉,将其转嫁给库设计人员。对大多数错误控制方案来说,最主要的一个问题是它们严重依赖程序员的警觉性,而不是依赖语言本身的强制标准。如果程序员不够警惕——若比较匆忙,这几乎是肯定会发生的——程序所依赖的错误控制方案便会失效。 5 | 6 | “异常控制”将错误控制方案内置到程序设计语言中,有时甚至内建到操作系统内。这里的“异常”(`Exception`)属于一个特殊的对象,它会从产生错误的地方“扔”或“抛”出来。随后,这个异常会被设计用于控制特定类型错误的“异常控制器”捕获。在情况变得不对劲的时候,可能有几个异常控制器并行捕获对应的异常对象。由于采用的是独立的执行路径,所以不会干扰我们的常规执行代码。这样便使代码的编写变得更加简单,因为不必经常性强制检查代码。除此以外,“抛”出的一个异常不同于从函数返回的错误值,也不同于由函数设置的一个标志。那些错误值或标志的作用是指示一个错误状态,是可以忽略的。但异常不能被忽略,所以肯定能在某个地方得到处置。最后,利用异常能够可靠地从一个糟糕的环境中恢复。此时一般不需要退出,我们可以采取某些处理,恢复程序的正常执行。显然,这样编制出来的程序显得更加可靠。 7 | 8 | Java的异常控制机制与大多数程序设计语言都有所不同。因为在Java中,异常控制模块是从一开始就封装好的,所以必须使用它!如果没有自己写一些代码来正确地控制异常,就会得到一条编译期出错提示。这样可保证程序的连贯性,使错误控制变得更加容易。 9 | 注意异常控制并不属于一种面向对象的特性,尽管在面向对象的程序设计语言中,异常通常是用一个对象表示的。早在面向对象语言问世以前,异常控制就已经存在了。 10 | -------------------------------------------------------------------------------- /1.9.md: -------------------------------------------------------------------------------- 1 | # 1.9 多线程 2 | 3 | 在计算机编程中,一个基本的概念就是同时对多个任务加以控制。许多程序设计问题都要求程序能够停下手头的工作,改为处理其他一些问题,再返回主进程。可以通过多种途径达到这个目的。最开始的时候,那些拥有机器低级知识的程序员编写一些“中断服务例程”,主进程的暂停是通过硬件级的中断实现的。尽管这是一种有用的方法,但编出的程序很难移植,由此造成了另一类的代价高昂问题。 4 | 5 | 有些时候,中断对那些实时性很强的任务来说是很有必要的。但还存在其他许多问题,它们只要求将问题划分进入独立运行的程序片断中,使整个程序能更迅速地响应用户的请求。在一个程序中,这些独立运行的片断叫作“线程”(`Thread`),利用它编程的概念就叫作“多线程处理”。多线程处理一个常见的例子就是用户界面。利用线程,用户可按下一个按钮,然后程序会立即作出响应,而不是让用户等待程序完成了当前任务以后才开始响应。 6 | 7 | 最开始,线程只是用于分配单个处理器的处理时间的一种工具。但假如操作系统本身支持多个处理器,那么每个线程都可分配给一个不同的处理器,真正进入“并行运算”状态。从程序设计语言的角度看,多线程操作最有价值的特性之一就是程序员不必关心到底使用了多少个处理器。程序在逻辑意义上被分割为数个线程;假如机器本身安装了多个处理器,那么程序会运行得更快,毋需作出任何特殊的调校。 8 | 9 | 根据前面的论述,大家可能感觉线程处理非常简单。但必须注意一个问题:共享资源!如果有多个线程同时运行,而且它们试图访问相同的资源,就会遇到一个问题。举个例子来说,两个进程不能将信息同时发送给一台打印机。为解决这个问题,对那些可共享的资源来说(比如打印机),它们在使用期间必须进入锁定状态。所以一个线程可将资源锁定,在完成了它的任务后,再解开(释放)这个锁,使其他线程可以接着使用同样的资源。 10 | 11 | Java的多线程机制已内建到语言中,这使一个可能较复杂的问题变得简单起来。对多线程处理的支持是在对象这一级支持的,所以一个执行线程可表达为一个对象。Java也提供了有限的资源锁定方案。它能锁定任何对象占用的内存(内存实际是多种共享资源的一种),所以同一时间只能有一个线程使用特定的内存空间。为达到这个目的,需要使用`synchronized`关键字。其他类型的资源必须由程序员明确锁定,这通常要求程序员创建一个对象,用它代表一把锁,所有线程在访问那个资源时都必须检查这把锁。 12 | -------------------------------------------------------------------------------- /1.md: -------------------------------------------------------------------------------- 1 | # 第1章 对象入门 2 | 3 | “为什么面向对象的编程会在软件开发领域造成如此震憾的影响?” 4 | 5 | 面向对象编程(OOP)具有多方面的吸引力。对管理人员,它实现了更快和更廉价的开发与维护过程。对分析与设计人员,建模处理变得更加简单,能生成清晰、易于维护的设计模式。对程序员,对象模型显得如此高雅和浅显。此外,面向对象工具以及库的巨大威力使编程成为一项更使人愉悦的任务。每个人都可从中获益,至少表面如此。 6 | 7 | 如果说它有缺点,那就是掌握它需付出的代价。思考对象的时候,需要采用形象思维,而不是程序化的思维。与程序化设计相比,对象的设计过程更具挑战性——特别是在尝试创建可重复使用(可复用)的对象时。过去,那些初涉面向对象编程领域的人都必须进行一项令人痛苦的选择: 8 | 9 | (1) 选择一种诸如Smalltalk的语言,“出师”前必须掌握一个巨型的库。 10 | 11 | (2) 选择几乎根本没有库的C++(注释①),然后深入学习这种语言,直至能自行编写对象库。 12 | 13 | ①:幸运的是,这一情况已有明显改观。现在有第三方库以及标准的C++库供选用。 14 | 15 | 事实上,很难很好地设计出对象——从而很难设计好任何东西。因此,只有数量相当少的“专家”能设计出最好的对象,然后让其他人享用。对于成功的OOP语言,它们不仅集成了这种语言的语法以及一个编译程序(编译器),而且还有一个成功的开发环境,其中包含设计优良、易于使用的库。所以,大多数程序员的首要任务就是用现有的对象解决自己的应用问题。本章的目标就是向大家揭示出面向对象编程的概念,并证明它有多么简单。 16 | 17 | 本章将向大家解释Java的多项设计思想,并从概念上解释面向对象的程序设计。但要注意在阅读完本章后,并不能立即编写出全功能的Java程序。所有详细的说明和示例会在本书的其他章节慢慢道来。 18 | -------------------------------------------------------------------------------- /10.1.md: -------------------------------------------------------------------------------- 1 | # 10.1 输入和输出 2 | 3 | 可将Java库的IO类分割为输入与输出两个部分,这一点在用Web浏览器阅读联机Java类文档时便可知道。通过继承,从`InputStream`(输入流)派生的所有类都拥有名为`read()`的基本方法,用于读取单个字节或者字节数组。类似地,从`OutputStream`派生的所有类都拥有基本方法`write()`,用于写入单个字节或者字节数组。然而,我们通常不会用到这些方法;它们之所以存在,是因为更复杂的类可以利用它们,以便提供一个更有用的接口。因此,我们很少用单个类创建自己的系统对象。一般情况下,我们都是将多个对象重叠在一起,提供自己期望的功能。我们之所以感到Java的流库(Stream Library)异常复杂,正是由于为了创建单独一个结果流,却需要创建多个对象的缘故。 4 | 5 | 很有必要按照功能对类进行分类。库的设计者首先决定与输入有关的所有类都从`InputStream`继承,而与输出有关的所有类都从`OutputStream`继承。 6 | 7 | ## 10.1.1 `InputStream`的类型 8 | 9 | `InputStream`的作用是标志那些从不同起源地产生输入的类。这些起源地包括(每个都有一个相关的`InputStream`子类): 10 | 11 | (1) 字节数组 12 | 13 | (2) `String`对象 14 | 15 | (3) 文件 16 | 17 | (4) “管道”,它的工作原理与现实生活中的管道类似:将一些东西置入一端,它们在另一端出来。 (5) 一系列其他流,以便我们将其统一收集到单独一个流内。 18 | 19 | (6) 其他起源地,如Internet连接等(将在本书后面的部分讲述)。 20 | 21 | 除此以外,`FilterInputStream`也属于`InputStream`的一种类型,用它可为“析构器”类提供一个基类,以便将属性或者有用的接口同输入流连接到一起。这将在以后讨论。 22 | 23 | ``` 24 | Class 25 | 26 | Function 27 | 28 | Constructor Arguments 29 | 30 | How to use it 31 | 32 | ByteArray-InputStream 33 | 34 | Allows a buffer in memory to be used as an InputStream. 35 | 36 | The buffer from which to extract the bytes. 37 | 38 | As a source of data. Connect it to a FilterInputStream object to provide a useful interface. 39 | 40 | StringBuffer-InputStream 41 | 42 | Converts a String into an InputStream. 43 | 44 | A String. The underlying implementation actually uses a StringBuffer. 45 | 46 | As a source of data. Connect it to a FilterInputStream object to provide a useful interface. 47 | 48 | File-InputStream 49 | 50 | For reading information from a file. 51 | 52 | A String representing the file name, or a File or FileDescriptor object. 53 | 54 | As a source of data. Connect it to a FilterInputStream object to provide a useful interface. 55 | ``` 56 | 57 | 58 | | 类 | 功能 | 构造器参数/如何使用 | 59 | | --- | --- | --- | 60 | | `ByteArrayInputStream |` 允许内存中的一个缓冲区作为`InputStream`使用 | 从中提取字节的缓冲区/作为一个数据源使用。通过将其同一个`FilterInputStream`对象连接,可提供一个有用的接口 | 61 | | `StringBufferInputStream` | 将一个`String`转换成`InputStream` | 一个`String`(字符串)。基础的实现方案实际采用一个 | 62 | | `StringBuffer`(字符串缓冲)/作为一个数据源使用。 | 通过将其同一个FilterInputStream对象连接,可提供一个有用的接口 | 63 | | `FileInputStream` | 用于从文件读取信息 | 代表文件名的一个`String`,或者一个`File`或`FileDescriptor`对象/作为一个数据源使用。通过将其同一个`FilterInputStream`对象连接,可提供一个有用的接口 | 64 | 65 | 66 | ``` 67 | Piped-InputStream 68 | 69 | Produces the data that’s being written to the associated PipedOutput-Stream. Implements the “piping” concept. 70 | 71 | PipedOutputStream 72 | 73 | As a source of data in multithreading. Connect it to a FilterInputStream object to provide a useful interface. 74 | 75 | Sequence-InputStream 76 | 77 | Coverts two or more InputStream objects into a single InputStream. 78 | 79 | Two InputStream objects or an Enumeration for a container of InputStream objects. 80 | 81 | As a source of data. Connect it to a FilterInputStream object to provide a useful interface. 82 | 83 | Filter-InputStream 84 | 85 | Abstract class which is an interface for decorators that provide useful functionality to the other InputStream classes. See Table 10-3. 86 | 87 | See Table 10-3. 88 | 89 | See Table 10-3. 90 | ``` 91 | 92 | | 类 | 功能 | 构造器参数/如何使用 | 93 | | --- | --- | --- | 94 | | `PipedInputString` | 产生为相关的`PipedOutputStream`写的数据。实现了“管道化”的概念 | `PipedOutputStream`/作为一个数据源使用。通过将其同一个`FilterInputStream`对象连接,可提供一个有用的接口 | 95 | | `SequenceInputStream` | 将两个或更多的`InputStream`对象转换成单个`InputStream`使用 | 两个`InputStream`对象或者一个`Enumeration`,用于`InputStream`对象的一个容器/作为一个数据源使用。通过将其同一个`FilterInputStream`对象连接,可提供一个有用的接口 | 96 | | `FilterInputStream` | 对作为析构器接口使用的类进行抽象;那个析构器为其他`InputStream`类提供了有用的功能。参见表10.3 | 参见表10.3/参见表10.3 | 97 | 98 | ## 10.1.2 `OutputStream`的类型 99 | 100 | 这一类别包括的类决定了我们的输入往何处去:一个字节数组(但没有`String`;假定我们可用字节数组创建一个);一个文件;或者一个“管道”。 101 | 102 | 除此以外,`FilterOutputStream`为“析构器”类提供了一个基类,它将属性或者有用的接口同输出流连接起来。这将在以后讨论。 103 | 104 | 表10.2 `OutputStream`的类型 105 | 106 | ``` 107 | Class 108 | 109 | Function 110 | 111 | Constructor Arguments 112 | 113 | How to use it 114 | 115 | ByteArray-OutputStream 116 | 117 | Creates a buffer in memory. All the data that you send to the stream is placed in this buffer. 118 | 119 | Optional initial size of the buffer. 120 | 121 | To designate the destination of your data. Connect it to a FilterOutputStream object to provide a useful interface. 122 | 123 | File-OutputStream 124 | 125 | For sending information to a file. 126 | 127 | A String representing the file name, or a File or FileDescriptor object. 128 | 129 | To designate the destination of your data. Connect it to a FilterOutputStream object to provide a useful interface. 130 | 131 | Piped-OutputStream 132 | 133 | Any information you write to this automatically ends up as input for the associated PipedInput-Stream. Implements the “piping” concept. 134 | 135 | PipedInputStream 136 | 137 | To designate the destination of your data for multithreading. Connect it to a FilterOutputStream object to provide a useful interface. 138 | 139 | Filter-OutputStream 140 | 141 | Abstract class which is an interface for decorators that provide useful functionality to the other OutputStream classes. See Table 142 | 10-4. 143 | 144 | See Table 10-4. 145 | 146 | See Table 10-4. 147 | ``` 148 | 149 | | 类 | 功能 | 构造器参数 / 如何使用 | 150 | | --- | --- | --- | --- | 151 | | `ByteArrayOutputStream` | 在内存中创建一个缓冲区。我们发送给流的所有数据都会置入这个缓冲区。| 可选缓冲区的初始大小 / 用于指出数据的目的地。若将其同`FilterOutputStream`对象连接到一起,可提供一个有用的接口 | 152 | | `FileOutputStream` | 将信息发给一个文件 | 用一个String代表文件名,或选用一个`File`或`FileDescriptor`对象 / 用于指出数据的目的地。若将其同`FilterOutputStream`对象连接到一起,可提供一个有用的接口 | 153 | | `PipedOutputStream ` | 我们写给它的任何信息都会自动成为相关的`PipedInputStream`的输出。实现了“管道化”的概念 | `PipedInputStream`/为多线程处理指出自己数据的目的地 / 将其同`FilterOutputStream`对象连接到一起,便可提供一个有用的接口 154 | | `FilterOutputStream` | 对作为析构器接口使用的类进行抽象处理;那个析构器为其他`OutputStream`类提供了有用的功能。参见表10.4 | 参见表10.4 | 155 | -------------------------------------------------------------------------------- /10.10.md: -------------------------------------------------------------------------------- 1 | # 10.10 总结 2 | 3 | Java IO流库能满足我们的许多基本要求:可以通过控制台、文件、内存块甚至因特网(参见第15章)进行读写。可以创建新的输入和输出对象类型(通过从`InputStream`和`OutputStream`继承)。向一个本来预期为收到字符串的方法传递一个对象时,由于Java已限制了“自动类型转换”,所以会自动调用`toString()`方法。而我们可以重新定义这个`toString()`,扩展一个数据流能接纳的对象种类。 4 | 5 | 在IO数据流库的联机文档和设计过程中,仍有些问题没有解决。比如当我们打开一个文件以便输出时,完全可以指定一旦有人试图覆盖该文件就“抛”出一个异常——有的编程系统允许我们自行指定想打开一个输出文件,但唯一的前提是它尚不存在。但在Java中,似乎必须用一个`File`对象来判断某个文件是否存在,因为假如将其作为`FileOutputStream`或者`FileWriter`打开,那么肯定会被覆盖。若同时指定文件和目录路径,`File`类设计上的一个缺陷就会暴露出来,因为它会说“不要试图在单个类里做太多的事情”! 6 | 7 | IO流库易使我们混淆一些概念。它确实能做许多事情,而且也可以移植。但假如假如事先没有吃透装饰器方案的概念,那么所有的设计都多少带有一点盲目性质。所以不管学它还是教它,都要特别花一些功夫才行。而且它并不完整:没有提供对输出格式化的支持,而其他几乎所有语言的IO包都提供了这方面的支持(这一点没有在Java 1.1里得以纠正,它完全错失了改变库设计模式的机会,反而增添了更特殊的一些情况,使复杂程度进一步提高)。Java 1.1转到那些尚未替换的IO库,而不是增加新库。而且库的设计人员似乎没有很好地指出哪些特性是不赞成的,哪些是首选的,造成库设计中经常都会出现一些令人恼火的反对消息。 8 | 9 | 然而,一旦掌握了装饰器方案,并开始在一些较为灵活的环境使用库,就会认识到这种设计的好处。到那个时候,为此多付出的代码行应该不至于使你觉得太生气。 10 | -------------------------------------------------------------------------------- /10.11.md: -------------------------------------------------------------------------------- 1 | # 10.11 练习 2 | 3 | (1) 打开一个文本文件,每次读取一行内容。将每行作为一个`String`读入,并将那个`String`对象置入一个`Vector`里。按相反的顺序打印出`Vector`中的所有行。 4 | 5 | (2) 修改练习1,使读取那个文件的名字作为一个命令行参数提供。 6 | 7 | (3) 修改练习2,又打开一个文本文件,以便将文字写入其中。将`Vector`中的行随同行号一起写入文件。 8 | 9 | (4) 修改练习2,强迫`Vector`中的所有行都变成大写形式,将结果发给`System.out`。 10 | 11 | (5) 修改练习2,在文件中查找指定的单词。打印出包含了欲找单词的所有文本行。 12 | 13 | (6) 在`Blips.java`中复制文件,将其重命名为`BlipCheck.java`。然后将类`Blip2`重命名为`BlipCheck`(在进程中将其标记为`public`)。删除文件中的`//!`记号,并执行程序。接下来,将`BlipCheck`的默认构造器变成注释信息。运行它,并解释为什么仍然能够工作。 14 | 15 | (7) 在`Blip3.java`中,将接在`"You must do this:"`字样后的两行变成注释,然后运行程序。解释得到的结果为什么会与执行了那两行代码不同。 16 | 17 | (8) 转换`SortedWordCount.java`程序,以便使用Java 1.1 IO流。 18 | 19 | (9) 根据本章正文的说明修改程序`CADState.java`。 20 | 21 | (10) 在第7章(中间部分)找到`GreenhouseControls.java`示例,它应该由三个文件构成。在`GreenhouseControls.java`中,`Restart()`内部类有一个硬编码的事件集。请修改这个程序,使其能从一个文本文件里动态读取事件以及它们的相关时间。 22 | -------------------------------------------------------------------------------- /10.2.md: -------------------------------------------------------------------------------- 1 | # 10.2 增添属性和有用的接口 2 | 3 | 4 | 利用层次化对象动态和透明地添加单个对象的能力的做法叫作“装饰器”(Decorator)方案——“方案”属于本书第16章的主题(注释①)。装饰器方案规定封装于初始化对象中的所有对象都拥有相同的接口,以便利用装饰器的“透明”性质——我们将相同的消息发给一个对象,无论它是否已被“装饰”。这正是在Java IO库里存在“过滤器”(Filter)类的原因:抽象的“过滤器”类是所有装饰器的基类(装饰器必须拥有与它装饰的那个对象相同的接口,但装饰器亦可对接口作出扩展,这种情况见诸于几个特殊的“过滤器”类中)。 5 | 6 | 子类处理要求大量子类对每种可能的组合提供支持时,便经常会用到装饰器——由于组合形式太多,造成子类处理变得不切实际。Java IO库要求许多不同的特性组合方案,这正是装饰器方案显得特别有用的原因。但是,装饰器方案也有自己的一个缺点。在我们写一个程序的时候,装饰器为我们提供了大得多的灵活性(因为可以方便地混合与匹配属性),但它们也使自己的代码变得更加复杂。原因在于Java IO库操作不便,我们必须创建许多类——“核心”IO类型加上所有装饰器——才能得到自己希望的单个IO对象。 7 | 8 | `FilterInputStream`和`FilterOutputStream`(这两个名字不十分直观)提供了相应的装饰器接口,用于控制一个特定的输入流(`InputStream`)或者输出流(`OutputStream`)。它们分别是从`InputStream`和`OutputStream`派生出来的。此外,它们都属于抽象类,在理论上为我们与一个流的不同通信手段都提供了一个通用的接口。事实上,`FilterInputStream`和`FilterOutputStream`只是简单地模仿了自己的基类,它们是一个装饰器的基本要求。 9 | 10 | ## 10.2.1 通过`FilterInputStream`从`InputStream`里读入数据 11 | 12 | `FilterInputStream`类要完成两件全然不同的事情。其中,`DataInputStream`允许我们读取不同的基本类型数据以及`String`对象(所有方法都以`read`开头,比如`readByte()`,`readFloat()`等等)。伴随对应的`DataOutputStream`,我们可通过数据“流”将基本类型的数据从一个地方搬到另一个地方。这些“地方”是由表10.1总结的那些类决定的。若读取块内的数据,并自己进行解析,就不需要用到`DataInputStream`。但在其他许多情况下,我们一般都想用它对自己读入的数据进行自动格式化。 13 | 14 | 剩下的类用于修改`InputStream`的内部行为方式:是否进行缓冲,是否跟踪自己读入的数据行,以及是否能够推回一个字符等等。后两种类看起来特别象提供对构建一个编译器的支持(换言之,添加它们为了支持Java编译器的构建),所以在常规编程中一般都用不着它们。 15 | 16 | 也许几乎每次都要缓冲自己的输入,无论连接的是哪个IO设备。所以IO库最明智的做法就是将未缓冲输入作为一种特殊情况处理,同时将缓冲输入接纳为标准做法。 17 | 18 | 表10.3 `FilterInputStream`的类型 19 | 20 | ``` 21 | Class 22 | 23 | Function 24 | 25 | Constructor Arguments 26 | 27 | How to use it 28 | 29 | Data-InputStream 30 | 31 | Used in concert with DataOutputStream, so you can read primitives (int, char, long, etc.) from a stream in a portable fashion. 32 | 33 | InputStream 34 | 35 | Contains a full interface to allow you to read primitive types. 36 | 37 | 38 | Buffered-InputStream 39 | 40 | Use this to prevent a physical read every time you want more data. You’re saying “Use a buffer.” 41 | 42 | InputStream, with optional buffer size. 43 | 44 | This doesn’t provide an interface per se, just a requirement that a buffer be used. Attach an interface object. 45 | 46 | LineNumber-InputStream 47 | 48 | Keeps track of line numbers in the input stream; you can call getLineNumber( ) and setLineNumber(int). 49 | 50 | InputStream 51 | 52 | This just adds line numbering, so you’ll probably attach an interface object. 53 | 54 | Pushback-InputStream 55 | 56 | Has a one byte push-back buffer so that you can push back the last character read. 57 | 58 | InputStream 59 | 60 | Generally used in the scanner for a compiler and probably included because the Java compiler needed it. You probably won’t use this. 61 | ``` 62 | 63 | | 类 | 功能 | 构造器参数/如何使用 64 | | --- | --- | --- | 65 | | `DataInputStream` | 与`DataOutputStream`联合使用,使自己能以机动方式读取一个流中的基本数据类型(`int`,`char`,`long`等等) | `InputStream`/包含了一个完整的接口,以便读取基本数据类型 | 66 | | `BufferedInputStream` | 避免每次想要更多数据时都进行物理性的读取,告诉它“请先在缓冲区里找” | `InputStream`,没有可选的缓冲区大小/本身并不能提供一个接口,只是发出使用缓冲区的要求。要求同一个接口对象连接到一起 | 67 | | `LineNumberInputStream` | 跟踪输入流中的行号;可调用`getLineNumber()`以及`setLineNumber(int) `| 只是添加对数据行编号的能力,所以可能需要同一个真正的接口对象连接 | 68 | | `PushbackInputStream` | 有一个字节的后推缓冲区,以便后推读入的上一个字符 | `InputStream`/通常由编译器在扫描器中使用,因为Java编译器需要它。一般不在自己的代码中使用 | 69 | 70 | ## 10.2.2 通过`FilterOutputStream向OutputStream`里写入数据 71 | 72 | 与`DataInputStream`对应的是`DataOutputStream`,后者对各个基本数据类型以及`String`对象进行格式化,并将其置入一个数据“流”中,以便任何机器上的`DataInputStream`都能正常地读取它们。所有方法都以`wirte`开头,例如`writeByte()`,`writeFloat()`等等。 73 | 74 | 若想进行一些真正的格式化输出,比如输出到控制台,请使用`PrintStrea`m。利用它可以打印出所有基本数据类型以及`String`对象,并可采用一种易于查看的格式。这与`DataOutputStream`正好相反,后者的目标是将那些数据置入一个数据流中,以便`DataInputStream`能够方便地重新构造它们。`System.out`静态对象是一个`PrintStream`。 75 | 76 | `PrintStream`内两个重要的方法是`print()`和`println()`。它们已进行了覆盖处理,可打印出所有数据类型。`print()`和`println()`之间的差异是后者在操作完毕后会自动添加一个新行。 77 | 78 | `BufferedOutputStream`属于一种“修改器”,用于指示数据流使用缓冲技术,使自己不必每次都向流内物理性地写入数据。通常都应将它应用于文件处理和控制器IO。 79 | 80 | 表10.4 `FilterOutputStream`的类型 81 | 82 | ``` 83 | Class 84 | 85 | Function 86 | 87 | Constructor Arguments 88 | 89 | How to use it 90 | 91 | Data-OutputStream 92 | 93 | Used in concert with DataInputStream so you can write primitives (int, char, long, etc.) to a stream in a portable fashion. 94 | 95 | OutputStream 96 | 97 | Contains full interface to allow you to write primitive types. 98 | 99 | PrintStream 100 | 101 | For producing formatted output. While DataOutputStream handles the storage of data, PrintStream handles display. 102 | 103 | OutputStream, with optional boolean indicating that the buffer is flushed with every newline. 104 | 105 | Should be the “final” wrapping for your OutputStream object. You’ll probably use this a lot. 106 | 107 | Buffered-OutputStream 108 | 109 | Use this to prevent a physical write every time you send a piece of data. You’re saying “Use a buffer.” You can call flush( ) to flush the buffer. 110 | 111 | OutputStream, with optional buffer size. 112 | 113 | This doesn’t provide an interface per se, just a requirement that a buffer is used. Attach an interface object. 114 | ``` 115 | 116 | | 类 | 功能 | 构造器参数/如何使用 | 117 | | --- | --- | --- | 118 | | `DataOutputStream` | 与`DataInputStream`配合使用,以便采用方便的形式将基本数据类型(`int`,`char`,`long`等)写入一个数据流 | `OutputStream`/包含了完整接口,以便我们写入基本数据类型 | 119 | | `PrintStream` | 用于产生格式化输出。 | `DataOutputStream`控制的是数据的“存储”,而`PrintStream`控制的是“显示” | 120 | | `OutputStream` | | 可选一个布尔参数,指示缓冲区是否与每个新行一同刷新/对于自己的OutputStream对象,应该用`final`将其封闭在内。可能经常都要用到它 | 121 | | `BufferedOutputStream` | 用它避免每次发出数据的时候都要进行物理性的写入,要求它“请先在缓冲区里找”。可调用`flush()`,对缓冲区进行刷新 | `OutputStream`,可选缓冲区大小/本身并不能提供一个接口,只是发出使用缓冲区的要求。需要同一个接口对象连接到一起 | 122 | -------------------------------------------------------------------------------- /10.3.md: -------------------------------------------------------------------------------- 1 | # 10.3 本身的缺陷:`RandomAccessFile` 2 | 3 | `RandomAccessFile`用于包含了已知长度记录的文件,以便我们能用`seek(`)从一条记录移至另一条;然后读取或修改那些记录。各记录的长度并不一定相同;只要知道它们有多大以及置于文件何处即可。 4 | 5 | 首先,我们有点难以相信`RandomAccessFile`不属于`InputStream`或者`OutputStream`分层结构的一部分。除了恰巧实现了`DataInput`以及`DataOutput`(这两者亦由`DataInputStream`和`DataOutputStream`实现)接口之外,它们与那些分层结构并无什么关系。它甚至没有用到现有`InputStream`或`OutputStream`类的功能——采用的是一个完全不相干的类。该类属于全新的设计,含有自己的全部(大多数为固有)方法。之所以要这样做,是因为`RandomAccessFile`拥有与其他IO类型完全不同的行为,因为我们可在一个文件里向前或向后移动。不管在哪种情况下,它都是独立运作的,作为`Object`的一个“直接继承人”使用。 6 | 7 | 从根本上说,`RandomAccessFile`类似`DataInputStream`和`DataOutputStream`的联合使用。其中,`getFilePointer()`用于了解当前在文件的什么地方,`seek()`用于移至文件内的一个新地点,而`length()`用于判断文件的最大长度。此外,构造器要求使用另一个参数(与C的`fopen()`完全一样),指出自己只是随机读(`"r"`),还是读写兼施(`"rw"`)。这里没有提供对“只写文件”的支持。也就是说,假如是从`DataInputStream`继承的,那么`RandomAccessFile`也有可能能很好地工作。 8 | 9 | 还有更难对付的。很容易想象我们有时要在其他类型的数据流中搜索,比如一个`ByteArrayInputStream`,但搜索方法只有`RandomAccessFile`才会提供。而后者只能针对文件才能操作,不能针对数据流操作。此时,`BufferedInputStream`确实允许我们标记一个位置(使用`mark()`,它的值容纳于单个内部变量中),并用`reset()`重设那个位置。但这些做法都存在限制,并不是特别有用。 10 | -------------------------------------------------------------------------------- /10.md: -------------------------------------------------------------------------------- 1 | # 第10章 Java IO系统 2 | 3 | 4 | “对语言设计人员来说,创建好的输入/输出系统是一项特别困难的任务。” 5 | 6 | 由于存在大量不同的设计模式,所以该任务的困难性是很容易证明的。其中最大的挑战似乎是如何覆盖所有可能的因素。不仅有三种不同的种类的IO需要考虑(文件、控制台、网络连接),而且需要通过大量不同的方式与它们通信(顺序、随机访问、二进制、字符、按行、按字等等)。 7 | 8 | Java库的设计者通过创建大量类来攻克这个难题。事实上,Java的IO系统采用了如此多的类,以致刚开始会产生不知从何处入手的感觉(具有讽刺意味的是,Java的IO设计初衷实际要求避免过多的类)。从Java 1.0升级到Java 1.1后,IO库的设计也发生了显著的变化。此时并非简单地用新库替换旧库,Sun的设计人员对原来的库进行了大手笔的扩展,添加了大量新的内容。因此,我们有时不得不混合使用新库与旧库,产生令人无奈的复杂代码。 9 | 10 | 本章将帮助大家理解标准Java库内的各种IO类,并学习如何使用它们。本章的第一部分将介绍“旧”的Java 1.0 IO流库,因为现在有大量代码仍在使用那个库。本章剩下的部分将为大家引入Java 1.1 IO库的一些新特性。注意若用Java 1.1编译器来编译本章第一部分介绍的部分代码,可能会得到一条“不建议使用该特性”(Deprecated feature)警告消息。代码仍然能够使用;编译器只是建议我们换用本章后面要讲述的一些新特性。但我们这样做是有价值的,因为可以更清楚地认识老方法与新方法之间的一些差异,从而加深我们的理解(并可顺利阅读为Java 1.0写的代码)。 11 | -------------------------------------------------------------------------------- /11-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/11-1.gif -------------------------------------------------------------------------------- /11.2.md: -------------------------------------------------------------------------------- 1 | # 11.2 RTTI语法 2 | 3 | 4 | Java用`Class`对象实现自己的RTTI功能——即便我们要做的只是象转换那样的一些工作。`Class`类也提供了其他大量方式,以方便我们使用RTTI。 5 | 6 | 首先必须获得指向适当`Class`对象的的一个引用。就象前例演示的那样,一个办法是用一个字符串以及`Class.forName()`方法。这是非常方便的,因为不需要那种类型的一个对象来获取`Class`引用。然而,对于自己感兴趣的类型,如果已有了它的一个对象,那么为了取得`Class`引用,可调用属于`Object`根类一部分的一个方法:`getClass()`。它的作用是返回一个特定的`Class`引用,用来表示对象的实际类型。`Class`提供了几个有趣且较为有用的方法,从下例即可看出: 7 | 8 | ``` 9 | //: ToyTest.java 10 | // Testing class Class 11 | 12 | interface HasBatteries {} 13 | interface Waterproof {} 14 | interface ShootsThings {} 15 | class Toy { 16 | // Comment out the following default 17 | // constructor to see 18 | // NoSuchMethodError from (*1*) 19 | Toy() {} 20 | Toy(int i) {} 21 | } 22 | 23 | class FancyToy extends Toy 24 | implements HasBatteries, 25 | Waterproof, ShootsThings { 26 | FancyToy() { super(1); } 27 | } 28 | 29 | public class ToyTest { 30 | public static void main(String[] args) { 31 | Class c = null; 32 | try { 33 | c = Class.forName("FancyToy"); 34 | } catch(ClassNotFoundException e) {} 35 | printInfo(c); 36 | Class[] faces = c.getInterfaces(); 37 | for(int i = 0; i < faces.length; i++) 38 | printInfo(faces[i]); 39 | Class cy = c.getSuperclass(); 40 | Object o = null; 41 | try { 42 | // Requires default constructor: 43 | o = cy.newInstance(); // (*1*) 44 | } catch(InstantiationException e) {} 45 | catch(IllegalAccessException e) {} 46 | printInfo(o.getClass()); 47 | } 48 | static void printInfo(Class cc) { 49 | System.out.println( 50 | "Class name: " + cc.getName() + 51 | " is interface? [" + 52 | cc.isInterface() + "]"); 53 | } 54 | } ///:~ 55 | ``` 56 | 57 | 从中可以看出,`class FancyToy`相当复杂,因为它从`Toy`中继承,并实现了`HasBatteries`,`Waterproof`以及`ShootsThings`的接口。在`main()`中创建了一个`Class`引用,并用位于相应`try`块内的`forName()`初始化成`FancyToy`。 58 | 59 | `Class.getInterfaces`方法会返回`Class`对象的一个数组,用于表示包含在`Class`对象内的接口。 60 | 61 | 若有一个`Class`对象,也可以用`getSuperclass()`查询该对象的直接基类是什么。当然,这种做会返回一个`Class`引用,可用它作进一步的查询。这意味着在运行期的时候,完全有机会调查到对象的完整层次结构。 62 | 63 | 若从表面看,`Class`的`newInstance()`方法似乎是克隆(`clone()`)一个对象的另一种手段。但两者是有区别的。利用`newInstance()`,我们可在没有现成对象供“克隆”的情况下新建一个对象。就象上面的程序演示的那样,当时没有`Toy`对象,只有`cy`——即`y`的`Class`对象的一个引用。利用它可以实现“虚拟构造器”。换言之,我们表达:“尽管我不知道你的准确类型是什么,但请你无论如何都正确地创建自己。”在上述例子中,`cy`只是一个`Class`引用,编译期间并不知道进一步的类型信息。一旦新建了一个实例后,可以得到`Object`引用。但那个引用指向一个`Toy`对象。当然,如果要将除`Object`能够接收的其他任何消息发出去,首先必须进行一些调查研究,再进行转换。除此以外,用`newInstance()`创建的类必须有一个默认构造器。没有办法用`newInstance()`创建拥有非默认构造器的对象,所以在Java 1.0中可能存在一些限制。然而,Java 1.1的“反射”API(下一节讨论)却允许我们动态地使用类里的任何构造器。 64 | 65 | 程序中的最后一个方法是`printInfo()`,它取得一个`Class`引用,通过`getName()`获得它的名字,并用`interface()`调查它是不是一个接口。 66 | 67 | 该程序的输出如下: 68 | 69 | ``` 70 | Class name: FancyToy is interface? [false] 71 | Class name: HasBatteries is interface? [true] 72 | Class name: Waterproof is interface? [true] 73 | Class name: ShootsThings is interface? [true] 74 | Class name: Toy is interface? [false] 75 | ``` 76 | 77 | 所以利用`Class`对象,我们几乎能将一个对象的祖宗十八代都调查出来。 78 | -------------------------------------------------------------------------------- /11.4.md: -------------------------------------------------------------------------------- 1 | # 11.4 总结 2 | 3 | 利用RTTI可根据一个匿名的基类引用调查出类型信息。但正是由于这个原因,新手们极易误用它,因为有些时候多态性方法便足够了。对那些以前习惯程序化编程的人来说,极易将他们的程序组织成一系列`switch`语句。他们可能用RTTI做到这一点,从而在代码开发和维护中损失多态性技术的重要价值。Java的要求是让我们尽可能地采用多态性,只有在极特别的情况下才使用RTTI。 4 | 5 | 但为了利用多态性,要求我们拥有对基类定义的控制权,因为有些时候在程序范围之内,可能发现基类并未包括我们想要的方法。若基类来自一个库,或者由别的什么东西控制着,RTTI便是一种很好的解决方案:可继承一个新类型,然后添加自己的额外方法。在代码的其他地方,可以侦测自己的特定类型,并调用那个特殊的方法。这样做不会破坏多态性以及程序的扩展能力,因为新类型的添加不要求查找程序中的`switch`语句。但在需要新特性的主体中添加新代码时,就必须用RTTI侦测自己特定的类型。 6 | 7 | 从某个特定类的利益的角度出发,在基类里加入一个特性后,可能意味着从那个基类派生的其他所有类都必须获得一些无意义的“鸡肋”。这使得接口变得含义模糊。若有人从那个基类继承,且必须覆盖抽象方法,这一现象便会使他们陷入困扰。比如现在用一个类结构来表示乐器(`Instrument`)。假定我们想清洁管弦乐队中所有适当乐器的通气音栓(Spit Valve),此时的一个办法是在基类`Instrument`中置入一个`ClearSpitValve()`方法。但这样做会造成一个误区,因为它暗示着打击乐器和电子乐器中也有音栓。针对这种情况,RTTI提供了一个更合理的解决方案,可将方法置入特定的类中(此时是`Wind`,即“通气口”)——这样做是可行的。但事实上一种更合理的方案是将`prepareInstrument()`置入基类中。初学者刚开始时往往看不到这一点,一般会认定自己必须使用RTTI。 8 | 9 | 最后,RTTI有时能解决效率问题。若代码大量运用了多态性,但其中的一个对象在执行效率上很有问题,便可用RTTI找出那个类型,然后写一段适当的代码,改进其效率。 10 | -------------------------------------------------------------------------------- /11.5.md: -------------------------------------------------------------------------------- 1 | # 11.5 练习 2 | 3 | (1) 写一个方法,向它传递一个对象,循环打印出对象层次结构中的所有类。 4 | 5 | (2) 在`ToyTest.java`中,将`Toy`的默认构造器标记成注释信息,解释随之发生的事情。 6 | 7 | (3) 新建一种类型的集合,令其使用一个`Vector`。捕获置入其中的第一个对象的类型,然后从那时起只允许用户插入那种类型的对象。 8 | 9 | (4) 写一个程序,判断一个`Char`数组属于基本数据类型,还是一个真正的对象。 10 | 11 | (5) 根据本章的说明,实现`clearSpitValve()`。 12 | 13 | (6) 实现本章介绍的`rotate(Shape)`方法,令其检查是否已经旋转了一个圆(若已旋转,就不再执行旋转操作)。 14 | -------------------------------------------------------------------------------- /11.md: -------------------------------------------------------------------------------- 1 | # 第11章 运行期类型识别 2 | 3 | 4 | 运行期类型识别(RTTI)的概念初看非常简单——手上只有基类型的一个引用时,利用它判断一个对象的正确类型。 5 | 6 | 然而,对RTTI的需要暴露出了面向对象设计许多有趣(而且经常是令人困惑的)的问题,并把程序的构造问题正式摆上了桌面。 7 | 8 | 本章将讨论如何利用Java在运行期间查找对象和类信息。这主要采取两种形式:一种是“传统”RTTI,它假定我们已在编译和运行期拥有所有类型;另一种是Java1.1特有的“反射”机制,利用它可在运行期独立查找类信息。首先讨论“传统”的RTTI,再讨论反射问题。 9 | -------------------------------------------------------------------------------- /12.1.md: -------------------------------------------------------------------------------- 1 | # 12.1 传递引用 2 | 3 | 将引用传递进入一个方法时,指向的仍然是相同的对象。一个简单的实验可以证明这一点(若执行这个程序时有麻烦,请参考第3章3.1.2小节“赋值”): 4 | 5 | ``` 6 | //: PassHandles.java 7 | // Passing handles around 8 | package c12; 9 | 10 | public class PassHandles { 11 | static void f(PassHandles h) { 12 | System.out.println("h inside f(): " + h); 13 | } 14 | public static void main(String[] args) { 15 | PassHandles p = new PassHandles(); 16 | System.out.println("p inside main(): " + p); 17 | f(p); 18 | } 19 | } ///:~ 20 | ``` 21 | 22 | `toString`方法会在打印语句里自动调用,而`PassHandles`直接从`Object`继承,没有`toString`的重新定义。因此,这里会采用`toString`的`Object`版本,打印出对象的类,接着是那个对象所在的位置(不是引用,而是对象的实际存储位置)。输出结果如下: 23 | 24 | ``` 25 | p inside main(): PassHandles@1653748 26 | h inside f() : PassHandles@1653748 27 | ``` 28 | 29 | 可以看到,无论`p`还是`h`引用的都是同一个对象。这比复制一个新的`PassHandles`对象有效多了,使我们能将一个参数发给一个方法。但这样做也带来了另一个重要的问题。 30 | 31 | ## 12.1.1 别名问题 32 | 33 | “别名”意味着多个引用都试图指向同一个对象,就象前面的例子展示的那样。若有人向那个对象里写入一点什么东西,就会产生别名问题。若其他引用的所有者不希望那个对象改变,恐怕就要失望了。这可用下面这个简单的例子说明: 34 | 35 | ``` 36 | //: Alias1.java 37 | // Aliasing two handles to one object 38 | 39 | public class Alias1 { 40 | int i; 41 | Alias1(int ii) { i = ii; } 42 | public static void main(String[] args) { 43 | Alias1 x = new Alias1(7); 44 | Alias1 y = x; // Assign the handle 45 | System.out.println("x: " + x.i); 46 | System.out.println("y: " + y.i); 47 | System.out.println("Incrementing x"); 48 | x.i++; 49 | System.out.println("x: " + x.i); 50 | System.out.println("y: " + y.i); 51 | } 52 | } ///:~ 53 | ``` 54 | 55 | 对下面这行: 56 | 57 | ``` 58 | Alias1 y = x; // Assign the handle 59 | ``` 60 | 61 | 它会新建一个`Alias1`引用,但不是把它分配给由new创建的一个新鲜对象,而是分配给一个现有的引用。所以引用x的内容——即对象`x`指向的地址——被分配给`y`,所以无论`x`还是`y`都与相同的对象连接起来。这样一来,一旦`x`的`i`在下述语句中自增: 62 | 63 | ``` 64 | x.i++; 65 | ``` 66 | 67 | `y`的`i`值也必然受到影响。从最终的输出就可以看出: 68 | 69 | ``` 70 | x: 7 71 | y: 7 72 | Incrementing x 73 | x: 8 74 | y: 8 75 | ``` 76 | 77 | 此时最直接的一个解决办法就是干脆不这样做:不要有意将多个引用指向同一个作用域内的同一个对象。这样做可使代码更易理解和调试。然而,一旦准备将引用作为一个变量或参数传递——这是Java设想的正常方法——别名问题就会自动出现,因为创建的本地引用可能修改“外部对象”(在方法作用域之外创建的对象)。下面是一个例子: 78 | 79 | ``` 80 | //: Alias2.java 81 | // Method calls implicitly alias their 82 | // arguments. 83 | 84 | public class Alias2 { 85 | int i; 86 | Alias2(int ii) { i = ii; } 87 | static void f(Alias2 handle) { 88 | handle.i++; 89 | } 90 | public static void main(String[] args) { 91 | Alias2 x = new Alias2(7); 92 | System.out.println("x: " + x.i); 93 | System.out.println("Calling f(x)"); 94 | f(x); 95 | System.out.println("x: " + x.i); 96 | } 97 | } ///:~ 98 | ``` 99 | 100 | 输出如下: 101 | 102 | ``` 103 | x: 7 104 | Calling f(x) 105 | x: 8 106 | ``` 107 | 108 | 方法改变了自己的参数——外部对象。一旦遇到这种情况,必须判断它是否合理,用户是否愿意这样,以及是不是会造成问题。 109 | 110 | 通常,我们调用一个方法是为了产生返回值,或者用它改变为其调用方法的那个对象的状态(方法其实就是我们向那个对象“发一条消息”的方式)。很少需要调用一个方法来处理它的参数;这叫作利用方法的“副作用”(Side Effect)。所以倘若创建一个会修改自己参数的方法,必须向用户明确地指出这一情况,并警告使用那个方法可能会有的后果以及它的潜在威胁。由于存在这些混淆和缺陷,所以应该尽量避免改变参数。 111 | 112 | 若需在一个方法调用期间修改一个参数,且不打算修改外部参数,就应在自己的方法内部制作一个副本,从而保护那个参数。本章的大多数内容都是围绕这个问题展开的。 113 | -------------------------------------------------------------------------------- /12.5.md: -------------------------------------------------------------------------------- 1 | # 12.5 总结 2 | 3 | 4 | 由于Java中的所有东西都是引用,而且由于每个对象都是在内存堆中创建的——只有不再需要的时候,才会当作垃圾收集掉,所以对象的操作方式发生了变化,特别是在传递和返回对象的时候。举个例子来说,在C和C++中,如果想在一个方法里初始化一些存储空间,可能需要请求用户将那片存储区域的地址传递进入方法。否则就必须考虑由谁负责清除那片区域。因此,这些方法的接口和对它们的理解就显得要复杂一些。但在Java中,根本不必关心由谁负责清除,也不必关心在需要一个对象的时候它是否仍然存在。因为系统会为我们照料一切。我们的程序可在需要的时候创建一个对象。而且更进一步地,根本不必担心那个对象的传输机制的细节:只需简单地传递引用即可。有些时候,这种简化非常有价值,但另一些时候却显得有些多余。 5 | 6 | 可从两个方面认识这一机制的缺点: 7 | 8 | (1) 肯定要为额外的内存管理付出效率上的损失(尽管损失不大),而且对于运行所需的时间,总是存在一丝不确定的因素(因为在内存不够时,垃圾收集器可能会被强制采取行动)。对大多数应用来说,优点显得比缺点重要,而且部分对时间要求非常苛刻的段落可以用`native`方法写成(参见附录A)。 9 | 10 | (2) 别名处理:有时会不慎获得指向同一个对象的两个引用。只有在这两个引用都假定指向一个“明确”的对象时,才有可能产生问题。对这个问题,必须加以足够的重视。而且应该尽可能地“克隆”一个对象,以防止另一个引用被不希望的改动影响。除此以外,可考虑创建“不可变”对象,使它的操作能返回同种类型或不同种类型的一个新对象,从而提高程序的执行效率。但千万不要改变原始对象,使对那个对象别名的其他任何方面都感觉不出变化。 11 | 12 | 有些人认为Java的克隆是一个笨拙的家伙,所以他们实现了自己的克隆方案(注释⑤),永远杜绝调用`Object.clone()`方法,从而消除了实现`Cloneable`和捕获`CloneNotSupportException`异常的需要。这一做法是合理的,而且由于`clone()`在Java标准库中很少得以支持,所以这显然也是一种“安全”的方法。只要不调用`Object.clone()`,就不必实现`Cloneable`或者捕获异常,所以那看起来也是能够接受的。 13 | 14 | ⑤:Doug Lea特别重视这个问题,并把这个方法推荐给了我,他说只需为每个类都创建一个名为`duplicate()`的函数即可。 15 | 16 | Java中一个有趣的关键字是`byvalue`(按值),它属于那些“保留但未实现”的关键字之一。在理解了别名和克隆问题以后,大家可以想象`byvalue`最终有一天会在Java中用于实现一种自动化的本地副本。这样做可以解决更多复杂的克隆问题,并使这种情况下的编写的代码变得更加简单和健壮。 17 | -------------------------------------------------------------------------------- /12.6.md: -------------------------------------------------------------------------------- 1 | # 12.6 练习 2 | 3 | 4 | (1) 创建一个`myString`类,在其中包含了一个`String`对象,以便用在构造器中用构造器的参数对其进行初始化。添加一个`toString()`方法以及一个`concatenate()`方法,令其将一个`String`对象追加到我们的内部字符串。在`myString`中实现`clone()`。创建两个`static`方法,每个都取得一个`myString x`引用作为自己的参数,并调用`x.concatenate("test")`。但在第二个方法中,请首先调用`clone()`。测试这两个方法,观察它们不同的结果。 5 | 6 | (2) 创建一个名为`Battery`(电池)的类,在其中包含一个`int`,用它表示电池的编号(采用独一无二的标识符的形式)。接下来,创建一个名为`Toy`的类,其中包含了一个`Battery`数组以及一个`toString`,用于打印出所有电池。为`Toy`写一个`clone()`方法,令其自动关闭所有`Battery`对象。克隆`Toy`并打印出结果,完成对它的测试。 7 | 8 | (3) 修改`CheckCloneable.java`,使所有`clone()`方法都能捕获`CloneNotSupportException`异常,而不是把它直接传递给调用者。 9 | 10 | (4) 修改`Compete.java`,为`Thing2`和`Thing4`类添加更多的成员对象,看看自己是否能判断计时随复杂性变化的规律——是一种简单的线性关系,还是看起来更加复杂。 11 | 12 | (5) 从`Snake.java`开始,创建`Snake`的一个深层复制版本。 13 | -------------------------------------------------------------------------------- /12.md: -------------------------------------------------------------------------------- 1 | # 第12章 传递和返回对象 2 | 3 | 4 | 到目前为止,读者应对对象的“传递”有了一个较为深刻的认识,记住实际传递的只是一个引用。 5 | 6 | 在许多程序设计语言中,我们可用语言的“普通”方式到处传递对象,而且大多数时候都不会遇到问题。但有些时候却不得不采取一些非常做法,使得情况突然变得稍微复杂起来(在C++中则是变得非常复杂)。Java亦不例外,我们十分有必要准确认识在对象传递和赋值时所发生的一切。这正是本章的宗旨。 7 | 8 | 若读者是从某些特殊的程序设计环境中转移过来的,那么一般都会问到:“Java有指针吗?”有些人认为指针的操作很困难,而且十分危险,所以一厢情愿地认为它没有好处。同时由于Java有如此好的口碑,所以应该很轻易地免除自己以前编程中的麻烦,其中不可能夹带有指针这样的“危险品”。然而准确地说,Java是有指针的!事实上,Java中每个对象(除基本数据类型以外)的标识符都属于指针的一种。但它们的使用受到了严格的限制和防范,不仅编译器对它们有“戒心”,运行期系统也不例外。或者换从另一个角度说,Java有指针,但没有传统指针的麻烦。我曾一度将这种指针叫做“引用”,但你可以把它想像成“安全指针”。和预备学校为学生提供的安全剪刀类似——除非特别有意,否则不会伤着自己,只不过有时要慢慢来,要习惯一些沉闷的工作。 9 | -------------------------------------------------------------------------------- /13.1.md: -------------------------------------------------------------------------------- 1 | # 13.1 为何要用AWT? 2 | 3 | 对于本章要学习的“老式”AWT,它最严重的缺点就是它无论在面向对象设计方面,还是在GUI开发包设计方面,都有不尽如人意的表现。它使我们回到了程序设计的黑暗年代(换成其他话就是“拙劣的”、“可怕的”、“恶劣的”等等)。必须为执行每一个事件编写代码,包括在其他环境中利用“资源”即可轻松完成的一些任务。 4 | 5 | 许多象这样的问题在Java 1.1里都得到了缓解或排除,因为: 6 | 7 | (1)Java 1.1的新型AWT是一个更好的编程模型,并向更好的库设计迈出了可喜的一步。而Java Beans则是那个库的框架。 8 | 9 | (2)“GUI构造器”(可视编程环境)将适用于所有开发系统。在我们用图形化工具将组件置入窗体的时候,Java Beans和新的AWT使GUI构造器能帮我们自动完成代码。其它组件技术如ActiveX等也将以相同的形式支持。 10 | 11 | 既然如此,为什么还要学习使用老的AWT呢?原因很简单,因为它的存在是个事实。就目前来说,这个事实对我们来说显得有些不利,它涉及到面向对象库设计的一个宗旨:一旦我们在库中公布一个组件,就再不能去掉它。如去掉它,就会损害别人已存在的代码。另外,当我们学习Java和所有使用老AWT的程序时,会发现有许多原来的代码使用的都是老式AWT。 12 | 13 | AWT必须能与固有操作系统的GUI组件打交通,这意味着它需要执行一个程序片不可能做到的任务。一个不被信任的程序片在操作系统中不能作出任何直接调用,否则它会对用户的机器做出不恰当的事情。一个不被信任的程序片不能访问重要的功能。例如,“在屏幕上画一个窗口”的唯一方法是通过调用拥有特殊接口和安全检查的标准Java库。Sun公司的原始模型创建的信任库将仅仅供给Web浏览器中的Java系统信任关系自动授权器使用,自动授权器将控制怎样进入到库中去。 14 | 15 | 但当我们想增加操作系统中访问新组件的功能时该怎么办?等待Sun来决定我们的扩展被合并到标准的Java库中,但这不一定会解决我们的问题。Java 1.1版中的新模型是“信任代码”或“签名代码”,因此一个特殊服务器将校验我们下载的、由规定的开发者使用的公共密钥加密系统的代码。这样我们就可知道代码从何而来,那真的是Bob的代码,还是由某人伪装成Bob的代码。这并不能阻止Bob犯错误或作某些恶意的事,但能防止Bob逃避匿名制造计算机病毒的责任。一个数字签名的程序片——“被信任的程序片”——在Java 1.1版能进入我们的机器并直接控制它,正像一些其它的应用程序从信任关系自动授权机中得到“信任”并安装在我们的机器上。 16 | 17 | 这是老AWT的所有特点。老的AWT代码将一直存在,新的Java编程者在从旧的书本中学习时将会遇到老的AWT代码。同样,老的AWT也是值得去学习的,例如在一个只有少量库的例程设计中。老的AWT所包括的范围在不考虑深度和枚举每一个程序和类,取而代之的是给了我们一个老AWT设计的概貌。 18 | -------------------------------------------------------------------------------- /13.10.md: -------------------------------------------------------------------------------- 1 | # 13.10 下拉列表 2 | 3 | 下拉列表像一个单选钮组,它是强制用户从一组可实现的选择中选择一个对象的方法。而且,它是一个实现这点的相当简洁的方法,也最易改变选择而不至使用户感到吃力(我们可以动态地改变单选钮,但那种方法显然不方便)。Java的选择框不像Windows中的组合框可以让我从列表中选择或输入自己的选择。在一个选择框中你只能从列表中选择仅仅一个项目。在下面的例子里,选择框从一个确定输入的数字开始,然后当按下一个按钮时,新输入的数字增加到框里。你将可以看到选择框的一些有趣的状态: 4 | 5 | ``` 6 | //: Choice1.java 7 | // Using drop-down lists 8 | import java.awt.*; 9 | import java.applet.*; 10 | 11 | public class Choice1 extends Applet { 12 | String[] description = { "Ebullient", "Obtuse", 13 | "Recalcitrant", "Brilliant", "Somnescent", 14 | "Timorous", "Florid", "Putrescent" }; 15 | TextField t = new TextField(30); 16 | Choice c = new Choice(); 17 | Button b = new Button("Add items"); 18 | int count = 0; 19 | public void init() { 20 | t.setEditable(false); 21 | for(int i = 0; i < 4; i++) 22 | c.addItem(description[count++]); 23 | add(t); 24 | add(c); 25 | add(b); 26 | } 27 | public boolean action (Event evt, Object arg) { 28 | if(evt.target.equals(c)) 29 | t.setText("index: " + c.getSelectedIndex() 30 | + " " + (String)arg); 31 | else if(evt.target.equals(b)) { 32 | if(count < description.length) 33 | c.addItem(description[count++]); 34 | } 35 | else 36 | return super.action(evt, arg); 37 | return true; 38 | } 39 | } ///:~ 40 | ``` 41 | 42 | 文本字字段中显示的`selected index,`也就是当前选择的项目的序列号,在事件中选择的字符串就像`action()`的第二个参数的字符串符描述的一样好。 43 | 44 | 运行这个程序片时,请注意对`Choice`框大小的判断:在windows里,这个大小是在我们拉下列表时确定的。这意味着如果我们拉下列表,然后增加更多的项目到列表中,这项目将在那,但这个下拉列表不再接受(我们可以通过项目来滚动观察——注释④)。然而,如果我们在第一次拉下下拉列表前将所的项目装入下拉列表,它的大小就会合适。当然,用户在使用时希望看到整个的列表,所以会在下拉列表的状态里对增加项目到选择框里加以特殊的限定。 45 | 46 | ④:这一行为显然是一种错误,会Java以后的版本里解决。 47 | -------------------------------------------------------------------------------- /13.11.md: -------------------------------------------------------------------------------- 1 | # 13.11 列表框 2 | 3 | 列表框与选择框有完全的不同,而不仅仅是当我们在激活选择框时的显示不同,列表框固定在屏幕的指定位置不会改变。另外,一个列表框允许多个选择:如果我们单击在超过一个的项目上,未选择的则表现为高亮度,我们可以选择象我们想要的一样的多。如果我们想察看项目列表,我们可以调用`getSelectedItem()`来产生一个被选择的项目列表。要想从一个组里删除一个项目,我们必须再一次的单击它。列表框,当然这里有一个问题就是它默认的动作是双击而不是单击。单击从组中增加或删除项目,双击调用`action()`。解决这个问题的方法是象下面的程序假设的一样重新培训我们的用户。 4 | 5 | ``` 6 | //: List1.java 7 | // Using lists with action() 8 | import java.awt.*; 9 | import java.applet.*; 10 | 11 | public class List1 extends Applet { 12 | String[] flavors = { "Chocolate", "Strawberry", 13 | "Vanilla Fudge Swirl", "Mint Chip", 14 | "Mocha Almond Fudge", "Rum Raisin", 15 | "Praline Cream", "Mud Pie" }; 16 | // Show 6 items, allow multiple selection: 17 | List lst = new List(6, true); 18 | TextArea t = new TextArea(flavors.length, 30); 19 | Button b = new Button("test"); 20 | int count = 0; 21 | public void init() { 22 | t.setEditable(false); 23 | for(int i = 0; i < 4; i++) 24 | lst.addItem(flavors[count++]); 25 | add(t); 26 | add(lst); 27 | add(b); 28 | } 29 | public boolean action (Event evt, Object arg) { 30 | if(evt.target.equals(lst)) { 31 | t.setText(""); 32 | String[] items = lst.getSelectedItems(); 33 | for(int i = 0; i < items.length; i++) 34 | t.appendText(items[i] + "\n"); 35 | } 36 | else if(evt.target.equals(b)) { 37 | if(count < flavors.length) 38 | lst.addItem(flavors[count++], 0); 39 | } 40 | else 41 | return super.action(evt, arg); 42 | return true; 43 | } 44 | } ///:~ 45 | ``` 46 | 47 | 按下按钮时,按钮增加项目到列表的顶部(因为`addItem()`的第二个参数为零)。增加项目到列表框比到选择框更加的合理,因为用户期望去滚动一个列表框(因为这个原因,它有内建的滚动条)但用户并不愿意像在前面的例子里不得不去计算怎样才能滚动到要要的那个项目。 48 | 然而,调用`action()`的唯一方法就是通过双击。如果我们想监视用户在我们的列表中的所作所为(尤其是单击),我们必须提供一个可供选择的方法。 49 | 50 | ## 13.11.1 handleEvent() 51 | 52 | 到目前为止,我们已使用了`action()`,现有另一种方法`handleEvent()`可对每一事件进行尝试。当一个事件发生时,它总是针对单独事件或发生在单独的事件对象上。该对象的`handleEvent()`方法是自动调用的,并且是被`handleEvent()`创建并传递到`handleEvent()`里。默认的`handleEvent()`(`handleEvent()`定义在组件里,基类的所有控件都在AWT里)将像我们以前一样调用`action()`或其它同样的方法去指明鼠标的活动、键盘活动或者指明移动的焦点。我们将会在本章的后面部分看到。 53 | 54 | 如果其它的方法-特别是`action()`-不能满足我们的需要怎么办呢?至于列表框,例如,如果我想捕捉鼠标单击,但`action()`只响应双击怎么办呢?这个解答是重载`handleEvent()`,毕竟它是从程序片中得到的,因此可以重载任何非确定的方法。当我们为程序片重载`handleEvent()`时,我们会得到所有的事件在它们发送出去之前,所以我们不能假设“这里有我的按钮可做的事件,所以我们可以假设按钮被按下了”从它被`action()`设为真值。在`handleEvent()`中按钮拥有焦点且某人对它进行分配都是可能的。不论它合理与否,我们可测试这些事件并遵照`handleEvent()`来进行操作。 55 | 56 | 为了修改列表样本,使它会响应鼠标的单击,在`action()`中按钮测试将被重载,但代码会处理的列表将像下面的例子被移进`handleEvent()`中去: 57 | 58 | ``` 59 | //: List2.java 60 | // Using lists with handleEvent() 61 | import java.awt.*; 62 | import java.applet.*; 63 | 64 | public class List2 extends Applet { 65 | String[] flavors = { "Chocolate", "Strawberry", 66 | "Vanilla Fudge Swirl", "Mint Chip", 67 | "Mocha Almond Fudge", "Rum Raisin", 68 | "Praline Cream", "Mud Pie" }; 69 | // Show 6 items, allow multiple selection: 70 | List lst = new List(6, true); 71 | TextArea t = new TextArea(flavors.length, 30); 72 | Button b = new Button("test"); 73 | int count = 0; 74 | public void init() { 75 | t.setEditable(false); 76 | for(int i = 0; i < 4; i++) 77 | lst.addItem(flavors[count++]); 78 | add(t); 79 | add(lst); 80 | add(b); 81 | } 82 | public boolean handleEvent(Event evt) { 83 | if(evt.id == Event.LIST_SELECT || 84 | evt.id == Event.LIST_DESELECT) { 85 | if(evt.target.equals(lst)) { 86 | t.setText(""); 87 | String[] items = lst.getSelectedItems(); 88 | for(int i = 0; i < items.length; i++) 89 | t.appendText(items[i] + "\n"); 90 | } 91 | else 92 | return super.handleEvent(evt); 93 | } 94 | else 95 | return super.handleEvent(evt); 96 | return true; 97 | } 98 | public boolean action(Event evt, Object arg) { 99 | if(evt.target.equals(b)) { 100 | if(count < flavors.length) 101 | lst.addItem(flavors[count++], 0); 102 | } 103 | else 104 | return super.action(evt, arg); 105 | return true; 106 | } 107 | } ///:~ 108 | ``` 109 | 110 | 这个例子同前面的例子相同除了增加了`handleEvent()`外简直一模一样。在程序中做了试验来验证是否列表框的选择和非选择存在。现在请记住,`handleEvent()`被程序片所重载,所以它能在窗体中任何存在,并且被其它的列表当成事件来处理。因此我们同样必须通过试验来观察目标。(虽然在这个例子中,程序片中只有一个列表框所以我们能假设所有的列表框事件必须服务于列表框。这是一个不好的习惯,一旦其它的列表框加入,它就会变成程序中的一个缺陷。)如果列表框匹配一个我们感兴趣的列表框,像前面的一样的代码将按上面的策略来运行。注意`handleEvent()`的窗体与`action()`的相同:如果我们处理一个单独的事件,将返回真值,但如果我们对其它的一些事件不感兴趣,通过`handleEvent()`我们必须返回`super.handleEvent()`值。这便是程序的核心,如果我们不那样做,其它的任何一个事件处理代码也不会被调用。例如,试注解在上面的代码中返回`super.handleEvent(evt)`的值。我们将发现`action()`没有被调用,当然那不是我们想得到的。对`action()`和`handlEvent()`而言,最重要的是跟着上面例子中的格式,并且当我们自己不处理事件时一直返回基类的方法版本信息。(在例子中我们将返回真值)。(幸运的是,这些类型的错误的仅属于Java 1.0版,在本章后面将看到的新设计的Java 1.1消除了这些类型的错误。) 111 | 112 | 在windows里,如果我们按下`shift`键,列表框自动允许我们做多个选择。这非常的棒,因为它允许用户做单个或多个的选择而不是编程期间固定的。我们可能会认为我们变得更加的精明,并且当一个鼠标单击被`evt.shiftdown()`产生时如果`shift`键是按下的将执行我们自己的试验程序。AWT的设计妨碍了我们-我们不得不去了解哪个项目被鼠标点击时是否按下了`shift`键,所以我们能取消其余部分所有的选择并且只选择那一个。不管怎样,我们是不可能在Java 1.0版中做出来的。(Java 1.1将所有的鼠标、键盘、焦点事件传送到列表中,所以我们能够完成它。) 113 | -------------------------------------------------------------------------------- /13.14.md: -------------------------------------------------------------------------------- 1 | # 13.14 程序片的局限 2 | 3 | 出于安全缘故,程序片十分受到限制,并且有很多的事我们都不能做。您一般会问:程序片看起来能做什么,传闻它又能做什么:扩展浏览器中WEB页的功能。自从作为一个网上冲浪者,我们从未真正想了解是否一个WEB页来自友好的或者不友好的站点,我们想要一些可以安全地行动的代码。所以我们可能会注意到大量的限制: 4 | 5 | (1) 一个程序片不能接触到本地的磁盘。这意味着不能在本地磁盘上写和读,我们不想一个程序片通过WEB页面阅读和传送重要的信息。写是被禁止的,当然,因为那将会引起病毒的侵入。当数字签名生效时,这些限制会被解除。 6 | 7 | (2) 程序片不能拥有菜单。(注意:这是规定在Swing中的)这可能会减少关于安全和关于程序简化的麻烦。我们可能会接到有关程序片协调利益以作为WEB页面的一部分的通知;而我们通常不去注意程序片的范围。这儿没有帧和标题条从菜单处弹出,出现的帧和标题条是属于WEB浏览器的。也许将来设计能被改变成允许我们将浏览器菜单和程序片菜单相结合起来——程序片可以影响它的环境将导致太危及整个系统的安全并使程序片过于的复杂。 8 | 9 | (3) 对话框是不被信任的。在Java中,对话框存在一些令人难解的地方。首先,它们不能正确地拒绝程序片,这实在是令人沮丧。如果我们从程序片弹出一个对话框,我们会在对话框上看到一个附上的消息框“不被信任的程序片”。这是因为在理论上,它有可能欺骗用户去考虑他们在通过WEB同一个老顾客的本地应用程序交易并且让他们输入他们的信用卡号。在看到AWT开发的那种GUI后,我们可能会难过地相信任何人都会被那种方法所愚弄。但程序片是一直附着在一个Web页面上的,并可以在浏览器中看到,而对话框没有这种依附关系,所以理论上是可能的。因此,我们很少会见到一个使用对话框的程序片。 10 | 11 | 在较新的浏览器中,对受到信任的程序片来说,许多限制都被放宽了(受信任程序片由一个信任源认证)。 12 | 13 | 涉及程序片的开发时,还有另一些问题需要考虑: 14 | 15 | + 程序片不停地从一个适合不同类的单独的服务器上下载。我们的浏览器能够缓存程序片,但这没有保证。在Java 1.1版中的一个改进是JAR(Java ARchive)文件,它允许将所有的程序片组件(包括其它的类文件、图像、声音)一起打包到一个的能被单个服务器处理下载的压缩文件。“数字签字”(能校验类创建器)可有效地加入每个单独的JAR文件。 16 | + 因为安全方面的缘故,我们做某些工作更加困难,例如访问数据库和发送电子邮件。另外,安全限制规则使访问多个主机变得非常的困难,因为每一件事都必须通过WEB服务器路由,形成一个性能瓶颈,并且单一环节的出错都会导致整个处理的停止。 17 | + 浏览器里的程序片不会拥有同样的本地应用程序运行的控件类型。例如,自从用户可以开关页面以来,在程序片中不会拥有一个形式上的对话框。当用户对一个WEB页面进行改变或退出浏览器时,对我们的程序片而言简直是一场灾难——这时没有办法保存状态,所以如果我们在处理和操作中时,信息会被丢失。另外,当我们离开一个WEB页面时,不同的浏览器会对我们的程序片做不同的操作,因此结果本来就是不确定的。 18 | 19 | ## 13.14.1 程序片的优点 20 | 21 | 如果能容忍那些限制,那么程序片的一些优点也是非常突出的,尤其是在我们构建客户/服务器应用或者其它网络应用时: 22 | 23 | + 没有安装方面的争议。程序片拥有真正的平台独立性(包括容易地播放声音文件等能力)所以我们不需要针对不同的平台修改代码也不需要任何人根据安装运行任何的“tweaking”。事实上,安装每次自动地将WEB页连同程序片一起,因此安静、自动地更新。在传统的客户端/服务器系统中,建立和安装一个新版本的客户端软件简直就是一场恶梦。 24 | + 因为安全的原因创建在核心Java语言和程序片结构中,我们不必担心坏的代码而导致毁坏某人的系统。这样,连同前面的优点,可使用Java(可从JavaScript和VBScript中选择客户端的WEB编程工具)为所谓的Intrant(在公司内部使用而不向Internet转移的企业内部网络)客户端/服务器开发应用程序。 25 | + 由于程序片是自动同HTML集成的,所以我们有一个内建的独立平台文件系统去支持程序片。这是一个很有趣的方法,因为我们惯于拥有程序文件的一部分而不是相反的拥有文件系统。 26 | -------------------------------------------------------------------------------- /13.20.md: -------------------------------------------------------------------------------- 1 | # 13.20 总结 2 | 3 | 对于AWT而言,Java 1.1到Java 1.2最大的改变就是Java中所有的库。Java 1.0版的AWT曾作为目前见过的最糟糕的一个设计被彻底地批评,并且当它允许我们在创建小巧精致的程序时,产生的GUI“在所有的平台上都同样的平庸”。它与在特殊平台上本地应用程序开发工具相比也是受到限制的,笨拙的并且也是不友好的。当Java 1.1版纳入新的事件模型和Java Beans时,平台被设置——现在它可以被拖放到可视化的应用程序构建工具中,创建GUI组件。另外,事件模型的设计和Bean无疑对轻松的编程和可维护的代码都非常的在意(这些在Java 1.0 AWT中不那么的明显)。但直至GUI组件-JFC/Swing类-显示工作结束它才这样。对于Swing组件而言,交叉平台GUI编程可以变成一种有教育意义的经验。 4 | 5 | 现在,唯一的情况是缺乏应用程序构建工具,并且这就是真正的变革的存在之处。微软的Visual Basic和Visual C++需要它们的应用程序构建工具,同样的是Borland的Delphi和C++构造器。如果我们需要应用程序构建工具变得更好,我们不得不交叉我们的指针并且希望自动授权机会给我们所需要的。Java是一个开放的环境,因此不但考虑到同其它的应用程序构建环境竞争,而且Java还促进它们的发展。这些工具被认真地使用,它们必须支持Java Beans。这意味着一个平等的应用领域:如果一个更好的应用程序构建工具出现,我们不需要去约束它就可以使用——我们可以采用并移动到新的工具上工作即可,这会提高我们的工作效率。这种竞争的环境对应用程序构建工具来说从未出现过,这种竞争能真正提高程序设计者的工作效率。 6 | -------------------------------------------------------------------------------- /13.21.md: -------------------------------------------------------------------------------- 1 | # 13.21 练习 2 | 3 | (1)创建一个有文字字段和三个按钮的程序片。当我们按下每个按钮时,使不同的文字显示在文字段中。 4 | 5 | (2)增加一个复选框到练习1创建的程序中,捕捉事件,并插入不同的文字到文字字段中。 6 | 7 | (3)创建一个程序片并增加所有导致`action()`被调用的组件,然后捕捉他们的事件并在文字字段中为每个组件显示一个特定的消息。 8 | 9 | (4)增加可以被`handleEvent()`方法测试事件的组件到练习3中。重载`handleEvent()`并在文字字段中为每个组件显示特定的消息。 10 | 11 | (5)创建一个有一个按钮和一个`TextField`的程序片。编写一个`handleEvent()`,以便如果按钮有焦点,输入字符到将显示的`TextField`中。 12 | 13 | (6)创建一个应用程序并将本章所有的组件增加主要的帧,包括菜单和对话框。 14 | 15 | (7)修改`TextNew.java`,以便字母在`t2`中保持输入时的样子,取代自动变成大写。 16 | 17 | (8)修改`CardLayout1.java`以便它使用Java 1.1的事件模型。 18 | 19 | (9)增加`Frog.class`到本章出现的清单文件中并运行`jar`以创建一个包括`Frog`和`BangBean`的JAR文件。现在从SUN公司处下载并安装BDK或者使用我们自己的可激活Bean的程序构建工具并增加JAR文件到我们的环境中,因此我们可以测试两个Bean。 20 | 21 | (10)创建我们自己的包括两个属性:一个布尔值为`on`,另一个为整型`level`,称为`Valve`的Java Bean。创建一个清单文件,利用`jar`打包我们的Bean,然后读入它到`beanbox`或到我们自己的激活程序构建工具里,因此我们可以测试它。 22 | 23 | (11)修改`Menus.java`,以便它处理多级菜单。这要假设读者已经熟悉了HTML的基础知识。但那些东西并不难理解,而且有一些书和资料可供参考。 24 | -------------------------------------------------------------------------------- /13.3.md: -------------------------------------------------------------------------------- 1 | # 13.3 制作按钮 2 | 3 | 制作一个按钮非常简单:只需要调用`Button`构造器,并指定想在按钮上出现的标签就行了(如果不想要标签,亦可使用默认构造器,但那种情况极少出现)。可参照后面的程序为按钮创建一个引用,以便以后能够引用它。 4 | 5 | `Button`是一个组件,象它自己的小窗口一样,会在更新时得以重绘。这意味着我们不必明确描绘一个按钮或者其他任意种类的控件;只需将它们纳入窗体,以后的描绘工作会由它们自行负责。所以为了将一个按钮置入窗体,需要重载`init()`方法,而不是重载`paint()`: 6 | 7 | ``` 8 | //: Button1.java 9 | // Putting buttons on an applet 10 | import java.awt.*; 11 | import java.applet.*; 12 | 13 | public class Button1 extends Applet { 14 | Button 15 | b1 = new Button("Button 1"), 16 | b2 = new Button("Button 2"); 17 | public void init() { 18 | add(b1); 19 | add(b2); 20 | } 21 | } ///:~ 22 | ``` 23 | 24 | 但这还不足以创建`Button`(或其他任何控件)。必须同时调用`Applet add()`方法,令按钮放置在程序片的窗体中。这看起来似乎比实际简单得多,因为对`add()`的调用实际会(间接地)决定将控件放在窗体的什么地方。对窗体布局的控件马上就要讲到。 25 | -------------------------------------------------------------------------------- /13.4.md: -------------------------------------------------------------------------------- 1 | # 13.4 捕获事件 2 | 3 | 大家可注意到假如编译和运行上面的程序片,按下按钮后不会发生任何事情。必须进入程序片内部,编写用于决定要发生什么事情的代码。对于由事件驱动的程序设计,它的基本目标就是用代码捕获发生的事件,并由代码对那些事件作出响应。事实上,GUI的大部分内容都是围绕这种事件驱动的程序设计展开的。 4 | 5 | 经过本书前面的学习,大家应该有了面向对象程序设计的一些基础,此时可能会想到应当有一些面向对象的方法来专门控制事件。例如,也许不得不继承每个按钮,并重载一些“按钮按下”方法(尽管这显得非常麻烦有有限)。大家也可能认为存在一些主控“事件”类,其中为希望响应的每个事件都包含了一个方法。 6 | 7 | 在对象以前,事件控制的典型方式是`switch`语句。每个事件都对应一个独一无二的整数编号;而且在主事件控制方法中,需要专门为那个值写一个`switch`。 8 | 9 | Java 1.0的AWT没有采用任何面向对象的手段。此外,它也没有使用`switch`语句,没有打算依靠那些分配给事件的数字。相反,我们必须创建`if`语句的一个嵌套系列。通过if语句,我们需要尝试做的事情是侦测到作为事件“目标”的对象。换言之,那是我们关心的全部内容——假如某个按钮是一个事件的目标,那么它肯定是一次鼠标点击,并要基于那个假设继续下去。但是,事件里也可能包含了其他信息。例如,假如想调查一次鼠标点击的像素位置,以便画一条引向那个位置的线,那么`Event`对象里就会包含那个位置的信息(也要注意Java 1.0的组件只能产生有限种类的事件,而Java 1.1和Swing/JFC组件则可产生完整的一系列事件)。 10 | 11 | Java 1.0版的AWT方法串联的条件语句中存在`action()`方法的调用。虽然整个Java 1.0版的事件模型不兼容Java 1.1版,但它在还不支持Java1.1版的机器和运行简单的程序片的系统中更广泛地使用,忠告您使用它会变得非常的舒适,包括对下面使用的`action()`程序方法而言。 12 | 13 | `action()`拥有两个参数:第一个是事件的类型,包括所有的触发调用`action()`的事件的有关信息。例如鼠标单击、普通按键按下或释放、特殊按键按下或释放、鼠标移动或者拖动、事件组件得到或丢失焦点,等等。第二个参数通常是我们忽略的事件目标。第二个参数封装在事件目标中,所以它像一个参数一样的冗长。 14 | 15 | 需调用`action()`时情况非常有限:将控件置入窗体时,一些类型的控件(按钮、复选框、下拉列表单、菜单)会发生一种“标准行动”,从而随相应的`Event`对象发起对`action()`的调用。比如对按钮来说,一旦按钮被按下,而且没有再多按一次,就会调用它的`action()`方法。这种行为通常正是我们所希望的,因为这正是我们对一个按钮正常观感。但正如本章后面要讲到的那样,还可通过`handleEvent()`方法来处理其他许多类型的事件。 16 | 17 | 前面的例程可进行一些扩展,以便象下面这样控制按钮的点击: 18 | 19 | ``` 20 | //: Button2.java 21 | // Capturing button presses 22 | import java.awt.*; 23 | import java.applet.*; 24 | 25 | public class Button2 extends Applet { 26 | Button 27 | b1 = new Button("Button 1"), 28 | b2 = new Button("Button 2"); 29 | public void init() { 30 | add(b1); 31 | add(b2); 32 | } 33 | public boolean action(Event evt, Object arg) { 34 | if(evt.target.equals(b1)) 35 | getAppletContext().showStatus("Button 1"); 36 | else if(evt.target.equals(b2)) 37 | getAppletContext().showStatus("Button 2"); 38 | // Let the base class handle it: 39 | else 40 | return super.action(evt, arg); 41 | return true; // We've handled it here 42 | } 43 | } ///:~ 44 | ``` 45 | 46 | 为了解目标是什么,需要向`Event`对象询问它的`target`(目标)成员是什么,然后用`equals()`方法检查它是否与自己感兴趣的目标对象引用相符。为所有感兴趣的对象写好引用后,必须在末尾的`else`语句中调用`super.action(evt, arg)`方法。我们在第7章已经说过(有关多态性的那一章),此时调用的是我们重载过的方法,而非它的基类版本。然而,基类版本也针对我们不感兴趣的所有情况提供了相应的控制代码。除非明确进行,否则它们是不会得到调用的。返回值指出我们是否已经处理了它,所以假如确实与一个事件相符,就应返回`true`;否则就返回由基类`event()`返回的东西。 47 | 48 | 对这个例子来说,最简单的行动就是打印出到底是什么按钮被按下。一些系统允许你弹出一个小消息窗口,但Java程序片却防碍窗口的弹出。不过我们可以用调用`Applet`方法的`getAppletContext()`来访问浏览器,然后用`showStatus()`在浏览器窗口底部的状态栏上显示一条信息(注释③)。还可用同样的方法打印出对事件的一段完整说明文字,方法是调用`getAppletConext().showStatus(evt + "")`。空字符串会强制编译器将`evt`转换成一个字符串。这些报告对于测试和调试特别有用,因为浏览器可能会覆盖我们的消息。 49 | 50 | ③:`ShowStatus()`也属于`Applet`的一个方法,所以可直接调用它,不必调用`getAppletContext()`。 51 | 52 | 尽管看起来似乎很奇怪,但我们确实也能通过`event()`中的第二个参数将一个事件与按钮上的文字相配。采用这种方法,上面的例子就变成了: 53 | 54 | ``` 55 | //: Button3.java 56 | // Matching events on button text 57 | import java.awt.*; 58 | import java.applet.*; 59 | 60 | public class Button3 extends Applet { 61 | Button 62 | b1 = new Button("Button 1"), 63 | b2 = new Button("Button 2"); 64 | public void init() { 65 | add(b1); 66 | add(b2); 67 | } 68 | public boolean action (Event evt, Object arg) { 69 | if(arg.equals("Button 1")) 70 | getAppletContext().showStatus("Button 1"); 71 | else if(arg.equals("Button 2")) 72 | getAppletContext().showStatus("Button 2"); 73 | // Let the base class handle it: 74 | else 75 | return super.action(evt, arg); 76 | return true; // We've handled it here 77 | } 78 | } ///:~ 79 | ``` 80 | 81 | 很难确切知道`equals()`方法在这儿要做什么。这种方法有一个很大的问题,就是开始使用这个新技术的Java程序员至少需要花费一个受挫折的时期来在比较按钮上的文字时发现他们要么大写了要么写错了(我就有这种经验)。同样,如果我们改变了按钮上的文字,程序代码将不再工作(但我们不会得到任何编译时和运行时的信息)。所以如果可能,我们就得避免使用这种方法。 82 | -------------------------------------------------------------------------------- /13.5.md: -------------------------------------------------------------------------------- 1 | # 13.5 文本字段 2 | 3 | “文本字段”是允许用户输入和编辑文字的一种线性区域。文本字段从文本组件那里继承了让我们选择文字、让我们像得到字符串一样得到选择的文字,得到或设置文字,设置文本字段是否可编辑以及连同我们从在线参考书中找到的相关方法。下面的例子将证明文本字段的其它功能;我们能注意到方法名是显而易见的: 4 | 5 | ``` 6 | //: TextField1.java 7 | // Using the text field control 8 | import java.awt.*; 9 | import java.applet.*; 10 | 11 | public class TextField1 extends Applet { 12 | Button 13 | b1 = new Button("Get Text"), 14 | b2 = new Button("Set Text"); 15 | TextField 16 | t = new TextField("Starting text", 30); 17 | String s = new String(); 18 | public void init() { 19 | add(b1); 20 | add(b2); 21 | add(t); 22 | } 23 | public boolean action (Event evt, Object arg) { 24 | if(evt.target.equals(b1)) { 25 | getAppletContext().showStatus(t.getText()); 26 | s = t.getSelectedText(); 27 | if(s.length() == 0) s = t.getText(); 28 | t.setEditable(true); 29 | } 30 | else if(evt.target.equals(b2)) { 31 | t.setText("Inserted by Button 2: " + s); 32 | t.setEditable(false); 33 | } 34 | // Let the base class handle it: 35 | else 36 | return super.action(evt, arg); 37 | return true; // We've handled it here 38 | } 39 | } ///:~ 40 | ``` 41 | 42 | 有几种方法均可构建一个文本字段;其中之一是提供一个初始字符串,并设置字符域的大小。 43 | 44 | 按下按钮1 是得到我们用鼠标选择的文字就是得到字段内所有的文字并转换成字符串`S`。它也允许字段被编辑。按下按钮2 放一条信息和字符串`s`到`Text fields`,并且阻止字段被编辑(尽管我们能够一直选择文字)。文字的可编辑性是通过`setEditable()`的真假值来控制的。 45 | -------------------------------------------------------------------------------- /13.6.md: -------------------------------------------------------------------------------- 1 | # 13.6 文本区域 2 | 3 | “文本区域”很像文字字段,只是它拥有更多的行以及一些引人注目的更多的功能。另外你能在给定位置对一个文本字段追加、插入或者修改文字。这看起来对文本字段有用的功能相当不错,所以设法发现它设计的特性会产生一些困惑。我们可以认为如果我们处处需要“文本区域”的功能,那么可以简单地使用一个线型文字区域在我们将另外使用文本字段的地方。在Java 1.0版中,当它们不是固定的时候我们也得到了一个文本区域的垂直和水平方向的滚动条。在Java 1.1版中,对高级构造器的修改允许我们选择哪个滚动条是当前的。下面的例子演示的仅仅是在Java1.0版的状况下滚动条一直打开。在下一章里我们将看到一个证明Java 1.1版中的文字区域的例程。 4 | 5 | ``` 6 | //: TextArea1.java 7 | // Using the text area control 8 | import java.awt.*; 9 | import java.applet.*; 10 | 11 | public class TextArea1 extends Applet { 12 | Button b1 = new Button("Text Area 1"); 13 | Button b2 = new Button("Text Area 2"); 14 | Button b3 = new Button("Replace Text"); 15 | Button b4 = new Button("Insert Text"); 16 | TextArea t1 = new TextArea("t1", 1, 30); 17 | TextArea t2 = new TextArea("t2", 4, 30); 18 | public void init() { 19 | add(b1); 20 | add(t1); 21 | add(b2); 22 | add(t2); 23 | add(b3); 24 | add(b4); 25 | } 26 | public boolean action (Event evt, Object arg) { 27 | if(evt.target.equals(b1)) 28 | getAppletContext().showStatus(t1.getText()); 29 | else if(evt.target.equals(b2)) { 30 | t2.setText("Inserted by Button 2"); 31 | t2.appendText(": " + t1.getText()); 32 | getAppletContext().showStatus(t2.getText()); 33 | } 34 | else if(evt.target.equals(b3)) { 35 | String s = " Replacement "; 36 | t2.replaceText(s, 3, 3 + s.length()); 37 | } 38 | else if(evt.target.equals(b4)) 39 | t2.insertText(" Inserted ", 10); 40 | // Let the base class handle it: 41 | else 42 | return super.action(evt, arg); 43 | return true; // We've handled it here 44 | } 45 | } ///:~ 46 | ``` 47 | 48 | 程序中有几个不同的“文本区域”构造器,这其中的一个在此处显示了一个初始字符串和行号和列号。不同的按钮显示得到、追加、修改和插入文字。 49 | -------------------------------------------------------------------------------- /13.7.md: -------------------------------------------------------------------------------- 1 | # 13.7 标签 2 | 3 | 标签准确地运作:安放一个标签到窗体上。这对没有标签的`TextFields`和`Text areas` 来说非常的重要,如果我们简单地想安放文字的信息在窗体上也能同样的使用。我们能像本章中第一个例程中演示的那样,使用`drawString()`里边的`paint()`在确定的位置去安置一个文字。当我们使用的标签允许我们通过布局管理加入其它的文字组件。(在这章的后面我们将进入讨论。) 4 | 5 | 使用构造器我们能创建一条包括初始化文字的标签(这是我们典型的作法),一个标签包括一行`CENTER`(中间)、`LEFT`(左)和`RIGHT`(右)(静态的结果取整定义在类标签里)。如果我们忘记了可以用`getText()`和`getalignment()`读取值,我们同样可以用`setText()`和`setAlignment()`来改变和调整。下面的例子将演示标签的特点: 6 | 7 | ``` 8 | //: Label1.java 9 | // Using labels 10 | import java.awt.*; 11 | import java.applet.*; 12 | 13 | public class Label1 extends Applet { 14 | TextField t1 = new TextField("t1", 10); 15 | Label labl1 = new Label("TextField t1"); 16 | Label labl2 = new Label(" "); 17 | Label labl3 = new Label(" ", 18 | Label.RIGHT); 19 | Button b1 = new Button("Test 1"); 20 | Button b2 = new Button("Test 2"); 21 | public void init() { 22 | add(labl1); add(t1); 23 | add(b1); add(labl2); 24 | add(b2); add(labl3); 25 | } 26 | public boolean action (Event evt, Object arg) { 27 | if(evt.target.equals(b1)) 28 | labl2.setText("Text set into Label"); 29 | else if(evt.target.equals(b2)) { 30 | if(labl3.getText().trim().length() == 0) 31 | labl3.setText("labl3"); 32 | if(labl3.getAlignment() == Label.LEFT) 33 | labl3.setAlignment(Label.CENTER); 34 | else if(labl3.getAlignment()==Label.CENTER) 35 | labl3.setAlignment(Label.RIGHT); 36 | else if(labl3.getAlignment() == Label.RIGHT) 37 | labl3.setAlignment(Label.LEFT); 38 | } 39 | else 40 | return super.action(evt, arg); 41 | return true; 42 | } 43 | } ///:~ 44 | ``` 45 | 46 | 首先是标签的最典型的用途:标记一个文本字段或文本区域。在例程的第二部分,当我们按下`test 1`按钮通过`setText()`将一串空的空格插入到的字段里。因为空的空格数不等于同样的字符数(在一个等比例间隔的字库里),当插入文字到标签里时我们会看到文字将被省略掉。在例子的第三部分保留的空的空格在我们第一次按下`test 2`会发现标签是空的(`trim()`删除了每个字符串结尾部分的空格)并且在开头的左列插入了一个短的标签。在工作的其余时间中我们按下按钮进行调整,因此就能看到效果。 47 | 48 | 我们可能会认为我们可以创建一个空的标签,然后用`setText()`安放文字在里面。然而我们不能在一个空标签内加入文字-这大概是因为空标签没有宽度-所以创建一个没有文字的空标签是没有用处的。在上面的例子里,`blank`标签里充满空的空格,所以它足够容纳后面加入的文字。 49 | 50 | 同样的,`setAlignment()`在我们用构造器创建的典型的文字标签上没有作用。这个标签的宽度就是文字的宽度,所以不能对它进行任何的调整。但是,如果我们启动一个长标签,然后把它变成短的,我们就可以看到调整的效果。 51 | 52 | 这些导致事件连同它们最小化的尺寸被挤压的状况被程序片使用的默认布局管理器所发现。有关布局管理器的部分包含在本章的后面。 53 | -------------------------------------------------------------------------------- /13.8.md: -------------------------------------------------------------------------------- 1 | # 13.8 复选框 2 | 3 | 复选框提供一个制造单一选择开关的方法;它包括一个小框和一个标签。典型的复选框有一个小的`X`(或者它设置的其它类型)或是空的,这依靠项目是否被选择来决定的。 4 | 5 | 我们会使用构造器正常地创建一个复选框,使用它的标签来充当它的参数。如果我们在创建复选框后想读出或改变它,我们能够获取和设置它的状态,同样也能获取和设置它的标签。注意,复选框的大写是与其它的控制相矛盾的。 6 | 7 | 无论何时一个复选框都可以设置和清除一个事件指令,我们可以捕捉同样的方法做一个按钮。在下面的例子里使用一个文字区域枚举所有被选中的复选框: 8 | 9 | ``` 10 | //: CheckBox1.java 11 | // Using check boxes 12 | import java.awt.*; 13 | import java.applet.*; 14 | 15 | public class CheckBox1 extends Applet { 16 | TextArea t = new TextArea(6, 20); 17 | Checkbox cb1 = new Checkbox("Check Box 1"); 18 | Checkbox cb2 = new Checkbox("Check Box 2"); 19 | Checkbox cb3 = new Checkbox("Check Box 3"); 20 | public void init() { 21 | add(t); add(cb1); add(cb2); add(cb3); 22 | } 23 | public boolean action (Event evt, Object arg) { 24 | if(evt.target.equals(cb1)) 25 | trace("1", cb1.getState()); 26 | else if(evt.target.equals(cb2)) 27 | trace("2", cb2.getState()); 28 | else if(evt.target.equals(cb3)) 29 | trace("3", cb3.getState()); 30 | else 31 | return super.action(evt, arg); 32 | return true; 33 | } 34 | void trace(String b, boolean state) { 35 | if(state) 36 | t.appendText("Box " + b + " Set\n"); 37 | else 38 | t.appendText("Box " + b + " Cleared\n"); 39 | } 40 | } ///:~ 41 | ``` 42 | 43 | `trace()`方法将选中的复选框名和当前状态用`appendText()`发送到文字区域中去,所以我们看到一个累积的被选中的复选框和它们的状态的列表。 44 | -------------------------------------------------------------------------------- /13.9.md: -------------------------------------------------------------------------------- 1 | # 13.9 单选钮 2 | 3 | 单选钮在GUI程序设计中的概念来自于老式的电子管汽车收音机的机械按钮:当我们按下一个按钮时,其它的按钮就会弹起。因此它允许我们强制从众多选择中作出单一选择。 4 | 5 | AWT没有单独的描述单选钮的类;取而代之的是复用复选框。然而将复选框放在单选钮组中(并且修改它的外形使它看起来不同于一般的复选框)我们必须使用一个特殊的构造器象一个参数一样的作用在`checkboxGroup`对象上。(我们同样能在创建复选框后调用`setCheckboxGroup()`方法。) 6 | 7 | 一个复选框组没有构造器的参数;它存在的唯一理由就是聚集一些复选框到单选钮组里。一个复选框对象必须在我们试图显示单选钮组之前将它的状态设置成`true`,否则在运行时我们就会得到一个异常。如果我们设置超过一个的单选钮为`true`,只有最后的一个能被设置成真。 8 | 9 | 这里有个简单的使用单选钮的例子。注意我们可以像其它的组件一样捕捉单选钮的事件: 10 | 11 | ``` 12 | //: RadioButton1.java 13 | // Using radio buttons 14 | import java.awt.*; 15 | import java.applet.*; 16 | 17 | public class RadioButton1 extends Applet { 18 | TextField t = 19 | new TextField("Radio button 2", 30); 20 | CheckboxGroup g = new CheckboxGroup(); 21 | Checkbox 22 | cb1 = new Checkbox("one", g, false), 23 | cb2 = new Checkbox("two", g, true), 24 | cb3 = new Checkbox("three", g, false); 25 | public void init() { 26 | t.setEditable(false); 27 | add(t); 28 | add(cb1); add(cb2); add(cb3); 29 | } 30 | public boolean action (Event evt, Object arg) { 31 | if(evt.target.equals(cb1)) 32 | t.setText("Radio button 1"); 33 | else if(evt.target.equals(cb2)) 34 | t.setText("Radio button 2"); 35 | else if(evt.target.equals(cb3)) 36 | t.setText("Radio button 3"); 37 | else 38 | return super.action(evt, arg); 39 | return true; 40 | } 41 | } ///:~ 42 | ``` 43 | 44 | 显示的状态是一个文字字段在被使用。这个字段被设置为不可编辑的,因为它只是用来显示数据而不是收集。这演示了一个使用标签的可取之道。注意字段内的文字是由最早选择的单选钮`Radio button 2`初始化的。 45 | 46 | 我们可以在窗体中拥有相当多的复选框组。 47 | -------------------------------------------------------------------------------- /13.md: -------------------------------------------------------------------------------- 1 | # 第13章 创建窗口和程序片 2 | 3 | 在Java 1.0中,图形用户接口(GUI)库最初的设计目标是让程序员构建一个通用的GUI,使其在所有平台上都能正常显示。 4 | 5 | 但遗憾的是,这个目标并未达到。事实上,Java 1.0版的“抽象Windows工具包”(AWT)产生的是在各系统看来都同样欠佳的图形用户接口。除此之外,它还限制我们只能使用四种字体,并且不能访问操作系统中现有的高级GUI元素。同时,Jave1.0版的AWT编程模型也不是面向对象的,极不成熟。这类情况在Java1.1版的AWT事件模型中得到了很好的改进,例如:更加清晰、面向对象的编程、遵循Java Beans的范例,以及一个可轻松创建可视编程环境的编程组件模型。Java1.2为老的Java 1.0 AWT添加了Java基类(AWT),这是一个被称为“Swing”的GUI的一部分。丰富的、易于使用和理解的Java Beans能经过拖放操作(像手工编程一样的好),创建出能使程序员满意的GUI。软件业的“3次修订版”规则看来对于程序设计语言也是成立的(一个产品除非经过第3次修订,否则不会尽如人意)。 6 | 7 | Java的主要设计目的之一是建立程序片,也就是建立运行在WEB 浏览器上的小应用程序。由于它们必须是安全的,所以程序片在运行时必须加以限制。无论怎样,它们都是支持客户端编程的强有力的工具,一个重要的应用便是在Web上。 8 | 9 | 在一个程序片中编程会受到很多的限制,我们一般说它“在沙箱内”,这是由于Java运行时一直会有某个东西——即Java运行期安全系统——在监视着我们。Jave 1.1为程序片提供了数字签名,所以可选出能信赖的程序片去访问主机。不过,我们也能跳出沙箱的限制写出可靠的程序。在这种情况下,我们可访问操作系统中的其他功能。在这本书中我们自始至终编写的都是可靠的程序,但它们成为了没有图形组件的控制台程序。AWT也能用来为可靠的程序建立GUI接口。 10 | 11 | 在这一章中我们将先学习使用老的AWT工具,我们会与许多支持和使用AWT的代码程序样本相遇。尽管这有一些困难,但却是必须的,因为我们必须用老的AWT来维护和阅读传统的Java代码。有时甚至需要我们编写AWT代码去支持不能从Java1.0升级的环境。在本章第二部分,我们将学习Java 1.1版中新的AWT结构并会看到它的事件模型是如此的优秀(如果能掌握的话,那么在编制新的程序时就可使用这最新的工具。最后,我们将学习新的能像类库一样加入到Java 1.1版中的JFC/Swing组件,这意味着不需要升级到Java 1.2便能使用这一类库。 12 | 13 | 大多数的例程都将展示程序片的建立,这并不仅仅是因为这非常的容易,更因为这是AWT的主要作用。另外,当用AWT创建一个可靠的程序时,我们将看到处理程序的不同之处,以及怎样创建能在命令行和浏览器中运行的程序。 14 | 15 | 请注意的是这不是为了描述类的所有程序的综合解释。这一章将带领我们从摘要开始。当我们查找更复杂的内容时,请确定我们的信息浏览器通过查找类和方法来解决编程中的问题(如果我们正在使用一个开发环境,信息浏览器也许是内建的;如果我们使用的是SUN公司的JDK则这时我们要使用WEB浏览器并在Java根目录下面开始)。附录F列出了用于深入学习库知识的其他一些参考资料。 -------------------------------------------------------------------------------- /14.6.md: -------------------------------------------------------------------------------- 1 | # 14.6 总结 2 | 3 | 4 | 何时使用多线程技术,以及何时避免用它,这是我们需要掌握的重要课题。骼它的主要目的是对大量任务进行有序的管理。通过多个任务的混合使用,可以更有效地利用计算机资源,或者对用户来说显得更方便。资源均衡的经典问题是在IO等候期间如何利用CPU。至于用户方面的方便性,最经典的问题就是如何在一个长时间的下载过程中监视并灵敏地反应一个“停止”(`stop`)按钮的按下。 5 | 多线程的主要缺点包括: 6 | 7 | (1) 等候使用共享资源时造成程序的运行速度变慢。 8 | 9 | (2) 对线程进行管理要求的额外CPU开销。 10 | 11 | (3) 复杂程度无意义的加大,比如用独立的线程来更新数组内每个元素的愚蠢主意。 12 | 13 | (4) 漫长的等待、浪费精力的资源竞争以及死锁等多线程症状。 14 | 15 | 线程另一个优点是它们用“轻度”执行切换(100条指令的顺序)取代了“重度”进程场景切换(1000条指令)。由于一个进程内的所有线程共享相同的内存空间,所以“轻度”场景切换只改变程序的执行和本地变量。而在“重度”场景切换时,一个进程的改变要求必须完整地交换内存空间。 16 | 17 | 线程处理看来好象进入了一个全新的领域,似乎要求我们学习一种全新的程序设计语言——或者至少学习一系列新的语言概念。由于大多数微机操作系统都提供了对线程的支持,所以程序设计语言或者库里也出现了对线程的扩展。不管在什么情况下,涉及线程的程序设计: 18 | 19 | (1) 刚开始会让人摸不着头脑,要求改换我们传统的编程思路; 20 | 21 | (2) 其他语言对线程的支持看来是类似的。所以一旦掌握了线程的概念,在其他环境也不会有太大的困难。尽管对线程的支持使Java语言的复杂程度多少有些增加,但请不要责怪Java。毕竟,利用线程可以做许多有益的事情。 22 | 23 | 多个线程可能共享同一个资源(比如一个对象里的内存),这是运用线程时面临的最大的一个麻烦。必须保证多个线程不会同时试图读取和修改那个资源。这要求技巧性地运用`synchronized`(同步)关键字。它是一个有用的工具,但必须真正掌握它,因为假若操作不当,极易出现死锁。 24 | 25 | 除此以外,运用线程时还要注意一个非常特殊的问题。由于根据Java的设计,它允许我们根据需要创建任意数量的线程——至少理论上如此(例如,假设为一项工程方面的有限元素分析创建数以百万的线程,这对Java来说并非实际)。然而,我们一般都要控制自己创建的线程数量的上限。因为在某些情况下,大量线程会将场面变得一团糟,所以工作都会几乎陷于停顿。临界点并不象对象那样可以达到几千个,而是在100以下。一般情况下,我们只创建少数几个关键线程,用它们解决某个特定的问题。这时数量的限制问题不大。但在较常规的一些设计中,这一限制确实会使我们感到束手束脚。 26 | 27 | 大家要注意线程处理中一个不是十分直观的问题。由于采用了线程“调度”机制,所以通过在`run()`的主循环中插入对`sleep()`的调用,一般都可以使自己的程序运行得更快一些。这使它对编程技巧的要求非常高,特别是在更长的延迟似乎反而能提高性能的时候。当然,之所以会出现这种情况,是由于在正在运行的线程准备进入“休眠”状态之前,较短的延迟可能造成“`sleep()`结束”调度机制的中断。这便强迫调度机制将其中止,并于稍后重新启动,以便它能做完自己的事情,再进入休眠状态。必须多想一想,才能意识到事情真正的麻烦程度。 28 | 29 | 本章遗漏的一件事情是一个动画例子,这是目前程序片最流行的一种应用。然而,Java JDK配套提供了解决这个问题的一整套方案(并可播放声音),大家可到`java.sun.com`的演示区域下载。此外,我们完全有理由相信未来版本的Java会提供更好的动画支持——尽管目前的Web涌现出了与传统方式完全不同的非Java、非程序化的许多动画方案。如果想系统学习Java动画的工作原理,可参考《Core Java——核心Java》一书,由Cornell&Horstmann编著,Prentice-Hall于1997年出版。若欲更深入地了解线程处理,请参考《Concurrent Programming in Java——Java中的并发编程》,由Doug Lea编著,Addison-Wiseley于1997年出版;或者《Java Threads——Java线程》,Oaks&Wong编著,O'Reilly于1997年出版。 30 | -------------------------------------------------------------------------------- /14.7.md: -------------------------------------------------------------------------------- 1 | # 14.7 练习 2 | 3 | 4 | (1) 从`Thread`继承一个类,并(重载)覆盖`run()`方法。在`run()`内,打印出一条消息,然后调用`sleep()`。重复三遍这些操作,然后从`run()`返回。在构造器中放置一条启动消息,并覆盖`finalize()`,打印一条关闭消息。创建一个独立的线程类,使它在`run()`内调用`System.gc()`和`System.runFinalization()`,并打印一条消息,表明调用成功。创建这两种类型的几个线程,然后运行它们,看看会发生什么。 5 | 6 | (2) 修改`Counter2.java`,使线程成为一个内部类,而且不需要明确保存指向`Counter2`的一个。 7 | 8 | (3) 修改`Sharing2.java`,在`TwoCounter`的`run()`方法内部添加一个`synchronized`(同步)块,而不是同步整个`run()`方法。 9 | 10 | (4) 创建两个`Thread`子类,第一个的`run()`方法用于最开始的启动,并捕获第二个`Thread`对象的引用,然后调用`wait()`。第二个类的`run()`应在过几秒后为第一个线程调用`modifyAll()`,使第一个线程能打印出一条消息。 11 | 12 | (5) 在`Ticker2`内的`Counter5.java`中,删除`yield()`,并解释一下结果。用一个`sleep()`换掉`yield()`,再解释一下结果。 13 | 14 | (6) 在`ThreadGroup1.java`中,将对`sys.suspend()`的调用换成对线程组的一个`wait()`调用,令其等候2秒钟。为了保证获得正确的结果,必须在一个同步块内取得`sys`的对象锁。 15 | 16 | (7) 修改`Daemons.java`,使`main()`有一个`sleep()`,而不是一个`readLine()`。实验不同的睡眠时间,看看会有什么发生。 17 | 18 | (8) 到第7章(中间部分)找到那个`GreenhouseControls.java`例子,它应该由三个文件构成。在`Event.java`中,`Event`类建立在对时间的监视基础上。修改这个`Event`,使其成为一个线程。然后修改其余的设计,使它们能与新的、以线程为基础的Event正常协作。 19 | -------------------------------------------------------------------------------- /14.md: -------------------------------------------------------------------------------- 1 | # 第14章 多线程 2 | 3 | 4 | 利用对象,可将一个程序分割成相互独立的区域。我们通常也需要将一个程序转换成多个独立运行的子任务。 5 | 6 | 象这样的每个子任务都叫作一个“线程”(`Thread`)。编写程序时,可将每个线程都想象成独立运行,而且都有自己的专用CPU。一些基础机制实际会为我们自动分割CPU的时间。我们通常不必关心这些细节问题,所以多线程的代码编写是相当简便的。 7 | 8 | 这时理解一些定义对以后的学习狠有帮助。“进程”是指一种“自包容”的运行程序,有自己的地址空间。“多任务”操作系统能同时运行多个进程(程序)——但实际是由于CPU分时机制的作用,使每个进程都能循环获得自己的CPU时间片。但由于轮换速度非常快,使得所有程序好象是在“同时”运行一样。“线程”是进程内部单一的一个顺序控制流。因此,一个进程可能容纳了多个同时执行的线程。 9 | 10 | 多线程的应用范围很广。但在一般情况下,程序的一些部分同特定的事件或资源联系在一起,同时又不想为它而暂停程序其他部分的执行。这样一来,就可考虑创建一个线程,令其与那个事件或资源关联到一起,并让它独立于主程序运行。一个很好的例子便是“Quit”或“退出”按钮——我们并不希望在程序的每一部分代码中都轮询这个按钮,同时又希望该按钮能及时地作出响应(使程序看起来似乎经常都在轮询它)。事实上,多线程最主要的一个用途就是构建一个“反应灵敏”的用户界面。 11 | -------------------------------------------------------------------------------- /15.1.md: -------------------------------------------------------------------------------- 1 | # 15.1 机器的标识 2 | 3 | 当然,为了分辨来自别处的一台机器,以及为了保证自己连接的是希望的那台机器,必须有一种机制能独一无二地标识出网络内的每台机器。早期网络只解决了如何在本地网络环境中为机器提供唯一的名字。但Java面向的是整个因特网,这要求用一种机制对来自世界各地的机器进行标识。为达到这个目的,我们采用了IP(互联网地址)的概念。IP以两种形式存在着: 4 | 5 | (1) 大家最熟悉的DNS(域名服务)形式。我自己的域名是`bruceeckel.com`。所以假定我在自己的域内有一台名为Opus的计算机,它的域名就可以是`Opus.bruceeckel.com`。这正是大家向其他人发送电子函件时采用的名字,而且通常集成到一个万维网(WWW)地址里。 6 | 7 | (2) 此外,亦可采用“四点”格式,亦即由点号(.)分隔的四组数字,比如`202.98.32.111`。 8 | 不管哪种情况,IP地址在内部都表达成一个由32个二进制位(bit)构成的数字(注释①),所以IP地址的每一组数字都不能超过255。利用由`java.net`提供的`static InetAddress.getByName()`,我们可以让一个特定的Java对象表达上述任何一种形式的数字。结果是类型为`InetAddress`的一个对象,可用它构成一个“套接字”(`Socket`),大家在后面会见到这一点。 9 | 10 | ①:这意味着最多只能得到40亿左右的数字组合,全世界的人很快就会把它用光。但根据目前正在研究的新IP编址方案,它将采用128 bit的数字,这样得到的唯一性IP地址也许在几百年的时间里都不会用完。 11 | 12 | 作为运用`InetAddress.getByName()`一个简单的例子,请考虑假设自己有一家拨号连接因特网服务提供者(ISP),那么会发生什么情况。每次拨号连接的时候,都会分配得到一个临时IP地址。但在连接期间,那个IP地址拥有与因特网上其他IP地址一样的有效性。如果有人按照你的IP地址连接你的机器,他们就有可能使用在你机器上运行的Web或者FTP服务器程序。当然这有个前提,对方必须准确地知道你目前分配到的IP。由于每次拨号连接获得的IP都是随机的,怎样才能准确地掌握你的IP呢? 13 | 14 | 下面这个程序利用`InetAddress.getByName()`来产生你的IP地址。为了让它运行起来,事先必须知道计算机的名字。该程序只在Windows 95中进行了测试,但大家可以依次进入自己的“开始”、“设置”、“控制面板”、“网络”,然后进入“标识”卡片。其中,“计算机名称”就是应在命令行输入的内容。 15 | 16 | ``` 17 | //: WhoAmI.java 18 | // Finds out your network address when you're 19 | // connected to the Internet. 20 | package c15; 21 | import java.net.*; 22 | 23 | public class WhoAmI { 24 | public static void main(String[] args) 25 | throws Exception { 26 | if(args.length != 1) { 27 | System.err.println( 28 | "Usage: WhoAmI MachineName"); 29 | System.exit(1); 30 | } 31 | InetAddress a = 32 | InetAddress.getByName(args[0]); 33 | System.out.println(a); 34 | } 35 | } ///:~ 36 | ``` 37 | 38 | 就我自己的情况来说,机器的名字叫作`Colossus`(来自同名电影,“巨人”的意思。我在这台机器上有一个很大的硬盘)。所以一旦连通我的ISP,就象下面这样执行程序: 39 | 40 | ``` 41 | java whoAmI Colossus 42 | ``` 43 | 44 | 得到的结果象下面这个样子(当然,这个地址可能每次都是不同的): 45 | 46 | ``` 47 | Colossus/202.98.41.151 48 | ``` 49 | 50 | 假如我把这个地址告诉一位朋友,他就可以立即登录到我的个人Web服务器,只需指定目标地址 `http://202.98.41.151` 即可(当然,我此时不能断线)。有些时候,这是向其他人发送信息或者在自己的Web站点正式出台以前进行测试的一种方便手段。 51 | 52 | ## 15.1.1 服务器和客户端 53 | 54 | 网络最基本的精神就是让两台机器连接到一起,并相互“交谈”或者“沟通”。一旦两台机器都发现了对方,就可以展开一次令人愉快的双向对话。但它们怎样才能“发现”对方呢?这就象在游乐园里那样:一台机器不得不停留在一个地方,监听其他机器说:“嘿,你在哪里呢?” 55 | 56 | “停留在一个地方”的机器叫作“服务器”(Server);到处“找人”的机器则叫作“客户端”(Client)或者“客户”。它们之间的区别只有在客户端试图同服务器连接的时候才显得非常明显。一旦连通,就变成了一种双向通信,谁来扮演服务器或者客户端便显得不那么重要了。 57 | 58 | 所以服务器的主要任务是监听建立连接的请求,这是由我们创建的特定服务器对象完成的。而客户端的任务是试着与一台服务器建立连接,这是由我们创建的特定客户端对象完成的。一旦连接建好,那么无论在服务器端还是客户端端,连接只是魔术般地变成了一个IO数据流对象。从这时开始,我们可以象读写一个普通的文件那样对待连接。所以一旦建好连接,我们只需象第10章那样使用自己熟悉的IO命令即可。这正是Java连网最方便的一个地方。 59 | 60 | (1) 在没有网络的前提下测试程序 61 | 62 | 由于多种潜在的原因,我们可能没有一台客户端、服务器以及一个网络来测试自己做好的程序。我们也许是在一个课堂环境中进行练习,或者写出的是一个不十分可靠的网络应用,还能拿到网络上去。IP的设计者注意到了这个问题,并建立了一个特殊的地址——`localhost`——来满足非网络环境中的测试要求。在Java中产生这个地址最一般的做法是: 63 | 64 | ``` 65 | InetAddress addr = InetAddress.getByName(null); 66 | ``` 67 | 68 | 如果向`getByName()`传递一个`null`(空)值,就默认为使用`localhost`。我们`用InetAddress`对特定的机器进行索引,而且必须在进行进一步的操作之前得到这个`InetAddress`(互联网地址)。我们不可以操纵一个`InetAddress`的内容(但可把它打印出来,就象下一个例子要演示的那样)。创建`InetAddress`的唯一途径就是那个类的static(静态)成员方法`getByName()`(这是最常用的)、`getAllByName()`或者`getLocalHost()`。 69 | 70 | 为得到本地主机地址,亦可向其直接传递字符串`"localhost"`: 71 | 72 | ``` 73 | InetAddress.getByName("localhost"); 74 | ``` 75 | 76 | 或者使用它的保留IP地址(四点形式),就象下面这样: 77 | 78 | ``` 79 | InetAddress.getByName("127.0.0.1"); 80 | ``` 81 | 82 | 这三种方法得到的结果是一样的。 83 | 84 | ## 15.1.2 端口:机器内独一无二的场所 85 | 86 | 有些时候,一个IP地址并不足以完整标识一个服务器。这是由于在一台物理性的机器中,往往运行着多个服务器(程序)。由IP表达的每台机器也包含了“端口”(Port)。我们设置一个客户端或者服务器的时候,必须选择一个无论客户端还是服务器都认可连接的端口。就象我们去拜会某人时,IP地址是他居住的房子,而端口是他在的那个房间。 87 | 88 | 注意端口并不是机器上一个物理上存在的场所,而是一种软件抽象(主要是为了表述的方便)。客户程序知道如何通过机器的IP地址同它连接,但怎样才能同自己真正需要的那种服务连接呢(一般每个端口都运行着一种服务,一台机器可能提供了多种服务,比如HTTP和FTP等等)?端口编号在这里扮演了重要的角色,它是必需的一种二级定址措施。也就是说,我们请求一个特定的端口,便相当于请求与那个端口编号关联的服务。“报时”便是服务的一个典型例子。通常,每个服务都同一台特定服务器机器上的一个独一 89 | 无二的端口编号关联在一起。客户程序必须事先知道自己要求的那项服务的运行端口号。 90 | 91 | 系统服务保留了使用端口1到端口1024的权力,所以不应让自己设计的服务占用这些以及其他任何已知正在使用的端口。本书的第一个例子将使用端口8080(为追忆我的第一台机器使用的老式8位Intel 8080芯片,那是一部使用CP/M操作系统的机子)。 92 | -------------------------------------------------------------------------------- /15.10.md: -------------------------------------------------------------------------------- 1 | # 15.10 练习 2 | 3 | (1) 编译和运行本章中的`JabberServer`和`JabberClient`程序。接着编辑一下程序,删去为输入和输出设计的所有缓冲机制,然后再次编译和运行,观察一下结果。 4 | 5 | (2) 创建一个服务器,用它请求用户输入密码,然后打开一个文件,并将文件通过网络连接传送出去。创建一个同该服务器连接的客户,为其分配适当的密码,然后捕获和保存文件。在自己的机器上用`localhost`(通过调用`InetAddress.getByName(null)`生成本地IP地址`127.0.0.1`)测试这两个程序。 6 | 7 | (3) 修改练习2中的程序,令其用多线程机制对多个客户进行控制。 8 | 9 | (4) 修改`JabberClient`,禁止输出刷新,并观察结果。 10 | 11 | (5) 以`ShowHTML.java`为基础,创建一个程序片,令其成为对自己Web站点的特定部分进行密码保护的大门。 12 | 13 | (6) (可能有些难度)创建一对客户/服务器程序,利用数据报(`Datagram`)将一个文件从一台机器传到另一台(参见本章数据报小节末尾的叙述)。 14 | 15 | (7) (可能有些难度)对`VLookup.java`程序作一番修改,使我们能点击得到的结果名字,然后程序会自动取得那个名字,并把它复制到剪贴板(以便我们方便地粘贴到自己的E-mail)。可能要回过头去研究一下IO数据流的那一章,回忆该如何使用Java 1.1剪贴板。 16 | -------------------------------------------------------------------------------- /15.9.md: -------------------------------------------------------------------------------- 1 | # 15.9 总结 2 | 3 | 由于篇幅所限,还有其他许多涉及连网的概念没有介绍给大家。Java也为URL提供了相当全面的支持,包括为因特网上不同类型的客户提供协议控制器等等。 4 | 5 | 除此以外,一种正在逐步流行的技术叫作Servlet Server。它是一种因特网服务器应用,通过Java控制客户请求,而非使用以前那种速度很慢、且相当麻烦的CGI(通用网关接口)协议。这意味着为了在服务器那一端提供服务,我们可以用Java编程,不必使用自己不熟悉的其他语言。由于Java具有优秀的移植能力,所以不必关心具体容纳这个服务器是什么平台。 6 | 7 | 所有这些以及其他特性都在《Java Network Programming》一书中得到了详细讲述。该书由Elliotte Rusty Harold编著,O'Reilly于1997年出版。 8 | -------------------------------------------------------------------------------- /15.md: -------------------------------------------------------------------------------- 1 | # 第15章 网络编程 2 | 3 | 历史上的网络编程都倾向于困难、复杂,而且极易出错。 4 | 5 | 程序员必须掌握与网络有关的大量细节,有时甚至要对硬件有深刻的认识。一般地,我们需要理解连网协议中不同的“层”(Layer)。而且对于每个连网库,一般都包含了数量众多的函数,分别涉及信息块的连接、打包和拆包;这些块的来回运输;以及握手等等。这是一项令人痛苦的工作。 6 | 7 | 但是,连网本身的概念并不是很难。我们想获得位于其他地方某台机器上的信息,并把它们移到这儿;或者相反。这与读写文件非常相似,只是文件存在于远程机器上,而且远程机器有权决定如何处理我们请求或者发送的数据。 8 | 9 | Java最出色的一个地方就是它的“无痛苦连网”概念。有关连网的基层细节已被尽可能地提取出去,并隐藏在JVM以及Java的本机安装系统里进行控制。我们使用的编程模型是一个文件的模型;事实上,网络连接(一个“套接字”)已被封装到系统对象里,所以可象对其他数据流那样采用同样的方法调用。除此以外,在我们处理另一个连网问题——同时控制多个网络连接——的时候,Java内建的多线程机制也是十分方便的。 10 | 11 | 本章将用一系列易懂的例子解释Java的连网支持。 12 | -------------------------------------------------------------------------------- /16-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/16-1.gif -------------------------------------------------------------------------------- /16-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/16-2.gif -------------------------------------------------------------------------------- /16-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/16-3.gif -------------------------------------------------------------------------------- /16-4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/16-4.gif -------------------------------------------------------------------------------- /16.1.md: -------------------------------------------------------------------------------- 1 | # 16.1 模式的概念 2 | 3 | 在最开始,可将模式想象成一种特别聪明、能够自我适应的手法,它可以解决特定类型的问题。也就是说,它类似一些需要全面认识某个问题的人。在了解了问题的方方面面以后,最后提出一套最通用、最灵活的解决方案。具体问题或许是以前见到并解决过的。然而,从前的方案也许并不是最完善的,大家会看到它如何在一个模式里具体表达出来。 4 | 5 | 尽管我们称之为“设计模式”,但它们实际上并不局限于设计领域。思考“模式”时,应脱离传统意义上分析、设计以及实现的思考方式。相反,“模式”是在一个程序里具体表达一套完整的思想,所以它有时可能出现在分析阶段或者高级设计阶段。这一点是非常有趣的,因为模式具有以代码形式直接实现的形式,所以可能不希望它在低级设计或者具体实现以前显露出来(而且事实上,除非真正进入那些阶段,否则一般意识不到自己需要一个模式来解决问题)。 6 | 7 | 模式的基本概念亦可看成是程序设计的基本概念:添加一层新的抽象!只要我们抽象了某些东西,就相当于隔离了特定的细节。而且这后面最引人注目的动机就是“将保持不变的东西身上发生的变化孤立出来”。这样做的另一个原因是一旦发现程序的某部分由于这样或那样的原因可能发生变化,我们一般都想防止那些改变在代码内部繁衍出其他变化。这样做不仅可以降低代码的维护代价,也更便于我们理解(结果同样是降低开销)。 8 | 9 | 为设计出功能强大且易于维护的应用项目,通常最困难的部分就是找出我称之为“领头变化”的东西。这意味着需要找出造成系统改变的最重要的东西,或者换一个角度,找出付出代价最高、开销最大的那一部分。一旦发现了“领头变化”,就可以为自己定下一个焦点,围绕它展开自己的设计。 10 | 11 | 所以设计模式的最终目标就是将代码中变化的内容隔离开。如果从这个角度观察,就会发现本书实际已采用了一些设计模式。举个例子来说,继承可以想象成一种设计模式(类似一个由编译器实现的)。在都拥有同样接口(即保持不变的东西)的对象内部,它允许我们表达行为上的差异(即发生变化的东西)。组合亦可想象成一种模式,因为它允许我们修改——动态或静态——用于实现类的对象,所以也能修改类的运作方式。 12 | 13 | 在《设计模式》一书中,大家还能看到另一种模式:“迭代器”(即`Iterator`,Java 1.0和1.1不负责任地把它叫作`Enumeration`,即“枚举”;Java1.2的集合则改回了“迭代器”的称呼)。当我们在集合里遍历,逐个选择不同的元素时,迭代器可将集合的实现细节有效地隐藏起来。利用迭代器,可以编写出通用的代码,以便对一个序列里的所有元素采取某种操作,同时不必关心这个序列是如何构建的。这样一来,我们的通用代码即可伴随任何能产生迭代器的集合使用。 14 | 15 | ## 16.1.1 单例 16 | 17 | 或许最简单的设计模式就是“单例”(`Singleton`),它能提供对象的一个(而且只有一个)实例。单例在Java库中得到了应用,但下面这个例子显得更直接一些: 18 | 19 | ``` 20 | //: SingletonPattern.java 21 | // The Singleton design pattern: you can 22 | // never instantiate more than one. 23 | package c16; 24 | 25 | // Since this isn't inherited from a Cloneable 26 | // base class and cloneability isn't added, 27 | // making it final prevents cloneability from 28 | // being added in any derived classes: 29 | final class Singleton { 30 | private static Singleton s = new Singleton(47); 31 | private int i; 32 | private Singleton(int x) { i = x; } 33 | public static Singleton getHandle() { 34 | return s; 35 | } 36 | public int getValue() { return i; } 37 | public void setValue(int x) { i = x; } 38 | } 39 | 40 | public class SingletonPattern { 41 | public static void main(String[] args) { 42 | Singleton s = Singleton.getHandle(); 43 | System.out.println(s.getValue()); 44 | Singleton s2 = Singleton.getHandle(); 45 | s2.setValue(9); 46 | System.out.println(s.getValue()); 47 | try { 48 | // Can't do this: compile-time error. 49 | // Singleton s3 = (Singleton)s2.clone(); 50 | } catch(Exception e) {} 51 | } 52 | } ///:~ 53 | ``` 54 | 55 | 创建单例的关键就是防止客户程序员采用除由我们提供的之外的任何一种方式来创建一个对象。必须将所有构造器都设为`private`(私有),而且至少要创建一个构造器,以防止编译器帮我们自动同步一个默认构造器(它会自做聪明地创建成为“友好的”——`friendly`,而非`private`)。 56 | 57 | 此时应决定如何创建自己的对象。在这儿,我们选择了静态创建的方式。但亦可选择等候客户程序员发出一个创建请求,然后根据他们的要求动态创建。不管在哪种情况下,对象都应该保存为“私有”属性。我们通过公用方法提供访问途径。在这里,`getHandle()`会产生指向`Singleton`的一个引用。剩下的接口(`getValue()`和`setValue()`)属于普通的类接口。 58 | 59 | Java也允许通过克隆(`Clone`)方式来创建一个对象。在这个例子中,将类设为`final`可禁止克隆的发生。由于`Singleton`是从`Object`直接继承的,所以`clone()`方法会保持`protected`(受保护)属性,不能够使用它(强行使用会造成编译期错误)。然而,假如我们是从一个类结构中继承,那个结构已经重载了`clone()`方法,使其具有`public`属性,并实现了`Cloneable`,那么为了禁止克隆,需要重载`clone()`,并抛出一个`CloneNotSupportedException`(不支持克隆异常),就象第12章介绍的那样。亦可重载`clone()`,并简单地返回`this`。那样做会造成一定的混淆,因为客户程序员可能错误地认为对象尚未克隆,仍然操纵的是原来的那个。 60 | 61 | 注意我们并不限于只能创建一个对象。亦可利用该技术创建一个有限的对象池。但在那种情况下,可能需要解决池内对象的共享问题。如果不幸真的遇到这个问题,可以自己设计一套方案,实现共享对象的登记与撤消登记。 62 | 63 | ## 16.1.2 模式分类 64 | 65 | 《设计模式》一书讨论了23种不同的模式,并依据三个标准分类(所有标准都涉及那些可能发生变化的方面)。这三个标准是: 66 | 67 | (1) 创建:对象的创建方式。这通常涉及对象创建细节的隔离,这样便不必依赖具体类型的对象,所以在新添一种对象类型时也不必改动代码。 68 | 69 | (2) 结构:设计对象,满足特定的项目限制。这涉及对象与其他对象的连接方式,以保证系统内的改变不会影响到这些连接。 70 | 71 | (3) 行为:对程序中特定类型的行动进行操纵的对象。这要求我们将希望采取的操作封装起来,比如解释一种语言、实现一个请求、在一个序列中遍历(就象在迭代器中那样)或者实现一种算法。本章提供了“观察器”(`Observer`)和“访问器”(`Visitor`)的模式的例子。 72 | 73 | 《设计模式》为所有这23种模式都分别使用了一节,随附的还有大量示例,但大多是用C++编写的,少数用Smalltalk编写(如看过这本书,就知道这实际并不是个大问题,因为很容易即可将基本概念从两种语言翻译到Java里)。现在这本书并不打算重复《设计模式》介绍的所有模式,因为那是一本独立的书,大家应该单独阅读。相反,本章只准备给出一些例子,让大家先对模式有个大致的印象,并理解它们的重要性到底在哪里。 74 | -------------------------------------------------------------------------------- /16.10.md: -------------------------------------------------------------------------------- 1 | # 16.10 练习 2 | 3 | 4 | (1) 将`SingletonPattern.java`作为起点,创建一个类,用它管理自己固定数量的对象。 5 | 6 | (2) 为`TrashVisitor.java`添加一个名为`Plastic`(塑料)的类。 7 | 8 | (3) 为`DynaTrash.java`同样添加一个`Plastic`(塑料)类。 9 | -------------------------------------------------------------------------------- /16.2.md: -------------------------------------------------------------------------------- 1 | # 16.2 观察器模式 2 | 3 | 观察器(`Observer`)模式解决的是一个相当普通的问题:由于某些对象的状态发生了改变,所以一组对象都需要更新,那么该如何解决?在Smalltalk的MVC(模型-视图-控制器)的“模型-视图”部分中,或在几乎等价的“文档-视图结构”中,大家可以看到这个问题。现在我们有一些数据(“文档”)以及多个视图,假定为一张图(`Plot`)和一个文本视图。若改变了数据,两个视图必须知道对自己进行更新,而那正是“观察器”要负责的工作。这是一种十分常见的问题,它的解决方案已包括进标准的`java.util`库中。 4 | 5 | 在Java中,有两种类型的对象用来实现观察器模式。其中,`Observable`类用于跟踪那些当发生一个改变时希望收到通知的所有个体——无论“状态”是否改变。如果有人说“好了,所有人都要检查自己,并可能要进行更新”,那么`Observable`类会执行这个任务——为列表中的每个“人”都调用`notifyObservers()`方法。`notifyObservers()`方法属于基类`Observable`的一部分。 6 | 7 | 在观察器模式中,实际有两个方面可能发生变化:观察对象的数量以及更新的方式。也就是说,观察器模式允许我们同时修改这两个方面,不会干扰围绕在它周围的其他代码。 8 | 9 | 下面这个例子类似于第14章的`ColorBoxes`示例。箱子(`Boxes`)置于一个屏幕网格中,每个都初始化一种随机的颜色。此外,每个箱子都“实现”(`implement`)了“观察器”(`Observer`)接口,而且随一个`Observable`对象进行了注册。若点击一个箱子,其他所有箱子都会收到一个通知,指出一个改变已经发生。这是由于`Observable`对象会自动调用每个`Observer`对象的`update()`方法。在这个方法内,箱子会检查被点中的那个箱子是否与自己紧邻。若答案是肯定的,那么也修改自己的颜色,保持与点中那个箱子的协调。 10 | 11 | ``` 12 | //: BoxObserver.java 13 | // Demonstration of Observer pattern using 14 | // Java's built-in observer classes. 15 | import java.awt.*; 16 | import java.awt.event.*; 17 | import java.util.*; 18 | 19 | // You must inherit a new type of Observable: 20 | class BoxObservable extends Observable { 21 | public void notifyObservers(Object b) { 22 | // Otherwise it won't propagate changes: 23 | setChanged(); 24 | super.notifyObservers(b); 25 | } 26 | } 27 | 28 | public class BoxObserver extends Frame { 29 | Observable notifier = new BoxObservable(); 30 | public BoxObserver(int grid) { 31 | setTitle("Demonstrates Observer pattern"); 32 | setLayout(new GridLayout(grid, grid)); 33 | for(int x = 0; x < grid; x++) 34 | for(int y = 0; y < grid; y++) 35 | add(new OCBox(x, y, notifier)); 36 | } 37 | public static void main(String[] args) { 38 | int grid = 8; 39 | if(args.length > 0) 40 | grid = Integer.parseInt(args[0]); 41 | Frame f = new BoxObserver(grid); 42 | f.setSize(500, 400); 43 | f.setVisible(true); 44 | f.addWindowListener( 45 | new WindowAdapter() { 46 | public void windowClosing(WindowEvent e) { 47 | System.exit(0); 48 | } 49 | }); 50 | } 51 | } 52 | 53 | class OCBox extends Canvas implements Observer { 54 | Observable notifier; 55 | int x, y; // Locations in grid 56 | Color cColor = newColor(); 57 | static final Color[] colors = { 58 | Color.black, Color.blue, Color.cyan, 59 | Color.darkGray, Color.gray, Color.green, 60 | Color.lightGray, Color.magenta, 61 | Color.orange, Color.pink, Color.red, 62 | Color.white, Color.yellow 63 | }; 64 | static final Color newColor() { 65 | return colors[ 66 | (int)(Math.random() * colors.length) 67 | ]; 68 | } 69 | OCBox(int x, int y, Observable notifier) { 70 | this.x = x; 71 | this.y = y; 72 | notifier.addObserver(this); 73 | this.notifier = notifier; 74 | addMouseListener(new ML()); 75 | } 76 | public void paint(Graphics g) { 77 | g.setColor(cColor); 78 | Dimension s = getSize(); 79 | g.fillRect(0, 0, s.width, s.height); 80 | } 81 | class ML extends MouseAdapter { 82 | public void mousePressed(MouseEvent e) { 83 | notifier.notifyObservers(OCBox.this); 84 | } 85 | } 86 | public void update(Observable o, Object arg) { 87 | OCBox clicked = (OCBox)arg; 88 | if(nextTo(clicked)) { 89 | cColor = clicked.cColor; 90 | repaint(); 91 | } 92 | } 93 | private final boolean nextTo(OCBox b) { 94 | return Math.abs(x - b.x) <= 1 && 95 | Math.abs(y - b.y) <= 1; 96 | } 97 | } ///:~ 98 | ``` 99 | 100 | 如果是首次查阅`Observable`的联机帮助文档,可能会多少感到有些困惑,因为它似乎表明可以用一个原始的`Observable`对象来管理更新。但这种说法是不成立的;大家可自己试试——在`BoxObserver`中,创建一个`Observable`对象,替换`BoxObservable`对象,看看会有什么事情发生。事实上,什么事情也不会发生。为真正产生效果,必须从`Observable`继承,并在派生类代码的某个地方调用`setChanged()`。这个方法需要设置`changed`(已改变)标志,它意味着当我们调用`notifyObservers()`的时候,所有观察器事实上都会收到通知。在上面的例子中,`setChanged()`只是简单地在`notifyObservers()`中调用,大家可依据符合实际情况的任何标准决定何时调用`setChanged()`。 101 | 102 | `BoxObserver`包含了单个`Observable`对象,名为`notifier`。每次创建一个`OCBox`对象时,它都会同`notifier`联系到一起。在`OCBox`中,只要点击鼠标,就会发出对`notifyObservers()`方法的调用,并将被点中的那个对象作为一个参数传递进去,使收到消息(用它们的`update()`方法)的所有箱子都能知道谁被点中了,并据此判断自己是否也要变动。通过`notifyObservers()`和`update()`中的代码的结合,我们可以应付一些非常复杂的局面。 103 | 104 | 在`notifyObservers()`方法中,表面上似乎观察器收到通知的方式必须在编译期间固定下来。然而,只要稍微仔细研究一下上面的代码,就会发现`BoxObserver`或`OCBox`中唯一需要留意是否使用`BoxObservable`的地方就是创建`Observable`对象的时候——从那时开始,所有东西都会使用基本的`Observable`接口。这意味着以后若想更改通知方式,可以继承其他`Observable`类,并在运行期间交换它们。 105 | -------------------------------------------------------------------------------- /16.3.md: -------------------------------------------------------------------------------- 1 | # 16.3 模拟垃圾回收站 2 | 3 | 这个问题的本质是若将垃圾丢进单个垃圾筒,事实上是未经分类的。但在以后,某些特殊的信息必须恢复,以便对垃圾正确地归类。在最开始的解决方案中,RTTI扮演了关键的角色(详见第11章)。 4 | 5 | 这并不是一种普通的设计,因为它增加了一个新的限制。正是这个限制使问题变得非常有趣——它更象我们在工作中碰到的那些非常麻烦的问题。这个额外的限制是:垃圾抵达垃圾回收站时,它们全都是混合在一起的。程序必须为那些垃圾的分类定出一个模型。这正是RTTI发挥作用的地方:我们有大量不知名的垃圾,程序将正确判断出它们所属的类型。 6 | 7 | ``` 8 | //: RecycleA.java 9 | // Recycling with RTTI 10 | package c16.recyclea; 11 | import java.util.*; 12 | import java.io.*; 13 | 14 | abstract class Trash { 15 | private double weight; 16 | Trash(double wt) { weight = wt; } 17 | abstract double value(); 18 | double weight() { return weight; } 19 | // Sums the value of Trash in a bin: 20 | static void sumValue(Vector bin) { 21 | Enumeration e = bin.elements(); 22 | double val = 0.0f; 23 | while(e.hasMoreElements()) { 24 | // One kind of RTTI: 25 | // A dynamically-checked cast 26 | Trash t = (Trash)e.nextElement(); 27 | // Polymorphism in action: 28 | val += t.weight() * t.value(); 29 | System.out.println( 30 | "weight of " + 31 | // Using RTTI to get type 32 | // information about the class: 33 | t.getClass().getName() + 34 | " = " + t.weight()); 35 | } 36 | System.out.println("Total value = " + val); 37 | } 38 | } 39 | 40 | class Aluminum extends Trash { 41 | static double val = 1.67f; 42 | Aluminum(double wt) { super(wt); } 43 | double value() { return val; } 44 | static void value(double newval) { 45 | val = newval; 46 | } 47 | } 48 | 49 | class Paper extends Trash { 50 | static double val = 0.10f; 51 | Paper(double wt) { super(wt); } 52 | double value() { return val; } 53 | static void value(double newval) { 54 | val = newval; 55 | } 56 | } 57 | 58 | class Glass extends Trash { 59 | static double val = 0.23f; 60 | Glass(double wt) { super(wt); } 61 | double value() { return val; } 62 | static void value(double newval) { 63 | val = newval; 64 | } 65 | } 66 | 67 | public class RecycleA { 68 | public static void main(String[] args) { 69 | Vector bin = new Vector(); 70 | // Fill up the Trash bin: 71 | for(int i = 0; i < 30; i++) 72 | switch((int)(Math.random() * 3)) { 73 | case 0 : 74 | bin.addElement(new 75 | Aluminum(Math.random() * 100)); 76 | break; 77 | case 1 : 78 | bin.addElement(new 79 | Paper(Math.random() * 100)); 80 | break; 81 | case 2 : 82 | bin.addElement(new 83 | Glass(Math.random() * 100)); 84 | } 85 | Vector 86 | glassBin = new Vector(), 87 | paperBin = new Vector(), 88 | alBin = new Vector(); 89 | Enumeration sorter = bin.elements(); 90 | // Sort the Trash: 91 | while(sorter.hasMoreElements()) { 92 | Object t = sorter.nextElement(); 93 | // RTTI to show class membership: 94 | if(t instanceof Aluminum) 95 | alBin.addElement(t); 96 | if(t instanceof Paper) 97 | paperBin.addElement(t); 98 | if(t instanceof Glass) 99 | glassBin.addElement(t); 100 | } 101 | Trash.sumValue(alBin); 102 | Trash.sumValue(paperBin); 103 | Trash.sumValue(glassBin); 104 | Trash.sumValue(bin); 105 | } 106 | } ///:~ 107 | ``` 108 | 109 | 要注意的第一个地方是`package`语句: 110 | 111 | ``` 112 | package c16.recyclea; 113 | ``` 114 | 115 | 这意味着在本书采用的源码目录中,这个文件会被置入从`c16`(代表第16章的程序)分支出来的`recyclea`子目录中。第17章的解包工具会负责将其置入正确的子目录。之所以要这样做,是因为本章会多次改写这个特定的例子;它的每个版本都会置入自己的“包”(`package`)内,避免类名的冲突。 116 | 117 | 其中创建了几个`Vector`对象,用于容纳`Trash`引用。当然,`Vector`实际容纳的是`Object`(对象),所以它们最终能够容纳任何东西。之所以要它们容纳`Trash`(或者从`Trash`派生出来的其他东西),唯一的理由是我们需要谨慎地避免放入除`Trash`以外的其他任何东西。如果真的把某些“错误”的东西置入`Vector`,那么不会在编译期得到出错或警告提示——只能通过运行期的一个异常知道自己已经犯了错误。 118 | 119 | `Trash`引用加入后,它们会丢失自己的特定标识信息,只会成为简单的`Object`引用(向上转换)。然而,由于存在多态性的因素,所以在我们通过`Enumeration sorter`调用动态绑定方法时,一旦结果`Object`已经转换回`Trash`,仍然会发生正确的行为。`sumValue()`也用一个`Enumeration`对`Vector`中的每个对象进行操作。 120 | 121 | 表面上持,先把`Trash`的类型向上转换到一个集合容纳基类型的引用,再回过头重新向下转换,这似乎是一种非常愚蠢的做法。为什么不只是一开始就将垃圾置入适当的容器里呢?(事实上,这正是拨开“回收”一团迷雾的关键)。在这个程序中,我们很容易就可以换成这种做法,但在某些情况下,系统的结构及灵活性都能从向下转换中得到极大的好处。 122 | 123 | 该程序已满足了设计的初衷:它能够正常工作!只要这是个一次性的方案,就会显得非常出色。但是,真正有用的程序应该能够在任何时候解决问题。所以必须问自己这样一个问题:“如果情况发生了变化,它还能工作吗?”举个例子来说,厚纸板现在是一种非常有价值的可回收物品,那么如何把它集成到系统中呢(特别是程序很大很复杂的时候)?由于前面在`switch`语句中的类型检查编码可能散布于整个程序,所以每次加入一种新类型时,都必须找到所有那些编码。若不慎遗漏一个,编译器除了指出存在一个错误之外,不能再提供任何有价值的帮助。 124 | 125 | RTTI在这里使用不当的关键是“每种类型都进行了测试”。如果由于类型的子集需要特殊的对待,所以只寻找那个子集,那么情况就会变得好一些。但假如在一个`switch`语句中查找每一种类型,那么很可能错过一个重点,使最终的代码很难维护。在下一节中,大家会学习如何逐步对这个程序进行改进,使其显得越来越灵活。这是在程序设计中一种非常有意义的例子。 126 | -------------------------------------------------------------------------------- /16.5.md: -------------------------------------------------------------------------------- 1 | # 16.5 抽象的应用 2 | 3 | 走到这一步,接下来该考虑一下设计模式剩下的部分了——在哪里使用类?既然归类到垃圾箱的办法非常不雅且过于暴露,为什么不隔离那个过程,把它隐藏到一个类里呢?这就是著名的“如果必须做不雅的事情,至少应将其本地化到一个类里”规则。看起来就象下面这样: 4 | 5 |  6 | 7 | 现在,只要一种新类型的`Trash`加入方法,对`TrashSorter`对象的初始化就必须变动。可以想象,`TrashSorter`类看起来应该象下面这个样子: 8 | 9 | ``` 10 | class TrashSorter extends Vector { 11 | void sort(Trash t) { /* ... */ } 12 | } 13 | ``` 14 | 15 | 也就是说,`TrashSorter`是由一系列引用构成的`Vector`(系列),而那些引用指向的又是由`Trash`引用构成的`Vector`;利用`addElement()`,可以安装新的`TrashSorter`,如下所示: 16 | 17 | ``` 18 | TrashSorter ts = new TrashSorter(); 19 | ts.addElement(new Vector()); 20 | ``` 21 | 22 | 但是现在,`sort()`却成为一个问题。用静态方式编码的方法如何应付一种新类型加入的事实呢?为解决这个问题,必须从`sort()`里将类型信息删除,使其需要做的所有事情就是调用一个通用方法,用它照料涉及类型处理的所有细节。这当然是对一个动态绑定方法进行描述的另一种方式。所以`sort()`会在序列中简单地遍历,并为每个`Vector`都调用一个动态绑定方法。由于这个方法的任务是收集它感兴趣的垃圾片,所以称之为`grab(Trash)`。结构现在变成了下面这样: 23 | 24 |  25 | 26 | 其中,`TrashSorter`需要调用每个`grab()`方法;然后根据当前`Vector`容纳的是什么类型,会获得一个不同的结果。也就是说,`Vector`必须留意自己容纳的类型。解决这个问题的传统方法是创建一个基础“Trash bin”(垃圾筒)类,并为希望容纳的每个不同的类型都继承一个新的派生类。若Java有一个参数化的类型机制,那就也许是最直接的方法。但对于这种机制应该为我们构建的各个类,我们不应该进行麻烦的手工编码,以后的“观察”方式提供了一种更好的编码方式。 27 | 28 | OOP设计一条基本的准则是“为状态的变化使用数据成员,为行为的变化使用多性形”。对于容纳`Paper`(纸张)的`Vector`,以及容纳`Glass`(玻璃)的`Vector`,大家最开始或许会认为分别用于它们的`grab()`方法肯定会产生不同的行为。但具体如何却完全取决于类型,而不是其他什么东西。可将其解释成一种不同的状态,而且由于Java有一个类可表示类型(`Class`),所以可用它判断特定的`Tbin`要容纳什么类型的`Trash`。 29 | 30 | 用于Tbin的构造器要求我们为其传递自己选择的一个`Class`。这样做可告诉`Vector`它希望容纳的是什么类型。随后,`grab()`方法用`Class BinType`和RTTI来检查我们传递给它的`Trash`对象是否与它希望收集的类型相符。 31 | 下面列出完整的解决方案。设定为注释的编号(如*1*)便于大家对照程序后面列出的说明。 32 | 33 | ``` 34 | //: RecycleB.java 35 | // Adding more objects to the recycling problem 36 | package c16.recycleb; 37 | import c16.trash.*; 38 | import java.util.*; 39 | 40 | // A vector that admits only the right type: 41 | class Tbin extends Vector { 42 | Class binType; 43 | Tbin(Class binType) { 44 | this.binType = binType; 45 | } 46 | boolean grab(Trash t) { 47 | // Comparing class types: 48 | if(t.getClass().equals(binType)) { 49 | addElement(t); 50 | return true; // Object grabbed 51 | } 52 | return false; // Object not grabbed 53 | } 54 | } 55 | 56 | class TbinList extends Vector { //(*1*) 57 | boolean sort(Trash t) { 58 | Enumeration e = elements(); 59 | while(e.hasMoreElements()) { 60 | Tbin bin = (Tbin)e.nextElement(); 61 | if(bin.grab(t)) return true; 62 | } 63 | return false; // bin not found for t 64 | } 65 | void sortBin(Tbin bin) { // (*2*) 66 | Enumeration e = bin.elements(); 67 | while(e.hasMoreElements()) 68 | if(!sort((Trash)e.nextElement())) 69 | System.out.println("Bin not found"); 70 | } 71 | } 72 | 73 | public class RecycleB { 74 | static Tbin bin = new Tbin(Trash.class); 75 | public static void main(String[] args) { 76 | // Fill up the Trash bin: 77 | ParseTrash.fillBin("Trash.dat", bin); 78 | 79 | TbinList trashBins = new TbinList(); 80 | trashBins.addElement( 81 | new Tbin(Aluminum.class)); 82 | trashBins.addElement( 83 | new Tbin(Paper.class)); 84 | trashBins.addElement( 85 | new Tbin(Glass.class)); 86 | // add one line here: (*3*) 87 | trashBins.addElement( 88 | new Tbin(Cardboard.class)); 89 | 90 | trashBins.sortBin(bin); // (*4*) 91 | 92 | Enumeration e = trashBins.elements(); 93 | while(e.hasMoreElements()) { 94 | Tbin b = (Tbin)e.nextElement(); 95 | Trash.sumValue(b); 96 | } 97 | Trash.sumValue(bin); 98 | } 99 | } ///:~ 100 | ``` 101 | 102 | (1) `TbinList`容纳一系列`Tbin`引用,所以在查找与我们传递给它的`Trash`对象相符的情况时,`sort()`能通过`Tbin`继承。 103 | 104 | (2) `sortBin()`允许我们将一个完整的`Tbin`传递进去,而且它会在`Tbin`里遍历,挑选出每种`Trash`,并将其归类到特定的`Tbin`中。请注意这些代码的通用性:新类型加入时,它本身不需要任何改动。只要新类型加入(或发生其他事件)时大量代码都不需要变化,就表明我们设计的是一个容易扩展的系统。 105 | 106 | (3) 现在可以体会添加新类型有多么容易了。为支持添加,只需要改动几行代码。如确实有必要,甚至可以进一步地改进设计,使更多的代码都保持“固定”。 107 | 108 | (4) 一个方法调用使`bin`的内容归类到对应的、特定类型的垃圾筒里。 109 | -------------------------------------------------------------------------------- /16.8.md: -------------------------------------------------------------------------------- 1 | # 16.8 RTTI真的有害吗 2 | 3 | 4 | 本章的各种设计模式都在努力避免使用RTTI,这或许会给大家留下“RTTI有害”的印象(还记得可怜的`goto`吗,由于给人印象不佳,根本就没有放到Java里来)。但实际情况并非绝对如此。正确地说,应该是RTTI使用不当才“有害”。我们之所以想避免RTTI的使用,是由于它的错误运用会造成扩展性受到损害。而我们事前提出的目标就是能向系统自由加入新类型,同时保证对周围的代码造成尽可能小的影响。由于RTTI常被滥用(让它查找系统中的每一种类型),会造成代码的扩展能力大打折扣——添加一种新类型时,必须找出使用了RTTI的所有代码。即使仅遗漏了其中的一个,也不能从编译器那里得到任何帮助。 5 | 6 | 然而,RTTI本身并不会自动产生非扩展性的代码。让我们再来看一看前面提到的垃圾回收例子。这一次准备引入一种新工具,我把它叫作`TypeMap`。其中包含了一个`Hashtable`(散列表),其中容纳了多个`Vector`,但接口非常简单:可以添加(`add()`)一个新对象,可以获得(`get()`)一个`Vector`,其中包含了属于某种特定类型的所有对象。对于这个包含的散列表,它的关键在于对应的`Vector`里的类型。这种设计模式的优点(根据Larry O'Brien的建议)是在遇到一种新类型的时候,`TypeMap`会动态加入一种新类型。所以不管什么时候,只要将一种新类型加入系统(即使在运行期间添加),它也会正确无误地得以接受。 7 | 8 | 我们的例子同样建立在`c16.Trash`这个“包”(`Package`)内的`Trash`类型结构的基础上(而且那儿使用的`Trash.dat`文件可以照搬到这里来)。 9 | 10 | ``` 11 | //: DynaTrash.java 12 | // Using a Hashtable of Vectors and RTTI 13 | // to automatically sort trash into 14 | // vectors. This solution, despite the 15 | // use of RTTI, is extensible. 16 | package c16.dynatrash; 17 | import c16.trash.*; 18 | import java.util.*; 19 | 20 | // Generic TypeMap works in any situation: 21 | class TypeMap { 22 | private Hashtable t = new Hashtable(); 23 | public void add(Object o) { 24 | Class type = o.getClass(); 25 | if(t.containsKey(type)) 26 | ((Vector)t.get(type)).addElement(o); 27 | else { 28 | Vector v = new Vector(); 29 | v.addElement(o); 30 | t.put(type,v); 31 | } 32 | } 33 | public Vector get(Class type) { 34 | return (Vector)t.get(type); 35 | } 36 | public Enumeration keys() { return t.keys(); } 37 | // Returns handle to adapter class to allow 38 | // callbacks from ParseTrash.fillBin(): 39 | public Fillable filler() { 40 | // Anonymous inner class: 41 | return new Fillable() { 42 | public void addTrash(Trash t) { add(t); } 43 | }; 44 | } 45 | } 46 | 47 | public class DynaTrash { 48 | public static void main(String[] args) { 49 | TypeMap bin = new TypeMap(); 50 | ParseTrash.fillBin("Trash.dat",bin.filler()); 51 | Enumeration keys = bin.keys(); 52 | while(keys.hasMoreElements()) 53 | Trash.sumValue( 54 | bin.get((Class)keys.nextElement())); 55 | } 56 | } ///:~ 57 | ``` 58 | 59 | 尽管功能很强,但对`TypeMap`的定义是非常简单的。它只是包含了一个散列表,同时`add()`负担了大部分的工作。添加一个新类型时,那种类型的`Class`对象的引用会被提取出来。随后,利用这个引用判断容纳了那类对象的一个`Vector`是否已存在于散列表中。如答案是肯定的,就提取出那个`Vector`,并将对象加入其中;反之,就将`Class`对象及新`Vector`作为一个“键-值”对加入。 60 | 61 | 利用`keys()`,可以得到对所有`Class`对象的一个“枚举”(`Enumeration`),而且可用`get()`,可通过`Class`对象获取对应的`Vector`。 62 | 63 | `filler()`方法非常有趣,因为它利用了`ParseTrash.fillBin()`的设计——不仅能尝试填充一个`Vector`,也能用它的`addTrash()`方法试着填充实现了`Fillable`(可填充)接口的任何东西。`filter()`需要做的全部事情就是将一个引用返回给实现了`Fillable`的一个接口,然后将这个引用作为参数传递给`fillBin()`,就象下面这样: 64 | 65 | ``` 66 | ParseTrash.fillBin("Trash.dat", bin.filler()); 67 | ``` 68 | 69 | 为产生这个引用,我们采用了一个“匿名内部类”(已在第7章讲述)。由于根本不需要用一个已命名的类来实现`Fillable`,只需要属于那个类的一个对象的引用即可,所以这里使用匿名内部类是非常恰当的。 70 | 71 | 对这个设计,要注意的一个地方是尽管没有设计成对归类加以控制,但在`fillBin()`每次进行归类的时候,都会将一个`Trash`对象插入`bin`。 72 | 73 | 通过前面那些例子的学习,`DynaTrash`类的大多数部分都应当非常熟悉了。这一次,我们不再将新的`Trash`对象置入类型`Vector`的一个`bin`内。由于`bin`的类型为`TypeMap`,所以将垃圾(`Trash`)丢进垃圾筒(`Bin`)的时候,`TypeMap`的内部归类机制会立即进行适当的分类。在`TypeMap`里遍历并对每个独立的`Vector`进行操作,这是一件相当简单的事情: 74 | 75 | 76 | ``` 77 | Enumeration keys = bin.keys(); 78 | while(keys.hasMoreElements()) 79 | Trash.sumValue( 80 | bin.get((Class)keys.nextElement())); 81 | ``` 82 | 83 | 就象大家看到的那样,新类型向系统的加入根本不会影响到这些代码,亦不会影响`TypeMap`中的代码。这显然是解决问题最圆满的方案。尽管它确实严重依赖RTTI,但请注意散列表中的每个键-值对都只查找一种类型。除此以外,在我们增加一种新类型的时候,不会陷入“忘记”向系统加入正确代码的尴尬境地,因为根本就没有什么代码需要添加。 84 | -------------------------------------------------------------------------------- /16.9.md: -------------------------------------------------------------------------------- 1 | # 16.9 总结 2 | 3 | 从表面看,由于象`TrashVisitor.java`这样的设计包含了比早期设计数量更多的代码,所以会留下效率不高的印象。试图用各种设计模式达到什么目的应该是我们考虑的重点。设计模式特别适合“将发生变化的东西与保持不变的东西隔离开”。而“发生变化的东西”可以代表许多种变化。之所以发生变化,可能是由于程序进入一个新环境,或者由于当前环境的一些东西发生了变化(例如“用户希望在屏幕上当前显示的图示中添加一种新的几何形状”)。或者就象本章描述的那样,变化可能是对代码主体的不断改进。尽管废品分类以前的例子强调了新型`Trash`向系统的加入,但`TrashVisitor.java`允许我们方便地添加新功能,同时不会对`Trash`结构造成干扰。`TrashVisitor.java`里确实多出了许多代码,但在`Visitor`里添加新功能只需要极小的代价。如果经常都要进行此类活动,那么多一些代码也是值得的。 4 | 5 | 变化序列的发现并非一件平常事;在程序的初始设计出台以前,那些分析家一般不可能预测到这种变化。除非进入项目设计的后期,否则一些必要的信息是不会显露出来的:有时只有进入设计或最终实现阶段,才能体会到对自己系统一个更深入或更不易察觉需要。添加新类型时(这是“回收”例子最主要的一个重点),可能会意识到只有自己进入维护阶段,而且开始扩充系统时,才需要一个特定的继承结构。 6 | 7 | 通过设计模式的学习,大家可体会到最重要的一件事情就是本书一直宣扬的一个观点——多态性是OOP(面向对象程序设计)的全部——已发生了彻底的改变。换句话说,很难“获得”多态性;而一旦获得,就需要尝试将自己的所有设计都转换到一个特定的模子里去。 8 | 9 | 设计模式要表明的观点是“OOP并不仅仅同多态性有关”。应当与OOP有关的是“将发生变化的东西同保持不变的东西分隔开来”。多态性是达到这一目的的特别重要的手段。而且假如编程语言直接支持多态性,那么它就显得尤其有用(由于直接支持,所以不必自己动手编写,从而节省大量的精力和时间)。但设计模式向我们揭示的却是达到基本目标的另一些常规途径。而且一旦熟悉并掌握了它的用法,就会发现自己可以做出更有创新性的设计。 10 | 11 | 由于《设计模式》这本书对程序员造成了如此重要的影响,所以他们纷纷开始寻找其他模式。随着的时间的推移,这类模式必然会越来越多。JimCoplien(`http://www.bell-labs.com/~cope` 主页作者)向我们推荐了这样的一些站点,上面有许多很有价值的模式说明: 12 | 13 | http://st-www.cs.uiuc.edu/users/patterns 14 | 15 | http://c2.com/cgi/wiki 16 | 17 | http://c2.com/ppr 18 | 19 | http://www.bell-labs.com/people/cope/Patterns/Process/index.html 20 | 21 | http://www.bell-labs.com/cgi-user/OrgPatterns/OrgPatterns 22 | 23 | http://st-www.cs.uiuc.edu/cgi-bin/wikic/wikic 24 | 25 | http://www.cs.wustl.edu/~schmidt/patterns.html 26 | 27 | http://www.espinc.com/patterns/overview.html 28 | 29 | 同时请留意每年都要召开一届权威性的设计模式会议,名为PLOP。会议会出版许多学术论文,第三届已在1997年底召开过了,会议所有资料均由Addison-Wesley出版。 30 | -------------------------------------------------------------------------------- /16.md: -------------------------------------------------------------------------------- 1 | # 第16章 设计模式 2 | 3 | 本章要向大家介绍重要但却并不是那么传统的“模式”(Pattern)程序设计方法。 4 | 5 | 在向面向对象程序设计的演化过程中,或许最重要的一步就是“设计模式”(Design Pattern)的问世。它在由Gamma,Helm和Johnson编著的《设计模式》一书中被定义成一个“里程碑”(该书由Addison-Wesley于1995年出版,注释①)。那本书列出了解决这个问题的23种不同的方法。在本章中,我们准备伴随几个例子揭示出设计模式的基本概念。这或许能激起您阅读《设计模式》一书的欲望。事实上,那本书现在已成为几乎所有OOP程序员都必备的参考书。 6 | 7 | ①:但警告大家:书中的例子是用C++写的。 8 | 9 | 本章的后一部分包含了展示设计进化过程的一个例子,首先是比较原始的方案,经过逐渐发展和改进,慢慢成为更符合逻辑、更为恰当的设计。该程序(仿真垃圾分类)一直都在进化,可将这种进化作为自己设计模式的一个原型——先为特定的问题提出一个适当的方案,再逐步改善,使其成为解决那类问题一种最灵活的方案。 10 | -------------------------------------------------------------------------------- /17.4.md: -------------------------------------------------------------------------------- 1 | # 17.4 总结 2 | 3 | 4 | 通过本章的学习,大家知道运用Java可做到一些较复杂的事情。通过这些例子亦可看出,尽管Java必定有自己的局限,但受那些局限影响的主要是性能(比如写好文字处理程序后,会发现C++的版本要快得多——这部分是由于IO库做得不完善造成的;而在你读到本书的时候,情况也许已发生了变化。但Java的局限也仅此而已,它在语言表达方面的能力是无以伦比的。利用Java,几乎可以表达出我们想得到的任何事情。而与此同时,Java在表达的方便性和易读性上,也做足了功夫。所以在使用Java时,一般不会陷入其他语言常见的那种复杂境地。使用那些语言时,会感觉它们象一个爱唠叨的老太婆,哪有Java那样清纯、简练!而且通过Java 1.2的JFC/Swing库,AWT的表达能力和易用性甚至又得到了进一步的增强。 5 | -------------------------------------------------------------------------------- /17.5.md: -------------------------------------------------------------------------------- 1 | # 17.5 练习 2 | 3 | (1) (稍微有些难度)改写`FieldOBeasts.java`,使它的状态能够保持固定。加上一些按钮,允许用户保存和恢复不同的状态文件,并从它们断掉的地方开始继续运行。请先参考第10章的`CADState.java`,再决定具体怎样做。 4 | 5 | (2) (大作业)以`FieldOBeasts.java`作为起点,构造一个自动化交通仿真系统。 6 | 7 | (3) (大作业)以`ClassScanner.java`作为起点,构造一个特殊的工具,用它找出那些虽然定义但从未用过的方法和字段。 8 | 9 | (4) (大作业)利用JDBC,构造一个联络管理程序。让这个程序以一个平面文件数据库为基础,其中包含了名字、地址、电话号码、E-mail地址等联系资料。应该能向数据库里方便地加入新名字。键入要查找的名字时,请采用在第15章的`VLookup.java`里介绍过的那种名字自动填充技术。 10 | -------------------------------------------------------------------------------- /17.md: -------------------------------------------------------------------------------- 1 | # 第17章 项目 2 | 3 | 本章包含了一系列项目,它们都以本书介绍的内容为基础,并对早期的章节进行了一定程度的扩充。 4 | 5 | 与以前经历过的项目相比,这儿的大多数项目都明显要复杂得多,它们充分演示了新技术以及类库的运用。 6 | -------------------------------------------------------------------------------- /2.1.md: -------------------------------------------------------------------------------- 1 | # 2.1 用引用操纵对象 2 | 3 | 每种编程语言都有自己的数据处理方式。有些时候,程序员必须时刻留意准备处理的是什么类型。您曾利用一些特殊语法直接操作过对象,或处理过一些间接表示的对象吗(C或C++里的指针)? 4 | 5 | 所有这些在Java里都得到了简化,任何东西都可看作对象。因此,我们可采用一种统一的语法,任何地方均可照搬不误。但要注意,尽管将一切都“看作”对象,但操纵的标识符实际是指向一个对象的“句柄”(Handle)。在其他Java参考书里,还可看到有的人将其称作一个“引用”,甚至一个“指针”。可将这一情形想象成用遥控板(引用)操纵电视机(对象)。只要握住这个遥控板,就相当于掌握了与电视机连接的通道。但一旦需要“换频道”或者“关小声音”,我们实际操纵的是遥控板(引用),再由遥控板自己操纵电视机(对象)。如果要在房间里四处走走,并想保持对电视机的控制,那么手上拿着的是遥控板,而非电视机。 6 | 7 | 此外,即使没有电视机,遥控板亦可独立存在。也就是说,只是由于拥有一个引用,并不表示必须有一个对象同它连接。所以如果想容纳一个词或句子,可创建一个`String`引用: 8 | 9 | ``` 10 | String s; 11 | ``` 12 | 13 | 但这里创建的只是引用,并不是对象。若此时向`s`发送一条消息,就会获得一个错误(运行期)。这是由于`s`实际并未与任何东西连接(即“没有电视机”)。因此,一种更安全的做法是:创建一个引用时,记住无论如何都进行初始化: 14 | 15 | ``` 16 | String s = "asdf"; 17 | ``` 18 | 19 | 然而,这里采用的是一种特殊类型:字符串可用加引号的文字初始化。通常,必须为对象使用一种更通用的初始化类型。 20 | -------------------------------------------------------------------------------- /2.10.md: -------------------------------------------------------------------------------- 1 | # 2.10 总结 2 | 3 | 4 | 通过本章的学习,大家已接触了足够多的Java编程知识,已知道如何自行编写一个简单的程序。此外,对语言的总体情况以及一些基本思想也有了一定程度的认识。然而,本章所有例子的模式都是单线形式的“这样做,再那样做,然后再做另一些事情”。如果想让程序作出一项选择,又该如何设计呢?例如,“假如这样做的结果是红色,就那样做;如果不是,就做另一些事情”。对于这种基本的编程方法,下一章会详细说明在Java里是如何实现的。 5 | 6 | -------------------------------------------------------------------------------- /2.11.md: -------------------------------------------------------------------------------- 1 | # 2.11 练习 2 | 3 | 4 | (1) 参照本章的第一个例子,创建一个“Hello,World”程序,在屏幕上简单地显示这句话。注意在自己的类里只需一个方法(`main`方法会在程序启动时执行)。记住要把它设为`static`形式,并置入参数列表——即使根本不会用到这个列表。用`javac`编译这个程序,再用`java`运行它。 5 | 6 | (2) 写一个程序,打印出从命令行获取的三个参数。 7 | 8 | (3) 找出`Property.java`第二个版本的代码,这是一个简单的注释文档示例。请对文件执行`javadoc`,并在自己的Web浏览器里观看结果。 9 | 10 | (4) 以练习(1)的程序为基础,向其中加入注释文档。利用`javadoc`,将这个注释文档提取为一个HTML文件,并用Web浏览器观看。 11 | -------------------------------------------------------------------------------- /2.2.md: -------------------------------------------------------------------------------- 1 | # 2.2 所有对象都必须创建 2 | 3 | 4 | 创建引用时,我们希望它同一个新对象连接。通常用`new`关键字达到这一目的。`new`的意思是:“把我变成这些对象的一种新类型”。所以在上面的例子中,可以说: 5 | 6 | ``` 7 | String s = new String("asdf"); 8 | ``` 9 | 10 | 它不仅指出“将我变成一个新字符串”,也通过提供一个初始字符串,指出了“如何生成这个新字符串”。 11 | 12 | 当然,字符串(`String`)并非唯一的类型。Java配套提供了数量众多的现成类型。对我们来讲,最重要的就是记住能自行创建类型。事实上,这应是Java程序设计的一项基本操作,是继续本书后余部分学习的基础。 13 | 14 | ## 2.2.1 保存到什么地方 15 | 16 | 程序运行时,我们最好对数据保存到什么地方做到心中有数。特别要注意的是内存的分配。有六个地方都可以保存数据: 17 | 18 | (1) 寄存器。这是最快的保存区域,因为它位于和其他所有保存方式不同的地方:处理器内部。然而,寄存器的数量十分有限,所以寄存器是根据需要由编译器分配。我们对此没有直接的控制权,也不可能在自己的程序里找到寄存器存在的任何踪迹。 19 | 20 | (2) 栈。驻留于常规RAM(随机访问存储器)区域,但可通过它的“栈指针”获得处理的直接支持。栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。这是一种特别快、特别有效的数据保存方式,仅次于寄存器。创建程序时,Java编译器必须准确地知道栈内保存的所有数据的“长度”以及“存在时间”。这是由于它必须生成相应的代码,以便向上和向下移动指针。这一限制无疑影响了程序的灵活性,所以尽管有些Java数据要保存在栈里——特别是对象引用,但Java对象并不放到其中。 21 | 22 | (3) 堆。一种常规用途的内存池(也在RAM区域),其中保存了Java对象。和栈不同,“内存堆”或“堆”(Heap)最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间! 23 | 24 | (4) 静态存储。这儿的“静态”(`Static`)是指“位于固定位置”(尽管也在RAM里)。程序运行期间,静态存储的数据将随时等候调用。可用`static`关键字指出一个对象的特定元素是静态的。但Java对象本身永远都不会置入静态存储空间。 25 | 26 | (5) 常数存储。常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。有的常数需要严格地保护,所以可考虑将它们置入只读存储器(ROM)。 27 | 28 | (6) 非RAM存储。若数据完全独立于一个程序之外,则程序不运行时仍可存在,并在程序的控制范围之外。其中两个最主要的例子便是“流式对象”和“固定对象”。对于流式对象,对象会变成字节流,通常会发给另一台机器。而对于固定对象,对象保存在磁盘中。即使程序中止运行,它们仍可保持自己的状态不变。对于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体中。一旦需要,甚至能将它们恢复成普通的、基于RAM的对象。Java 1.1提供了对Lightweight persistence的支持。未来的版本甚至可能提供更完整的方案。 29 | 30 | ## 2.2.2 特殊情况:基本类型 31 | 32 | 有一系列类需特别对待;可将它们想象成“基本”、“主要”或者“主”(Primitive)类型,进行程序设计时要频繁用到它们。之所以要特别对待,是由于用`new`创建对象(特别是小的、简单的变量)并不是非常有效,因为`new`将对象置于“堆”里。对于这些类型,Java采纳了与C和C++相同的方法。也就是说,不是用`new`创建变量,而是创建一个并非引用的“自动”变量。这个变量容纳了具体的值,并置于栈中,能够更高效地存取。 33 | 34 | Java决定了每种主要类型的大小。就象在大多数语言里那样,这些大小并不随着机器结构的变化而变化。这种大小的不可更改正是Java程序具有很强移植能力的原因之一。 35 | 36 | | 基本类型 | 大小 | 最小值 | 最大值 | 包装器类型 | 37 | |---------|---------|-----------|----------------|------------| 38 | | `boolean` | 1-bit | – | – | `Boolean` | 39 | | `char` | 16-bit | Unicode 0 | Unicode 216- 1 | `Character` | 40 | | `byte` | 8-bit | -128 | +127 | `Byte`[11] | 41 | | `short` | 16-bit | -215 | +215 – 1 | `Short`1 | 42 | | `int` | 32-bit | -231 | +231 – 1 | `Integer` | 43 | | `long` | 64-bit | -263 | +263 – 1 | `Long` | 44 | | `float` | 32-bit | IEEE754 | IEEE754 | `Float` | 45 | | `double` | 64-bit | IEEE754 | IEEE754 | `Double` | 46 | | `void` | – | – | – | `Void`1 | 47 | 48 | 49 | ①:到Java 1.1才有,1.0版没有。 50 | 51 | 数值类型全都是有符号(正负号)的,所以不必费劲寻找没有符号的类型。 52 | 主数据类型也拥有自己的“包装器”(wrapper)类。这意味着假如想让堆内一个非主要对象表示那个基本类型,就要使用对应的包装器。例如: 53 | 54 | ``` 55 | char c = 'x'; 56 | Character C = new Character('c'); 57 | ``` 58 | 59 | 也可以直接使用: 60 | 61 | ``` 62 | Character C = new Character('x'); 63 | ``` 64 | 65 | 这样做的原因将在以后的章节里解释。 66 | 67 | **1. 高精度数字** 68 | 69 | Java 1.1增加了两个类,用于进行高精度的计算:`BigInteger`和`BigDecimal`。尽管它们大致可以划分为“包装器”类型,但两者都没有对应的“基本类型”。 70 | 71 | 这两个类都有自己特殊的“方法”,对应于我们针对基本类型执行的操作。也就是说,能对`int`或`float`做的事情,对`BigInteger`和`BigDecimal`一样可以做。只是必须使用方法调用,不能使用运算符。此外,由于牵涉更多,所以运算速度会慢一些。我们牺牲了速度,但换来了精度。 72 | 73 | `BigInteger`支持任意精度的整数。也就是说,我们可精确表示任意大小的整数值,同时在运算过程中不会丢失任何信息。 74 | `BigDecimal`支持任意精度的定点数字。例如,可用它进行精确的币值计算。 75 | 76 | 至于调用这两个类时可选用的构造器和方法,请自行参考联机帮助文档。 77 | 78 | ## 2.2.3 Java的数组 79 | 80 | 几乎所有程序设计语言都支持数组。在C和C++里使用数组是非常危险的,因为那些数组只是内存块。若程序访问自己内存块以外的数组,或者在初始化之前使用内存(属于常规编程错误),会产生不可预测的后果(注释②)。 81 | 82 | ②:在C++里,应尽量不要使用数组,换用标准模板库(Standard TemplateLibrary)里更安全的容器。 83 | 84 | Java的一项主要设计目标就是安全性。所以在C和C++里困扰程序员的许多问题都未在Java里重复。一个Java可以保证被初始化,而且不可在它的范围之外访问。由于系统自动进行范围检查,所以必然要付出一些代价:针对每个数组,以及在运行期间对索引的校验,都会造成少量的内存开销。但由此换回的是更高的安全性,以及更高的工作效率。为此付出少许代价是值得的。 85 | 86 | 创建对象数组时,实际创建的是一个引用数组。而且每个引用都会自动初始化成一个特殊值,并带有自己的关键字:`null`(空)。一旦Java看到`null`,就知道该引用并未指向一个对象。正式使用前,必须为每个引用都分配一个对象。若试图使用依然为null的一个引用,就会在运行期报告问题。因此,典型的数组错误在Java里就得到了避免。 87 | 88 | 也可以创建基本类型数组。同样地,编译器能够担保对它的初始化,因为会将那个数组的内存划分成零。 89 | 90 | 数组问题将在以后的章节里详细讨论。 91 | -------------------------------------------------------------------------------- /2.3.md: -------------------------------------------------------------------------------- 1 | # 2.3 绝对不要清除对象 2 | 3 | 4 | 在大多数程序设计语言中,变量的“存在时间”(Lifetime)一直是程序员需要着重考虑的问题。变量应持续多长的时间?如果想清除它,那么何时进行?在变量存在时间上纠缠不清会造成大量的程序错误。在下面的小节里,将阐示Java如何帮助我们完成所有清除工作,从而极大了简化了这个问题。 5 | 6 | ## 2.3.1 作用域 7 | 8 | 大多数程序设计语言都提供了“作用域”(Scope)的概念。对于在作用域里定义的名字,作用域同时决定了它的“可见性”以及“存在时间”。在C,C++和Java里,作用域是由花括号的位置决定的。参考下面这个例子: 9 | 10 | ``` 11 | { 12 | int x = 12; 13 | /* only x available */ 14 | { 15 | int q = 96; 16 | /* both x & q available */ 17 | } 18 | /* only x available */ 19 | /* q “out of scope” */ 20 | } 21 | ``` 22 | 23 | 作为在作用域里定义的一个变量,它只有在那个作用域结束之前才可使用。 24 | 25 | 在上面的例子中,缩进排版使Java代码更易辨读。由于Java是一种形式自由的语言,所以额外的空格、制表位以及回车都不会对结果程序造成影响。 26 | 27 | 注意尽管在C和C++里是合法的,但在Java里不能象下面这样书写代码: 28 | 29 | ``` 30 | { 31 | int x = 12; 32 | { 33 | int x = 96; /* illegal */ 34 | } 35 | } 36 | ``` 37 | 38 | 编译器会认为变量`x`已被定义。所以C和C++能将一个变量“隐藏”在一个更大的作用域里。但这种做法在Java里是不允许的,因为Java的设计者认为这样做使程序产生了混淆。 39 | 40 | ## 2.3.2 对象的作用域 41 | 42 | Java对象不具备与基本类型一样的存在时间。用`new`关键字创建一个Java对象的时候,它会超出作用域的范围之外。所以假若使用下面这段代码: 43 | 44 | ``` 45 | { 46 | String s = new String("a string"); 47 | } /* 作用域的终点 */ 48 | ``` 49 | 50 | 那么引用`s`会在作用域的终点处消失。然而,`s`指向的`String`对象依然占据着内存空间。在上面这段代码里,我们没有办法访问对象,因为指向它的唯一一个引用已超出了作用域的边界。在后面的章节里,大家还会继续学习如何在程序运行期间传递和复制对象引用。 51 | 52 | 这样造成的结果便是:对于用`new`创建的对象,只要我们愿意,它们就会一直保留下去。这个编程问题在C和C++里特别突出。看来在C++里遇到的麻烦最大:由于不能从语言获得任何帮助,所以在需要对象的时候,根本无法确定它们是否可用。而且更麻烦的是,在C++里,一旦工作完成,必须保证将对象清除。 53 | 54 | 这样便带来了一个有趣的问题。假如Java让对象依然故我,怎样才能防止它们大量充斥内存,并最终造成程序的“凝固”呢。在C++里,这个问题最令程序员头痛。但Java以后,情况却发生了改观。Java有一个特别的“垃圾收集器”,它会查找用new创建的所有对象,并辨别其中哪些不再被引用。随后,它会自动释放由那些闲置对象占据的内存,以便能由新对象使用。这意味着我们根本不必操心内存的回收问题。只需简单地创建对象,一旦不再需要它们,它们就会自动离去。这样做可防止在C++里很常见的一个编程问题:由于程序员忘记释放内存造成的“内存溢出”。 55 | -------------------------------------------------------------------------------- /2.4.md: -------------------------------------------------------------------------------- 1 | # 2.4 新建数据类型:类 2 | 3 | 4 | (2)4 新建数据类型:类 5 | 6 | 如果说一切东西都是对象,那么用什么决定一个“类”(Class)的外观与行为呢?换句话说,是什么建立起了一个对象的“类型”(Type)呢?大家可能猜想有一个名为`type`的关键字。但从历史看来,大多数面向对象的语言都用关键字`class`表达这样一个意思:“我准备告诉你对象一种新类型的外观”。`class`关键字太常用了,以至于本书许多地方并没有用粗体字或双引号加以强调。在这个关键字的后面,应该跟随新数据类型的名称。例如: 7 | 8 | ``` 9 | class ATypeName {/*类主体置于这里} 10 | ``` 11 | 12 | 这样就引入了一种新类型,接下来便可用`new`创建这种类型的一个新对象: 13 | 14 | ``` 15 | ATypeName a = new ATypeName(); 16 | ``` 17 | 18 | 在`ATypeName`里,类主体只由一条注释构成(星号和斜杠以及其中的内容,本章后面还会详细讲述),所以并不能对它做太多的事情。事实上,除非为其定义了某些方法,否则根本不能指示它做任何事情。 19 | 20 | ## 2.4.1 字段和方法 21 | 22 | 定义一个类时(我们在Java里的全部工作就是定义类、制作那些类的对象以及将消息发给那些对象),可在自己的类里设置两种类型的元素:数据成员(有时也叫“字段”)以及成员函数(通常叫“方法”)。其中,数据成员是一种对象(通过它的引用与其通信),可以为任何类型。它也可以是基本类型(并不是引用)之一。如果是指向对象的一个引用,则必须初始化那个引用,用一种名为“构造器”(第4章会对此详述)的特殊函数将其与一个实际对象连接起来(就象早先看到的那样,使用`new`关键字)。但若是一种基本类型,则可在类定义位置直接初始化(正如后面会看到的那样,引用亦可在定义位置初始化)。 23 | 24 | 每个对象都为自己的数据成员保有存储空间;数据成员不会在对象之间共享。下面是定义了一些数据成员的类示例: 25 | 26 | ``` 27 | class DataOnly { 28 | int i; 29 | float f; 30 | boolean b; 31 | } 32 | ``` 33 | 34 | 这个类并没有做任何实质性的事情,但我们可创建一个对象: 35 | 36 | ``` 37 | DataOnly d = new DataOnly(); 38 | ``` 39 | 40 | 可将值赋给数据成员,但首先必须知道如何引用一个对象的成员。为达到引用对象成员的目的,首先要写上对象引用的名字,再跟随一个点号(句点),再跟随对象内部成员的名字。即“对象引用.成员”。例如: 41 | 42 | ``` 43 | d.i = 47; 44 | d.f = 1.1f; 45 | d.b = false; 46 | ``` 47 | 48 | 一个对象也可能包含了另一个对象,而另一个对象里则包含了我们想修改的数据。对于这个问题,只需保持“连接句点”即可。例如: 49 | 50 | ``` 51 | myPlane.leftTank.capacity = 100; 52 | ``` 53 | 54 | 除容纳数据之外,`DataOnly`类再也不能做更多的事情,因为它没有成员函数(方法)。为正确理解工作原理,首先必须知道“参数”和“返回值”的概念。我们马上就会详加解释。 55 | 56 | **1. 基本类型的成员的默认值** 57 | 58 | 若某个类成员属于基本类型,那么即使不明确(显式)进行初始化,也可以保证它们获得一个默认值。 59 | 60 | 基本类型 默认值 61 | 62 | ``` 63 | Boolean false 64 | Char '\u0000'(null) 65 | byte (byte)0 66 | short (short)0 67 | int 0 68 | long 0L 69 | float 0.0f 70 | double 0.0d 71 | ``` 72 | 73 | 一旦将变量作为类成员使用,就要特别注意由Java分配的默认值。这样做可保证基本类型的成员变量肯定得到了初始化(C++不具备这一功能),可有效遏止多种相关的编程错误。 74 | 75 | 然而,这种保证却并不适用于“局部”变量——那些变量并非一个类的字段。所以,假若在一个函数定义中写入下述代码: 76 | 77 | ``` 78 | int x; 79 | ``` 80 | 81 | 那么`x`会得到一些随机值(这与C和C++是一样的),不会自动初始化成零。我们责任是在正式使用x前分配一个适当的值。如果忘记,就会得到一条编译期错误,告诉我们变量可能尚未初始化。这种处理正是Java优于C++的表现之一。许多C++编译器会对变量未初始化发出警告,但在Java里却是错误。 82 | -------------------------------------------------------------------------------- /2.5.md: -------------------------------------------------------------------------------- 1 | # 2.5 方法、参数和返回值 2 | 3 | 4 | 迄今为止,我们一直用“函数”(Function)这个词指代一个已命名的子例程。但在Java里,更常用的一个词却是“方法”(Method),代表“完成某事的途径”。尽管它们表达的实际是同一个意思,但从现在开始,本书将一直使用“方法”,而不是“函数”。 5 | 6 | Java的“方法”决定了一个对象能够接收的消息。通过本节的学习,大家会知道方法的定义有多么简单! 7 | 8 | 方法的基本组成部分包括名字、参数、返回类型以及主体。下面便是它最基本的形式: 9 | 10 | ``` 11 | 返回类型 方法名( /* 参数列表*/ ) {/* 方法主体 */} 12 | ``` 13 | 14 | 返回类型是指调用方法之后返回的数值类型。显然,方法名的作用是对具体的方法进行标识和引用。参数列表列出了想传递给方法的信息类型和名称。 15 | 16 | Java的方法只能作为类的一部分创建。只能针对某个对象调用一个方法(注释③),而且那个对象必须能够执行那个方法调用。若试图为一个对象调用错误的方法,就会在编译期得到一条出错消息。为一个对象调用方法时,需要先列出对象的名字,在后面跟上一个句点,再跟上方法名以及它的参数列表。亦即`对象名.方法名(参数1,参数2,参数3...)`。举个例子来说,假设我们有一个方法名叫`f()`,它没有参数,返回的是类型为`int`的一个值。那么,假设有一个名为`a`的对象,可为其调用方法`f()`,则代码如下: 17 | 18 | ``` 19 | int x = a.f(); 20 | ``` 21 | 22 | 返回值的类型必须兼容x的类型。 23 | 24 | 象这样调用一个方法的行动通常叫作“向对象发送一条消息”。在上面的例子中,消息是`f()`,而对象是`a`。面向对象的程序设计通常简单地归纳为“向对象发送消息”。 25 | 26 | ③:正如马上就要学到的那样,“静态”方法可针对类调用,毋需一个对象。 27 | 28 | ## 2.5.1 参数列表 29 | 30 | 参数列表规定了我们传送给方法的是什么信息。正如大家或许已猜到的那样,这些信息——如同Java内其他任何东西——采用的都是对象的形式。因此,我们必须在参数列表里指定要传递的对象类型,以及每个对象的名字。正如在Java其他地方处理对象时一样,我们实际传递的是“引用”(注释④)。然而,引用的类型必须正确。倘若希望参数是一个“字符串”,那么传递的必须是一个字符串。 31 | 32 | ④:对于前面提及的“特殊”数据类型`boolean`,`char`,`byte`,`short`,`int`,`long`,,`float`以及`double`来说是一个例外。但在传递对象时,通常都是指传递指向对象的引用。 33 | 34 | 下面让我们考虑将一个字符串作为参数使用的方法。下面列出的是定义代码,必须将它置于一个类定义里,否则无法编译: 35 | 36 | ``` 37 | int storage(String s) { 38 | return s.length() * 2; 39 | } 40 | ``` 41 | 42 | 这个方法告诉我们需要多少字节才能容纳一个特定字符串里的信息(字符串里的每个字符都是16位,或者说2个字节、长整数,以便提供对Unicode字符的支持)。参数的类型为`String`,而且叫作`s`。一旦将`s`传递给方法,就可将它当作其他对象一样处理(可向其发送消息)。在这里,我们调用的是`length()`方法,它是`String`的方法之一。该方法返回的是一个字符串里的字符数。 43 | 44 | 通过上面的例子,也可以了解`return`关键字的运用。它主要做两件事情。首先,它意味着“离开方法,我已完工了”。其次,假设方法生成了一个值,则那个值紧接在`return`语句的后面。在这种情况下,返回值是通过计算表达式`s.length()*2`而产生的。 45 | 可按自己的愿望返回任意类型,但倘若不想返回任何东西,就可指示方法返回`void`(空)。下面列出一些例子。 46 | 47 | ``` 48 | boolean flag() { return true; } 49 | float naturalLogBase() { return 2.718; } 50 | void nothing() { return; } 51 | void nothing2() {} 52 | ``` 53 | 54 | 若返回类型为`void`,则`return`关键字唯一的作用就是退出方法。所以一旦抵达方法末尾,该关键字便不需要了。可在任何地方从一个方法返回。但假设已指定了一种非`void`的返回类型,那么无论从何地返回,编译器都会确保我们返回的是正确的类型。 55 | 56 | 到此为止,大家或许已得到了这样的一个印象:一个程序只是一系列对象的集合,它们的方法将其他对象作为自己的参数使用,而且将消息发给那些对象。这种说法大体正确,但通过以后的学习,大家还会知道如何在一个方法里作出决策,做一些更细致的基层工作。至于这一章,只需理解消息传送就足够了。 57 | -------------------------------------------------------------------------------- /2.6.md: -------------------------------------------------------------------------------- 1 | # 2.6 构建Java程序 2 | 3 | 4 | 正式构建自己的第一个Java程序前,还有几个问题需要注意。 5 | 6 | ## 2.6.1 名字的可见性 7 | 8 | 在所有程序设计语言里,一个不可避免的问题是对名字或名称的控制。假设您在程序的某个模块里使用了一个名字,而另一名程序员在另一个模块里使用了相同的名字。此时,如何区分两个名字,并防止两个名字互相冲突呢?这个问题在C语言里特别突出。因为程序未提供很好的名字管理方法。C++的类(即Java类的基础)嵌套使用类里的函数,使其不至于同其他类里的嵌套函数名冲突。然而,C++仍然允许使用全局数据以及全局函数,所以仍然难以避免冲突。为解决这个问题,C++用额外的关键字引入了“命名空间”的概念。 9 | 10 | 由于采用全新的机制,所以Java能完全避免这些问题。为了给一个库生成明确的名字,采用了与Internet域名类似的名字。事实上,Java的设计者鼓励程序员反转使用自己的Internet域名,因为它们肯定是独一无二的。由于我的域名是`BruceEckel.com`,所以我的实用工具库就可命名为`com.bruceeckel.utility.foibles`。反转了域名后,可将点号想象成子目录。 11 | 12 | 在Java 1.0和Java 1.1中,域扩展名`com`,`edu`,`org`,`net`等都约定为大写形式。所以库的样子就变成:`COM.bruceeckel.utility.foibles`。然而,在Java 1.2的开发过程中,设计者发现这样做会造成一些问题。所以目前的整个软件包都以小写字母为标准。 13 | 14 | Java的这种特殊机制意味着所有文件都自动存在于自己的命名空间里。而且一个文件里的每个类都自动获得一个独一无二的标识符(当然,一个文件里的类名必须是唯一的)。所以不必学习特殊的语言知识来解决这个问题——语言本身已帮我们照顾到这一点。 15 | 16 | ## 2.6.2 使用其他组件 17 | 18 | 一旦要在自己的程序里使用一个预先定义好的类,编译器就必须知道如何找到它。当然,这个类可能就在发出调用的那个相同的源码文件里。如果是那种情况,只需简单地使用这个类即可——即使它直到文件的后面仍未得到定义。Java消除了“向前引用”的问题,所以不要关心这些事情。 19 | 20 | 但假若那个类位于其他文件里呢?您或许认为编译器应该足够“联盟”,可以自行发现它。但实情并非如此。假设我们想使用一个具有特定名称的类,但那个类的定义位于多个文件里。或者更糟,假设我们准备写一个程序,但在创建它的时候,却向自己的库加入了一个新类,它与现有某个类的名字发生了冲突。 21 | 22 | 为解决这个问题,必须消除所有潜在的、纠缠不清的情况。为达到这个目的,要用`import`关键字准确告诉Java编译器我们希望的类是什么。`import`的作用是指示编译器导入一个“包”——或者说一个“类库”(在其他语言里,可将“库”想象成一系列函数、数据以及类的集合。但请记住,Java的所有代码都必须写入一个类中)。 23 | 24 | 大多数时候,我们直接采用来自标准Java库的组件(部件)即可,它们是与编译器配套提供的。使用这些组件时,没有必要关心冗长的保留域名;举个例子来说,只需象下面这样写一行代码即可: 25 | 26 | ``` 27 | import java.util.Vector; 28 | ``` 29 | 30 | 它的作用是告诉编译器我们想使用Java的`Vector`类。然而,`util`包含了数量众多的类,我们有时希望使用其中的几个,同时不想全部明确地声明它们。为达到这个目的,可使用`*`通配符。如下所示: 31 | 32 | ``` 33 | import java.util.*; 34 | ``` 35 | 36 | 需导入一系列类时,采用的通常是这个办法。应尽量避免一个一个地导入类。 37 | 38 | ## 2.6.3 `static`关键字 39 | 40 | 通常,我们创建类时会指出那个类的对象的外观与行为。除非用`new`创建那个类的一个对象,否则实际上并未得到任何东西。只有执行了`new`后,才会正式生成数据存储空间,并可使用相应的方法。 41 | 42 | 但在两种特殊的情形下,上述方法并不堪用。一种情形是只想用一个存储区域来保存一个特定的数据——无论要创建多少个对象,甚至根本不创建对象。另一种情形是我们需要一个特殊的方法,它没有与这个类的任何对象关联。也就是说,即使没有创建对象,也需要一个能调用的方法。为满足这两方面的要求,可使用`static`(静态)关键字。一旦将什么东西设为`static`,数据或方法就不会同那个类的任何对象实例联系到一起。所以尽管从未创建那个类的一个对象,仍能调用一个`static`方法,或访问一些`static`数据。而在这之前,对于非`static`数据和方法,我们必须创建一个对象,并用那个对象访问数据或方法。这是由于非`static`数据和方法必须知道它们操作的具体对象。当然,在正式使用前,由于`static`方法不需要创建任何对象,所以它们不可简单地调用其他那些成员,同时不引用一个已命名的对象,从而直接访问非`static`成员或方法(因为非`static`成员和方法必须同一个特定的对象关联到一起)。 43 | 44 | 有些面向对象的语言使用了“类数据”和“类方法”这两个术语。它们意味着数据和方法只是为作为一个整体的类而存在的,并不是为那个类的任何特定对象。有时,您会在其他一些Java书刊里发现这样的称呼。 45 | 46 | 为了将数据成员或方法设为`static`,只需在定义前置和这个关键字即可。例如,下述代码能生成一个`static`数据成员,并对其初始化: 47 | 48 | ``` 49 | class StaticTest { 50 | Static int i = 47; 51 | } 52 | ``` 53 | 54 | 现在,尽管我们制作了两个`StaticTest`对象,但它们仍然只占据`StaticTest.i`的一个存储空间。这两个对象都共享同样的`i`。请考察下述代码: 55 | 56 | ``` 57 | StaticTest st1 = new StaticTest(); 58 | StaticTest st2 = new StaticTest(); 59 | ``` 60 | 61 | 此时,无论`st1.i`还是`st2.i`都有同样的值47,因为它们引用的是同样的内存区域。 62 | 63 | 有两个办法可引用一个`static`变量。正如上面展示的那样,可通过一个对象命名它,如`st2.i`。亦可直接用它的类名引用,而这在非静态成员里是行不通的(最好用这个办法引用`static`变量,因为它强调了那个变量的“静态”本质)。 64 | 65 | ``` 66 | StaticTest.i++; 67 | ``` 68 | 69 | 其中,`++`运算符会使变量自增。此时,无论`st1.i`还是`st2.i`的值都是48。 70 | 71 | 类似的逻辑也适用于静态方法。既可象对其他任何方法那样通过一个对象引用静态方法,亦可用特殊的语法格式`类名.方法()`加以引用。静态方法的定义是类似的: 72 | 73 | ``` 74 | class StaticFun { 75 | static void incr() { StaticTest.i++; } 76 | } 77 | ``` 78 | 79 | 从中可看出,`StaticFun`的方法`incr()`使静态数据`i`自增。通过对象,可用典型的方法调用`incr()`: 80 | 81 | ``` 82 | StaticFun sf = new StaticFun(); 83 | sf.incr(); 84 | ``` 85 | 86 | 或者,由于`incr()`是一种静态方法,所以可通过它的类直接调用: 87 | 88 | ``` 89 | StaticFun.incr(); 90 | ``` 91 | 92 | 尽管是“静态”的,但只要应用于一个数据成员,就会明确改变数据的创建方式(一个类一个成员,以及每个对象一个非静态成员)。若应用于一个方法,就没有那么戏剧化了。对方法来说,`static`一项重要的用途就是帮助我们在不必创建对象的前提下调用那个方法。正如以后会看到的那样,这一点是至关重要的——特别是在定义程序运行入口方法`main()`的时候。 93 | 94 | 和其他任何方法一样,`static`方法也能创建自己类型的命名对象。所以经常把`static`方法作为一个“领头羊”使用,用它生成一系列自己类型的“实例”。 95 | -------------------------------------------------------------------------------- /2.7.md: -------------------------------------------------------------------------------- 1 | # 2.7 我们的第一个Java程序 2 | 3 | 4 | 最后,让我们正式编一个程序(注释⑤)。它能打印出与当前运行的系统有关的资料,并利用了来自Java标准库的`System`对象的多种方法。注意这里引入了一种额外的注释样式:`//`。它表示到本行结束前的所有内容都是注释: 5 | 6 | ``` 7 | // Property.java 8 | import java.util.*; 9 | 10 | public class Property { 11 | public static void main(String[] args) { 12 | System.out.println(new Date()); 13 | Properties p = System.getProperties(); 14 | p.list(System.out); 15 | System.out.println("--- Memory Usage:"); 16 | Runtime rt = Runtime.getRuntime(); 17 | System.out.println("Total Memory = " 18 | + rt.totalMemory() 19 | + " Free Memory = " 20 | + rt.freeMemory()); 21 | } 22 | } 23 | ``` 24 | 25 | ⑤:在某些编程环境里,程序会在屏幕上一切而过,甚至没机会看到结果。可将下面这段代码置于`main()`的末尾,用它暂停输出: 26 | 27 | ``` 28 | try { 29 | Thread.currentThread().sleep(5 * 1000); 30 | } catch(InterruptedException e) {} 31 | } 32 | ``` 33 | 34 | 它的作用是暂停输出5秒钟。这段代码涉及的一些概念要到本书后面才会讲到。所以目前不必深究,只知道它是让程序暂停的一个技巧便可。 35 | 36 | 37 | 在每个程序文件的开头,都必须放置一个`import`语句,导入那个文件的代码里要用到的所有额外的类。注意我们说它们是“额外”的,因为一个特殊的类库会自动导入每个Java文件:`java.lang`。启动您的Web浏览器,查看由Sun提供的用户文档(如果尚未从 `http://www.java.sun.com` 下载,或用其他方式安装了Java文档,请立即下载)。在`packages.html`文件里,可找到Java配套提供的所有类库名称。请选择其中的`java.lang`。在“Class Index”下面,可找到属于那个库的全部类的列表。由于`java.lang`默认进入每个Java代码文件,所以这些类在任何时候都可直接使用。在这个列表里,可发现`System`和`Runtime`,我们在`Property.java`里用到了它们。`java.lang`里没有列出`Date`类,所以必须导入另一个类库才能使用它。如果不清楚一个特定的类在哪个类库里,或者想检视所有的类,可在Java用户文档里选择“Class Hierarchy”(类分级结构)。在Web浏览器中,虽然要花不短的时间来建立这个结构,但可清楚找到与Java配套提供的每一个类。随后,可用浏览器的“查找”(Find)功能搜索关键字`Date`。经这样处理后,可发现我们的搜索目标以`java.util.Date`的形式列出。我们终于知道它位于`util`库里,所以必须导入 `java.util.*`;否则便不能使用`Date`。 38 | 39 | 观察`packages.html`文档最开头的部分(我已将其设为自己的默认起始页),请选择`java.lang`,再选`System`。这时可看到`System`类有几个字段。若选择`out`,就可知道它是一个`static PrintStream`对象。由于它是“静态”的,所以不需要我们创建任何东西。`out`对象肯定是3,所以只需直接用它即可。我们能对这个`out`对象做的事情由它的类型决定:`PrintStream`。`PrintStream`在说明文字中以一个超链接的形式列出,这一点做得非常方便。所以假若单击那个链接,就可看到能够为`PrintStream`调用的所有方法。方法的数量不少,本书后面会详细介绍。就目前来说,我们感兴趣的只有`println()`。它的意思是“把我给你的东西打印到控制台,并用一个新行结束”。所以在任何Java程序中,一旦要把某些内容打印到控制台,就可条件反射地写上`System.out.println("内容")`。 40 | 41 | 类名与文件是一样的。若象现在这样创建一个独立的程序,文件中的一个类必须与文件同名(如果没这样做,编译器会及时作出反应)。类里必须包含一个名为`main()`的方法,形式如下: 42 | 43 | ``` 44 | public static void main(String[] args) { 45 | ``` 46 | 47 | 其中,关键字`public`意味着方法可由外部世界调用(第5章会详细解释)。`main()`的参数是包含了`String`对象的一个数组。`args`不会在本程序中用到,但需要在这个地方列出,因为它们保存了在命令行调用的参数。 48 | 程序的第一行非常有趣: 49 | 50 | ``` 51 | System.out.println(new Date()); 52 | ``` 53 | 54 | 请观察它的参数:创建`Date`对象唯一的目的就是将它的值发送给`println()`。一旦这个语句执行完毕,`Date`就不再需要。随之而来的“垃圾收集器”会发现这一情况,并在任何可能的时候将其回收。事实上,我们没太大的必要关心“清除”的细节。 55 | 56 | 第二行调用了`System.getProperties()`。若用Web浏览器查看联机用户文档,就可知道`getProperties()`是`System`类的一个`static`方法。由于它是“静态”的,所以不必创建任何对象便可调用该方法。无论是否存在该类的一个对象,`static`方法随时都可使用。调用`getProperties()`时,它会将系统属性作为`Properties`类的一个对象生成(注意`Properties`是“属性”的意思)。随后的的引用保存在一个名为`p`的`Properties`引用里。在第三行,大家可看到`Properties`对象有一个名为`list()`的方法,它将自己的全部内容都发给一个我们作为参数传递的`PrintStream`对象。 57 | 58 | `main()` 的第四和第六行是典型的打印语句。注意为了打印多个`String`值,用加号(`+`)分隔它们即可。然而,也要在这里注意一些奇怪的事情。在`String`对象中使用时,加号并不代表真正的“相加”。处理字符串时,我们通常不必考虑`+`的任何特殊含义。但是,Java的`String`类要受一种名为“运算符重载”的机制的制约。也就是说,只有在随同`String`对象使用时,加号才会产生与其他任何地方不同的表现。对于字符串,它的意思是“连接这两个字符串”。 59 | 60 | 但事情到此并未结束。请观察下述语句: 61 | 62 | ``` 63 | System.out.println("Total Memory = " 64 | + rt.totalMemory() 65 | + " Free Memory = " 66 | + rt.freeMemory()); 67 | ``` 68 | 69 | 其中,`totalMemory()`和`freeMemory()`返回的是数值,并非`String`对象。如果将一个数值“加”到一个字符串身上,会发生什么情况呢?同我们一样,编译器也会意识到这个问题,并魔术般地调用一个方法,将那个数值(`int`,`float`等等)转换成字符串。经这样处理后,它们当然能利用加号“加”到一起。这种“自动类型转换”亦划入“运算符重载”处理的范畴。 70 | 71 | 许多Java著作都在热烈地辩论“运算符重载”(C++的一项特性)是否有用。目前就是反对它的一个好例子!然而,这最多只能算编译器(程序)的问题,而且只是对`String`对象而言。对于自己编写的任何源代码,都不可能使运算符“重载”。 72 | 73 | 通过为`Runtime`类调用`getRuntime()`方法,`main()`的第五行创建了一个`Runtime`对象。返回的则是指向一个`Runtime`对象的引用。而且,我们不必关心它是一个静态对象,还是由`new`命令创建的一个对象。这是由于我们不必为清除工作负责,可以大模大样地使用对象。正如显示的那样,`Runtime`可告诉我们与内存使用有关的信息。 74 | -------------------------------------------------------------------------------- /2.8.md: -------------------------------------------------------------------------------- 1 | # 2.8 注释和嵌入文档 2 | 3 | 4 | (2)8 注释和嵌入文档 5 | 6 | Java里有两种类型的注释。第一种是传统的、C语言风格的注释,是从C++继承而来的。这些注释用一个 `/*` 起头,随后是注释内容,并可跨越多行,最后用一个`*/`结束。注意许多程序员在连续注释内容的每一行都用一个 `*` 开头,所以经常能看到象下面这样的内容: 7 | 8 | ``` 9 | /* 这是 10 | * 一段注释, 11 | * 它跨越了多个行 12 | */ 13 | ``` 14 | 15 | 但请记住,进行编译时,`/*`和`*/`之间的所有东西都会被忽略,所以上述注释与下面这段注释并没有什么不同: 16 | 17 | ``` 18 | /* 这是一段注释, 19 | 它跨越了多个行 */ 20 | ``` 21 | 22 | 第二种类型的注释也起源于C++。这种注释叫作“单行注释”,以一个 `//` 起头,表示这一行的所有内容都是注释。这种类型的注释更常用,因为它书写时更方便。没有必要在键盘上寻找 `/` ,再寻找 `*` (只需按同样的键两次),而且不必在注释结尾时加一个结束标记。下面便是这类注释的一个例子: 23 | 24 | ``` 25 | // 这是一条单行注释 26 | ``` 27 | 28 | ## 2.8.1 注释文档 29 | 30 | 对于Java语言,最体贴的一项设计就是它并没有打算让人们为了写程序而写程序——人们也需要考虑程序的文档化问题。对于程序的文档化,最大的问题莫过于对文档的维护。若文档与代码分离,那么每次改变代码后都要改变文档,这无疑会变成相当麻烦的一件事情。解决的方法看起来似乎很简单:将代码同文档“链接”起来。为达到这个目的,最简单的方法是将所有内容都置于同一个文件。然而,为使一切都整齐划一,还必须使用一种特殊的注释语法,以便标记出特殊的文档;另外还需要一个工具,用于提取这些注释,并按有价值的形式将其展现出来。这些都是Java必须做到的。 31 | 32 | 用于提取注释的工具叫作`javadoc`。它采用了部分来自Java编译器的技术,查找我们置入程序的特殊注释标记。它不仅提取由这些标记指示的信息,也将毗邻注释的类名或方法名提取出来。这样一来,我们就可用最轻的工作量,生成十分专业的程序文档。 33 | 34 | `javadoc`输出的是一个HTML文件,可用自己的Web浏览器查看。该工具允许我们创建和管理单个源文件,并生动生成有用的文档。由于有了`javadoc`,所以我们能够用标准的方法创建文档。而且由于它非常方便,所以我们能轻松获得所有Java库的文档。 35 | 36 | ## 2.8.2 具体语法 37 | 38 | 所有javadoc命令都只能出现于 `/**` 注释中。但和平常一样,注释结束于一个 `*/` 。主要通过两种方式来使用`javadoc`:嵌入的HTML,或使用“文档标记”。其中,“文档标记”(Doc tags)是一些以`@`开头的命令,置于注释行的起始处(但前导的`*`会被忽略)。 39 | 40 | 有三种类型的注释文档,它们对应于位于注释后面的元素:类、变量或者方法。也就是说,一个类注释正好位于一个类定义之前;变量注释正好位于变量定义之前;而一个方法定义正好位于一个方法定义的前面。如下面这个简单的例子所示: 41 | 42 | ``` 43 | /** 一个类注释 */ 44 | public class docTest { 45 | /** 一个变量注释 */ 46 | public int i; 47 | /** 一个方法注释 */ 48 | public void f() {} 49 | } 50 | ``` 51 | 52 | 注意`javadoc`只能为`public`(公共)和`protected`(受保护)成员处理注释文档。`private`(私有)和“友好”(详见5章)成员的注释会被忽略,我们看不到任何输出(也可以用`-private`标记包括`private`成员)。这样做是有道理的,因为只有`public`和`protected`成员才可在文件之外使用,这是客户程序员的希望。然而,所有类注释都会包含到输出结果里。 53 | 54 | 上述代码的输出是一个HTML文件,它与其他Java文档具有相同的标准格式。因此,用户会非常熟悉这种格式,可在您设计的类中方便地“漫游”。设计程序时,请务必考虑输入上述代码,用`javadoc`处理一下,观看最终HTML文件的效果如何。 55 | 56 | ## 2.8.3 嵌入HTML 57 | 58 | `javadoc`将HTML命令传递给最终生成的HTML文档。这便使我们能够充分利用HTML的巨大威力。当然,我们的最终动机是格式化代码,不是为了哗众取宠。下面列出一个例子: 59 | 60 | ``` 61 | /** 62 | *
63 | * System.out.println(new Date()); 64 | *65 | */ 66 | ``` 67 | 68 | 亦可象在其他Web文档里那样运用HTML,对普通文本进行格式化,使其更具条理、更加美观: 69 | 70 | ``` 71 | /** 72 | * 您甚至可以插入一个列表: 73 | *