├── .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 | ![](1-1.gif) 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 | ![](1-2.gif) 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 | ![](16-1.gif) 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 | ![](16-2.gif) 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 | *
    74 | *
  1. 项目一 75 | *
  2. 项目二 76 | *
  3. 项目三 77 | *
78 | */ 79 | ``` 80 | 81 | 注意在文档注释中,位于一行最开头的星号会被`javadoc`丢弃。同时丢弃的还有前导空格。`javadoc` 会对所有内容进行格式化,使其与标准的文档外观相符。不要将`

`或`
`这样的标题当作嵌入HTML使用,因为`javadoc`会插入自己的标题,我们给出的标题会与之冲撞。 82 | 83 | 所有类型的注释文档——类、变量和方法——都支持嵌入HTML。 84 | 85 | ## 2.8.4 `@see`:引用其他类 86 | 87 | 所有三种类型的注释文档都可包含`@see`标记,它允许我们引用其他类里的文档。对于这个标记,`javadoc`会生成相应的HTML,将其直接链接到其他文档。格式如下: 88 | 89 | ``` 90 | @see 类名 91 | @see 完整类名 92 | @see 完整类名#方法名 93 | ``` 94 | 95 | 每一格式都会在生成的文档里自动加入一个超链接的“See Also”(参见)条目。注意`javadoc`不会检查我们指定的超链接,不会验证它们是否有效。 96 | 97 | ## 2.8.5 类文档标记 98 | 99 | 随同嵌入HTML和`@se`e引用,类文档还可以包括用于版本信息以及作者姓名的标记。类文档亦可用于“接口”目的(本书后面会详细解释)。 100 | 101 | 102 | **1. `@version`** 103 | 104 | 105 | 格式如下: 106 | 107 | ``` 108 | @version 版本信息 109 | ``` 110 | 111 | 其中,“版本信息”代表任何适合作为版本说明的资料。若在`javadoc`命令行使用了`-version`标记,就会从生成的HTML文档里提取出版本信息。 112 | 113 | **2. `@author`** 114 | 115 | 格式如下: 116 | 117 | ``` 118 | @author 作者信息 119 | ``` 120 | 121 | 其中,“作者信息”包括您的姓名、电子函件地址或者其他任何适宜的资料。若在`javadoc`命令行使用了`-author`标记,就会专门从生成的HTML文档里提取出作者信息。 122 | 123 | 可为一系列作者使用多个这样的标记,但它们必须连续放置。全部作者信息会一起存入最终HTML代码的单独一个段落里。 124 | 125 | ## 2.8.6 变量文档标记 126 | 127 | 变量文档只能包括嵌入的HTML以及`@see`引用。 128 | 129 | ## 2.8.7 方法文档标记 130 | 131 | 除嵌入HTML和`@see`引用之外,方法还允许使用针对参数、返回值以及异常的文档标记。 132 | 133 | **1. `@param`** 134 | 格式如下: 135 | 136 | ``` 137 | @param 参数名 说明 138 | ``` 139 | 140 | 其中,“参数名”是指参数列表内的标识符,而“说明”代表一些可延续到后续行内的说明文字。一旦遇到一个新文档标记,就认为前一个说明结束。可使用任意数量的说明,每个参数一个。 141 | 142 | **2. `@return`** 143 | 144 | 格式如下: 145 | 146 | ``` 147 | @return 说明 148 | ``` 149 | 150 | 其中,“说明”是指返回值的含义。它可延续到后面的行内。 151 | 152 | **3. `@exception`** 153 | 154 | 有关“异常”(`Exception`)的详细情况,我们会在第9章讲述。简言之,它们是一些特殊的对象,若某个方法失败,就可将它们“扔出”对象。调用一个方法时,尽管只有一个异常对象出现,但一些特殊的方法也许能产生任意数量的、不同类型的异常。所有这些异常都需要说明。所以,异常标记的格式如下: 155 | 156 | ``` 157 | @exception 完整类名 说明 158 | ``` 159 | 160 | 其中,“完整类名”明确指定了一个异常类的名字,它是在其他某个地方定义好的。而“说明”(同样可以延续到下面的行)告诉我们为什么这种特殊类型的异常会在方法调用中出现。 161 | 162 | **4. `@deprecated`** 163 | 164 | 这是Java 1.1的新特性。该标记用于指出一些旧功能已由改进过的新功能取代。该标记的作用是建议用户不必再使用一种特定的功能,因为未来改版时可能摒弃这一功能。若将一个方法标记为`@deprecated`,则使用该方法时会收到编译器的警告。 165 | 166 | ## 2.8.8 文档示例 167 | 168 | 下面还是我们的第一个Java程序,只不过已加入了完整的文档注释: 169 | 170 | 92页程序 171 | 172 | 第一行: 173 | 174 | ``` 175 | //: Property.java 176 | ``` 177 | 178 | 采用了我自己的方法:将一个`:`作为特殊的记号,指出这是包含了源文件名字的一个注释行。最后一行也用这样的一条注释结尾,它标志着源代码清单的结束。这样一来,可将代码从本书的正文中方便地提取出来,并用一个编译器检查。这方面的细节在第17章讲述。 179 | -------------------------------------------------------------------------------- /2.9.md: -------------------------------------------------------------------------------- 1 | # 2.9 编码样式 2 | 3 | 4 | 一个非正式的Java编程标准是大写一个类名的首字母。若类名由几个单词构成,那么把它们紧靠到一起(也就是说,不要用下划线来分隔名字)。此外,每个嵌入单词的首字母都采用大写形式。例如: 5 | 6 | ``` 7 | class AllTheColorsOfTheRainbow { // ...} 8 | ``` 9 | 10 | 对于其他几乎所有内容:方法、字段(成员变量)以及对象引用名称,可接受的样式与类样式差不多,只是标识符的第一个字母采用小写。例如: 11 | 12 | ``` 13 | class AllTheColorsOfTheRainbow { 14 | int anIntegerRepresentingColors; 15 | void changeTheHueOfTheColor(int newHue) { 16 | // ... 17 | } 18 | // ... 19 | } 20 | ``` 21 | 22 | 当然,要注意用户也必须键入所有这些长名字,而且不能输错。 23 | -------------------------------------------------------------------------------- /2.md: -------------------------------------------------------------------------------- 1 | # 第2章 一切都是对象 2 | 3 | 4 | “尽管以C++为基础,但Java是一种更纯粹的面向对象程序设计语言”。 5 | 6 | 无论C++还是Java都属于杂合语言。但在Java中,设计者觉得这种杂合并不象在C++里那么重要。杂合语言允许采用多种编程风格;之所以说C++是一种杂合语言,是因为它支持与C语言的向后兼容能力。由于C++是C的一个超集,所以包含的许多特性都是后者不具备的,这些特性使C++在某些地方显得过于复杂。 7 | 8 | Java语言首先便假定了我们只希望进行面向对象的程序设计。也就是说,正式用它设计之前,必须先将自己的思想转入一个面向对象的世界(除非早已习惯了这个世界的思维方式)。只有做好这个准备工作,与其他OOP语言相比,才能体会到Java的易学易用。在本章,我们将探讨Java程序的基本组件,并体会为什么说Java乃至Java程序内的一切都是对象。 9 | 10 | -------------------------------------------------------------------------------- /3.3.md: -------------------------------------------------------------------------------- 1 | # 3.3 总结 2 | 3 | 本章总结了大多数程序设计语言都具有的基本特性:计算、运算符优先顺序、类型转换以及选择和循环等等。现在,我们作好了相应的准备,可继续向面向对象的程序设计领域迈进。在下一章里,我们将讨论对象的初始化与清除问题,再后面则讲述隐藏的基本实现方法。 4 | 5 | -------------------------------------------------------------------------------- /3.4.md: -------------------------------------------------------------------------------- 1 | # 3.4 练习 2 | 3 | (1) 写一个程序,打印出1到100间的整数。 4 | 5 | (2) 修改练习(1),在值为47时用一个`break`退出程序。亦可换成`return`试试。 6 | 7 | (3) 创建一个`switch`语句,为每一种`case`都显示一条消息。并将`switch`置入一个`for`循环里,令其尝试每一种`case`。在每个`case`后面都放置一个`break`,并对其进行测试。然后,删除`break`,看看会有什么情况出现。 8 | -------------------------------------------------------------------------------- /3.md: -------------------------------------------------------------------------------- 1 | # 第3章 控制程序流程 2 | 3 | 4 | 5 | “就象任何有感知的生物一样,程序必须能操纵自己的世界,在执行过程中作出判断与选择。” 6 | 7 | 在Java里,我们利用运算符操纵对象和数据,并用执行控制语句作出选择。Java是建立在C++基础上的,所以对C和C++程序员来说,对Java这方面的大多数语句和运算符都应是非常熟悉的。当然,Java也进行了自己的一些改进与简化工作。 8 | 9 | -------------------------------------------------------------------------------- /4.1.md: -------------------------------------------------------------------------------- 1 | # 4.1 用构造器自动初始化 2 | 3 | 4 | 对于方法的创建,可将其想象成为自己写的每个类都调用一次`initialize()`。这个名字提醒我们在使用对象之前,应首先进行这样的调用。但不幸的是,这也意味着用户必须记住调用方法。在Java中,由于提供了名为“构造器”的一种特殊方法,所以类的设计者可担保每个对象都会得到正确的初始化。若某个类有一个构造器,那么在创建对象时,Java会自动调用那个构造器——甚至在用户毫不知觉的情况下。所以说这是可以担保的! 5 | 6 | 接着的一个问题是如何命名这个方法。存在两方面的问题。第一个是我们使用的任何名字都可能与打算为某个类成员使用的名字冲突。第二是由于编译器的责任是调用构造器,所以它必须知道要调用是哪个方法。C++采取的方案看来是最简单的,且更有逻辑性,所以也在Java里得到了应用:构造器的名字与类名相同。这样一来,可保证象这样的一个方法会在初始化期间自动调用。 7 | 8 | 下面是带有构造器的一个简单的类(若执行这个程序有问题,请参考第3章的“赋值”小节)。 9 | 10 | ``` 11 | //: SimpleConstructor.java 12 | // Demonstration of a simple constructor 13 | package c04; 14 | 15 | class Rock { 16 | Rock() { // This is the constructor 17 | System.out.println("Creating Rock"); 18 | } 19 | } 20 | 21 | public class SimpleConstructor { 22 | public static void main(String[] args) { 23 | for(int i = 0; i < 10; i++) 24 | new Rock(); 25 | } 26 | } ///:~ 27 | ``` 28 | 29 | 现在,一旦创建一个对象: 30 | 31 | ``` 32 | new Rock(); 33 | ``` 34 | 35 | 就会分配相应的存储空间,并调用构造器。这样可保证在我们经手之前,对象得到正确的初始化。 36 | 请注意所有方法首字母小写的编码规则并不适用于构造器。这是由于构造器的名字必须与类名完全相同! 37 | 38 | 和其他任何方法一样,构造器也能使用参数,以便我们指定对象的具体创建方式。可非常方便地改动上述例子,以便构造器使用自己的参数。如下所示: 39 | 40 | ``` 41 | class Rock { 42 | Rock(int i) { 43 | System.out.println( 44 | "Creating Rock number " + i); 45 | } 46 | } 47 | 48 | public class SimpleConstructor { 49 | public static void main(String[] args) { 50 | for(int i = 0; i < 10; i++) 51 | new Rock(i); 52 | } 53 | } 54 | ``` 55 | 56 | 57 | 利用构造器的参数,我们可为一个对象的初始化设定相应的参数。举个例子来说,假设类`Tree`有一个构造器,它用一个整数参数标记树的高度,那么就可以象下面这样创建一个`Tree`对象: 58 | 59 | ``` 60 | tree t = new Tree(12); // 12英尺高的树 61 | ``` 62 | 63 | 若`Tree(int)`是我们唯一的构造器,那么编译器不会允许我们以其他任何方式创建一个`Tree`对象。 64 | 65 | 构造器有助于消除大量涉及类的问题,并使代码更易阅读。例如在前述的代码段中,我们并未看到对`initialize()`方法的明确调用——那些方法在概念上独立于定义内容。在Java中,定义和初始化属于统一的概念——两者缺一不可。 66 | 67 | 构造器属于一种较特殊的方法类型,因为它没有返回值。这与`void`返回值存在着明显的区别。对于`void`返回值,尽管方法本身不会自动返回什么,但仍然可以让它返回另一些东西。构造器则不同,它不仅什么也不会自动返回,而且根本不能有任何选择。若存在一个返回值,而且假设我们可以自行选择返回内容,那么编译器多少要知道如何对那个返回值作什么样的处理。 68 | -------------------------------------------------------------------------------- /4.6.md: -------------------------------------------------------------------------------- 1 | # 4.6 总结 2 | 3 | 作为初始化的一种具体操作形式,构造器应使大家明确感受到在语言中进行初始化的重要性。与C++的程序设计一样,判断一个程序效率如何,关键是看是否由于变量的初始化不正确而造成了严重的编程错误(Bug)。这些形式的错误很难发现,而且类似的问题也适用于不正确的清除或收尾工作。由于构造器使我们能保证正确的初始化和清除(若没有正确的构造器调用,编译器不允许对象创建),所以能获得完全的控制权和安全性。 4 | 5 | 在C++中,与“构建”相反的“析构”(Destruction)工作也是相当重要的,因为用`new`创建的对象必须明确地清除。在Java中,垃圾收集器会自动为所有对象释放内存,所以Java中等价的清除方法并不是经常都需要用到的。如果不需要类似于构造器的行为,Java的垃圾收集器可以极大简化编程工作,而且在内存的管理过程中增加更大的安全性。有些垃圾收集器甚至能清除其他资源,比如图形和文件引用等。然而,垃圾收集器确实也增加了运行期的开销。但这种开销到底造成了多大的影响却是很难看出的,因为到目前为止,Java解释器的总体运行速度仍然是比较慢的。随着这一情况的改观,我们应该能判断出垃圾收集器的开销是否使Java不适合做一些特定的工作(其中一个问题是垃圾收集器不可预测的性质)。 6 | 7 | 由于所有对象都肯定能获得正确的构建,所以同这儿讲述的情况相比,构造器实际做的事情还要多得多。特别地,当我们通过“创作”或“继承”生成新类的时候,对构建的保证仍然有效,而且需要一些附加的语法来提供对它的支持。大家将在以后的章节里详细了解创作、继承以及它们对构造器造成的影响。 8 | -------------------------------------------------------------------------------- /4.7.md: -------------------------------------------------------------------------------- 1 | # 4.7 练习 2 | 3 | 4 | (1) 用默认构造器创建一个类(没有参数),用它打印一条消息。创建属于这个类的一个对象。 5 | 6 | (2) 在练习1的基础上增加一个重载的构造器,令其采用一个`String`参数,并随同自己的消息打印出来。 7 | 8 | (3) 以练习2创建的类为基础上,创建属于它的对象引用的一个数组,但不要实际创建对象并分配到数组里。运行程 9 | 序时,注意是否打印出来自构造器调用的初始化消息。 10 | 11 | (4) 创建同引用数组联系起来的对象,最终完成练习3。 12 | 13 | (5) 用参数`before`,`after`和`none`运行程序,试验`Garbage.java`。重复这个操作,观察是否从输出中看出了一些固定的模式。改变代码,使`System.runFinalization()`在`System.gc()`之前调用,再观察结果。 14 | -------------------------------------------------------------------------------- /4.md: -------------------------------------------------------------------------------- 1 | # 第4章 初始化和清除 2 | 3 | 4 | “随着计算机的进步,‘不安全’的程序设计已成为造成编程代价高昂的罪魁祸首之一。” 5 | 6 | “初始化”和“清除”是这些安全问题的其中两个。许多C程序的错误都是由于程序员忘记初始化一个变量造成的。对于现成的库,若用户不知道如何初始化库的一个组件,就往往会出现这一类的错误。清除是另一个特殊的问题,因为用完一个元素后,由于不再关心,所以很容易把它忘记。这样一来,那个元素占用的资源会一直保留下去,极易产生资源(主要是内存)用尽的后果。 7 | 8 | C++为我们引入了“构造器”的概念。这是一种特殊的方法,在一个对象创建之后自动调用。Java也沿用了这个概念,但新增了自己的“垃圾收集器”,能在资源不再需要的时候自动释放它们。本章将讨论初始化和清除的问题,以及Java如何提供它们的支持。 9 | -------------------------------------------------------------------------------- /5.3.md: -------------------------------------------------------------------------------- 1 | # 5.3 接口与实现 2 | 3 | 4 | 我们通常认为访问控制是“隐藏实现细节”的一种方式。将数据和方法封装到类内后,可生成一种数据类型,它具有自己的特征与行为。但由于两方面重要的原因,访问为那个数据类型加上了自己的边界。第一个原因是规定客户程序员哪些能够使用,哪些不能。我们可在结构里构建自己的内部机制,不用担心客户程序员将其当作接口的一部分,从而自由地使用或者“滥用”。 5 | 6 | 这个原因直接导致了第二个原因:我们需要将接口同实现细节分离开。若结构在一系列程序中使用,但用户除了将消息发给`public`接口之外,不能做其他任何事情,我们就可以改变不属于`public`的所有东西(如“友好的”、`protected`以及`private`),同时不要求用户对他们的代码作任何修改。 7 | 8 | 我们现在是在一个面向对象的编程环境中,其中的一个类(`class`)实际是指“一类对象”,就象我们说“鱼类”或“鸟类”那样。从属于这个类的所有对象都共享这些特征与行为。“类”是对属于这一类的所有对象的外观及行为进行的一种描述。 9 | 10 | 在一些早期OOP语言中,如Simula-67,关键字`class`的作用是描述一种新的数据类型。同样的关键字在大多数面向对象的编程语言里都得到了应用。它其实是整个语言的焦点:需要新建数据类型的场合比那些用于容纳数据和方法的“容器”多得多。 11 | 12 | 在Java中,类是最基本的OOP概念。它是本书未采用粗体印刷的关键字之一——由于数量太多,所以会造成页面排版的严重混乱。 13 | 14 | 为清楚起见,可考虑用特殊的样式创建一个类:将`public`成员置于最开头,后面跟随`protected`、友好以及`private`成员。这样做的好处是类的使用者可从上向下依次阅读,并首先看到对自己来说最重要的内容(即`public`成员,因为它们可从文件的外部访问),并在遇到非公共成员后停止阅读,后者已经属于内部实现细节的一部分了。然而,利用由`javadoc`提供支持的注释文档(已在第2章介绍),代码的可读性问题已在很大程度上得到了解决。 15 | 16 | ``` 17 | public class X { 18 | public void pub1( ) { /* . . . */ } 19 | public void pub2( ) { /* . . . */ } 20 | public void pub3( ) { /* . . . */ } 21 | private void priv1( ) { /* . . . */ } 22 | private void priv2( ) { /* . . . */ } 23 | private void priv3( ) { /* . . . */ } 24 | private int i; 25 | // . . . 26 | } 27 | ``` 28 | 29 | 由于接口和实现细节仍然混合在一起,所以只是部分容易阅读。也就是说,仍然能够看到源码——实现的细节,因为它们需要保存在类里面。向一个类的消费者显示出接口实际是“类浏览器”的工作。这种工具能查找所有可用的类,总结出可对它们采取的全部操作(比如可以使用哪些成员等),并用一种清爽悦目的形式显示出来。到大家读到这本书的时候,所有优秀的Java开发工具都应推出了自己的浏览器。 30 | -------------------------------------------------------------------------------- /5.4.md: -------------------------------------------------------------------------------- 1 | # 5.4 类访问 2 | 3 | 4 | 在Java中,亦可用访问指示符判断出一个库内的哪些类可由那个库的用户使用。若想一个类能由客户程序员调用,可在类主体的起始花括号前面某处放置一个`public`关键字。它控制着客户程序员是否能够创建属于这个类的一个对象。 5 | 6 | 为控制一个类的访问,指示符必须在关键字`class`之前出现。所以我们能够使用: 7 | 8 | ``` 9 | public class Widget { 10 | ``` 11 | 12 | 也就是说,假若我们的库名是`mylib`,那么所有客户程序员都能访问`Widget`——通过下述语句: 13 | 14 | ``` 15 | import mylib.Widget; 16 | ``` 17 | 18 | 或者 19 | 20 | ``` 21 | import mylib.*; 22 | ``` 23 | 24 | 然而,我们同时还要注意到一些额外的限制: 25 | 26 | (1) 每个编译单元(文件)都只能有一个`public`类。每个编译单元有一个公共接口的概念是由那个公共类表达出来的。根据自己的需要,它可拥有任意多个提供支撑的“友好”类。但若在一个编译单元里使用了多个`public`类,编译器就会向我们提示一条出错消息。 27 | 28 | (2) `public`类的名字必须与包含了编译单元的那个文件的名字完全相符,甚至包括它的大小写形式。所以对于`Widget`来说,文件的名字必须是`Widget.java`,而不应是`widget.java`或者`WIDGET.java`。同样地,如果出现不符,就会报告一个编译期错误。 29 | 30 | (3) 可能(但并常见)有一个编译单元根本没有任何公共类。此时,可按自己的意愿任意指定文件名。 31 | 32 | 如果已经获得了`mylib`内部的一个类,准备用它完成由`Widget`或者`mylib`内部的其他某些`public`类执行的任务,此时又会出现什么情况呢?我们不希望花费力气为客户程序员编制文档,并感觉以后某个时候也许会进行大手笔的修改,并将自己的类一起删掉,换成另一个不同的类。为获得这种灵活处理的能力,需要保证没有客户程序员能够依赖自己隐藏于`mylib`内部的特定实现细节。为达到这个目的,只需将`public`关键字从类中剔除即可,这样便把类变成了“友好的”(类仅能在包内使用)。 33 | 34 | 注意不可将类设成`private`(那样会使除类之外的其他东西都不能访问它),也不能设成`protected`(注释④)。因此,我们现在对于类的访问只有两个选择:“友好的”或者`public`。若不愿其他任何人访问那个类,可将所有构造器设为`private`。这样一来,在类的一个`static`成员内部,除自己之外的其他所有人都无法创建属于那个类的一个对象(注释⑤)。如下例所示: 35 | 36 | ``` 37 | //: Lunch.java 38 | // Demonstrates class access specifiers. 39 | // Make a class effectively private 40 | // with private constructors: 41 | 42 | class Soup { 43 | private Soup() {} 44 | // (1) Allow creation via static method: 45 | public static Soup makeSoup() { 46 | return new Soup(); 47 | } 48 | // (2) Create a static object and 49 | // return a reference upon request. 50 | // (The "Singleton" pattern): 51 | private static Soup ps1 = new Soup(); 52 | public static Soup access() { 53 | return ps1; 54 | } 55 | public void f() {} 56 | } 57 | 58 | class Sandwich { // Uses Lunch 59 | void f() { new Lunch(); } 60 | } 61 | 62 | // Only one public class allowed per file: 63 | public class Lunch { 64 | void test() { 65 | // Can't do this! Private constructor: 66 | //! Soup priv1 = new Soup(); 67 | Soup priv2 = Soup.makeSoup(); 68 | Sandwich f1 = new Sandwich(); 69 | Soup.access().f(); 70 | } 71 | } ///:~ 72 | ``` 73 | 74 | ④:实际上,Java 1.1内部类既可以是“受到保护的”,也可以是“私有的”,但那属于特别情况。第7章会详细解释这个问题。 75 | 76 | ⑤:亦可通过从那个类继承来实现。 77 | 78 | 迄今为止,我们创建过的大多数方法都是要么返回void,要么返回一个基本数据类型。所以对下述定义来说: 79 | 80 | ``` 81 | public static Soup access() { 82 | return psl; 83 | } 84 | ``` 85 | 86 | 它最开始多少会使人有些迷惑。位于方法名(`access`)前的单词指出方法到底返回什么。在这之前,我们看到的都是`void`,它意味着“什么也不返回”(`void`在英语里是“虚无”的意思。但亦可返回指向一个对象的引用,此时出现的就是这个情况。该方法返回一个引用,它指向类`Soup`的一个对象。 87 | 88 | `Soup`类向我们展示出如何通过将所有构造器都设为`private`,从而防止直接创建一个类。请记住,假若不明确地至少创建一个构造器,就会自动创建默认构造器(没有参数)。若自己编写默认构造器,它就不会自动创建。把它变成`private`后,就没人能为那个类创建一个对象。但别人怎样使用这个类呢?上面的例子为我们揭示出了两个选择。第一个选择,我们可创建一个`static`方法,再通过它创建一个新的`Soup`,然后返回指向它的一个引用。如果想在返回之前对`Soup`进行一些额外的操作,或者想了解准备创建多少个`Soup`对象(可能是为了限制它们的个数),这种方案无疑是特别有用的。 89 | 90 | 第二个选择是采用“设计模式”(Design Pattern)技术,本书后面会对此进行详细介绍。通常方案叫作“单例”,因为它仅允许创建一个对象。类`Soup`的对象被创建成`Soup`的一个`static private`成员,所以有一个而且只能有一个。除非通过`public`方法`access()`,否则根本无法访问它。 91 | 92 | 正如早先指出的那样,如果不针对类的访问设置一个访问指示符,那么它会自动默认为“友好的”。这意味着那个类的对象可由包内的其他类创建,但不能由包外创建。请记住,对于相同目录内的所有文件,如果没有明确地进行`package`声明,那么它们都默认为那个目录的默认包的一部分。然而,假若那个类一个`static`成员的属性是`public`,那么客户程序员仍然能够访问那个`static`成员——即使它们不能创建属于那个类的一个对象。 93 | -------------------------------------------------------------------------------- /5.5.md: -------------------------------------------------------------------------------- 1 | # 5.5 总结 2 | 3 | 对于任何关系,最重要的一点都是规定好所有方面都必须遵守的界限或规则。创建一个库时,相当于建立了同那个库的用户(即“客户程序员”)的一种关系——那些用户属于另外的程序员,可能用我们的库自行构建一个应用程序,或者用我们的库构建一个更大的库。 4 | 5 | 如果不制订规则,客户程序员就可以随心所欲地操作一个类的所有成员,无论我们本来愿不愿意其中的一些成员被直接操作。所有东西都在别人面前都暴露无遗。 6 | 7 | 本章讲述了如何构建类,从而制作出理想的库。首先,我们讲述如何将一组类封装到一个库里。其次,我们讲述类如何控制对自己成员的访问。 8 | 9 | 一般情况下,一个C程序项目会在50K到100K行代码之间的某个地方开始中断。这是由于C仅有一个“命名空间”,所以名字会开始互相抵触,从而造成额外的管理开销。而在Java中,`package`关键字、包命名方案以及`import`关键字为我们提供对名字的完全控制,所以命名冲突的问题可以很轻易地得到避免。 10 | 11 | 有两方面的原因要求我们控制对成员的访问。第一个是防止用户接触那些他们不应碰的工具。对于数据类型的内部机制,那些工具是必需的。但它们并不属于用户接口的一部分,用户不必用它来解决自己的特定问题。所以将方法和字段变成“私有”(`private`)后,可极大方便用户。因为他们能轻易看出哪些对于自己来说是最重要的,以及哪些是自己需要忽略的。这样便简化了用户对一个类的理解。 12 | 13 | 进行访问控制的第二个、也是最重要的一个原因是:允许库设计者改变类的内部工作机制,同时不必担心它会对客户程序员产生什么影响。最开始的时候,可用一种方法构建一个类,后来发现需要重新构建代码,以便达到更快的速度。如接口和实现细节早已进行了明确的分隔与保护,就可以轻松地达到自己的目的,不要求用户改写他们的代码。 14 | 15 | 利用Java中的访问指示符,可有效控制类的创建者。那个类的用户可确切知道哪些是自己能够使用的,哪些则是可以忽略的。但更重要的一点是,它可确保没有任何用户能依赖一个类的基础实现机制的任何部分。作为一个类的创建者,我们可自由修改基础的实现细节,这一改变不会对客户程序员产生任何影响,因为他们不能访问类的那一部分。 16 | 17 | 有能力改变基础的实现细节后,除了能在以后改进自己的设置之外,也同时拥有了“犯错误”的自由。无论当初计划与设计时有多么仔细,仍然有可能出现一些失误。由于知道自己能相当安全地犯下这种错误,所以可以放心大胆地进行更多、更自由的试验。这对自己编程水平的提高是很有帮助的,使整个项目最终能更快、更好地完成。 18 | 19 | 一个类的公共接口是所有用户都能看见的,所以在进行分析与设计的时候,这是应尽量保证其准确性的最重要的一个部分。但也不必过于紧张,少许的误差仍然是允许的。若最初设计的接口存在少许问题,可考虑添加更多的方法,只要保证不删除客户程序员已在他们的代码里使用的东西。 20 | -------------------------------------------------------------------------------- /5.6.md: -------------------------------------------------------------------------------- 1 | # 5.6 练习 2 | 3 | 4 | (1) 用`public`、`private`、`protected`以及“友好的”数据成员及方法成员创建一个类。创建属于这个类的一个对象,并观察在试图访问所有类成员时会获得哪种类型的编译器错误提示。注意同一个目录内的类属于“默认”包的一部分。 5 | 6 | (2) 用`protected`数据创建一个类。在相同的文件里创建第二个类,用一个方法操纵第一个类里的`protected`数据。 7 | 8 | (3) 新建一个目录,并编辑自己的`CLASSPATH`,以便包括那个新目录。将`P.class`文件复制到自己的新目录,然后改变文件名、`P`类以及方法名(亦可考虑添加额外的输出,观察它的运行过程)。在一个不同的目录里创建另一个程序,令其使用自己的新类。 9 | 10 | (4) 在`c05`目录(假定在自己的`CLASSPATH`里)创建下述文件: 11 | 12 | 214页程序 13 | 14 | 然后在`c05`之外的另一个目录里创建下述文件: 15 | 16 | 214-215页程序 17 | 18 | 解释编译器为什么会产生一个错误。将`Foreign`(外部)类作为`c05`包的一部分改变了什么东西吗? 19 | -------------------------------------------------------------------------------- /5.md: -------------------------------------------------------------------------------- 1 | # 第5章 隐藏实现过程 2 | 3 | 4 | “进行面向对象的设计时,一项基本的考虑是:如何将发生变化的东西与保持不变的东西分隔开。” 5 | 6 | 这一点对于库来说是特别重要的。那个库的用户(客户程序员)必须能依赖自己使用的那一部分,并知道一旦新版本的库出台,自己不需要改写代码。而与此相反,库的创建者必须能自由地进行修改与改进,同时保证客户程序员代码不会受到那些变动的影响。 7 | 8 | 为达到这个目的,需遵守一定的约定或规则。例如,库程序员在修改库内的一个类时,必须保证不删除已有的方法,因为那样做会造成客户程序员代码出现断点。然而,相反的情况却是令人痛苦的。对于一个数据成员,库的创建者怎样才能知道哪些数据成员已受到客户程序员的访问呢?若方法属于某个类唯一的一部分,而且并不一定由客户程序员直接使用,那么这种痛苦的情况同样是真实的。如果库的创建者想删除一种旧有的实现方案,并置入新代码,此时又该怎么办呢?对那些成员进行的任何改动都可能中断客户程序员的代码。所以库创建者处在一个尴尬的境地,似乎根本动弹不得。 9 | 10 | 为解决这个问题,Java推出了“访问指示符”的概念,允许库创建者声明哪些东西是客户程序员可以使用的,哪些是不可使用的。这种访问控制的级别在“最大访问”和“最小访问”的范围之间,分别包括:`public`,“友好的”(无关键字),`protected`以及`private`。根据前一段的描述,大家或许已总结出作为一名库设计者,应将所有东西都尽可能保持为`private`(私有),并只展示出那些想让客户程序员使用的方法。这种思路是完全正确的,尽管它有点儿违背那些用其他语言(特别是C)编程的人的直觉,那些人习惯于在没有任何限制的情况下访问所有东西。到这一章结束时,大家应该可以深刻体会到Java访问控制的价值。 11 | 12 | 然而,组件库以及控制谁能访问那个库的组件的概念现在仍不是完整的。仍存在这样一个问题:如何将组件绑定到单独一个统一的库单元里。这是通过Java的`package`(打包)关键字来实现的,而且访问指示符要受到类在相同的包还是在不同的包里的影响。所以在本章的开头,大家首先要学习库组件如何置入包里。这样才能理解访问指示符的完整含义。 13 | -------------------------------------------------------------------------------- /6.1.md: -------------------------------------------------------------------------------- 1 | # 6.1 組合的语法 2 | 3 | 4 | 就以前的学习情况来看,事实上已进行了多次“组合”操作。为进行组合,我们只需在新类里简单地置入对象引用即可。举个例子来说,假定需要在一个对象里容纳几个`String`对象、两种基本数据类型以及属于另一个类的一个对象。对于非基本类型的对象来说,只需将引用置于新类即可;而对于基本数据类型来说,则需在自己的类中定义它们。如下所示(若执行该程序时有麻烦,请参见第3章3.1.2小节“赋值”): 5 | 6 | ``` 7 | //: SprinklerSystem.java 8 | // Composition for code reuse 9 | package c06; 10 | 11 | class WaterSource { 12 | private String s; 13 | WaterSource() { 14 | System.out.println("WaterSource()"); 15 | s = new String("Constructed"); 16 | } 17 | public String toString() { return s; } 18 | } 19 | 20 | public class SprinklerSystem { 21 | private String valve1, valve2, valve3, valve4; 22 | WaterSource source; 23 | int i; 24 | float f; 25 | void print() { 26 | System.out.println("valve1 = " + valve1); 27 | System.out.println("valve2 = " + valve2); 28 | System.out.println("valve3 = " + valve3); 29 | System.out.println("valve4 = " + valve4); 30 | System.out.println("i = " + i); 31 | System.out.println("f = " + f); 32 | System.out.println("source = " + source); 33 | } 34 | public static void main(String[] args) { 35 | SprinklerSystem x = new SprinklerSystem(); 36 | x.print(); 37 | } 38 | } ///:~ 39 | ``` 40 | 41 | `WaterSource`内定义的一个方法是比较特别的:`toString()`。大家不久就会知道,每种非基本类型的对象都有一个`toString()`方法。若编译器本来希望一个`String`,但却获得某个这样的对象,就会调用这个方法。所以在下面这个表达式中: 42 | 43 | ``` 44 | System.out.println("source = " + source) ; 45 | ``` 46 | 47 | 编译器会发现我们试图向一个`WaterSource`添加一个`String`对象(`source =`)。这对它来说是不可接受的,因为我们只能将一个字符串“添加”到另一个字符串,所以它会说:“我要调用`toString()`,把`source`转换成字符串!”经这样处理后,它就能编译两个字符串,并将结果字符串传递给一个`System.out.println()`。每次随同自己创建的一个类允许这种行为的时候,都只需要写一个`toString()`方法。 48 | 49 | 如果不深究,可能会草率地认为编译器会为上述代码中的每个引用都自动构造对象(由于Java的安全和谨慎的形象)。例如,可能以为它会为`WaterSource`调用默认构造器,以便初始化`source`。打印语句的输出事实上是: 50 | 51 | ``` 52 | valve1 = null 53 | valve2 = null 54 | valve3 = null 55 | valve4 = null 56 | i = 0 57 | f = 0.0 58 | source = null 59 | ``` 60 | 61 | 在类内作为字段使用的基本数据会初始化成零,就象第2章指出的那样。但对象引用会初始化成`null`。而且假若试图为它们中的任何一个调用方法,就会产生一次“异常”。这种结果实际是相当好的(而且很有用),我们可在不丢弃一次异常的前提下,仍然把它们打印出来。 62 | 63 | 编译器并不只是为每个引用创建一个默认对象,因为那样会在许多情况下招致不必要的开销。如希望引用得到初始化,可在下面这些地方进行: 64 | 65 | (1) 在对象定义的时候。这意味着它们在构造器调用之前肯定能得到初始化。 66 | 67 | (2) 在那个类的构造器中。 68 | 69 | (3) 紧靠在要求实际使用那个对象之前。这样做可减少不必要的开销——假如对象并不需要创建的话。 70 | 71 | 下面向大家展示了所有这三种方法: 72 | 73 | ``` 74 | //: Bath.java 75 | // Constructor initialization with composition 76 | 77 | class Soap { 78 | private String s; 79 | Soap() { 80 | System.out.println("Soap()"); 81 | s = new String("Constructed"); 82 | } 83 | public String toString() { return s; } 84 | } 85 | 86 | public class Bath { 87 | private String 88 | // Initializing at point of definition: 89 | s1 = new String("Happy"), 90 | s2 = "Happy", 91 | s3, s4; 92 | Soap castille; 93 | int i; 94 | float toy; 95 | Bath() { 96 | System.out.println("Inside Bath()"); 97 | s3 = new String("Joy"); 98 | i = 47; 99 | toy = 3.14f; 100 | castille = new Soap(); 101 | } 102 | void print() { 103 | // Delayed initialization: 104 | if(s4 == null) 105 | s4 = new String("Joy"); 106 | System.out.println("s1 = " + s1); 107 | System.out.println("s2 = " + s2); 108 | System.out.println("s3 = " + s3); 109 | System.out.println("s4 = " + s4); 110 | System.out.println("i = " + i); 111 | System.out.println("toy = " + toy); 112 | System.out.println("castille = " + castille); 113 | } 114 | public static void main(String[] args) { 115 | Bath b = new Bath(); 116 | b.print(); 117 | } 118 | } ///:~ 119 | ``` 120 | 121 | 请注意在`Bath`构造器中,在所有初始化开始之前执行了一个语句。如果不在定义时进行初始化,仍然不能保证能在将一条消息发给一个对象引用之前会执行任何初始化——除非出现不可避免的运行期异常。 122 | 下面是该程序的输出: 123 | 124 | ``` 125 | Inside Bath() 126 | Soap() 127 | s1 = Happy 128 | s2 = Happy 129 | s3 = Joy 130 | s4 = Joy 131 | i = 47 132 | toy = 3.14 133 | castille = Constructed 134 | ``` 135 | 136 | 调用`print()`时,它会填充`s4`,使所有字段在使用之前都获得正确的初始化。 137 | -------------------------------------------------------------------------------- /6.10.md: -------------------------------------------------------------------------------- 1 | # 6.10 总结 2 | 3 | 4 | 无论继承还是组合,我们都可以在现有类型的基础上创建一个新类型。但在典型情况下,我们通过组合来实现现有类型的“复用”或“重复使用”,将其作为新类型基础实现过程的一部分使用。但如果想实现接口的“复用”,就应使用继承。由于派生或派生出来的类拥有基类的接口,所以能够将其“向上转换”为基类。对于下一章要讲述的多态性问题,这一点是至关重要的。 5 | 6 | 尽管继承在面向对象的程序设计中得到了特别的强调,但在实际启动一个设计时,最好还是先考虑采用组合技术。只有在特别必要的时候,才应考虑采用继承技术(下一章还会讲到这个问题)。组合显得更加灵活。但是,通过对自己的成员类型应用一些继承技巧,可在运行期准确改变那些成员对象的类型,由此可改变它们的行为。 7 | 8 | 尽管对于快速项目开发来说,通过组合和继承实现的代码复用具有很大的帮助作用。但在允许其他程序员完全依赖它之前,一般都希望能重新设计自己的类结构。我们理想的类结构应该是每个类都有自己特定的用途。它们不能过大(如集成的功能太多,则很难实现它的复用),也不能过小(造成不能由自己使用,或者不能增添新功能)。最终实现的类应该能够方便地复用。 9 | -------------------------------------------------------------------------------- /6.11.md: -------------------------------------------------------------------------------- 1 | # 6.11 练习 2 | 3 | 4 | (1) 用默认构造器(空参数列表)创建两个类:`A`和`B`,令它们自己声明自己。从`A`继承一个名为`C`的新类,并在`C`内创建一个成员`B`。不要为`C`创建一个构造器。创建类`C`的一个对象,并观察结果。 5 | 6 | (2) 修改练习1,使`A`和`B`都有含有参数的构造器,则不是采用默认构造器。为`C`写一个构造器,并在`C`的构造器中执行所有初始化工作。 7 | 8 | (3) 使用文件`Cartoon.java`,将`Cartoon`类的构造器代码变成注释内容标注出去。解释会发生什么事情。 9 | 10 | (4) 使用文件`Chess.java`,将`Chess`类的构造器代码作为注释标注出去。同样解释会发生什么。 11 | -------------------------------------------------------------------------------- /6.2.md: -------------------------------------------------------------------------------- 1 | # 6.2 继承的语法 2 | 3 | 4 | 继承与Java(以及其他OOP语言)非常紧密地结合在一起。我们早在第1章就为大家引入了继承的概念,并在那章之后到本章之前的各章里不时用到,因为一些特殊的场合要求必须使用继承。除此以外,创建一个类时肯定会进行继承,因为若非如此,会从Java的标准根类`Object`中继承。 5 | 6 | 用于组合的语法是非常简单且直观的。但为了进行继承,必须采用一种全然不同的形式。需要继承的时候,我们会说:“这个新类和那个旧类差不多。”为了在代码里表面这一观念,需要给出类名。但在类主体的起始花括号之前,需要放置一个关键字`extends`,在后面跟随“基类”的名字。若采取这种做法,就可自动获得基类的所有数据成员以及方法。下面是一个例子: 7 | 8 | ``` 9 | //: Detergent.java 10 | // Inheritance syntax & properties 11 | 12 | class Cleanser { 13 | private String s = new String("Cleanser"); 14 | public void append(String a) { s += a; } 15 | public void dilute() { append(" dilute()"); } 16 | public void apply() { append(" apply()"); } 17 | public void scrub() { append(" scrub()"); } 18 | public void print() { System.out.println(s); } 19 | public static void main(String[] args) { 20 | Cleanser x = new Cleanser(); 21 | x.dilute(); x.apply(); x.scrub(); 22 | x.print(); 23 | } 24 | } 25 | 26 | public class Detergent extends Cleanser { 27 | // Change a method: 28 | public void scrub() { 29 | append(" Detergent.scrub()"); 30 | super.scrub(); // Call base-class version 31 | } 32 | // Add methods to the interface: 33 | public void foam() { append(" foam()"); } 34 | // Test the new class: 35 | public static void main(String[] args) { 36 | Detergent x = new Detergent(); 37 | x.dilute(); 38 | x.apply(); 39 | x.scrub(); 40 | x.foam(); 41 | x.print(); 42 | System.out.println("Testing base class:"); 43 | Cleanser.main(args); 44 | } 45 | } ///:~ 46 | ``` 47 | 48 | 这个例子向大家展示了大量特性。首先,在`Cleanser append()`方法里,字符串同一个`s`连接起来。这是用`+=`运算符实现的。同`+`一样,`+=`被Java用于对字符串进行“重载”处理。 49 | 50 | 其次,无论`Cleanser`还是`Detergent`都包含了一个`main()`方法。我们可为自己的每个类都创建一个`main()`。通常建议大家象这样进行编写代码,使自己的测试代码能够封装到类内。即便在程序中含有数量众多的类,但对于在命令行请求的`public`类,只有`main()`才会得到调用。所以在这种情况下,当我们使用`java Detergent`的时候,调用的是`Degergent.main()`——即使`Cleanser`并非一个`public`类。采用这种将`main()`置入每个类的做法,可方便地为每个类都进行单元测试。而且在完成测试以后,毋需将`main()`删去;可把它保留下来,用于以后的测试。 51 | 52 | 在这里,大家可看到`Deteregent.main()`对`Cleanser.main()`的调用是明确进行的。 53 | 54 | 需要着重强调的是`Cleanser`中的所有类都是`public`属性。请记住,倘若省略所有访问指示符,则成员默认为“友好的”。这样一来,就只允许对包成员进行访问。在这个包内,任何人都可使用那些没有访问指示符的方法。例如,`Detergent`将不会遇到任何麻烦。然而,假设来自另外某个包的类准备继承`Cleanser`,它就只能访问那些`public`成员。所以在计划继承的时候,一个比较好的规则是将所有字段都设为`private`,并将所有方法都设为`public`(`protected`成员也允许派生出来的类访问它;以后还会深入探讨这一问题)。当然,在一些特殊的场合,我们仍然必须作出一些调整,但这并不是一个好的做法。 55 | 56 | 注意`Cleanser`在它的接口中含有一系列方法:`append()`,`dilute()`,`apply()`,`scrub()`以及`print()`。由于`Detergent`是从`Cleanser`派生出来的(通过`extends`关键字),所以它会自动获得接口内的所有这些方法——即使我们在`Detergent`里并未看到对它们的明确定义。这样一来,就可将继承想象成“对接口的重复利用”或者“接口的复用”(以后的实现细节可以自由设置,但那并非我们强调的重点)。 57 | 58 | 正如在`scrub()`里看到的那样,可以获得在基类里定义的一个方法,并对其进行修改。在这种情况下,我们通常想在新版本里调用来自基类的方法。但在`scrub()`里,不可只是简单地发出对`scrub()`的调用。那样便造成了递归调用,我们不愿看到这一情况。为解决这个问题,Java提供了一个`super`关键字,它引用当前类已从中继承的一个“超类”(Superclass)。所以表达式`super.scrub()`调用的是方法`scrub()`的基类版本。 59 | 60 | 进行继承时,我们并不限于只能使用基类的方法。亦可在派生出来的类里加入自己的新方法。这时采取的做法与在普通类里添加其他任何方法是完全一样的:只需简单地定义它即可。`extends`关键字提醒我们准备将新方法加入基类的接口里,对其进行“扩展”。`foam()`便是这种做法的一个产物。 61 | 62 | 在`Detergent.main()`里,我们可看到对于`Detergent`对象,可调用`Cleanser`以及`Detergent`内所有可用的方法(如`foam()`)。 63 | 64 | ## 6.2.1 初始化基类 65 | 66 | 由于这儿涉及到两个类——基类及派生类,而不再是以前的一个,所以在想象派生类的结果对象时,可能会产生一些迷惑。从外部看,似乎新类拥有与基类相同的接口,而且可包含一些额外的方法和字段。但继承并非仅仅简单地复制基类的接口了事。创建派生类的一个对象时,它在其中包含了基类的一个“子对象”。这个子对象就象我们根据基类本身创建了它的一个对象。从外部看,基类的子对象已封装到派生类的对象里了。 67 | 68 | 当然,基类子对象应该正确地初始化,而且只有一种方法能保证这一点:在构造器中执行初始化,通过调用基类构造器,后者有足够的能力和权限来执行对基类的初始化。在派生类的构造器中,Java会自动插入对基类构造器的调用。下面这个例子向大家展示了对这种三级继承的应用: 69 | 70 | ``` 71 | //: Cartoon.java 72 | // Constructor calls during inheritance 73 | 74 | class Art { 75 | Art() { 76 | System.out.println("Art constructor"); 77 | } 78 | } 79 | 80 | class Drawing extends Art { 81 | Drawing() { 82 | System.out.println("Drawing constructor"); 83 | } 84 | } 85 | 86 | public class Cartoon extends Drawing { 87 | Cartoon() { 88 | System.out.println("Cartoon constructor"); 89 | } 90 | public static void main(String[] args) { 91 | Cartoon x = new Cartoon(); 92 | } 93 | } ///:~ 94 | ``` 95 | 96 | 该程序的输出显示了自动调用: 97 | 98 | ``` 99 | Art constructor 100 | Drawing constructor 101 | Cartoon constructor 102 | ``` 103 | 104 | 可以看出,构建是在基类的“外部”进行的,所以基类会在派生类访问它之前得到正确的初始化。 105 | 即使没有为`Cartoon()`创建一个构造器,编译器也会为我们自动生成一个默认构造器,并发出对基类构造器的调用。 106 | 107 | (1) 含有参数的构造器 108 | 109 | 上述例子有自己默认的构造器;也就是说,它们不含任何参数。编译器可以很容易地调用它们,因为不存在具体传递什么参数的问题。如果类没有默认的参数,或者想调用含有一个参数的某个基类构造器,必须明确地编写对基类的调用代码。这是用`super`关键字以及适当的参数列表实现的,如下所示: 110 | 111 | ``` 112 | //: Chess.java 113 | // Inheritance, constructors and arguments 114 | 115 | class Game { 116 | Game(int i) { 117 | System.out.println("Game constructor"); 118 | } 119 | } 120 | 121 | class BoardGame extends Game { 122 | BoardGame(int i) { 123 | super(i); 124 | System.out.println("BoardGame constructor"); 125 | } 126 | } 127 | 128 | public class Chess extends BoardGame { 129 | Chess() { 130 | super(11); 131 | System.out.println("Chess constructor"); 132 | } 133 | public static void main(String[] args) { 134 | Chess x = new Chess(); 135 | } 136 | } ///:~ 137 | ``` 138 | 139 | 如果不调用`BoardGames()`内的基类构造器,编译器就会报告自己找不到`Games()`形式的一个构造器。除此以外,在派生类构造器中,对基类构造器的调用是必须做的第一件事情(如操作失当,编译器会向我们指出)。 140 | 141 | (2) 捕获基本构造器的异常 142 | 143 | 正如刚才指出的那样,编译器会强迫我们在派生类构造器的主体中首先设置对基类构造器的调用。这意味着在它之前不能出现任何东西。正如大家在第9章会看到的那样,这同时也会防止派生类构造器捕获来自一个基类的任何异常事件。显然,这有时会为我们造成不便。 144 | -------------------------------------------------------------------------------- /6.4.md: -------------------------------------------------------------------------------- 1 | # 6.4 到底选择组合还是继承 2 | 3 | 4 | 无论组合还是继承,都允许我们将子对象置于自己的新类中。大家或许会奇怪两者间的差异,以及到底该如何选择。 5 | 6 | 如果想利用新类内部一个现有类的特性,而不想使用它的接口,通常应选择组合。也就是说,我们可嵌入一个对象,使自己能用它实现新类的特性。但新类的用户会看到我们已定义的接口,而不是来自嵌入对象的接口。考虑到这种效果,我们需在新类里嵌入现有类的`private`对象。 7 | 8 | 有些时候,我们想让类用户直接访问新类的组合。也就是说,需要将成员对象的属性变为`public`。成员对象会将自身隐藏起来,所以这是一种安全的做法。而且在用户知道我们准备组合一系列组件时,接口就更容易理解。`car`(汽车)对象便是一个很好的例子: 9 | 10 | ``` 11 | //: Car.java 12 | // Composition with public objects 13 | 14 | class Engine { 15 | public void start() {} 16 | public void rev() {} 17 | public void stop() {} 18 | } 19 | 20 | class Wheel { 21 | public void inflate(int psi) {} 22 | } 23 | 24 | class Window { 25 | public void rollup() {} 26 | public void rolldown() {} 27 | } 28 | 29 | class Door { 30 | public Window window = new Window(); 31 | public void open() {} 32 | public void close() {} 33 | } 34 | 35 | public class Car { 36 | public Engine engine = new Engine(); 37 | public Wheel[] wheel = new Wheel[4]; 38 | public Door left = new Door(), 39 | right = new Door(); // 2-door 40 | Car() { 41 | for(int i = 0; i < 4; i++) 42 | wheel[i] = new Wheel(); 43 | } 44 | public static void main(String[] args) { 45 | Car car = new Car(); 46 | car.left.window.rollup(); 47 | car.wheel[0].inflate(72); 48 | } 49 | } ///:~ 50 | ``` 51 | 52 | 由于汽车的装配是故障分析时需要考虑的一项因素(并非只是基础设计简单的一部分),所以有助于客户程序员理解如何使用类,而且类创建者的编程复杂程度也会大幅度降低。 53 | 54 | 如选择继承,就需要取得一个现成的类,并制作它的一个特殊版本。通常,这意味着我们准备使用一个常规用途的类,并根据特定的需求对其进行定制。只需稍加想象,就知道自己不能用一个车辆对象来组合一辆汽车——汽车并不“包含”车辆;相反,它“属于”车辆的一种类别。“属于”关系是用继承来表达的,而“包含”关系是用组合来表达的。 55 | -------------------------------------------------------------------------------- /6.5.md: -------------------------------------------------------------------------------- 1 | # 6.5 `protected` 2 | 3 | 现在我们已理解了继承的概念,`protected`这个关键字最后终于有了意义。在理想情况下,`private`成员随时都是“私有”的,任何人不得访问。但在实际应用中,经常想把某些东西深深地藏起来,但同时允许访问派生类的成员。`protected`关键字可帮助我们做到这一点。它的意思是“它本身是私有的,但可由从这个类继承的任何东西或者同一个包内的其他任何东西访问”。也就是说,Java中的`protected`会成为进入“友好”状态。 4 | 5 | 我们采取的最好的做法是保持成员的`private`状态——无论如何都应保留对基 础的实现细节进行修改的权利。在这一前提下,可通过`protected`方法允许类的继承者进行受到控制的访问: 6 | 7 | ``` 8 | //: Orc.java 9 | // The protected keyword 10 | import java.util.*; 11 | 12 | class Villain { 13 | private int i; 14 | protected int read() { return i; } 15 | protected void set(int ii) { i = ii; } 16 | public Villain(int ii) { i = ii; } 17 | public int value(int m) { return m*i; } 18 | } 19 | 20 | public class Orc extends Villain { 21 | private int j; 22 | public Orc(int jj) { super(jj); j = jj; } 23 | public void change(int x) { set(x); } 24 | } ///:~ 25 | ``` 26 | 27 | 可以看到,`change()`拥有对`set()`的访问权限,因为它的属性是`protected`(受到保护的)。 28 | -------------------------------------------------------------------------------- /6.6.md: -------------------------------------------------------------------------------- 1 | # 6.6 累积开发 2 | 3 | 4 | 继承的一个好处是它支持“累积开发”,允许我们引入新的代码,同时不会为现有代码造成错误。这样可将新错误隔离到新代码里。通过从一个现成的、功能性的类继承,同时增添成员新的数据成员及方法(并重新定义现有方法),我们可保持现有代码原封不动(另外有人也许仍在使用它),不会为其引入自己的编程错误。一旦出现错误,就知道它肯定是由于自己的新代码造成的。这样一来,与修改现有代码的主体相比,改正错误所需的时间和精力就可以少很多。 5 | 6 | 类的隔离效果非常好,这是许多程序员事先没有预料到的。甚至不需要方法的源代码来实现代码的复用。最多只需要导入一个包(这对于继承和合并都是成立的)。 7 | 8 | 大家要记住这样一个重点:程序开发是一个不断递增或者累积的过程,就象人们学习知识一样。当然可根据要求进行尽可能多的分析,但在一个项目的设计之初,谁都不可能提前获知所有的答案。如果能将自己的项目看作一个有机的、能不断进步的生物,从而不断地发展和改进它,就有望获得更大的成功以及更直接的反馈。 9 | 10 | 尽管继承是一种非常有用的技术,但在某些情况下,特别是在项目稳定下来以后,仍然需要从新的角度考察自己的类结构,将其收缩成一个更灵活的结构。请记住,继承是对一种特殊关系的表达,意味着“这个新类属于那个旧类的一种类型”。我们的程序不应纠缠于一些细树末节,而应着眼于创建和操作各种类型的对象,用它们表达出来自“问题空间”的一个模型。 11 | -------------------------------------------------------------------------------- /6.7.md: -------------------------------------------------------------------------------- 1 | # 6.7 向上转换 2 | 3 | 4 | 继承最值得注意的地方就是它没有为新类提供方法。继承是对新类和基类之间的关系的一种表达。可这样总结该关系:“新类属于现有类的一种类型”。 5 | 6 | 这种表达并不仅仅是对继承的一种形象化解释,继承是直接由语言提供支持的。作为一个例子,大家可考虑一个名为`Instrument`的基类,它用于表示乐器;另一个派生类叫作`Wind`。由于继承意味着基类的所有方法亦可在派生出来的类中使用,所以我们发给基类的任何消息亦可发给派生类。若`Instrument`类有一个`play()`方法,则`Wind`设备也会有这个方法。这意味着我们能肯定地认为一个`Wind`对象也是`Instrument`的一种类型。下面这个例子揭示出编译器如何提供对这一概念的支持: 7 | 8 | ``` 9 | //: Wind.java 10 | // Inheritance & upcasting 11 | import java.util.*; 12 | 13 | class Instrument { 14 | public void play() {} 15 | static void tune(Instrument i) { 16 | // ... 17 | i.play(); 18 | } 19 | } 20 | 21 | // Wind objects are instruments 22 | // because they have the same interface: 23 | class Wind extends Instrument { 24 | public static void main(String[] args) { 25 | Wind flute = new Wind(); 26 | Instrument.tune(flute); // Upcasting 27 | } 28 | } ///:~ 29 | ``` 30 | 31 | 这个例子中最有趣的无疑是`tune()`方法,它能接受一个`Instrument`引用。但在`Wind.main()`中,`tune()`方法是通过为其赋予一个`Wind`引用来调用的。由于Java对类型检查特别严格,所以大家可能会感到很奇怪,为什么接收一种类型的方法也能接收另一种类型呢?但是,我们一定要认识到一个`Wind`对象也是一个`Instrument`对象。而且对于不在`Wind`中的一个`Instrument`(乐器),没有方法可以由`tune()`调用。在`tune()`中,代码适用于`Instrument`以及从`Instrument`派生出来的任何东西。在这里,我们将从一个`Wind`引用转换成一个`Instrument`引用的行为叫作“向上转换”。 32 | 33 | ## 6.7.1 何谓“向上转换”? 34 | 35 | 之所以叫作这个名字,除了有一定的历史原因外,也是由于在传统意义上,类继承图的画法是根位于最顶部,再逐渐向下扩展(当然,可根据自己的习惯用任何方法描绘这种图)。因素,`Wind.java`的继承图就象下面这个样子: 36 | 37 | 由于转换的方向是从派生类到基类,箭头朝上,所以通常把它叫作“向上转换”,即`Upcasting`。向上转换肯定是安全的,因为我们是从一个更特殊的类型到一个更常规的类型。换言之,派生类是基类的一个超集。它可以包含比基类更多的方法,但它至少包含了基类的方法。进行向上转换的时候,类接口可能出现的唯一一个问题是它可能丢失方法,而不是赢得这些方法。这便是在没有任何明确的转换或者其他特殊标注的情况下,编译器为什么允许向上转换的原因所在。 38 | 39 | 也可以执行向下转换,但这时会面临第11章要详细讲述的一种困境。 40 | 41 | (1) 再论组合与继承 42 | 43 | 在面向对象的程序设计中,创建和使用代码最可能采取的一种做法是:将数据和方法统一封装到一个类里,并且使用那个类的对象。有些时候,需通过“组合”技术用现成的类来构造新类。而继承是最少见的一种做法。因此,尽管继承在学习OOP的过程中得到了大量的强调,但并不意味着应该尽可能地到处使用它。相反,使用它时要特别慎重。只有在清楚知道继承在所有方法中最有效的前提下,才可考虑它。为判断自己到底应该选用组合还是继承,一个最简单的办法就是考虑是否需要从新类向上转换回基类。若必须上溯,就需要继承。但如果不需要向上转换,就应提醒自己防止继承的滥用。在下一章里(多态性),会向大家介绍必须进行向上转换的一种场合。但只要记住经常问自己“我真的需要向上转换吗”,对于组合还是继承的选择就不应该是个太大的问题。 44 | -------------------------------------------------------------------------------- /6.9.md: -------------------------------------------------------------------------------- 1 | # 6.9 初始化和类装载 2 | 3 | 4 | 在许多传统语言里,程序都是作为启动过程的一部分一次性载入的。随后进行的是初始化,再是正式执行程序。在这些语言中,必须对初始化过程进行慎重的控制,保证`static`数据的初始化不会带来麻烦。比如在一个`static`数据获得初始化之前,就有另一个`static`数据希望它是一个有效值,那么在C++中就会造成问题。 5 | 6 | Java则没有这样的问题,因为它采用了不同的装载方法。由于Java中的一切东西都是对象,所以许多活动变得更加简单,这个问题便是其中的一例。正如下一章会讲到的那样,每个对象的代码都存在于独立的文件中。除非真的需要代码,否则那个文件是不会载入的。通常,我们可认为除非那个类的一个对象构造完毕,否则代码不会真的载入。由于`static`方法存在一些细微的歧义,所以也能认为“类代码在首次使用的时候载入”。 7 | 8 | 首次使用的地方也是`static`初始化发生的地方。装载的时候,所有`static`对象和`static`代码块都会按照本来的顺序初始化(亦即它们在类定义代码里写入的顺序)。当然,`static`数据只会初始化一次。 9 | 10 | ## 6.9.1 继承初始化 11 | 12 | 我们有必要对整个初始化过程有所认识,其中包括继承,对这个过程中发生的事情有一个整体性的概念。请观察下述代码: 13 | 14 | ``` 15 | //: Beetle.java 16 | // The full process of initialization. 17 | 18 | class Insect { 19 | int i = 9; 20 | int j; 21 | Insect() { 22 | prt("i = " + i + ", j = " + j); 23 | j = 39; 24 | } 25 | static int x1 = 26 | prt("static Insect.x1 initialized"); 27 | static int prt(String s) { 28 | System.out.println(s); 29 | return 47; 30 | } 31 | } 32 | 33 | public class Beetle extends Insect { 34 | int k = prt("Beetle.k initialized"); 35 | Beetle() { 36 | prt("k = " + k); 37 | prt("j = " + j); 38 | } 39 | static int x2 = 40 | prt("static Beetle.x2 initialized"); 41 | static int prt(String s) { 42 | System.out.println(s); 43 | return 63; 44 | } 45 | public static void main(String[] args) { 46 | prt("Beetle constructor"); 47 | Beetle b = new Beetle(); 48 | } 49 | } ///:~ 50 | ``` 51 | 52 | 该程序的输出如下: 53 | 54 | ``` 55 | static Insect.x initialized 56 | static Beetle.x initialized 57 | Beetle constructor 58 | i = 9, j = 0 59 | Beetle.k initialized 60 | k = 63 61 | j = 39 62 | ``` 63 | 64 | 对`Beetle`运行`java`时,发生的第一件事情是装载程序到外面找到那个类。在装载过程中,装载程序注意它有一个基类(即`extends`关键字要表达的意思),所以随之将其载入。无论是否准备生成那个基类的一个对象,这个过程都会发生(请试着将对象的创建代码当作注释标注出来,自己去证实)。 65 | 66 | 若基类含有另一个基类,则另一个基类随即也会载入,以此类推。接下来,会在根基类(此时是`Insect`)执行`static`初始化,再在下一个派生类执行,以此类推。保证这个顺序是非常关键的,因为派生类的初始化可能要依赖于对基类成员的正确初始化。 67 | 68 | 此时,必要的类已全部装载完毕,所以能够创建对象。首先,这个对象中的所有基本数据类型都会设成它们的默认值,而将对象引用设为`null`。随后会调用基类构造器。在这种情况下,调用是自动进行的。但也完全可以用`super`来自行指定构造器调用(就象在`Beetle()`构造器中的第一个操作一样)。基类的构建采用与派生类构造器完全相同的处理过程。基础顺构造器完成以后,实例变量会按本来的顺序得以初始化。最后,执行构造器剩余的主体部分。 69 | -------------------------------------------------------------------------------- /6.md: -------------------------------------------------------------------------------- 1 | # 第6章 类复用 2 | 3 | 4 | “Java引人注目的一项特性是代码的重复使用或者复用。但最具革命意义的是,除代码的复制和修改以外,我们还能做多得多的其他事情。” 5 | 6 | 在象C那样的程序化语言里,代码的重复使用早已可行,但效果不是特别显著。与Java的其他地方一样,这个方案解决的也是与类有关的问题。我们通过创建新类来重复使用代码,但却用不着重新创建,可以直接使用别人已建好并调试好的现成类。 7 | 8 | 但这样做必须保证不会干扰原有的代码。在这一章里,我们将介绍两个达到这一目标的方法。第一个最简单:在新类里简单地创建原有类的对象。我们把这种方法叫作“组合”,因为新类由现有类的对象合并而成。我们只是简单地重复利用代码的功能,而不是采用它的形式。 9 | 10 | 第二种方法则显得稍微有些技巧。它创建一个新类,将其作为现有类的一个“类型”。我们可以原样采取现有类的形式,并在其中加入新代码,同时不会对现有的类产生影响。这种魔术般的行为叫作“继承”(Inheritance),涉及的大多数工作都是由编译器完成的。对于面向对象的程序设计,“继承”是最重要的基础概念之一。它对我们下一章要讲述的内容会产生一些额外的影响。 11 | 12 | 对于组合与继承这两种方法,大多数语法和行为都是类似的(因为它们都要根据现有的类型生成新类型)。在本章,我们将深入学习这些代码复用或者重复使用的机制。 13 | -------------------------------------------------------------------------------- /7-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/7-1.gif -------------------------------------------------------------------------------- /7-10.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/7-10.gif -------------------------------------------------------------------------------- /7-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/7-2.gif -------------------------------------------------------------------------------- /7-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/7-3.gif -------------------------------------------------------------------------------- /7-4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/7-4.gif -------------------------------------------------------------------------------- /7-5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/7-5.gif -------------------------------------------------------------------------------- /7-6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/7-6.gif -------------------------------------------------------------------------------- /7-7.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/7-7.gif -------------------------------------------------------------------------------- /7-8.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/7-8.gif -------------------------------------------------------------------------------- /7-9.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/7-9.gif -------------------------------------------------------------------------------- /7.1.md: -------------------------------------------------------------------------------- 1 | # 7.1 向上转换 2 | 3 | 4 | 在第6章,大家已知道可将一个对象作为它自己的类型使用,或者作为它的基类型的一个对象使用。取得一个对象引用,并将其作为基类型引用使用的行为就叫作“向上转换”——因为继承树的画法是基类位于最上方。 5 | 6 | 但这样做也会遇到一个问题,如下例所示(若执行这个程序遇到麻烦,请参考第3章的3.1.2小节“赋值”): 7 | 8 | ``` 9 | //: Music.java 10 | // Inheritance & upcasting 11 | package c07; 12 | 13 | class Note { 14 | private int value; 15 | private Note(int val) { value = val; } 16 | public static final Note 17 | middleC = new Note(0), 18 | cSharp = new Note(1), 19 | cFlat = new Note(2); 20 | } // Etc. 21 | 22 | class Instrument { 23 | public void play(Note n) { 24 | System.out.println("Instrument.play()"); 25 | } 26 | } 27 | 28 | // Wind objects are instruments 29 | // because they have the same interface: 30 | class Wind extends Instrument { 31 | // Redefine interface method: 32 | public void play(Note n) { 33 | System.out.println("Wind.play()"); 34 | } 35 | } 36 | 37 | public class Music { 38 | public static void tune(Instrument i) { 39 | // ... 40 | i.play(Note.middleC); 41 | } 42 | public static void main(String[] args) { 43 | Wind flute = new Wind(); 44 | tune(flute); // Upcasting 45 | } 46 | } ///:~ 47 | ``` 48 | 49 | 其中,方法`Music.tune()`接收一个`Instrument`引用,同时也接收从`Instrument`派生出来的所有东西。当一个`Wind`引用传递给`tune()`的时候,就会出现这种情况。此时没有转换的必要。这样做是可以接受的;`Instrument`里的接口必须存在于`Wind`中,因为`Wind`是从`Instrument`里继承得到的。从`Wind`向`Instrument`的向上转换可能“缩小”那个接口,但不可能把它变得比`Instrument`的完整接口还要小。 50 | 51 | ## 7.1.1 为什么要向上转换 52 | 53 | 这个程序看起来也许显得有些奇怪。为什么所有人都应该有意忘记一个对象的类型呢?进行向上转换时,就可能产生这方面的疑惑。而且如果让`tune()`简单地取得一个`Wind`引用,将其作为自己的参数使用,似乎会更加简单、直观得多。但要注意:假如那样做,就需为系统内`Instrument`的每种类型写一个全新的`tune()`。假设按照前面的推论,加入`Stringed`(弦乐)和`Brass`(铜管)这两种`Instrument`(乐器): 54 | 55 | ``` 56 | //: Music2.java 57 | // Overloading instead of upcasting 58 | 59 | class Note2 { 60 | private int value; 61 | private Note2(int val) { value = val; } 62 | public static final Note2 63 | middleC = new Note2(0), 64 | cSharp = new Note2(1), 65 | cFlat = new Note2(2); 66 | } // Etc. 67 | 68 | class Instrument2 { 69 | public void play(Note2 n) { 70 | System.out.println("Instrument2.play()"); 71 | } 72 | } 73 | 74 | class Wind2 extends Instrument2 { 75 | public void play(Note2 n) { 76 | System.out.println("Wind2.play()"); 77 | } 78 | } 79 | 80 | class Stringed2 extends Instrument2 { 81 | public void play(Note2 n) { 82 | System.out.println("Stringed2.play()"); 83 | } 84 | } 85 | 86 | class Brass2 extends Instrument2 { 87 | public void play(Note2 n) { 88 | System.out.println("Brass2.play()"); 89 | } 90 | } 91 | 92 | public class Music2 { 93 | public static void tune(Wind2 i) { 94 | i.play(Note2.middleC); 95 | } 96 | public static void tune(Stringed2 i) { 97 | i.play(Note2.middleC); 98 | } 99 | public static void tune(Brass2 i) { 100 | i.play(Note2.middleC); 101 | } 102 | public static void main(String[] args) { 103 | Wind2 flute = new Wind2(); 104 | Stringed2 violin = new Stringed2(); 105 | Brass2 frenchHorn = new Brass2(); 106 | tune(flute); // No upcasting 107 | tune(violin); 108 | tune(frenchHorn); 109 | } 110 | } ///:~ 111 | ``` 112 | 113 | 这样做当然行得通,但却存在一个极大的弊端:必须为每种新增的`Instrument2`类编写与类紧密相关的方法。这意味着第一次就要求多得多的编程量。以后,假如想添加一个象`tune()`那样的新方法或者为`Instrument`添加一个新类型,仍然需要进行大量编码工作。此外,即使忘记对自己的某个方法进行重载设置,编译器也不会提示任何错误。这样一来,类型的整个操作过程就显得极难管理,有失控的危险。 114 | 115 | 但假如只写一个方法,将基类作为参数使用,而不是使用那些特定的派生类,岂不是会简单得多?也就是说,如果我们能不顾派生类,只让自己的代码与基类打交道,那么省下的工作量将是难以估计的。 116 | 117 | 这正是“多态性”大显身手的地方。然而,大多数程序员(特别是有程序化编程背景的)对于多态性的工作原理仍然显得有些生疏。 118 | -------------------------------------------------------------------------------- /7.10.md: -------------------------------------------------------------------------------- 1 | # 7.10 练习 2 | 3 | (1) 创建`Rodent`(啮齿动物):`Mouse`(老鼠),`Gerbil`(鼹鼠),`Hamster`(大颊鼠)等的一个继承分级结构。在基类中,提供适用于所有`Rodent`的方法,并在派生类中覆盖它们,从而根据不同类型的`Rodent`采取不同的行动。创建一个`Rodent`数组,在其中填充不同类型的`Rodent`,然后调用自己的基类方法,看看会有什么情况发生。 4 | 5 | (2) 修改练习1,使`Rodent`成为一个接口。 6 | 7 | (3) 改正`WindError.java`中的问题。 8 | 9 | (4) 在`GreenhouseControls.java`中,添加`Event`内部类,使其能打开和关闭风扇。 10 | -------------------------------------------------------------------------------- /7.3.md: -------------------------------------------------------------------------------- 1 | # 7.3 覆盖与重载 2 | 3 | 4 | 现在让我们用不同的眼光来看看本章的头一个例子。在下面这个程序中,方法`play()`的接口会在被覆盖的过程中发生变化。这意味着我们实际并没有“覆盖”方法,而是使其“重载”。编译器允许我们对方法进行重载处理,使其不报告出错。但这种行为可能并不是我们所希望的。下面是这个例子: 5 | 6 | ``` 7 | //: WindError.java 8 | // Accidentally changing the interface 9 | 10 | class NoteX { 11 | public static final int 12 | MIDDLE_C = 0, C_SHARP = 1, C_FLAT = 2; 13 | } 14 | 15 | class InstrumentX { 16 | public void play(int NoteX) { 17 | System.out.println("InstrumentX.play()"); 18 | } 19 | } 20 | 21 | class WindX extends InstrumentX { 22 | // OOPS! Changes the method interface: 23 | public void play(NoteX n) { 24 | System.out.println("WindX.play(NoteX n)"); 25 | } 26 | } 27 | 28 | public class WindError { 29 | public static void tune(InstrumentX i) { 30 | // ... 31 | i.play(NoteX.MIDDLE_C); 32 | } 33 | public static void main(String[] args) { 34 | WindX flute = new WindX(); 35 | tune(flute); // Not the desired behavior! 36 | } 37 | } ///:~ 38 | ``` 39 | 40 | 这里还向大家引入了另一个易于混淆的概念。在`InstrumentX`中,`play()`方法采用了一个`int`(整数)数值,它的标识符是`NoteX`。也就是说,即使`NoteX`是一个类名,也可以把它作为一个标识符使用,编译器不会报告出错。但在`WindX`中,`play()`采用一个`NoteX`引用,它有一个标识符`n`。即便我们使用`play(NoteX NoteX)`,编译器也不会报告错误。这样一来,看起来就象是程序员有意覆盖`play()`的功能,但对方法的类型定义却稍微有些不确切。然而,编译器此时假定的是程序员有意进行“重载”,而非“覆盖”。请仔细体会这两个术语的区别。“重载”是指同一样东西在不同的地方具有多种含义;而“覆盖”是指它随时随地都只有一种含义,只是原先的含义完全被后来的含义取代了。请注意如果遵守标准的Java命名规范,参数标识符就应该是`noteX`,这样可把它与类名区分开。 41 | 42 | 在`tune`中,`InstrumentX i`会发出`play()`消息,同时将某个`NoteX`成员作为参数使用(`MIDDLE_C`)。由于`NoteX`包含了`int`定义,重载的`play()`方法的`int`版本会得到调用。同时由于它尚未被“覆盖”,所以会使用基类版本。 43 | 44 | 输出是: 45 | 46 | ``` 47 | InstrumentX.play() 48 | ``` 49 | -------------------------------------------------------------------------------- /7.4.md: -------------------------------------------------------------------------------- 1 | # 7.4 抽象类和方法 2 | 3 | 4 | 在我们所有乐器(`Instrument`)例子中,基类`Instrument`内的方法都肯定是“伪”方法。若去调用这些方法,就会出现错误。那是由于`Instrument`的意图是为从它派生出去的所有类都创建一个通用接口。 5 | 6 | 之所以要建立这个通用接口,唯一的原因就是它能为不同的子类型作出不同的表示。它为我们建立了一种基本形式,使我们能定义在所有派生类里“通用”的一些东西。为阐述这个观念,另一个方法是把`Instrument`称为“抽象基类”(简称“抽象类”)。若想通过该通用接口处理一系列类,就需要创建一个抽象类。对所有与基类声明的签名相符的派生类方法,都可以通过动态绑定机制进行调用(然而,正如上一节指出的那样,如果方法名与基类相同,但参数不同,就会出现重载现象,那或许并非我们所愿意的)。 7 | 8 | 如果有一个象`Instrument`那样的抽象类,那个类的对象几乎肯定没有什么意义。换言之,`Instrument`的作用仅仅是表达接口,而不是表达一些具体的实现细节。所以创建一个`Instrument`对象是没有意义的,而且我们通常都应禁止用户那样做。为达到这个目的,可令`Instrument`内的所有方法都显示出错消息。但这样做会延迟信息到运行期,并要求在用户那一面进行彻底、可靠的测试。无论如何,最好的方法都是在编译期间捕捉到问题。 9 | 10 | 针对这个问题,Java专门提供了一种机制,名为“抽象方法”。它属于一种不完整的方法,只含有一个声明,没有方法主体。下面是抽象方法声明时采用的语法: 11 | 12 | ``` 13 | abstract void X(); 14 | ``` 15 | 16 | 包含了抽象方法的一个类叫作“抽象类”。如果一个类里包含了一个或多个抽象方法,类就必须指定成`abstract`(抽象)。否则,编译器会向我们报告一条出错消息。 17 | 18 | 若一个抽象类是不完整的,那么一旦有人试图生成那个类的一个对象,编译器又会采取什么行动呢?由于不能安全地为一个抽象类创建属于它的对象,所以会从编译器那里获得一条出错提示。通过这种方法,编译器可保证抽象类的“纯洁性”,我们不必担心会误用它。 19 | 20 | 如果从一个抽象类继承,而且想生成新类型的一个对象,就必须为基类中的所有抽象方法提供方法定义。如果不这样做(完全可以选择不做),则派生类也会是抽象的,而且编译器会强迫我们用`abstract`关键字标志那个类的“抽象”本质。 21 | 22 | 即使不包括任何`abstract`方法,亦可将一个类声明成“抽象类”。如果一个类没必要拥有任何抽象方法,而且我们想禁止那个类的所有实例,这种能力就会显得非常有用。 23 | 24 | `Instrument`类可很轻松地转换成一个抽象类。只有其中一部分方法会变成抽象方法,因为使一个类抽象以后,并不会强迫我们将它的所有方法都同时变成抽象。下面是它看起来的样子: 25 | 26 | ![](7-3.gif) 27 | 28 | 下面是我们修改过的“管弦”乐器例子,其中采用了抽象类以及方法: 29 | 30 | ``` 31 | //: Music4.java 32 | // Abstract classes and methods 33 | import java.util.*; 34 | 35 | abstract class Instrument4 { 36 | int i; // storage allocated for each 37 | public abstract void play(); 38 | public String what() { 39 | return "Instrument4"; 40 | } 41 | public abstract void adjust(); 42 | } 43 | 44 | class Wind4 extends Instrument4 { 45 | public void play() { 46 | System.out.println("Wind4.play()"); 47 | } 48 | public String what() { return "Wind4"; } 49 | public void adjust() {} 50 | } 51 | 52 | class Percussion4 extends Instrument4 { 53 | public void play() { 54 | System.out.println("Percussion4.play()"); 55 | } 56 | public String what() { return "Percussion4"; } 57 | public void adjust() {} 58 | } 59 | 60 | class Stringed4 extends Instrument4 { 61 | public void play() { 62 | System.out.println("Stringed4.play()"); 63 | } 64 | public String what() { return "Stringed4"; } 65 | public void adjust() {} 66 | } 67 | 68 | class Brass4 extends Wind4 { 69 | public void play() { 70 | System.out.println("Brass4.play()"); 71 | } 72 | public void adjust() { 73 | System.out.println("Brass4.adjust()"); 74 | } 75 | } 76 | 77 | class Woodwind4 extends Wind4 { 78 | public void play() { 79 | System.out.println("Woodwind4.play()"); 80 | } 81 | public String what() { return "Woodwind4"; } 82 | } 83 | 84 | public class Music4 { 85 | // Doesn't care about type, so new types 86 | // added to the system still work right: 87 | static void tune(Instrument4 i) { 88 | // ... 89 | i.play(); 90 | } 91 | static void tuneAll(Instrument4[] e) { 92 | for(int i = 0; i < e.length; i++) 93 | tune(e[i]); 94 | } 95 | public static void main(String[] args) { 96 | Instrument4[] orchestra = new Instrument4[5]; 97 | int i = 0; 98 | // Upcasting during addition to the array: 99 | orchestra[i++] = new Wind4(); 100 | orchestra[i++] = new Percussion4(); 101 | orchestra[i++] = new Stringed4(); 102 | orchestra[i++] = new Brass4(); 103 | orchestra[i++] = new Woodwind4(); 104 | tuneAll(orchestra); 105 | } 106 | } ///:~ 107 | ``` 108 | 109 | 可以看出,除基类以外,实际并没有进行什么改变。 110 | 111 | 创建抽象类和方法有时对我们非常有用,因为它们使一个类的抽象变成明显的事实,可明确告诉用户和编译器自己打算如何用它。 112 | -------------------------------------------------------------------------------- /7.8.md: -------------------------------------------------------------------------------- 1 | # 7.8 通过继承进行设计 2 | 3 | 4 | 学习了多态性的知识后,由于多态性是如此“聪明”的一种工具,所以看起来似乎所有东西都应该继承。但假如过度使用继承技术,也会使自己的设计变得不必要地复杂起来。事实上,当我们以一个现成类为基础建立一个新类时,如首先选择继承,会使情况变得异常复杂。 5 | 6 | 一个更好的思路是首先选择“组合”——如果不能十分确定自己应使用哪一个。组合不会强迫我们的程序设计进入继承的分级结构中。同时,组合显得更加灵活,因为可以动态选择一种类型(以及行为),而继承要求在编译期间准确地知道一种类型。下面这个例子对此进行了阐释: 7 | 8 | ``` 9 | //: Transmogrify.java 10 | // Dynamically changing the behavior of 11 | // an object via composition. 12 | 13 | interface Actor { 14 | void act(); 15 | } 16 | 17 | class HappyActor implements Actor { 18 | public void act() { 19 | System.out.println("HappyActor"); 20 | } 21 | } 22 | 23 | class SadActor implements Actor { 24 | public void act() { 25 | System.out.println("SadActor"); 26 | } 27 | } 28 | 29 | class Stage { 30 | Actor a = new HappyActor(); 31 | void change() { a = new SadActor(); } 32 | void go() { a.act(); } 33 | } 34 | 35 | public class Transmogrify { 36 | public static void main(String[] args) { 37 | Stage s = new Stage(); 38 | s.go(); // Prints "HappyActor" 39 | s.change(); 40 | s.go(); // Prints "SadActor" 41 | } 42 | } ///:~ 43 | ``` 44 | 45 | 在这里,一个`Stage`对象包含了指向一个`Actor`的引用,后者被初始化成一个`HappyActor`对象。这意味着`go()`会产生特定的行为。但由于引用在运行期间可以重新与一个不同的对象绑定或结合起来,所以`SadActor`对象的引用可在a中得到替换,然后由`go()`产生的行为发生改变。这样一来,我们在运行期间就获得了很大的灵活性。与此相反,我们不能在运行期间换用不同的形式来进行继承;它要求在编译期间完全决定下来。 46 | 47 | 一条常规的设计准则是:用继承表达行为间的差异,并用成员变量表达状态的变化。在上述例子中,两者都得到了应用:继承了两个不同的类,用于表达`act()`方法的差异;而`Stage`通过组合技术允许它自己的状态发生变化。在这种情况下,那种状态的改变同时也产生了行为的变化。 48 | 49 | ## 7.8.1 纯继承与扩展 50 | 51 | 学习继承时,为了创建继承分级结构,看来最明显的方法是采取一种“纯粹”的手段。也就是说,只有在基类或“接口”中已建立的方法才可在派生类中被覆盖,如下面这张图所示: 52 | 53 | ![](7-6.gif) 54 | 55 | 可将其描述成一种纯粹的“属于”关系,因为一个类的接口已规定了它到底“是什么”或者“属于什么”。通过继承,可保证所有派生类都只拥有基类的接口。如果按上述示意图操作,派生出来的类除了基类的接口之外,也不会再拥有其他什么。 56 | 57 | 可将其想象成一种“纯替换”,因为派生类对象可为基类完美地替换掉。使用它们的时候,我们根本没必要知道与子类有关的任何额外信息。如下所示: 58 | 59 | ![](7-7.gif) 60 | 61 | 也就是说,基类可接收我们发给派生类的任何消息,因为两者拥有完全一致的接口。我们要做的全部事情就是从派生向上转换,而且永远不需要回过头来检查对象的准确类型是什么。所有细节都已通过多态性获得了完美的控制。 62 | 63 | 若按这种思路考虑问题,那么一个纯粹的“属于”关系似乎是唯一明智的设计方法,其他任何设计方法都会导致混乱不清的思路,而且在定义上存在很大的困难。但这种想法又属于另一个极端。经过细致的研究,我们发现扩展接口对于一些特定问题来说是特别有效的方案。可将其称为“类似于”关系,因为扩展后的派生类“类似于”基类——它们有相同的基础接口——但它增加了一些特性,要求用额外的方法加以实现。如下所示: 64 | 65 | ![](7-8.gif) 66 | 67 | 尽管这是一种有用和明智的做法(由具体的环境决定),但它也有一个缺点:派生类中对接口扩展的那一部分不可在基类中使用。所以一旦向上转换,就不可再调用新方法: 68 | 69 | ![](7-9.gif) 70 | 71 | 若在此时不进行向上转换,则不会出现此类问题。但在许多情况下,都需要重新核实对象的准确类型,使自己能访问那个类型的扩展方法。在后面的小节里,我们具体讲述了这是如何实现的。 72 | 73 | ## 7.8.2 向下转换与运行期类型识别 74 | 75 | 由于我们在向上转换(在继承结构中向上移动)期间丢失了具体的类型信息,所以为了获取具体的类型信息——亦即在分级结构中向下移动——我们必须使用 “向下转换”技术。然而,我们知道一个向上转换肯定是安全的;基类不可能再拥有一个比派生类更大的接口。因此,我们通过基类接口发送的每一条消息都肯定能够接收到。但在进行向下转换的时候,我们(举个例子来说)并不真的知道一个几何形状实际是一个圆,它完全可能是一个三角形、方形或者其他形状。 76 | 77 | ![](7-10.gif) 78 | 79 | 为解决这个问题,必须有一种办法能够保证向下转换正确进行。只有这样,我们才不会冒然转换成一种错误的类型,然后发出一条对象不可能收到的消息。这样做是非常不安全的。 80 | 81 | 在某些语言中(如C++),为了进行保证“类型安全”的向下转换,必须采取特殊的操作。但在Java中,所有转换都会自动得到检查和核实!所以即使我们只是进行一次普通的括弧转换,进入运行期以后,仍然会毫无留情地对这个转换进行检查,保证它的确是我们希望的那种类型。如果不是,就会得到一个`ClassCastException`(类转换异常)。在运行期间对类型进行检查的行为叫作“运行期类型识别”(RTTI)。下面这个例子向大家演示了RTTI的行为: 82 | 83 | ``` 84 | //: RTTI.java 85 | // Downcasting & Run-Time Type 86 | // Identification (RTTI) 87 | import java.util.*; 88 | 89 | class Useful { 90 | public void f() {} 91 | public void g() {} 92 | } 93 | 94 | class MoreUseful extends Useful { 95 | public void f() {} 96 | public void g() {} 97 | public void u() {} 98 | public void v() {} 99 | public void w() {} 100 | } 101 | 102 | public class RTTI { 103 | public static void main(String[] args) { 104 | Useful[] x = { 105 | new Useful(), 106 | new MoreUseful() 107 | }; 108 | x[0].f(); 109 | x[1].g(); 110 | // Compile-time: method not found in Useful: 111 | //! x[1].u(); 112 | ((MoreUseful)x[1]).u(); // Downcast/RTTI 113 | ((MoreUseful)x[0]).u(); // Exception thrown 114 | } 115 | } ///:~ 116 | ``` 117 | 118 | 和在示意图中一样,`MoreUseful`(更有用的)对`Useful`(有用的)的接口进行了扩展。但由于它是继承来的,所以也能向上转换到一个`Useful`。我们可看到这会在对数组`x`(位于`main()`中)进行初始化的时候发生。由于数组中的两个对象都属于`Useful`类,所以可将`f()`和`g()`方法同时发给它们两个。而且假如试图调用`u()`(它只存在于`MoreUseful`),就会收到一条编译期出错提示。 119 | 120 | 若想访问一个`MoreUseful`对象的扩展接口,可试着进行向下转换。如果它是正确的类型,这一行动就会成功。否则,就会得到一个`ClassCastException`。我们不必为这个异常编写任何特殊的代码,因为它指出的是一个可能在程序中任何地方发生的一个编程错误。 121 | 122 | RTTI的意义远不仅仅反映在转换处理上。例如,在试图向下转换之前,可通过一种方法了解自己处理的是什么类型。整个第11章都在讲述Java运行期类型识别的方方面面。 123 | -------------------------------------------------------------------------------- /7.9.md: -------------------------------------------------------------------------------- 1 | # 7.9 总结 2 | 3 | 4 | “多态性”意味着“不同的形式”。在面向对象的程序设计中,我们有相同的外观(基类的通用接口)以及使用那个外观的不同形式:动态绑定或组织的、不同版本的方法。 5 | 6 | 通过这一章的学习,大家已知道假如不利用数据抽象以及继承技术,就不可能理解、甚至去创建多态性的一个例子。多态性是一种不可独立应用的特性(就象一个`switch`语句),只可与其他元素协同使用。我们应将其作为类总体关系的一部分来看待。人们经常混淆Java其他的、非面向对象的特性,比如方法重载等,这些特性有时也具有面向对象的某些特征。但不要被愚弄:如果以后没有绑定,就不成其为多态性。 7 | 8 | 为使用多态性乃至面向对象的技术,特别是在自己的程序中,必须将自己的编程视野扩展到不仅包括单独一个类的成员和消息,也要包括类与类之间的一致性以及它们的关系。尽管这要求学习时付出更多的精力,但却是非常值得的,因为只有这样才可真正有效地加快自己的编程速度、更好地组织代码、更容易做出包容面广的程序以及更易对自己的代码进行维护与扩展。 9 | -------------------------------------------------------------------------------- /7.md: -------------------------------------------------------------------------------- 1 | # 第7章 多态性 2 | 3 | 4 | “对于面向对象的程序设计语言,多型性是第三种最基本的特征(前两种是数据抽象和继承。” 5 | 6 | “多态性”(Polymorphism)从另一个角度将接口从具体的实现细节中分离出来,亦即实现了“是什么”与“怎样做”两个模块的分离。利用多态性的概念,代码的组织以及可读性均能获得改善。此外,还能创建“易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地“成长”。 7 | 8 | 通过合并各种特征与行为,封装技术可创建出新的数据类型。通过对具体实现细节的隐藏,可将接口与实现细节分离,使所有细节成为`private`(私有)。这种组织方式使那些有程序化编程背景人感觉颇为舒适。但多态性却涉及对“类型”的分解。通过上一章的学习,大家已知道通过继承可将一个对象当作它自己的类型或者它自己的基类型对待。这种能力是十分重要的,因为多个类型(从相同的基类型中派生出来)可被当作同一种类型对待。而且只需一段代码,即可对所有不同的类型进行同样的处理。利用具有多态性的方法调用,一种类型可将自己与另一种相似的类型区分开,只要它们都是从相同的基类型中派生出来的。这种区分是通过各种方法在行为上的差异实现的,可通过基类实现对那些方法的调用。 9 | 10 | 在这一章中,大家要由浅入深地学习有关多态性的问题(也叫作动态绑定、推迟绑定或者运行期绑定)。同时举一些简单的例子,其中所有无关的部分都已剥除,只保留与多态性有关的代码。 11 | -------------------------------------------------------------------------------- /8-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/8-1.gif -------------------------------------------------------------------------------- /8.3.md: -------------------------------------------------------------------------------- 1 | # 8.3 枚举器(迭代器) 2 | 3 | 4 | 在任何集合类中,必须通过某种方法在其中置入对象,再用另一种方法从中取得对象。毕竟,容纳各种各样的对象正是集合的首要任务。在`Vector`中,`addElement()`便是我们插入对象采用的方法,而`elementAt()`是提取对象的唯一方法。`Vector`非常灵活,我们可在任何时候选择任何东西,并可使用不同的索引选择多个元素。 5 | 6 | 若从更高的角度看这个问题,就会发现它的一个缺陷:需要事先知道集合的准确类型,否则无法使用。乍看来,这一点似乎没什么关系。但假若最开始决定使用`Vector`,后来在程序中又决定(考虑执行效率的原因)改变成一个`List`(属于Java1.2集合库的一部分),这时又该如何做呢? 7 | 8 | 可利用“迭代器”(`Iterator`)的概念达到这个目的。它可以是一个对象,作用是遍历一系列对象,并选择那个序列中的每个对象,同时不让客户程序员知道或关注那个序列的基础结构。此外,我们通常认为迭代器是一种“轻量级”对象;也就是说,创建它只需付出极少的代价。但也正是由于这个原因,我们常发现迭代器存在一些似乎很奇怪的限制。例如,有些迭代器只能朝一个方向移动。 9 | Java的`Enumeration`(枚举,注释②)便是具有这些限制的一个迭代器的例子。除下面这些外,不可再用它做其他任何事情: 10 | 11 | (1) 用一个名为`elements()`的方法要求集合为我们提供一个`Enumeration`。我们首次调用它的`nextElement()`时,这个`Enumeration`会返回序列中的第一个元素。 12 | 13 | (2) 用`nextElement()`获得下一个对象。 14 | 15 | (3) 用`hasMoreElements()`检查序列中是否还有更多的对象。 16 | 17 | ②:“迭代器”这个词在C++和OOP的其他地方是经常出现的,所以很难确定为什么Java的开发者采用了这样一个奇怪的名字。Java 1.2的集合库修正了这个问题以及其他许多问题。 18 | 19 | 只可用`Enumeration`做这些事情,不能再有更多。它属于迭代器一种简单的实现方式,但功能依然十分强大。为体会它的运作过程,让我们复习一下本章早些时候提到的`CatsAndDogs.java`程序。在原始版本中,`elementAt()`方法用于选择每一个元素,但在下述修订版中,可看到使用了一个“枚举”: 20 | 21 | ``` 22 | //: CatsAndDogs2.java 23 | // Simple collection with Enumeration 24 | import java.util.*; 25 | 26 | class Cat2 { 27 | private int catNumber; 28 | Cat2(int i) { 29 | catNumber = i; 30 | } 31 | void print() { 32 | System.out.println("Cat number " +catNumber); 33 | } 34 | } 35 | 36 | class Dog2 { 37 | private int dogNumber; 38 | Dog2(int i) { 39 | dogNumber = i; 40 | } 41 | void print() { 42 | System.out.println("Dog number " +dogNumber); 43 | } 44 | } 45 | 46 | public class CatsAndDogs2 { 47 | public static void main(String[] args) { 48 | Vector cats = new Vector(); 49 | for(int i = 0; i < 7; i++) 50 | cats.addElement(new Cat2(i)); 51 | // Not a problem to add a dog to cats: 52 | cats.addElement(new Dog2(7)); 53 | Enumeration e = cats.elements(); 54 | while(e.hasMoreElements()) 55 | ((Cat2)e.nextElement()).print(); 56 | // Dog is detected only at run-time 57 | } 58 | } ///:~ 59 | ``` 60 | 61 | 我们看到唯一的改变就是最后几行。不再是: 62 | 63 | ``` 64 | for(int i = 0; i < cats.size(); i++) 65 | ((Cat)cats.elementAt(i)).print(); 66 | ``` 67 | 68 | 而是用一个`Enumeration`遍历整个序列: 69 | 70 | ``` 71 | while(e.hasMoreElements()) 72 | ((Cat2)e.nextElement()).print(); 73 | ``` 74 | 75 | 使用`Enumeration`,我们不必关心集合中的元素数量。所有工作均由`hasMoreElements()`和`nextElement()`自动照管了。 76 | 77 | 下面再看看另一个例子,让我们创建一个常规用途的打印方法: 78 | 79 | ``` 80 | //: HamsterMaze.java 81 | // Using an Enumeration 82 | import java.util.*; 83 | 84 | class Hamster { 85 | private int hamsterNumber; 86 | Hamster(int i) { 87 | hamsterNumber = i; 88 | } 89 | public String toString() { 90 | return "This is Hamster #" + hamsterNumber; 91 | } 92 | } 93 | 94 | class Printer { 95 | static void printAll(Enumeration e) { 96 | while(e.hasMoreElements()) 97 | System.out.println( 98 | e.nextElement().toString()); 99 | } 100 | } 101 | 102 | public class HamsterMaze { 103 | public static void main(String[] args) { 104 | Vector v = new Vector(); 105 | for(int i = 0; i < 3; i++) 106 | v.addElement(new Hamster(i)); 107 | Printer.printAll(v.elements()); 108 | } 109 | } ///:~ 110 | ``` 111 | 112 | 仔细研究一下打印方法: 113 | 114 | ``` 115 | static void printAll(Enumeration e) { 116 | while(e.hasMoreElements()) 117 | System.out.println( 118 | e.nextElement().toString()); 119 | } 120 | ``` 121 | 122 | 注意其中没有与序列类型有关的信息。我们拥有的全部东西便是`Enumeration`。为了解有关序列的情况,一个`Enumeration`便足够了:可取得下一个对象,亦可知道是否已抵达了末尾。取得一系列对象,然后在其中遍历,从而执行一个特定的操作——这是一个颇有价值的编程概念,本书许多地方都会沿用这一思路。 123 | 124 | 这个看似特殊的例子甚至可以更为通用,因为它使用了常规的`toString()`方法(之所以称为常规,是由于它属于`Object`类的一部分)。下面是调用打印的另一个方法(尽管在效率上可能会差一些): 125 | 126 | ``` 127 | System.out.println("" + e.nextElement()); 128 | ``` 129 | 130 | 它采用了封装到Java内部的“自动转换成字符串”技术。一旦编译器碰到一个字符串,后面跟随一个`+`,就会希望后面又跟随一个字符串,并自动调用`toString()`。在Java 1.1中,第一个字符串是不必要的;所有对象都会转换成字符串。亦可对此执行一次转换,获得与调用`toString()`同样的效果: 131 | 132 | ``` 133 | System.out.println((String)e.nextElement()) 134 | ``` 135 | 136 | 但我们想做的事情通常并不仅仅是调用`Object`方法,所以会再度面临类型转换的问题。对于自己感兴趣的类型,必须假定自己已获得了一个`Enumeration`,然后将结果对象转换成为那种类型(若操作错误,会得到运行期异常)。 137 | -------------------------------------------------------------------------------- /8.6.md: -------------------------------------------------------------------------------- 1 | # 8.6 通用集合库 2 | 3 | 4 | 通过本章的学习,大家已知道标准Java库提供了一些特别有用的集合,但距完整意义的集合尚远。除此之外,象排序这样的算法根本没有提供支持。C++出色的一个地方就是它的库,特别是“标准模板库”(STL)提供了一套相当完整的集合,以及许多象排序和检索这样的算法,可以非常方便地对那些集合进行操作。有感这一现状,并以这个模型为基础,ObjectSpace公司设计了Java版本的“通用集合库”(从前叫作“Java通用库”,即JGL;但JGL这个缩写形式侵犯了Sun公司的版权——尽管本书仍然沿用这个简称)。这个库尽可能遵照STL的设计(照顾到两种语言间的差异)。JGL实现了许多功能,可满足对一个集合库的大多数常规需求,它与C++的模板机制非常相似。JGL包括相互链接起来的列表、设置、队列、映射、栈、序列以及迭代器,它们的功能比`Enumeration`(枚举)强多了。同时提供了一套完整的算法,如检索和排序等。在某些方面,ObjectSpace的设计也显得比Sun的库设计模式“智能”一些。举个例子来说,JGL集合中的方法不会进入`final`状态,所以很容易继承和改写那些方法。 5 | 6 | JGL已包括到一些厂商发行的Java套件中,而且ObjectSpace公司自己也允许所有用户免费使用JGL,包括商业性的使用。详细情况和软件下载可访问 `http://www.ObjectSpace.com` 。与JGL配套提供的联机文档做得非常好,可作为自己的一个绝佳起点使用。 7 | -------------------------------------------------------------------------------- /8.8.md: -------------------------------------------------------------------------------- 1 | # 8.8 总结 2 | 3 | 下面复习一下由标准Java(1.0和1.1)库提供的集合(`BitSet`未包括在这里,因为它更象一种负有特殊使命的类): 4 | 5 | (1) 数组包含了对象的数字化索引。它容纳的是一种已知类型的对象,所以在查找一个对象时,不必对结果进行转换处理。数组可以是多维的,而且能够容纳基本数据类型。但是,一旦把它创建好以后,大小便不能变化了。 6 | 7 | (2) `Vector`(向量)也包含了对象的数字索引——可将数组和`Vector`想象成随机访问集合。当我们加入更多的元素时,`Vector`能够自动改变自身的大小。但`Vector`只能容纳对象的引用,所以它不可包含基本数据类型;而且将一个对象引用从集合中取出来的时候,必须对结果进行转换处理。 8 | 9 | (3) `Hashtable`(散列表)属于`Dictionary`(字典)的一种类型,是一种将对象(而不是数字)同其他对象关联到一起的方式。散列表也支持对对象的随机访问,事实上,它的整个设计模式都在突出访问的“高速度”。 10 | 11 | (4) `Stack`(栈)是一种“后入先出”(LIFO)的队列。 12 | 13 | 若你曾经熟悉数据结构,可能会疑惑为何没看到一套更大的集合。从功能的角度出发,你真的需要一套更大的集合吗?对于`Hashtable`,可将任何东西置入其中,并以非常快的速度检索;对于`Enumeration`(枚举),可遍历一个序列,并对其中的每个元素都采取一个特定的操作。那是一种功能足够强劲的工具。 14 | 15 | 但`Hashtable`没有“顺序”的概念。`Vector`和数组为我们提供了一种线性顺序,但若要把一个元素插入它们任何一个的中部,一般都要付出“惨重”的代价。除此以外,队列、拆散队列、优先级队列以及树都涉及到元素的“排序”——并非仅仅将它们置入,以便以后能按线性顺序查找或移动它们。这些数据结构也非常有用,这也正是标准C++中包含了它们的原因。考虑到这个原因,只应将标准Java库的集合看作自己的一个起点。而且倘若必须使用Java 1.0或1.1,则可在需要超越它们的时候使用JGL。 16 | 17 | 如果能使用Java 1.2,那么只使用新集合即可,它一般能满足我们的所有需要。注意本书在Java 1.1身上花了大量篇幅,所以书中用到的大量集合都是只能在Java1.1中用到的那些:`Vector`和`Hashtable`。就目前来看,这是一个不得以而为之的做法。但是,这样处理亦可提供与老Java代码更出色的向后兼容能力。若要用Java1.2写新代码,新的集合往往能更好地为你服务。 18 | -------------------------------------------------------------------------------- /8.9.md: -------------------------------------------------------------------------------- 1 | # 8.9 练习 2 | 3 | (1) 新建一个名为`Gerbil`的类,在构造器中初始化一个`int gerbilNumber`(类似本章的`Mouse`例子)。为其写一个名为`hop()`的方法,用它打印出符合`hop()`条件的`Gerbil`的编号。建一个`Vector`,并为`Vector`添加一系列`Gerbil`对象。现在,用`elementAt()`方法在`Vector`中遍历,并为每个`Gerbil`都调用`hop()`。 4 | 5 | (2) 修改练习1,用`Enumeration`在调用`hop()`的同时遍历`Vector`。 6 | 7 | (3) 在`AssocArray.java`中,修改这个例子,令其使用一个`Hashtable`,而不是`AssocArray`。 8 | 9 | (4) 获取练习1用到的`Gerbil`类,改为把它置入一个`Hashtable`,然后将`Gerbil`的名称作为一个`String`(键)与置入表格的每个`Gerbil`(值)都关联起来。获得用于`keys()`的一个`Enumeration`,并用它在`Hashtable`里遍历,查找每个键的`Gerbil`,打印出键,然后将`gerbil`告诉给`hop()`。 10 | 11 | (5) 修改第7章的练习1,用一个`Vector`容纳`Rodent`(啮齿动物),并用`Enumeration`在`Rodent`序列中遍历。记住`Vector`只能容纳对象,所以在访问单独的`Rodent`时必须采用一个转换(如RTTI)。 12 | 13 | (6) 转到第7章的中间位置,找到那个`GreenhouseControls.java`(温室控制)例子,该例应该由三个文件构成。在`Controller.java`中,类`EventSet`仅是一个集合。修改它的代码,用一个`Stack`代替`EventSet`。当然,这时可能并不仅仅用`Stack`取代`EventSet`这样简单;也需要用一个`Enumeration`遍历事件集。可考虑在某些时候将集合当作`Stack`对待,另一些时候则当作`Vector`对待——这样或许能使事情变得更加简单。 14 | 15 | (7) (有一定挑战性)在与所有Java发行包配套提供的Java源码库中找出用于`Vector`的源码。复制这些代码,制作名为 16 | `intVector`的一个特殊版本,只在其中包含`int`数据。思考是否能为所有基本数据类型都制作`Vector`的一个特殊版本。接下来,考虑假如制作一个链接列表类,令其能随同所有基本数据类型使用,那么会发生什么情况。若在Java中提供了参数化类型,利用它们便可自动完成这一工作(还有其他许多好处)。 17 | -------------------------------------------------------------------------------- /8.md: -------------------------------------------------------------------------------- 1 | # 第8章 对象的容纳 2 | 3 | 4 | “如果一个程序只含有数量固定的对象,而且已知它们的存在时间,那么这个程序可以说是相当简单的。” 5 | 6 | 通常,我们的程序需要根据程序运行时才知道的一些标准创建新对象。若非程序正式运行,否则我们根本不知道自己到底需要多少数量的对象,甚至不知道它们的准确类型。为了满足常规编程的需要,我们要求能在任何时候、任何地点创建任意数量的对象。所以不可依赖一个已命名的引用来容纳自己的每一个对象,就象下面这样: 7 | 8 | ``` 9 | MyObject myHandle; 10 | ``` 11 | 12 | 因为根本不知道自己实际需要多少这样的东西。 13 | 14 | 为解决这个非常关键的问题,Java提供了容纳对象(或者对象的引用)的多种方式。其中内建的类型是数组,我们之前已讨论过它,本章准备加深大家对它的认识。此外,Java的工具(实用程序)库提供了一些“集合类”(亦称作“容器类”,但该术语已由AWT使用,所以这里仍采用“集合”这一称呼)。利用这些集合类,我们可以容纳乃至操纵自己的对象。本章的剩余部分会就此进行详细讨论。 15 | -------------------------------------------------------------------------------- /9.1.md: -------------------------------------------------------------------------------- 1 | # 9.1 基本异常 2 | 3 | “异常条件”表示在出现什么问题的时候应中止方法或作用域的继续。为了将异常条件与普通问题区分开,异常条件是非常重要的一个因素。在普通问题的情况下,我们在当地已拥有足够的信息,可在某种程度上解决碰到的问题。而在异常条件的情况下,却无法继续下去,因为当地没有提供解决问题所需的足够多的信息。此时,我们能做的唯一事情就是跳出当地环境,将那个问题委托给一个更高级的负责人。这便是出现异常时出现的情况。 4 | 5 | 一个简单的例子是“除法”。如可能被零除,就有必要进行检查,确保程序不会冒进,并在那种情况下执行除法。但具体通过什么知道分母是零呢?在那个特定的方法里,在我们试图解决的那个问题的环境中,我们或许知道该如何对待一个零分母。但假如它是一个没有预料到的值,就不能对其进行处理,所以必须产生一个异常,而非不顾一切地继续执行下去。 6 | 7 | 产生一个异常时,会发生几件事情。首先,按照与创建Java对象一样的方法创建异常对象:在内存“堆”里,使用`new`来创建。随后,停止当前执行路径(记住不可沿这条路径继续下去),然后从当前的环境中释放出异常对象的引用。此时,异常控制机制会接管一切,并开始查找一个恰当的地方,用于继续程序的执行。这个恰当的地方便是“异常控制器”,它的职责是从问题中恢复,使程序要么尝试另一条执行路径,要么简单地继续。 8 | 9 | 作为产生异常的一个简单示例,大家可思考一个名为`t`的对象引用。有些时候,程序可能传递一个尚未初始化的引用。所以在用那个对象引用调用一个方法之前,最好进行一番检查。可将与错误有关的信息发送到一个更大的场景中,方法是创建一个特殊的对象,用它代表我们的信息,并将其“抛”(Throw)出我们当前的场景之外。这就叫作“产生一个异常”或者“抛出一个异常”。下面是它的大概形式: 10 | 11 | ``` 12 | if(t == null) 13 | throw new NullPointerException(); 14 | ``` 15 | 16 | 这样便“抛”出了一个异常。在当前场景中,它使我们能放弃进一步解决该问题的企图。该问题会被转移到其他更恰当的地方解决。准确地说,那个地方不久就会显露出来。 17 | 18 | ## 9.1.1 异常参数 19 | 20 | 和Java的其他任何对象一样,需要用`new`在内存堆里创建异常,并需调用一个构造器。在所有标准异常中,存在着两个构造器:第一个是默认构造器,第二个则需使用一个字符串参数,使我们能在异常里置入相关信息: 21 | 22 | ``` 23 | if(t == null) 24 | throw new NullPointerException("t = null"); 25 | ``` 26 | 27 | 稍后,字符串可用各种方法提取出来,就象稍后会展示的那样。 28 | 29 | 在这儿,关键字`throw`会象变戏法一样做出一系列不可思议的事情。它首先执行`new`表达式,创建一个不在程序常规执行范围之内的对象。而且理所当然,会为那个对象调用构造器。随后,对象实际会从方法中返回——尽管对象的类型通常并不是方法设计为返回的类型。为深入理解异常控制,可将其想象成另一种返回机制——但是不要在这个问题上深究,否则会遇到麻烦。通过“抛”出一个异常,亦可从原来的作用域中退出。但是会先返回一个值,再退出方法或作用域。 30 | 31 | 但是,与普通方法返回的相似性到此便全部结束了,因为我们返回的地方与从普通方法调用中返回的地方是迥然有异的(我们结束于一个恰当的异常控制器,它距离异常“抛”出的地方可能相当遥远——在调用栈中要低上许多级)。 32 | 33 | 此外,我们可根据需要抛出任何类型的“可抛”对象。典型情况下,我们要为每种不同类型的错误“抛”出一类不同的异常。我们的思路是在异常对象以及挑选的异常对象类型中保存信息,所以在更大场景中的某个人可知道如何对待我们的异常(通常,唯一的信息是异常对象的类型,而异常对象中保存的没什么意义)。 34 | -------------------------------------------------------------------------------- /9.10.md: -------------------------------------------------------------------------------- 1 | # 9.10 练习 2 | 3 | (1) 用`main()`创建一个类,令其抛出`try`块内的`Exception`类的一个对象。为`Exception`的构造器赋予一个字符串参数。在`catch`从句内捕获异常,并打印出字符串参数。添加一个`finally`从句,并打印一条消息,证明自己真正到达那里。 4 | 5 | (2) 用`extends`关键字创建自己的异常类。为这个类写一个构造器,令其采用`String`参数,并随同`String`引用把它保存到对象内。写一个方法,令其打印出保存下来的`String`。创建一个`try-catch`从句,练习实际操作新异常。 6 | 7 | (3) 写一个类,并令一个方法抛出在练习2中创建的类型的一个异常。试着在没有异常规范的前提下编译它,观察编译器会报告什么。接着添加适当的异常规范。在一个`try-catch`从句中尝试自己的类以及它的异常。 8 | 9 | (4) 在第5章,找到调用了`Assert.java`的两个程序,并修改它们,令其抛出自己的异常类型,而不是打印到`System.err`。该异常应是扩展了`RuntimeException`的一个内部类。 10 | -------------------------------------------------------------------------------- /9.3.md: -------------------------------------------------------------------------------- 1 | # 9.3 标准Java异常 2 | 3 | 4 | Java包含了一个名为`Throwable`的类,它对可以作为异常“抛”出的所有东西进行了描述。`Throwable`对象有两种常规类型(亦即“从`Throwable`继承”)。其中,`Error`代表编译期和系统错误,我们一般不必特意捕获它们(除在特殊情况以外)。`Exception`是可以从任何标准Java库的类方法中“抛”出的基本类型。此外,它们亦可从我们自己的方法以及运行期偶发事件中“抛”出。 5 | 6 | 为获得异常的一个综合概念,最好的方法是阅读由`http://java.sun.com`提供的联机Java文档(当然,首先下载它们更好)。为了对各种异常有一个大概的印象,这个工作是相当有价值的。但大家不久就会发现,除名字外,一个异常和下一个异常之间并不存在任何特殊的地方。此外,Java提供的异常数量正在日益增多;从本质上说,把它们印到一本书里是没有意义的。大家从其他地方获得的任何新库可能也提供了它们自己的异常。我们最需要掌握的是基本概念,以及用这些异常能够做什么。 7 | 8 | ``` 9 | java.lang.Exception 10 | ``` 11 | 12 | 这是程序能捕获的基本异常。其他异常都是从它派生出去的。这里要注意的是异常的名字代表发生的问题,而且异常名通常都是精心挑选的,可以很清楚地说明到底发生了什么事情。异常并不全是在`java.lang`中定义的;有些是为了提供对其他库的支持,如`util`,`net`以及`io`等——我们可以从它们的完整类名中看出这一点,或者观察它们从什么继承。例如,所有IO异常都是从`java.io.IOException`继承的。 13 | 14 | ## 9.3.1 `RuntimeException`的特殊情况 15 | 16 | 本章的第一个例子是: 17 | 18 | ``` 19 | if(t == null) 20 | throw new NullPointerException(); 21 | ``` 22 | 23 | 看起来似乎在传递进入一个方法的每个引用中都必须检查`null`(因为不知道调用者是否已传递了一个有效的引用),这无疑是相当可怕的。但幸运的是,我们根本不必这样做——它属于Java进行的标准运行期检查的一部分。若对一个空引用发出了调用,Java会自动产生一个`NullPointerException`异常。所以上述代码在任何情况下都是多余的。 24 | 25 | 这个类别里含有一系列异常类型。它们全部由Java自动生成,毋需我们亲自动手把它们包含到自己的异常规范里。最方便的是,通过将它们置入单独一个名为`RuntimeException`的基类下面,它们全部组合到一起。这是一个很好的继承例子:它建立了一系列具有某种共通性的类型,都具有某些共通的特征与行为。此外,我们没必要专门写一个异常规范,指出一个方法可能会“抛”出一个`RuntimeException`,因为已经假定可能出现那种情况。由于它们用于指出编程中的错误,所以几乎永远不必专门捕获一个“运行期异常”——`RuntimeException`——它在默认情况下会自动得到处理。若必须检查`RuntimeException`,我们的代码就会变得相当繁复。在我们自己的包里,可选择“抛”出一部分`RuntimeException`。 26 | 27 | 如果不捕获这些异常,又会出现什么情况呢?由于编译器并不强制异常规范捕获它们,所以假如不捕获的话,一个`RuntimeException`可能过滤掉我们到达`main()`方法的所有途径。为体会此时发生的事情,请试试下面这个例子: 28 | 29 | ``` 30 | //: NeverCaught.java 31 | // Ignoring RuntimeExceptions 32 | 33 | public class NeverCaught { 34 | static void f() { 35 | throw new RuntimeException("From f()"); 36 | } 37 | static void g() { 38 | f(); 39 | } 40 | public static void main(String[] args) { 41 | g(); 42 | } 43 | } ///:~ 44 | ``` 45 | 46 | 大家已经看到,一个`RuntimeException`(或者从它继承的任何东西)属于一种特殊情况,因为编译器不要求为这些类型指定异常规范。 47 | 48 | 输出如下: 49 | 50 | ``` 51 | java.lang.RuntimeException: From f() 52 | at NeverCaught.f(NeverCaught.java:9) 53 | at NeverCaught.g(NeverCaught.java:12) 54 | at NeverCaught.main(NeverCaught.java:15) 55 | ``` 56 | 57 | 所以答案就是:假若一个`RuntimeException`获得到达`main()`的所有途径,同时不被捕获,那么当程序退出时,会为那个异常调用`printStackTrace()`。 58 | 59 | 注意也许能在自己的代码中仅忽略`RuntimeException`,因为编译器已正确实行了其他所有控制。因为`RuntimeException`在此时代表一个编程错误: 60 | 61 | (1) 一个我们不能捕获的错误(例如,由客户程序员接收传递给自己方法的一个空引用)。 62 | 63 | (2) 作为一名程序员,一个应在自己的代码中检查的错误(如`ArrayIndexOutOfBoundException`,此时应注意数组的大小)。 64 | 可以看出,最好的做法是在这种情况下异常,因为它们有助于程序的调试。 65 | 66 | 另外一个有趣的地方是,我们不可将Java异常划分为单一用途的工具。的确,它们设计用于控制那些讨厌的运行期错误——由代码控制范围之外的其他力量产生。但是,它也特别有助于调试某些特殊类型的编程错误,那些是编译器侦测不到的。 67 | -------------------------------------------------------------------------------- /9.4.md: -------------------------------------------------------------------------------- 1 | # 9.4 创建自己的异常 2 | 3 | 并不一定非要使用Java异常。这一点必须掌握,因为经常都需要创建自己的异常,以便指出自己的库可能生成的一个特殊错误——但创建Java分级结构的时候,这个错误是无法预知的。 4 | 5 | 为创建自己的异常类,必须从一个现有的异常类型继承——最好在含义上与新异常近似。继承一个异常相当简单: 6 | 7 | ``` 8 | //: Inheriting.java 9 | // Inheriting your own exceptions 10 | 11 | class MyException extends Exception { 12 | public MyException() {} 13 | public MyException(String msg) { 14 | super(msg); 15 | } 16 | } 17 | 18 | public class Inheriting { 19 | public static void f() throws MyException { 20 | System.out.println( 21 | "Throwing MyException from f()"); 22 | throw new MyException(); 23 | } 24 | public static void g() throws MyException { 25 | System.out.println( 26 | "Throwing MyException from g()"); 27 | throw new MyException("Originated in g()"); 28 | } 29 | public static void main(String[] args) { 30 | try { 31 | f(); 32 | } catch(MyException e) { 33 | e.printStackTrace(); 34 | } 35 | try { 36 | g(); 37 | } catch(MyException e) { 38 | e.printStackTrace(); 39 | } 40 | } 41 | } ///:~ 42 | ``` 43 | 44 | 继承在创建新类时发生: 45 | 46 | ``` 47 | class MyException extends Exception { 48 | public MyException() {} 49 | public MyException(String msg) { 50 | super(msg); 51 | } 52 | } 53 | ``` 54 | 55 | 这里的关键是`extends Exception`,它的意思是:除包括一个`Exception`的全部含义以外,还有更多的含义。增加的代码数量非常少——实际只添加了两个构造器,对`MyException`的创建方式进行了定义。请记住,假如我们不明确调用一个基类构造器,编译器会自动调用基类默认构造器。在第二个构造器中,通过使用`super`关键字,明确调用了带有一个`String`参数的基类构造器。 56 | 57 | 该程序输出结果如下: 58 | 59 | ``` 60 | Throwing MyException from f() 61 | MyException 62 | at Inheriting.f(Inheriting.java:16) 63 | at Inheriting.main(Inheriting.java:24) 64 | Throwing MyException from g() 65 | MyException: Originated in g() 66 | at Inheriting.g(Inheriting.java:20) 67 | at Inheriting.main(Inheriting.java:29) 68 | ``` 69 | 70 | 可以看到,在从`f()`“抛”出的`MyException`异常中,缺乏详细的消息。 71 | 72 | 创建自己的异常时,还可以采取更多的操作。我们可添加额外的构造器及成员: 73 | 74 | ``` 75 | //: Inheriting2.java 76 | // Inheriting your own exceptions 77 | 78 | class MyException2 extends Exception { 79 | public MyException2() {} 80 | public MyException2(String msg) { 81 | super(msg); 82 | } 83 | public MyException2(String msg, int x) { 84 | super(msg); 85 | i = x; 86 | } 87 | public int val() { return i; } 88 | private int i; 89 | } 90 | 91 | public class Inheriting2 { 92 | public static void f() throws MyException2 { 93 | System.out.println( 94 | "Throwing MyException2 from f()"); 95 | throw new MyException2(); 96 | } 97 | public static void g() throws MyException2 { 98 | System.out.println( 99 | "Throwing MyException2 from g()"); 100 | throw new MyException2("Originated in g()"); 101 | } 102 | public static void h() throws MyException2 { 103 | System.out.println( 104 | "Throwing MyException2 from h()"); 105 | throw new MyException2( 106 | "Originated in h()", 47); 107 | } 108 | public static void main(String[] args) { 109 | try { 110 | f(); 111 | } catch(MyException2 e) { 112 | e.printStackTrace(); 113 | } 114 | try { 115 | g(); 116 | } catch(MyException2 e) { 117 | e.printStackTrace(); 118 | } 119 | try { 120 | h(); 121 | } catch(MyException2 e) { 122 | e.printStackTrace(); 123 | System.out.println("e.val() = " + e.val()); 124 | } 125 | } 126 | } ///:~ 127 | ``` 128 | 129 | 此时添加了一个数据成员`i`;同时添加了一个特殊的方法,用它读取那个值;也添加了一个额外的构造器,用它设置那个值。输出结果如下: 130 | 131 | ``` 132 | Throwing MyException2 from f() 133 | MyException2 134 | at Inheriting2.f(Inheriting2.java:22) 135 | at Inheriting2.main(Inheriting2.java:34) 136 | Throwing MyException2 from g() 137 | MyException2: Originated in g() 138 | at Inheriting2.g(Inheriting2.java:26) 139 | at Inheriting2.main(Inheriting2.java:39) 140 | Throwing MyException2 from h() 141 | MyException2: Originated in h() 142 | at Inheriting2.h(Inheriting2.java:30) 143 | at Inheriting2.main(Inheriting2.java:44) 144 | e.val() = 47 145 | ``` 146 | 147 | 由于异常不过是另一种形式的对象,所以可以继续这个进程,进一步增强异常类的能力。但要注意,对使用自己这个包的客户程序员来说,他们可能错过所有这些增强。因为他们可能只是简单地寻找准备生成的异常,除此以外不做任何事情——这是大多数Java库异常的标准用法。若出现这种情况,有可能创建一个新异常类型,其中几乎不包含任何代码: 148 | 149 | ``` 150 | //: SimpleException.java 151 | class SimpleException extends Exception { 152 | } ///:~ 153 | ``` 154 | 155 | 它要依赖编译器来创建默认构造器(会自动调用基类的默认构造器)。当然,在这种情况下,我们不会得到一个`SimpleException(String)`构造器,但它实际上也不会经常用到。 156 | -------------------------------------------------------------------------------- /9.5.md: -------------------------------------------------------------------------------- 1 | # 9.5 异常的限制 2 | 3 | 4 | 覆盖一个方法时,只能产生已在方法的基类版本中定义的异常。这是一个重要的限制,因为它意味着与基类协同工作的代码也会自动应用于从基类派生的任何对象(当然,这属于基本的OOP概念),其中包括异常。 5 | 6 | 下面这个例子演示了强加在异常身上的限制类型(在编译期): 7 | 8 | ``` 9 | //: StormyInning.java 10 | // Overridden methods may throw only the 11 | // exceptions specified in their base-class 12 | // versions, or exceptions derived from the 13 | // base-class exceptions. 14 | 15 | class BaseballException extends Exception {} 16 | class Foul extends BaseballException {} 17 | class Strike extends BaseballException {} 18 | 19 | abstract class Inning { 20 | Inning() throws BaseballException {} 21 | void event () throws BaseballException { 22 | // Doesn't actually have to throw anything 23 | } 24 | abstract void atBat() throws Strike, Foul; 25 | void walk() {} // Throws nothing 26 | } 27 | 28 | class StormException extends Exception {} 29 | class RainedOut extends StormException {} 30 | class PopFoul extends Foul {} 31 | 32 | interface Storm { 33 | void event() throws RainedOut; 34 | void rainHard() throws RainedOut; 35 | } 36 | 37 | public class StormyInning extends Inning 38 | implements Storm { 39 | // OK to add new exceptions for constructors, 40 | // but you must deal with the base constructor 41 | // exceptions: 42 | StormyInning() throws RainedOut, 43 | BaseballException {} 44 | StormyInning(String s) throws Foul, 45 | BaseballException {} 46 | // Regular methods must conform to base class: 47 | //! void walk() throws PopFoul {} //Compile error 48 | // Interface CANNOT add exceptions to existing 49 | // methods from the base class: 50 | //! public void event() throws RainedOut {} 51 | // If the method doesn't already exist in the 52 | // base class, the exception is OK: 53 | public void rainHard() throws RainedOut {} 54 | // You can choose to not throw any exceptions, 55 | // even if base version does: 56 | public void event() {} 57 | // Overridden methods can throw 58 | // inherited exceptions: 59 | void atBat() throws PopFoul {} 60 | public static void main(String[] args) { 61 | try { 62 | StormyInning si = new StormyInning(); 63 | si.atBat(); 64 | } catch(PopFoul e) { 65 | } catch(RainedOut e) { 66 | } catch(BaseballException e) {} 67 | // Strike not thrown in derived version. 68 | try { 69 | // What happens if you upcast? 70 | Inning i = new StormyInning(); 71 | i.atBat(); 72 | // You must catch the exceptions from the 73 | // base-class version of the method: 74 | } catch(Strike e) { 75 | } catch(Foul e) { 76 | } catch(RainedOut e) { 77 | } catch(BaseballException e) {} 78 | } 79 | } ///:~ 80 | ``` 81 | 82 | 在`Inning`中,可以看到无论构造器还是`event()`方法都指出自己会“抛”出一个异常,但它们实际上没有那样做。这是合法的,因为它允许我们强迫用户捕获可能在覆盖过的event()版本里添加的任何异常。同样的道理也适用于`abstract`方法,就象在`atBat()`里展示的那样。 83 | 84 | `interface Storm`非常有趣,因为它包含了在`Incoming`中定义的一个方法——`event()`,以及不是在其中定义的一个方法。这两个方法都会“抛”出一个新的异常类型:`RainedOut`。当执行到`StormyInning extends`和`implements Storm`的时候,可以看到`Storm`中的`event()`方法不能改变`Inning`中的`event()`的异常接口。同样地,这种设计是十分合理的;否则的话,当我们操作基类时,便根本无法知道自己捕获的是否正确的东西。当然,假如`interface`中定义的一个方法不在基类里,比如`rainHard()`,它产生异常时就没什么问题。 85 | 86 | 对异常的限制并不适用于构造器。在`StormyInning`中,我们可看到一个构造器能够“抛”出它希望的任何东西,无论基类构造器“抛”出什么。然而,由于必须坚持按某种方式调用基类构造器(在这里,会自动调用默认构造器),所以派生类构造器必须在自己的异常规范中声明所有基类构造器异常。 87 | 88 | `StormyInning.walk()`不会编译的原因是它“抛”出了一个异常,而`Inning.walk()`却不会“抛”出。若允许这种情况发生,就可让自己的代码调用`Inning.walk()`,而且它不必控制任何异常。但在以后替换从`Inning`派生的一个类的对象时,异常就会“抛”出,造成代码执行的中断。通过强迫派生类方法遵守基类方法的异常规范,对象的替换可保持连贯性。 89 | 90 | 覆盖过的`event()`方法向我们显示出一个方法的派生类版本可以不产生任何异常——即便基类版本要产生异常。同样地,这样做是必要的,因为它不会中断那些已假定基类版本会产生异常的代码。差不多的道理亦适用于`atBat()`,它会“抛”出`PopFoul`——从`Foul`派生出来的一个异常,而`Foul`异常是由`atBat()`的基类版本产生的。这样一来,假如有人在自己的代码里操作`Inning`,同时调用了`atBat()`,就必须捕获`Foul`异常。由于`PopFoul`是从`Foul`派生的,所以异常控制器(模块)也会捕获`PopFoul`。 91 | 92 | 最后一个有趣的地方在`main()`内部。在这个地方,假如我们明确操作一个`StormyInning`对象,编译器就会强迫我们只捕获特定于那个类的异常。但假如我们向上转换到基类型,编译器就会强迫我们捕获针对基类的异常。通过所有这些限制,异常控制代码的“健壮”程度获得了大幅度改善(注释③)。 93 | 94 | ③:ANSI/ISO C++施加了类似的限制,要求派生方法异常与基类方法抛出的异常相同,或者从后者派生。在这种情况下,C++实际上能够在编译期间检查异常规范。 95 | 96 | 我们必须认识到这一点:尽管异常规范是由编译器在继承期间强行遵守的,但异常规范并不属于方法类型的一部分,后者仅包括了方法名以及参数类型。因此,我们不可在异常规范的基础上覆盖方法。除此以外,尽管异常规范存在于一个方法的基类版本中,但并不表示它必须在方法的派生类版本中存在。这与方法的“继承”颇有不同(进行继承时,基类中的方法也必须在派生类中存在)。换言之,用于一个特定方法的“异常规范接口”可能在继承和覆盖时变得更“窄”,但它不会变得更“宽”——这与继承时的类接口规则是正好相反的。 97 | -------------------------------------------------------------------------------- /9.7.md: -------------------------------------------------------------------------------- 1 | # 9.7 构造器 2 | 3 | 4 | 为异常编写代码时,我们经常要解决的一个问题是:“一旦产生异常,会正确地进行清除吗?”大多数时候都会非常安全,但在构造器中却是一个大问题。构造器将对象置于一个安全的起始状态,但它可能执行一些操作——如打开一个文件。除非用户完成对象的使用,并调用一个特殊的清除方法,否则那些操作不会得到正确的清除。若从一个构造器内部“抛”出一个异常,这些清除行为也可能不会正确地发生。所有这些都意味着在编写构造器时,我们必须特别加以留意。 5 | 6 | 由于前面刚学了`finally`,所以大家可能认为它是一种合适的方案。但事情并没有这么简单,因为`finally`每次都会执行清除代码——即使我们在清除方法运行之前不想执行清除代码。因此,假如真的用`finally`进行清除,必须在构造器正常结束时设置某种形式的标志。而且只要设置了标志,就不要执行`finally`块内的任何东西。由于这种做法并不完美(需要将一个地方的代码同另一个地方的结合起来),所以除非特别需要,否则一般不要尝试在`finally`中进行这种形式的清除。 7 | 8 | 在下面这个例子里,我们创建了一个名为`InputFile`的类。它的作用是打开一个文件,然后每次读取它的一行内容(转换为一个字符串)。它利用了由Java标准IO库提供的`FileReader`以及`BufferedReader`类(将于第10章讨论)。这两个类都非常简单,大家现在可以毫无困难地掌握它们的基本用法: 9 | 10 | ``` 11 | //: Cleanup.java 12 | // Paying attention to exceptions 13 | // in constructors 14 | import java.io.*; 15 | 16 | class InputFile { 17 | private BufferedReader in; 18 | InputFile(String fname) throws Exception { 19 | try { 20 | in = 21 | new BufferedReader( 22 | new FileReader(fname)); 23 | // Other code that might throw exceptions 24 | } catch(FileNotFoundException e) { 25 | System.out.println( 26 | "Could not open " + fname); 27 | // Wasn't open, so don't close it 28 | throw e; 29 | } catch(Exception e) { 30 | // All other exceptions must close it 31 | try { 32 | in.close(); 33 | } catch(IOException e2) { 34 | System.out.println( 35 | "in.close() unsuccessful"); 36 | } 37 | throw e; 38 | } finally { 39 | // Don't close it here!!! 40 | } 41 | } 42 | String getLine() { 43 | String s; 44 | try { 45 | s = in.readLine(); 46 | } catch(IOException e) { 47 | System.out.println( 48 | "readLine() unsuccessful"); 49 | s = "failed"; 50 | } 51 | return s; 52 | } 53 | void cleanup() { 54 | try { 55 | in.close(); 56 | } catch(IOException e2) { 57 | System.out.println( 58 | "in.close() unsuccessful"); 59 | } 60 | } 61 | } 62 | 63 | public class Cleanup { 64 | public static void main(String[] args) { 65 | try { 66 | InputFile in = 67 | new InputFile("Cleanup.java"); 68 | String s; 69 | int i = 1; 70 | while((s = in.getLine()) != null) 71 | System.out.println(""+ i++ + ": " + s); 72 | in.cleanup(); 73 | } catch(Exception e) { 74 | System.out.println( 75 | "Caught in main, e.printStackTrace()"); 76 | e.printStackTrace(); 77 | } 78 | } 79 | } ///:~ 80 | ``` 81 | 82 | 该例使用了Java 1.1 IO类。 83 | 84 | 用于`InputFile`的构造器采用了一个`String`(字符串)参数,它代表我们想打开的那个文件的名字。在一个`try`块内部,它用该文件名创建了一个`FileReader`。对`FileReader`来说,除非转移并用它创建一个能够实际与之“交谈”的`BufferedReader`,否则便没什么用处。注意`InputFile`的一个好处就是它同时合并了这两种行动。 85 | 86 | 若`FileReader`构造器不成功,就会产生一个`FileNotFoundException`(文件未找到异常)。必须单独捕获这个异常——这属于我们不想关闭文件的一种特殊情况,因为文件尚未成功打开。其他任何捕获从句(`catch`)都必须关闭文件,因为文件已在进入那些捕获从句时打开(当然,如果多个方法都能产生一个`FileNotFoundException`异常,就需要稍微用一些技巧。此时,我们可将不同的情况分隔到数个`try`块内)。`close()`方法会抛出一个尝试过的异常。即使它在另一个`catch`从句的代码块内,该异常也会得以捕获——对Java编译器来说,那个`catch`从句不过是另一对花括号而已。执行完本地操作后,异常会被重新“抛”出。这样做是必要的,因为这个构造器的执行已经失败,我们不希望调用方法来假设对象已正确创建以及有效。 87 | 88 | 在这个例子中,没有采用前述的标志技术,`finally`从句显然不是关闭文件的正确地方,因为这可能在每次构造器结束的时候关闭它。由于我们希望文件在`InputFile`对象处于活动状态时一直保持打开状态,所以这样做并不恰当。 89 | 90 | `getLine()`方法会返回一个字符串,其中包含了文件中下一行的内容。它调用了`readLine()`,后者可能产生一个异常,但那个异常会被捕获,使`getLine()`不会再产生任何异常。对异常来说,一项特别的设计问题是决定在这一级完全控制一个异常,还是进行部分控制,并传递相同(或不同)的异常,或者只是简单地传递它。在适当的时候,简单地传递可极大简化我们的编码工作。 91 | 92 | `getLine()`方法会变成: 93 | 94 | ``` 95 | String getLine() throws IOException { 96 | return in.readLine(); 97 | } 98 | ``` 99 | 100 | 但是当然,调用者现在需要对可能产生的任何`IOException`进行控制。 101 | 102 | 用户使用完毕`InputFile`对象后,必须调用`cleanup()`方法,以便释放由`BufferedReader`以及/或者`FileReader`占用的系统资源(如文件引用)——注释⑥。除非`InputFile`对象使用完毕,而且到了需要弃之不用的时候,否则不应进行清除。大家可能想把这样的机制置入一个`finalize()`方法内,但正如第4章指出的那样,并非总能保证`finalize()`获得正确的调用(即便确定它会调用,也不知道何时开始)。这属于Java的一项缺陷——除内存清除之外的所有清除都不会自动进行,所以必须知会客户程序员,告诉他们有责任用`finalize()`保证清除工作的正确进行。 103 | 104 | ⑥:在C++里,“析构器”可帮我们控制这一局面。 105 | 106 | 在`Cleanup.java`中,我们创建了一个`InputFile`,用它打开用于创建程序的相同的源文件。同时一次读取该文件的一行内容,而且添加相应的行号。所有异常都会在`main()`中被捕获——尽管我们可选择更大的可靠性。 107 | 108 | 这个示例也向大家展示了为何在本书的这个地方引入异常的概念。异常与Java的编程具有很高的集成度,这主要是由于编译器会强制它们。只有知道了如何操作那些异常,才可更进一步地掌握编译器的知识。 109 | -------------------------------------------------------------------------------- /9.8.md: -------------------------------------------------------------------------------- 1 | # 9.8 异常匹配 2 | 3 | “抛”出一个异常后,异常控制系统会按当初编写的顺序搜索“最接近”的控制器。一旦找到相符的控制器,就认为异常已得到控制,不再进行更多的搜索工作。 4 | 5 | 在异常和它的控制器之间,并不需要非常精确的匹配。一个派生类对象可与基类的一个控制器相配,如下例所示: 6 | 7 | ``` 8 | //: Human.java 9 | // Catching Exception Hierarchies 10 | 11 | class Annoyance extends Exception {} 12 | class Sneeze extends Annoyance {} 13 | 14 | public class Human { 15 | public static void main(String[] args) { 16 | try { 17 | throw new Sneeze(); 18 | } catch(Sneeze s) { 19 | System.out.println("Caught Sneeze"); 20 | } catch(Annoyance a) { 21 | System.out.println("Caught Annoyance"); 22 | } 23 | } 24 | } ///:~ 25 | ``` 26 | 27 | `Sneeze`异常会被相符的第一个`catch`从句捕获。当然,这只是第一个。然而,假如我们删除第一个`catch`从句: 28 | 29 | ``` 30 | try { 31 | throw new Sneeze(); 32 | } catch(Annoyance a) { 33 | System.out.println("Caught Annoyance"); 34 | } 35 | ``` 36 | 37 | 那么剩下的`catch`从句依然能够工作,因为它捕获的是`Sneeze`的基类。换言之,`catch(Annoyance e)`能捕获一个`Annoyance`以及从它派生的任何类。这一点非常重要,因为一旦我们决定为一个方法添加更多的异常,而且它们都是从相同的基类继承的,那么客户程序员的代码就不需要更改。至少能够假定它们捕获的是基类。 38 | 39 | 若将基类捕获从句置于第一位,试图“屏蔽”派生类异常,就象下面这样: 40 | 41 | ``` 42 | try { 43 | throw new Sneeze(); 44 | } catch(Annoyance a) { 45 | System.out.println("Caught Annoyance"); 46 | } catch(Sneeze s) { 47 | System.out.println("Caught Sneeze"); 48 | } 49 | ``` 50 | 51 | 则编译器会产生一条出错消息,因为它发现永远不可能抵达`Sneeze`捕获从句。 52 | 53 | ## 9.8.1 异常准则 54 | 55 | 用异常做下面这些事情: 56 | 57 | (1) 解决问题并再次调用造成异常的方法。 58 | 59 | (2) 平息事态的发展,并在不重新尝试方法的前提下继续。 60 | 61 | (3) 计算另一些结果,而不是希望方法产生的结果。 62 | 63 | (4) 在当前环境中尽可能解决问题,以及将相同的异常重新“抛”出一个更高级的环境。 64 | 65 | (5) 在当前环境中尽可能解决问题,以及将不同的异常重新“抛”出一个更高级的环境。 66 | 67 | (6) 中止程序执行。 68 | 69 | (7) 简化编码。若异常方案使事情变得更加复杂,那就会令人非常烦恼,不如不用。 70 | 71 | (8) 使自己的库和程序变得更加安全。这既是一种“短期投资”(便于调试),也是一种“长期投资”(改善应用程序的健壮性) 72 | -------------------------------------------------------------------------------- /9.9.md: -------------------------------------------------------------------------------- 1 | # 9.9 总结 2 | 3 | 通过先进的错误纠正与恢复机制,我们可以有效地增强代码的健壮程度。对我们编写的每个程序来说,错误恢复都属于一个基本的考虑目标。它在Java中显得尤为重要,因为该语言的一个目标就是创建不同的程序组件,以便其他用户(客户程序员)使用。为构建一套健壮的系统,每个组件都必须非常健壮。 4 | 5 | 在Java里,异常控制的目的是使用尽可能精简的代码创建大型、可靠的应用程序,同时排除程序里那些不能控制的错误。 6 | 7 | 异常的概念很难掌握。但只有很好地运用它,才可使自己的项目立即获得显著的收益。Java强迫遵守异常所有方面的问题,所以无论库设计者还是客户程序员,都能够连续一致地使用它。 8 | -------------------------------------------------------------------------------- /9.md: -------------------------------------------------------------------------------- 1 | # 第9章 异常差错控制 2 | 3 | Java的基本原理就是“形式错误的代码不会运行”。 4 | 5 | 与C++类似,捕获错误最理想的是在编译期间,最好在试图运行程序以前。然而,并非所有错误都能在编译期间侦测到。有些问题必须在运行期间解决,让错误的缔结者通过一些手续向接收者传递一些适当的信息,使其知道该如何正确地处理遇到的问题。 6 | 7 | 在C++和其他早期语言中,可通过几种手续来达到这个目的。而且它们通常是作为一种规定建立起来的,而非作为程序设计语言的一部分。典型地,我们需要返回一个值或设置一个标志(位),接收者会检查这些值或标志,判断具体发生了什么事情。然而,随着时间的流逝,终于发现这种做法会助长那些使用一个库的程序员的麻痹情绪。他们往往会这样想:“是的,错误可能会在其他人的代码中出现,但不会在我的代码中”。这样的后果便是他们一般不检查是否出现了错误(有时出错条件确实显得太愚蠢,不值得检验;注释①)。另一方面,若每次调用一个方法时都进行全面、细致的错误检查,那么代码的可读性也可能大幅度降低。由于程序员可能仍然在用这些语言维护自己的系统,所以他们应该对此有着深刻的体会:若按这种方式控制错误,那么在创建大型、健壮、易于维护的程序时,肯定会遇到不小的阻挠。 8 | 9 | ①:C程序员研究一下`printf()`的返回值便知端详。 10 | 11 | 解决的方法是在错误控制中排除所有偶然性,强制格式的正确。这种方法实际已有很长的历史,因为早在60年代便在操作系统里采用了“异常控制”手段;甚至可以追溯到BASIC语言的on error `goto`语句。但C++的异常控制建立在Ada的基础上,而Java又主要建立在C++的基础上(尽管它看起来更象Object Pascal)。 12 | 13 | “异常”(`Exception`)这个词表达的是一种“例外”情况,亦即正常情况之外的一种“异常”。在问题发生的时候,我们可能不知具体该如何解决,但肯定知道已不能不顾一切地继续下去。此时,必须坚决地停下来,并由某人、某地指出发生了什么事情,以及该采取何种对策。但为了真正解决问题,当地可能并没有足够多的信息。因此,我们需要将其移交给更级的负责人,令其作出正确的决定(类似一个命令链)。 14 | 15 | 异常机制的另一项好处就是能够简化错误控制代码。我们再也不用检查一个特定的错误,然后在程序的多处地方对其进行控制。此外,也不需要在方法调用的时候检查错误(因为保证有人能捕获这里的错误)。我们只需要在一个地方处理问题:“异常控制模块”或者“异常控制器”。这样可有效减少代码量,并将那些用于描述具体操作的代码与专门纠正错误的代码分隔开。一般情况下,用于读取、写入以及调试的代码会变得更富有条理。 16 | 17 | 由于异常控制是由Java编译器强行实现的,所以毋需深入学习异常控制,便可正确使用本书编写的大量例子。本章向大家介绍了用于正确控制异常所需的代码,以及在某个方法遇到麻烦的时候,该如何生成自己的异常。 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Thinking in Java (Java 编程思想) 2 | 3 | 原书:[Thinking in Java 4th Edition](http://mindview.net/Books/TIJ4) 4 | 5 | 作者:[Bruce Eckel](http://www.mindviewinc.com/Index.php) 6 | 7 | 原始中文版:[Thinking in Java 在线中文版](http://www.yq1012.com/ThinkingInJava/) 8 | 9 | 译者: 10 | 11 | + [yyc](mailto:yycmail@263.net) 12 | 13 | + [spirit](mailto:nimbusyyf@sohu.com) 14 | 15 | 修订: 16 | 17 | + [wizardforcel](https://github.com/wizardforcel) 18 | 19 | 整理: 20 | 21 | + [quanke](http://quanke.name) 22 | 23 | + [wizardforcel](https://github.com/wizardforcel) 24 | 25 | --- 26 | 27 | + [在线阅读](https://www.gitbook.com/book/wizardforcel/thinking-in-java/details) 28 | + [PDF格式](https://www.gitbook.com/download/pdf/book/wizardforcel/thinking-in-java) 29 | + [EPUB格式](https://www.gitbook.com/download/epub/book/wizardforcel/thinking-in-java) 30 | + [MOBI格式](https://www.gitbook.com/download/mobi/book/wizardforcel/thinking-in-java) 31 | + [代码仓库](http://github.com/it-ebooks/thinking-in-java-zh) 32 | 33 | > 注 34 | > 35 | > 我们没有翻译第五版(On Java 8)的计划,请访问 [LingCoder/OnJava8](https://github.com/LingCoder/OnJava8)。 36 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | + [Thinking in Java (Java 编程思想)](README.md) 2 | + [写在前面的话](0.1.md) 3 | + [引言](0.2.md) 4 | + [第1章 对象入门](1.md) 5 | + [1.1 抽象的进步](1.1.md) 6 | + [1.2 对象的接口](1.2.md) 7 | + [1.3 实现方案的隐藏](1.3.md) 8 | + [1.4 方案的重复使用](1.4.md) 9 | + [1.5 继承:重新使用接口](1.5.md) 10 | + [1.6 多态对象的互换使用](1.6.md) 11 | + [1.7 对象的创建和存在时间](1.7.md) 12 | + [1.8 异常控制:解决错误](1.8.md) 13 | + [1.9 多线程](1.9.md) 14 | + [1.10 永久性](1.10.md) 15 | + [1.11 Java和因特网](1.11.md) 16 | + [1.12 分析和设计](1.12.md) 17 | + [1.13 Java还是C++](1.13.md) 18 | + [第2章 一切都是对象](2.md) 19 | + [2.1 用引用操纵对象](2.1.md) 20 | + [2.2 所有对象都必须创建](2.2.md) 21 | + [2.3 绝对不要清除对象](2.3.md) 22 | + [2.4 新建数据类型:类](2.4.md) 23 | + [2.5 方法、参数和返回值](2.5.md) 24 | + [2.6 构建Java程序](2.6.md) 25 | + [2.7 我们的第一个Java程序](2.7.md) 26 | + [2.8 注释和嵌入文档](2.8.md) 27 | + [2.9 编码样式](2.9.md) 28 | + [2.10 总结](2.10.md) 29 | + [2.11 练习](2.11.md) 30 | + [第3章 控制程序流程](3.md) 31 | + [3.1 使用Java运算符](3.1.md) 32 | + [3.2 执行控制](3.2.md) 33 | + [3.3 总结](3.3.md) 34 | + [3.4 练习](3.4.md) 35 | + [第4章 初始化和清除](4.md) 36 | + [4.1 用构造器自动初始化](4.1.md) 37 | + [4.2 方法重载](4.2.md) 38 | + [4.3 清除:收尾和垃圾收集](4.3.md) 39 | + [4.4 成员初始化](4.4.md) 40 | + [4.5 数组初始化](4.5.md) 41 | + [4.6 总结](4.6.md) 42 | + [4.7 练习](4.7.md) 43 | + [第5章 隐藏实现过程](5.md) 44 | + [5.1 包:库单元](5.1.md) 45 | + [5.2 Java访问指示符](5.2.md) 46 | + [5.3 接口与实现](5.3.md) 47 | + [5.4 类访问](5.4.md) 48 | + [5.5 总结](5.5.md) 49 | + [5.6 练习](5.6.md) 50 | + [第6章 类复用](6.md) 51 | + [6.1 組合的语法](6.1.md) 52 | + [6.2 继承的语法](6.2.md) 53 | + [6.3 组合与继承的结合](6.3.md) 54 | + [6.4 到底选择组合还是继承](6.4.md) 55 | + [6.5 `protected`](6.5.md) 56 | + [6.6 累积开发](6.6.md) 57 | + [6.7 向上转换](6.7.md) 58 | + [6.8 `final`关键字](6.8.md) 59 | + [6.9 初始化和类装载](6.9.md) 60 | + [6.10 总结](6.10.md) 61 | + [6.11 练习](6.11.md) 62 | + [第7章 多态性](7.md) 63 | + [7.1 向上转换](7.1.md) 64 | + [7.2 深入理解](7.2.md) 65 | + [7.3 覆盖与重载](7.3.md) 66 | + [7.4 抽象类和方法](7.4.md) 67 | + [7.5 接口](7.5.md) 68 | + [7.6 内部类](7.6.md) 69 | + [7.7 构造器和多态性](7.7.md) 70 | + [7.8 通过继承进行设计](7.8.md) 71 | + [7.9 总结](7.9.md) 72 | + [7.10 练习](7.10.md) 73 | + [第8章 对象的容纳](8.md) 74 | + [8.1 数组](8.1.md) 75 | + [8.2 集合](8.2.md) 76 | + [8.3 枚举器(迭代器)](8.3.md) 77 | + [8.4 集合的类型](8.4.md) 78 | + [8.5 排序](8.5.md) 79 | + [8.6 通用集合库](8.6.md) 80 | + [8.7 新集合](8.7.md) 81 | + [8.8 总结](8.8.md) 82 | + [8.9 练习](8.9.md) 83 | + [第9章 异常差错控制](9.md) 84 | + [9.1 基本异常](9.1.md) 85 | + [9.2 异常的捕获](9.2.md) 86 | + [9.3 标准Java异常](9.3.md) 87 | + [9.4 创建自己的异常](9.4.md) 88 | + [9.5 异常的限制](9.5.md) 89 | + [9.6 用finally清除](9.6.md) 90 | + [9.7 构造器](9.7.md) 91 | + [9.8 异常匹配](9.8.md) 92 | + [9.9 总结](9.9.md) 93 | + [9.10 练习](9.10.md) 94 | + [第10章 Java IO系统](10.md) 95 | + [10.1 输入和输出](10.1.md) 96 | + [10.2 增添属性和有用的接口](10.2.md) 97 | + [10.3 本身的缺陷:`RandomAccessFile`](10.3.md) 98 | + [10.4 File类](10.4.md) 99 | + [10.5 IO流的典型应用](10.5.md) 100 | + [10.6 `StreamTokenizer`](10.6.md) 101 | + [10.7 Java 1.1的IO流](10.7.md) 102 | + [10.8 压缩](10.8.md) 103 | + [10.9 对象序列化](10.9.md) 104 | + [10.10 总结](10.10.md) 105 | + [10.11 练习](10.11.md) 106 | + [第11章 运行期类型识别](11.md) 107 | + [11.1 对RTTI的需要](11.1.md) 108 | + [11.2 RTTI语法](11.2.md) 109 | + [11.3 反射:运行期类信息](11.3.md) 110 | + [11.4 总结](11.4.md) 111 | + [11.5 练习](11.5.md) 112 | + [第12章 传递和返回对象](12.md) 113 | + [12.1 传递引用](12.1.md) 114 | + [12.2 制作本地副本](12.2.md) 115 | + [12.3 克隆的控制](12.3.md) 116 | + [12.4 只读类](12.4.md) 117 | + [12.5 总结](12.5.md) 118 | + [12.6 练习](12.6.md) 119 | + [第13章 创建窗口和程序片](13.md) 120 | + [13.1 为何要用AWT?](13.1.md) 121 | + [13.2 基本程序片](13.2.md) 122 | + [13.3 制作按钮](13.3.md) 123 | + [13.4 捕获事件](13.4.md) 124 | + [13.5 文本字段](13.5.md) 125 | + [13.6 文本区域](13.6.md) 126 | + [13.7 标签](13.7.md) 127 | + [13.8 复选框](13.8.md) 128 | + [13.9 单选钮](13.9.md) 129 | + [13.10 下拉列表](13.10.md) 130 | + [13.11 列表框](13.11.md) 131 | + [13.12 布局的控制](13.12.md) 132 | + [13.13 `action`的替代品](13.13.md) 133 | + [13.14 程序片的局限](13.14.md) 134 | + [13.15 视窗化应用](13.15.md) 135 | + [13.16 新型AWT](13.16.md) 136 | + [13.17 Java 1.1用户接口API](13.17.md) 137 | + [13.18 可视编程和Beans](13.18.md) 138 | + [13.19 Swing入门(注释⑦)](13.19.md) 139 | + [13.20 总结](13.20.md) 140 | + [13.21 练习](13.21.md) 141 | + [第14章 多线程](14.md) 142 | + [14.1 反应灵敏的用户界面](14.1.md) 143 | + [14.2 共享有限的资源](14.2.md) 144 | + [14.3 堵塞](14.3.md) 145 | + [14.4 优先级](14.4.md) 146 | + [14.5 回顾runnable](14.5.md) 147 | + [14.6 总结](14.6.md) 148 | + [14.7 练习](14.7.md) 149 | + [第15章 网络编程](15.md) 150 | + [15.1 机器的标识](15.1.md) 151 | + [15.10 练习](15.10.md) 152 | + [15.2 套接字](15.2.md) 153 | + [15.3 服务多个客户](15.3.md) 154 | + [15.4 数据报](15.4.md) 155 | + [15.5 一个Web应用](15.5.md) 156 | + [15.6 Java与CGI的沟通](15.6.md) 157 | + [15.7 用JDBC连接数据库](15.7.md) 158 | + [15.8 远程方法](15.8.md) 159 | + [15.9 总结](15.9.md) 160 | + [第16章 设计模式](16.md) 161 | + [16.1 模式的概念](16.1.md) 162 | + [16.2 观察器模式](16.2.md) 163 | + [16.3 模拟垃圾回收站](16.3.md) 164 | + [16.4 改进设计](16.4.md) 165 | + [16.5 抽象的应用](16.5.md) 166 | + [16.6 多重分发](16.6.md) 167 | + [16.7 访问器模式](16.7.md) 168 | + [16.8 RTTI真的有害吗](16.8.md) 169 | + [16.9 总结](16.9.md) 170 | + [16.10 练习](16.10.md) 171 | + [第17章 项目](17.md) 172 | + [17.1 文字处理](17.1.md) 173 | + [17.2 方法查找工具](17.2.md) 174 | + [17.3 复杂性理论](17.3.md) 175 | + [17.4 总结](17.4.md) 176 | + [17.5 练习](17.5.md) 177 | + [附录A 使用非JAVA代码](a.md) 178 | + [附录B 对比C++和Java](b.md) 179 | + [附录C Java编程规则](c.md) 180 | + [附录D 性能](d.md) 181 | + [附录E 关于垃圾收集的一些话](e.md) 182 | + [附录F 推荐读物](f.md) 183 | -------------------------------------------------------------------------------- /c.md: -------------------------------------------------------------------------------- 1 | # 附录C Java编程规则 2 | 3 | 4 | 本附录包含了大量有用的建议,帮助大家进行低级程序设计,并提供了代码编写的一般性指导: 5 | 6 | (1) 类名首字母应该大写。字段、方法以及对象(引用)的首字母应小写。对于所有标识符,其中包含的所有单词都应紧靠在一起,而且大写中间单词的首字母。例如: 7 | 8 | ``` 9 | ThisIsAClassName 10 | thisIsMethodOrFieldName 11 | ``` 12 | 13 | 若在定义中出现了常数初始化字符,则大写`static final`基本类型识别符中的所有字母。这样便可标志出它们属于编译期的常数。 14 | 15 | Java包(`Package`)属于一种特殊情况:它们全都是小写字母,即便中间的单词亦是如此。对于域名扩展名称,如`com,org,net`或者`edu`等,全部都应小写(这也是Java 1.1和Java 1.2的区别之一)。 16 | 17 | (2) 为了常规用途而创建一个类时,请采取“经典形式”,并包含对下述元素的定义: 18 | 19 | ``` 20 | equals() 21 | hashCode() 22 | toString() 23 | clone()(implement Cloneable) 24 | implement Serializable 25 | ``` 26 | 27 | (3) 对于自己创建的每一个类,都考虑置入一个`main()`,其中包含了用于测试那个类的代码。为使用一个项目中的类,我们没必要删除测试代码。若进行了任何形式的改动,可方便地返回测试。这些代码也可作为如何使用类的一个示例使用。 28 | 29 | (4) 应将方法设计成简要的、功能性单元,用它描述和实现一个不连续的类接口部分。理想情况下,方法应简明扼要。若长度很大,可考虑通过某种方式将其分割成较短的几个方法。这样做也便于类内代码的重复使用(有些时候,方法必须非常大,但它们仍应只做同样的一件事情)。 30 | 31 | (5) 设计一个类时,请设身处地为客户程序员考虑一下(类的使用方法应该是非常明确的)。然后,再设身处地为管理代码的人考虑一下(预计有可能进行哪些形式的修改,想想用什么方法可把它们变得更简单)。 32 | 33 | (6) 使类尽可能短小精悍,而且只解决一个特定的问题。下面是对类设计的一些建议: 34 | 35 | + 一个复杂的开关语句:考虑采用“多态”机制 36 | 37 | + 数量众多的方法涉及到类型差别极大的操作:考虑用几个类来分别实现 38 | 39 | + 许多成员变量在特征上有很大的差别:考虑使用几个类 40 | 41 | (7) 让一切东西都尽可能地“私有”——`private`。可使库的某一部分“公共化”(一个方法、类或者一个字段等等),就永远不能把它拿出。若强行拿出,就可能破坏其他人现有的代码,使他们不得不重新编写和设计。若只公布自己必须公布的,就可放心大胆地改变其他任何东西。在多线程环境中,隐私是特别重要的一个因素——只有`private`字段才能在非同步使用的情况下受到保护。 42 | 43 | (8) 谨惕“巨大对象综合症”。对一些习惯于顺序编程思维、且初涉OOP领域的新手,往往喜欢先写一个顺序执行的程序,再把它嵌入一个或两个巨大的对象里。根据编程原理,对象表达的应该是应用程序的概念,而非应用程序本身。 44 | 45 | (9) 若不得已进行一些不太雅观的编程,至少应该把那些代码置于一个类的内部。 46 | 47 | (10) 任何时候只要发现类与类之间结合得非常紧密,就需要考虑是否采用内部类,从而改善编码及维护工作(参见第14章14.1.2小节的“用内部类改进代码”)。 48 | 49 | (11) 尽可能细致地加上注释,并用`javadoc`注释文档语法生成自己的程序文档。 50 | 51 | (12) 避免使用“魔术数字”,这些数字很难与代码很好地配合。如以后需要修改它,无疑会成为一场噩梦,因为根本不知道“100”到底是指“数组大小”还是“其他全然不同的东西”。所以,我们应创建一个常数,并为其使用具有说服力的描述性名称,并在整个程序中都采用常数标识符。这样可使程序更易理解以及更易维护。 52 | 53 | (13) 涉及构造器和异常的时候,通常希望重新丢弃在构造器中捕获的任何异常——如果它造成了那个对象的创建失败。这样一来,调用者就不会以为那个对象已正确地创建,从而盲目地继续。 54 | 55 | (14) 当客户程序员用完对象以后,若你的类要求进行任何清除工作,可考虑将清除代码置于一个良好定义的方法里,采用类似于`cleanup()`这样的名字,明确表明自己的用途。除此以外,可在类内放置一个`boolean`(布尔)标记,指出对象是否已被清除。在类的`finalize()`方法里,请确定对象已被清除,并已丢弃了从`RuntimeException`继承的一个类(如果还没有的话),从而指出一个编程错误。在采取象这样的方案之前,请确定`finalize()`能够在自己的系统中工作(可能需要调用`System.runFinalizersOnExit(true)`,从而确保这一行为)。 56 | 57 | (15) 在一个特定的作用域内,若一个对象必须清除(非由垃圾收集机制处理),请采用下述方法:初始化对象;若成功,则立即进入一个含有`finally`从句的`try`块,开始清除工作。 58 | 59 | (16) 若在初始化过程中需要覆盖(取消)`finalize()`,请记住调用`super.finalize()`(若`Object`属于我们的直接超类,则无此必要)。在对`finalize()`进行覆盖的过程中,对`super.finalize()`的调用应属于最后一个行动,而不应是第一个行动,这样可确保在需要基类组件的时候它们依然有效。 60 | 61 | (17) 创建大小固定的对象集合时,请将它们传输至一个数组(若准备从一个方法里返回这个集合,更应如此操作)。这样一来,我们就可享受到数组在编译期进行类型检查的好处。此外,为使用它们,数组的接收者也许并不需要将对象“转换”到数组里。 62 | 63 | (18) 尽量使用`interfaces`,不要使用`abstract`类。若已知某样东西准备成为一个基类,那么第一个选择应是将其变成一个`interface`(接口)。只有在不得不使用方法定义或者成员变量的时候,才需要将其变成一个`abstract`(抽象)类。接口主要描述了客户希望做什么事情,而一个类则致力于(或允许)具体的实现细节。 64 | 65 | (19) 在构造器内部,只进行那些将对象设为正确状态所需的工作。尽可能地避免调用其他方法,因为那些方法可能被其他人覆盖或取消,从而在构建过程中产生不可预知的结果(参见第7章的详细说明)。 66 | 67 | (20) 对象不应只是简单地容纳一些数据;它们的行为也应得到良好的定义。 68 | 69 | (21) 在现成类的基础上创建新类时,请首先选择“新建”或“创作”。只有自己的设计要求必须继承时,才应考虑这方面的问题。若在本来允许新建的场合使用了继承,则整个设计会变得没有必要地复杂。 70 | 71 | (22) 用继承及方法覆盖来表示行为间的差异,而用字段表示状态间的区别。一个非常极端的例子是通过对不同类的继承来表示颜色,这是绝对应该避免的:应直接使用一个“颜色”字段。 72 | 73 | (23) 为避免编程时遇到麻烦,请保证在自己类路径指到的任何地方,每个名字都仅对应一个类。否则,编译器可能先找到同名的另一个类,并报告出错消息。若怀疑自己碰到了类路径问题,请试试在类路径的每一个起点,搜索一下同名的`.class`文件。 74 | 75 | (24) 在Java 1.1 AWT中使用事件“适配器”时,特别容易碰到一个陷阱。若覆盖了某个适配器方法,同时拼写方法没有特别讲究,最后的结果就是新添加一个方法,而不是覆盖现成方法。然而,由于这样做是完全合法的,所以不会从编译器或运行期系统获得任何出错提示——只不过代码的工作就变得不正常了。 76 | 77 | (25) 用合理的设计模式消除“伪功能”。也就是说,假若只需要创建类的一个对象,就不要提前限制自己使用应用程序,并加上一条“只生成其中一个”注释。请考虑将其封装成一个“独生子”的形式。若在主程序里有大量散乱的代码,用于创建自己的对象,请考虑采纳一种创造性的方案,将些代码封装起来。 78 | 79 | (26) 警惕“分析瘫痪”。请记住,无论如何都要提前了解整个项目的状况,再去考察其中的细节。由于把握了全局,可快速认识自己未知的一些因素,防止在考察细节的时候陷入“死逻辑”中。 80 | 81 | (27) 警惕“过早优化”。首先让它运行起来,再考虑变得更快——但只有在自己必须这样做、而且经证实在某部分代码中的确存在一个性能瓶颈的时候,才应进行优化。除非用专门的工具分析瓶颈,否则很有可能是在浪费自己的时间。性能提升的隐含代价是自己的代码变得难于理解,而且难于维护。 82 | 83 | (28) 请记住,阅读代码的时间比写代码的时间多得多。思路清晰的设计可获得易于理解的程序,但注释、细致的解释以及一些示例往往具有不可估量的价值。无论对你自己,还是对后来的人,它们都是相当重要的。如对此仍有怀疑,那么请试想自己试图从联机Java文档里找出有用信息时碰到的挫折,这样或许能将你说服。 84 | 85 | (29) 如认为自己已进行了良好的分析、设计或者实现,那么请稍微更换一下思维角度。试试邀请一些外来人士——并不一定是专家,但可以是来自本公司其他部门的人。请他们用完全新鲜的眼光考察你的工作,看看是否能找出你一度熟视无睹的问题。采取这种方式,往往能在最适合修改的阶段找出一些关键性的问题,避免产品发行后再解决问题而造成的金钱及精力方面的损失。 86 | 87 | (30) 良好的设计能带来最大的回报。简言之,对于一个特定的问题,通常会花较长的时间才能找到一种最恰当的解决方案。但一旦找到了正确的方法,以后的工作就轻松多了,再也不用经历数小时、数天或者数月的痛苦挣扎。我们的努力工作会带来最大的回报(甚至无可估量)。而且由于自己倾注了大量心血,最终获得一个出色的设计模式,成功的快感也是令人心动的。坚持抵制草草完工的诱惑——那样做往往得不偿失。 88 | 89 | (31) 可在Web上找到大量的编程参考资源,甚至包括大量新闻组、讨论组、邮寄列表等。下面这个地方提供了大量有益的链接: 90 | 91 | http://www.ulb.ac.be/esp/ip-Links/Java/joodcs/mm-WebBiblio.html 92 | -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/thinking-in-java-zh/bf86bde9f3a79090d550fe6ee2cbfb2570499f5f/cover.jpg -------------------------------------------------------------------------------- /e.md: -------------------------------------------------------------------------------- 1 | # 附录E 关于垃圾收集的一些话 2 | 3 | 4 | “很难相信Java居然能和C++一样快,甚至还能更快一些。” 5 | 6 | 据我自己的实践,这种说法确实成立。然而,我也发现许多关于速度的怀疑都来自一些早期的实现方式。由于这些方式并非特别有效,所以没有一个模型可供参考,不能解释Java速度快的原因。 7 | 8 | 我之所以想到速度,部分原因是由于C++模型。C++将自己的主要精力放在编译期间“静态”发生的所有事情上,所以程序的运行期版本非常短小和快速。C++也直接建立在C模型的基础上(主要为了向后兼容),但有时仅仅由于它在C中能按特定的方式工作,所以也是C++中最方便的一种方法。最重要的一种情况是C和C++对内存的管理方式,它是某些人觉得Java速度肯定慢的重要依据:在Java中,所有对象都必须在内存“堆”里创建。 9 | 10 | 而在C++中,对象是在栈中创建的。这样可达到更快的速度,因为当我们进入一个特定的作用域时,栈指针会向下移动一个单位,为那个作用域内创建的、以栈为基础的所有对象分配存储空间。而当我们离开作用域的时候(调用完毕所有局部构造器后),栈指针会向上移动一个单位。然而,在C++里创建“内存堆”(Heap)对象通常会慢得多,因为它建立在C的内存堆基础上。这种内存堆实际是一个大的内存池,要求必须进行再循环(复用)。在C++里调用`delete`以后,释放的内存会在堆里留下一个洞,所以再调用`new`的时候,存储分配机制必须进行某种形式的搜索,使对象的存储与堆内任何现成的洞相配,否则就会很快用光堆的存储空间。之所以内存堆的分配会在C++里对性能造成如此重大的性能影响,对可用内存的搜索正是一个重要的原因。所以创建基于栈的对象要快得多。 11 | 12 | 同样地,由于C++如此多的工作都在编译期间进行,所以必须考虑这方面的因素。但在Java的某些地方,事情的发生却要显得“动态”得多,它会改变模型。创建对象的时候,垃圾收集器的使用对于提高对象创建的速度产生了显著的影响。从表面上看,这种说法似乎有些奇怪——存储空间的释放会对存储空间的分配造成影响,但它正是JVM采取的重要手段之一,这意味着在Java中为堆对象分配存储空间几乎能达到与C++中在栈里创建存储空间一样快的速度。 13 | 14 | 可将C++的堆(以及更慢的Java堆)想象成一个庭院,每个对象都拥有自己的一块地皮。在以后的某个时间,这种“不动产”会被抛弃,而且必须复用。但在某些JVM里,Java堆的工作方式却是颇有不同的。它更象一条传送带:每次分配了一个新对象后,都会朝前移动。这意味着对象存储空间的分配可以达到非常快的速度。“堆指针”简单地向前移至处女地,所以它与C++的栈分配方式几乎是完全相同的(当然,在数据记录上会多花一些开销,但要比搜索存储空间快多了)。 15 | 16 | 现在,大家可能注意到了堆事实并非一条传送带。如按那种方式对待它,最终就要求进行大量的页交换(这对性能的发挥会产生巨大干扰),这样终究会用光内存,出现内存分页错误。所以这儿必须采取一个技巧,那就是著名的“垃圾收集器”。它在收集“垃圾”的同时,也负责压缩堆里的所有对象,将“堆指针”移至尽可能靠近传送带开头的地方,远离发生(内存)分页错误的地点。垃圾收集器会重新安排所有东西,使其成为一个高速、无限自由的堆模型,同时游刃有余地分配存储空间。 17 | 18 | 为真正掌握它的工作原理,我们首先需要理解不同垃圾收集器(GC)采取的工作方案。一种简单、但速度较慢的GC技术是引用计数。这意味着每个对象都包含了一个引用计数器。每当一个引用同一个对象连接起来时,引用计数器就会自增。每当一个引用超出自己的作用域,或者设为`null`时,引用计数就会自减。这样一来,只要程序处于运行状态,就需要连续进行引用计数管理——尽管这种管理本身的开销比较少。垃圾收集器会在整个对象列表中移动巡视,一旦它发现其中一个引用计数成为0,就释放它占据的存储空间。但这样做也有一个缺点:若对象相互之间进行循环引用,那么即使引用计数不是0,仍有可能属于应收掉的“垃圾”。为了找出这种自引用的组,要求垃圾收集器进行大量额外的工作。引用计数属于垃圾收集的一种类型,但它看起来并不适合在所有JVM方案中采用。 19 | 20 | 在速度更快的方案里,垃圾收集并不建立在引用计数的基础上。相反,它们基于这样一个原理:所有非死锁的对象最终都肯定能回溯至一个引用,该引用要么存在于栈中,要么存在于静态存储空间。这个回溯链可能经历了几层对象。所以,如果从栈和静态存储区域开始,并经历所有引用,就能找出所有活动的对象。对于自己找到的每个引用,都必须跟踪到它指向的那个对象,然后跟随那个对象中的所有引用,“跟踪追击”到它们指向的对象……等等,直到遍历了从栈或静态存储区域中的引用发起的整个链接网路为止。中途移经的每个对象都必须仍处于活动状态。注意对于那些特殊的自引用组,并不会出现前述的问题。由于它们根本找不到,所以会自动当作垃圾处理。 21 | 22 | 在这里阐述的方法中,JVM采用一种“自适应”的垃圾收集方案。对于它找到的那些活动对象,具体采取的操作取决于当前正在使用的是什么变体。其中一个变体是“停止和复制”。这意味着由于一些不久之后就会非常明显的原因,程序首先会停止运行(并非一种后台收集方案)。随后,已找到的每个活动对象都会从一个内存堆复制到另一个,留下所有的垃圾。除此以外,随着对象复制到新堆,它们会一个接一个地聚焦在一起。这样可使新堆显得更加紧凑(并使新的存储区域可以简单地抽离末尾,就象前面讲述的那样)。 23 | 24 | 当然,将一个对象从一处挪到另一处时,指向那个对象的所有引用(引用)都必须改变。对于那些通过跟踪内存堆的对象而获得的引用,以及那些静态存储区域,都可以立即改变。但在“遍历”过程中,还有可能遇到指向这个对象的其他引用。一旦发现这个问题,就当即进行修正(可想象一个散列表将老地址映射成新地址)。 25 | 26 | 有两方面的问题使复制收集器显得效率低下。第一个问题是我们拥有两个堆,所有内存都在这两个独立的堆内来回移动,要求付出的管理量是实际需要的两倍。为解决这个问题,有些JVM根据需要分配内存堆,并将一个堆简单地复制到另一个。 27 | 28 | 第二个问题是复制。随着程序变得越来越“健壮”,它几乎不产生或产生很少的垃圾。尽管如此,一个副本收集器仍会将所有内存从一处复制到另一处,这显得非常浪费。为避免这个问题,有些JVM能侦测是否没有产生新的垃圾,并随即改换另一种方案(这便是“自适应”的缘由)。另一种方案叫作“标记和清除”,Sun公司的JVM一直采用的都是这种方案。对于常规性的应用,标记和清除显得非常慢,但一旦知道自己不产生垃圾,或者只产生很少的垃圾,它的速度就会非常快。 29 | 30 | 标记和清除采用相同的逻辑:从栈和静态存储区域开始,并跟踪所有引用,寻找活动对象。然而,每次发现一个活动对象的时候,就会设置一个标记,为那个对象作上“记号”。但此时尚不收集那个对象。只有在标记过程结束,清除过程才正式开始。在清除过程中,死锁的对象会被释放然而,不会进行任何形式的复制,所以假若收集器决定压缩一个断续的内存堆,它通过移动周围的对象来实现。 31 | 32 | “停止和复制”向我们表明这种类型的垃圾收集并不是在后台进行的;相反,一旦发生垃圾收集,程序就会停止运行。在Sun公司的文档库中,可发现许多地方都将垃圾收集定义成一种低优先级的后台进程,但它只是一种理论上的实验,实际根本不能工作。在实际应用中,Sun的垃圾收集器会在内存减少时运行。除此以外,“标记和清除”也要求程序停止运行。 33 | 34 | 正如早先指出的那样,在这里介绍的JVM中,内存是按大块分配的。若分配一个大块头对象,它会获得自己的内存块。严格的“停止和复制”要求在释放旧堆之前,将每个活动的对象从源堆复制到一个新堆,此时会涉及大量的内存转换工作。通过内存块,垃圾收集器通常可利用死块复制对象,就象它进行收集时那样。每个块都有一个生成计数,用于跟踪它是否依然“存活”。通常,只有自上次垃圾收集以来创建的块才会得到压缩;对于其他所有块,如果已从其他某些地方进行了引用,那么生成计数都会溢出。这是许多短期的、临时的对象经常遇到的情况。会周期性地进行一次完整清除工作——大块头的对象仍未复制(只是让它们的生成计数溢出),而那些包含了小对象的块会进行复制和压缩。JVM会监视垃圾收集器的效率,如果由于所有对象都属于长期对象,造成垃圾收集成为浪费时间的一个过程,就会切换到“标记和清除”方案。类似地,JVM会跟踪监视成功的“标记与清除”工作,若内存堆变得越来越“散乱”,就会换回“停止和复制”方案。“自定义”的说法就是从这种行为来的,我们将其最后总结为:“根据情况,自动转换停止和复制/标记和清除这两种模式”。 35 | 36 | JVM还采用了其他许多加速方案。其中一个特别重要的涉及装载器以及JIT编译器。若必须装载一个类(通常是我们首次想创建那个类的一个对象时),会找到`.class`文件,并将那个类的字节码送入内存。此时,一个方法是用JIT编译所有代码,但这样做有两方面的缺点:它会花更多的时间,若与程序的运行时间综合考虑,编译时间还有可能更长;而且它增大了执行文件的长度(字节码比扩展过的JIT代码精简得多),这有可能造成内存页交换,从而显著放慢一个程序的执行速度。另一种替代办法是:除非确有必要,否则不经JIT编译。这样一来,那些根本不会执行的代码就可能永远得不到JIT的编译。 37 | 38 | 由于JVM对浏览器来说是外置的,大家可能希望在使用浏览器的时候从一些JVM的速度提高中获得好处。但非常不幸,JVM目前不能与不同的浏览器进行沟通。为发挥一种特定JVM的潜力,要么使用内建了那种JVM的浏览器,要么只有运行独立的Java应用程序。 39 | -------------------------------------------------------------------------------- /f.md: -------------------------------------------------------------------------------- 1 | # 附录F 推荐读物 2 | 3 | 《Java in a Nutshell:A Desktop Quick Reference,第2版》 4 | 5 | 作者:David Flanagan 6 | 7 | 出版社:O'Reilly & Assoc 8 | 9 | 出版时间:1997 10 | 11 | 简介:对Java 1.1联机文档的一个简要总结。就个人来说,我更喜欢在线阅览文档,特别是在它们变化得如此快的时候。然而,许多人仍然喜欢印刷出来的文档,这样可以省一些上网费。而且这本书也提供了比联机文档更多的讨论。 12 | 13 | 《The Java Class Libraries:An Annotated Reference》 14 | 15 | 作者:Patrick Chan和Rosanna Lee 16 | 17 | 出版社:Addison-Wesley 18 | 19 | 出版时间:1997 20 | 21 | 简介:作为一种联机参考资源,应向读者提供足够多的说明,使其简单易用。《Thinking in Java》的一名技术审定员说道:“如果我只能有一本Java书,那么肯定选它。”不过我可没有他那么激动。它太大、太贵,而且示例的质量并不能令我满意。但在遇到麻烦的时候,该书还是很有参考价值的。而且与《Java in a Nutshell》相比,它看起来有更大的深度(当然也有更多的文字)。 22 | 23 | 《Java Network Programming》 24 | 25 | 作者:Elliote Rusty Harold 26 | 27 | David Flanagan 28 | 29 | 出版社:O'Reilly 30 | 31 | 出版时间:1997 32 | 33 | 简介:在阅读本书前,我可以说根本不理解Java有关网络的问题。后来,我也发现他的Web站点“Cafe au Lait”是个令人激动的、很人个性的以及经常更新的去处,涉及大量有价值的Java开发资源。由于几乎每天更新,所以在这里能看到与Java有关的大量新闻。站点地址是:`http://sunsite.unc.edu/javafaq/`。 34 | 35 | 《Core Java,第3版》 36 | 37 | 作者:Cornel和Horstmann 38 | 39 | 出版社:Prentice-Hall 40 | 41 | 出版时间:1997 42 | 43 | 简介:对于自己碰到的问题,若在《Thinking in Java》里找不到答案,这就是一个很好的参考地点。注意:Java 1.1的版本是《Core Java 1.1 Volume 1-Fundamentals & Core Java 1.1 Volume 2-Advanced Features》 44 | 45 | 《JDBC Database Access with Java》 46 | 47 | 作者:Hamilton,Cattell和Fisher 48 | 49 | 出版社:Addison-Wesley 50 | 51 | 出版时间:1997 52 | 53 | 简介:如果对SQL和数据库一无所知,这本书就可以作为一个相当好的起点。它也对API进行了详尽的解释,并提供一个“注释参考。与“Java系列”(由JavaSoft授权的唯一一套丛书)的其他所有书籍一样,这本书的缺点也是进行了过份的渲染,只说Java的好话——在这一系列书籍里找不到任何不利于Java的地方。 54 | 55 | 《Java Programming with CORBA》 56 | 57 | 作者:Andreas Vogel和Keith Duddy 58 | 59 | 出版社:Jonh Wiley & Sons 60 | 61 | 出版时间:1997 62 | 63 | 简介:针对三种主要的Java ORB(Visbroker,Orbix,Joe),本书分别用大量代码实例进行了详尽的阐述。 64 | 65 | 《设计模式》 66 | 67 | 作者:Gamma,Helm,Johnson和Vlissides 68 | 69 | 出版社:Addison-Wesley 70 | 71 | 出版时间:1995 72 | 73 | 简介:这是一本发起了编程领域方案革命的经典书籍。 74 | 75 | 《UML Toolkit》 76 | 77 | 作者:Hans-Erik Eriksson和Magnus Penker 78 | 79 | 出版社:Jonh Wiley & Sons 80 | 81 | 出版时间:1997 82 | 83 | 简介:解释UML以及如何使用它,并提供Java的实际案例供参考。配套CD-ROM包含了Java代码以及Rational Rose的一个删减版本。本书对UML进行了非常出色的描述,并解释了如何用它构建实际的系统。 84 | 85 | 《Practical Algorithms for Programmers》 86 | 87 | 作者:Binstock和Rex 88 | 89 | 出版社:Addison-Wesley 90 | 91 | 出版时间:1995 92 | 93 | 简介:算法是用C描述的,所以它们很容易就能转换到Java里面。每种算法都有详尽的解释。 94 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | git add -A 2 | git commit -am "$(date "+%Y-%m-%d %H:%M:%S")" 3 | git push --------------------------------------------------------------------------------