├── .gitignore ├── 1-1.gif ├── 1-2.gif ├── 1.1 抽象的进步.md ├── 1.10 永久性.md ├── 1.11 Java和因特网.md ├── 1.12 分析和设计.md ├── 1.13 Java还是C++.md ├── 1.2 对象的接口.md ├── 1.3 实现方案的隐藏.md ├── 1.4 方案的重复使用.md ├── 1.5 继承:重新使用接口.md ├── 1.6 多态对象的互换使用.md ├── 1.7 对象的创建和存在时间.md ├── 1.9 多线程.md ├── 10.1 输入和输出.md ├── 10.10 总结.md ├── 10.11 练习.md ├── 10.2 增添属性和有用的接口.md ├── 10.3 本身的缺陷:RandomAccessFile.md ├── 10.4 File类.md ├── 10.5 IO流的典型应用.md ├── 10.6 StreamTokenizer.md ├── 10.7 Java 1.1的IO流.md ├── 10.8 压缩.md ├── 10.9 对象序列化.md ├── 11-1.gif ├── 11.1 对RTTI的需要.md ├── 11.2 RTTI语法.md ├── 11.3 反射:运行期类信息.md ├── 11.4 总结.md ├── 11.5 练习.md ├── 12.1 传递指针.md ├── 12.2 制作本地副本.md ├── 12.3 克隆的控制.md ├── 12.4 只读类.md ├── 12.5 总结.md ├── 12.6 练习.md ├── 14.1 反应灵敏的用户界面.md ├── 14.2 共享有限的资源.md ├── 14.3 堵塞.md ├── 14.4 优先级.md ├── 14.5 回顾runnable.md ├── 14.6 总结.md ├── 14.7 练习.md ├── 15.1 机器的标识.md ├── 15.10 练习.md ├── 15.2 套接字.md ├── 15.3 服务多个客户.md ├── 15.4 数据报.md ├── 15.5 一个Web应用.md ├── 15.6 Java与CGI的沟通.md ├── 15.7 用JDBC连接数据库.md ├── 15.8 远程方法.md ├── 15.9 总结.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 RTTI真的有害吗.md ├── 16.9 总结.md ├── 17.1 文字处理.md ├── 17.2 方法查找工具.md ├── 17.3 复杂性理论.md ├── 17.4 总结.md ├── 17.5 练习.md ├── 2.1 用指针操纵对象.md ├── 2.10 总结.md ├── 2.11 练习.md ├── 2.2 所有对象都必须创建.md ├── 2.3 绝对不要清除对象.md ├── 2.4 新建数据类型:类.md ├── 2.5 方法、自变量和返回值.md ├── 2.6 构建Java程序.md ├── 2.7 我们的第一个Java程序.md ├── 2.8 注释和嵌入文档.md ├── 2.9 编码样式.md ├── 3.1 使用Java运算符.md ├── 3.2 执行控制.md ├── 3.3 总结.md ├── 3.4 练习.md ├── 4.1 用构造器自动初始化.md ├── 4.2 方法重载.md ├── 4.3 清除:收尾和垃圾收集.md ├── 4.4 成员初始化.md ├── 4.5 数组初始化.md ├── 4.6 总结.md ├── 4.7 练习.md ├── 5.1 包:库单元.md ├── 5.2 Java访问指示符.md ├── 5.3 接口与实现.md ├── 5.4 类访问.md ├── 5.5 总结.md ├── 5.6 练习.md ├── 6.1 合成的语法.md ├── 6.10 总结.md ├── 6.11 练习.md ├── 6.2 继承的语法.md ├── 6.3 合成与继承的结合.md ├── 6.4 到底选择合成还是继承.md ├── 6.5 protected.md ├── 6.6 累积开发.md ├── 6.7 上溯造型.md ├── 6.8 final关键字.md ├── 6.9 初始化和类装载.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 ├── 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 ├── 9.10 练习.md ├── 9.6 用finally清除.md ├── 9.7 构造器.md ├── 9.9 总结.md ├── README.md ├── SUMMARY.md ├── assets └── qrcode_for_gh_26893aa0a4ea_258.jpg ├── book.json ├── 写在前面的话.md ├── 引言.md ├── 第10章 Java IO系统.md ├── 第11章 运行期类型鉴定.md ├── 第12章 传递和返回对象.md ├── 第13章 创建窗口和程序片.md ├── 第14章 多线程.md ├── 第15章 网络编程.md ├── 第16章 设计范式.md ├── 第17章 项目.md ├── 第1章 对象入门.md ├── 第2章 一切都是对象.md ├── 第3章 控制程序流程.md ├── 第4章 初始化和清除.md ├── 第5章 隐藏实施过程.md ├── 第6章 类再生.md ├── 第7章 多态性.md ├── 第8章 对象的容纳.md ├── 第9章 异常差错控制.md ├── 附录A 使用非JAVA代码.md ├── 附录B 对比C++和Java.md ├── 附录C Java编程规则.md ├── 附录D 性能.md ├── 附录E 关于垃圾收集的一些话.md └── 附录F 推荐读物.md /.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 -------------------------------------------------------------------------------- /1-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/1-1.gif -------------------------------------------------------------------------------- /1-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/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 | 26 | -------------------------------------------------------------------------------- /1.10 永久性.md: -------------------------------------------------------------------------------- 1 | # 1.10 永久性 2 | 3 | 4 | 创建一个对象后,只要我们需要,它就会一直存在下去。但在程序结束运行时,对象的“生存期”也会宣告结束。尽管这一现象表面上非常合理,但深入追究就会发现,假如在程序停止运行以后,对象也能继续存在,并能保留它的全部信息,那么在某些情况下将是一件非常有价值的事情。下次启动程序时,对象仍然在那里,里面保留的信息仍然是程序上一次运行时的那些信息。当然,可以将信息写入一个文件或者数据库,从而达到相同的效果。但尽管可将所有东西都看作一个对象,如果能将对象声明成“永久性”,并令其为我们照看其他所有细节,无疑也是一件相当方便的事情。 5 | 6 | Java 1.1提供了对“有限永久性”的支持,这意味着我们可将对象简单地保存到磁盘上,以后任何时间都可取回。之所以称它为“有限”的,是由于我们仍然需要明确发出调用,进行对象的保存和取回工作。这些工作不能自动进行。在Java未来的版本中,对“永久性”的支持有望更加全面。 7 | -------------------------------------------------------------------------------- /1.12 分析和设计.md: -------------------------------------------------------------------------------- 1 | # 1.12 分析和设计 2 | 3 | 4 | 面向对象的范式是思考程序设计时一种新的、而且全然不同的方式,许多人最开始都会在如何构造一个项目上皱起了眉头。事实上,我们可以作出一个“好”的设计,它能充分利用OOP提供的所有优点。 5 | 6 | 有关OOP分析与设计的书籍大多数都不尽如人意。其中的大多数书都充斥着莫名其妙的话语、笨拙的笔调以及许多听起来似乎很重要的声明(注释⑨)。我认为这种书最好压缩到一章左右的空间,至多写成一本非常薄的书。具有讽剌意味的是,那些特别专注于复杂事物管理的人往往在写一些浅显、明白的书上面大费周章!如果不能说得简单和直接,一定没多少人喜欢看这方面的内容。毕竟,OOP的全部宗旨就是让软件开发的过程变得更加容易。尽管这可能影响了那些喜欢解决复杂问题的人的生计,但为什么不从一开始就把事情弄得简单些呢?因此,希望我能从开始就为大家打下一个良好的基础,尽可能用几个段落来说清楚分析与设计的问题。 7 | 8 | ⑨:最好的入门书仍然是Grady Booch的《Object-Oriented Design withApplications,第2版本》,Wiely & Sons于1996年出版。这本书讲得很有深度,而且通俗易懂,尽管他的记号方法对大多数设计来说都显得不必要地复杂。 9 | 10 | 1.12.1 不要迷失 11 | 12 | 在整个开发过程中,最重要的事情就是:不要将自己迷失!但事实上这种事情很容易发生。大多数方法都设计用来解决最大范围内的问题。当然,也存在一些特别困难的项目,需要作者付出更为艰辛的努力,或者付出更大的代价。但是,大多数项目都是比较“常规”的,所以一般都能作出成功的分析与设计,而且只需用到推荐的一小部分方法。但无论多么有限,某些形式的处理总是有益的,这可使整个项目的开发更加容易,总比直接了当开始编码好! 13 | 14 | 也就是说,假如你正在考察一种特殊的方法,其中包含了大量细节,并推荐了许多步骤和文档,那么仍然很难正确判断自己该在何时停止。时刻提醒自己注意以下几个问题: 15 | 16 | (1) 对象是什么?(怎样将自己的项目分割成一系列单独的组件?) 17 | 18 | (2) 它们的接口是什么?(需要将什么消息发给每一个对象?) 19 | 20 | 在确定了对象和它们的接口后,便可着手编写一个程序。出于对多方面原因的考虑,可能还需要比这更多的说明及文档,但要求掌握的资料绝对不能比这还少。 21 | 22 | 整个过程可划分为四个阶段,阶段0刚刚开始采用某些形式的结构。 23 | 24 | 1.12.2 阶段0:拟出一个计划 25 | 26 | 第一步是决定在后面的过程中采取哪些步骤。这听起来似乎很简单(事实上,我们这儿说的一切都似乎很简单),但很常见的一种情况是:有些人甚至没有进入阶段1,便忙忙慌慌地开始编写代码。如果你的计划本来就是“直接开始开始编码”,那样做当然也无可非议(若对自己要解决的问题已有很透彻的理解,便可考虑那样做)。但最低程度也应同意自己该有个计划。 27 | 28 | 在这个阶段,可能要决定一些必要的附加处理结构。但非常不幸,有些程序员写程序时喜欢随心所欲,他们认为“该完成的时候自然会完成”。这样做刚开始可能不会有什么问题,但我觉得假如能在整个过程中设置几个标志,或者“路标”,将更有益于你集中注意力。这恐怕比单纯地为了“完成工作”而工作好得多。至少,在达到了一个又一个的目标,经过了一个接一个的路标以后,可对自己的进度有清晰的把握,干劲也会相应地提高,不会产生“路遥漫漫无期”的感觉。 29 | 30 | 从我刚开始学习故事结构起(我想有一天能写本小说出来),就一直坚持这种做法,感觉就象简单地让文字“流”到纸上。在我写与计算机有关的东西时,发现结构要比小说简单得多,所以不需要考虑太多这方面的问题。但我仍然制订了整个写作的结构,使自己对要写什么做到心中有数。因此,即使你的计划就是直接开始写程序,仍然需要经历以下的阶段,同时向自己提出一些特定的问题。 31 | 32 | 1.12.3 阶段1:要制作什么? 33 | 在上一代程序设计中(即“过程化或程序化设计”),这个阶段称为“建立需求分析和系统规格”。当然,那些操作今天已经不再需要了,或者至少改换了形式。大量令人头痛的文档资料已成为历史。但当时的初衷是好的。需求分析的意思是“建立一系列规则,根据它判断任务什么时候完成,以及客户怎样才能满意”。系统规格则表示“这里是一些具体的说明,让你知道程序需要做什么(而不是怎样做)才能满足要求”。需求分析实际就是你和客户之间的一份合约(即使客户就在本公司内部工作,或者是其他对象及系统)。系统规格是对所面临问题的最高级别的一种揭示,我们依据它判断任务是否完成,以及需要花多长的时间。由于这些都需要取得参与者的一致同意,所以我建议尽可能地简化它们——最好采用列表和基本图表的形式——以节省时间。可能还会面临另一些限制,需要把它们扩充成为更大的文档。 34 | 35 | 我们特别要注意将重点放在这一阶段的核心问题上,不要纠缠于细枝末节。这个核心问题就是:决定采用什么系统。对这个问题,最有价值的工具就是一个名为“使用条件”的集合。对那些采用“假如……,系统该怎样做?”形式的问题,这便是最有说服力的回答。例如,“假如客户需要提取一张现金支票,但当时又没有这么多的现金储备,那么自动取款机该怎样反应?”对这个问题,“使用条件”可以指示自动取款机在那种“条件”下的正确操作。 36 | 37 | 应尽可能总结出自己系统的一套完整的“使用条件”或者“应用场合”。一旦完成这个工作,就相当于摸清了想让系统完成的核心任务。由于将重点放在“使用条件”上,一个很好的效果就是它们总能让你放精力放在最关键的东西上,并防止自己分心于对完成任务关系不大的其他事情上面。也就是说,只要掌握了一套完整的“使用条件”,就可以对自己的系统作出清晰的描述,并转移到下一个阶段。在这一阶段,也有可能无法完全掌握系统日后的各种应用场合,但这也没有关系。只要肯花时间,所有问题都会自然而然暴露出来。不要过份在意系统规格的“完美”,否则也容易产生挫败感和焦燥情绪。 38 | 39 | 在这一阶段,最好用几个简单的段落对自己的系统作出描述,然后围绕它们再进行扩充,添加一些“名词”和“动词”。“名词”自然成为对象,而“动词”自然成为要整合到对象接口中的“方法”。只要亲自试着做一做,就会发现这是多么有用的一个工具;有些时候,它能帮助你完成绝大多数的工作。 40 | 41 | 尽管仍处在初级阶段,但这时的一些日程安排也可能会非常管用。我们现在对自己要构建的东西应该有了一个较全面的认识,所以可能已经感觉到了它大概会花多长的时间来完成。此时要考虑多方面的因素:如果估计出一个较长的日程,那么公司也许决定不再继续下去;或者一名主管已经估算出了这个项目要花多长的时间,并会试着影响你的估计。但无论如何,最好从一开始就草拟出一份“诚实”的时间表,以后再进行一些暂时难以作出的决策。目前有许多技术可帮助我们计算出准确的日程安排(就象那些预测股票市场起落的技术),但通常最好的方法还是依赖自己的经验和直觉(不要忘记,直觉也要建立在经验上)。感觉一下大概需要花多长的时间,然后将这个时间加倍,再加上10%。你的感觉可能是正确的;“也许”能在那个时间里完成。但“加倍”使那个时间更加充裕,“10%”的时间则用于进行最后的推敲和深化。但同时也要对此向上级主管作出适当的解释,无论对方有什么抱怨和修改,只要明确地告诉他们:这样的一个日程安排,只是我的一个估计! 42 | 43 | 1.12.4 阶段2:如何构建? 44 | 45 | 在这一阶段,必须拿出一套设计方案,并解释其中包含的各类对象在外观上是什么样子,以及相互间是如何沟通的。此时可考虑采用一种特殊的图表工具:“统一建模语言”(UML)。请到http://www.rational.com 去下载一份UML规格书。作为第1阶段中的描述工具,UML也是很有帮助的。此外,还可用它在第2阶段中处理一些图表(如流程图)。当然并非一定要使用UML,但它对你会很有帮助,特别是在希望描绘一张详尽的图表,让许多人在一起研究的时候。除UML外,还可选择对对象以及它们的接口进行文字化描述(就象我在《Thinking in C++》里说的那样,但这种方法非常原始,发挥的作用亦较有限。 46 | 47 | 我曾有一次非常成功的咨询经历,那时涉及到一小组人的初始设计。他们以前还没有构建过OOP(面向对象程序设计)项目,将对象画在白板上面。我们谈到各对象相互间该如何沟通(通信),并删除了其中的一部分,以及替换了另一部分对象。这个小组(他们知道这个项目的目的是什么)实际上已经制订出了设计方案;他们自己“拥有”了设计,而不是让设计自然而然地显露出来。我在那里做的事情就是对设计进行指导,提出一些适当的问题,尝试作出一些假设,并从小组中得到反馈,以便修改那些假设。这个过程中最美妙的事情就是整个小组并不是通过学习一些抽象的例子来进行面向对象的设计,而是通过实践一个真正的设计来掌握OOP的窍门,而那个设计正是他们当时手上的工作! 48 | 49 | 作出了对对象以及它们的接口的说明后,就完成了第2阶段的工作。当然,这些工作可能并不完全。有些工作可能要等到进入阶段3才能得知。但这已经足够了。我们真正需要关心的是最终找出所有的对象。能早些发现当然好,但OOP提供了足够完美的结构,以后再找出它们也不迟。 50 | 51 | 1.12.5 阶段3:开始创建 52 | 53 | 读这本书的可能是程序员,现在进入的正是你可能最感兴趣的阶段。由于手头上有一个计划——无论它有多么简要,而且在正式编码前掌握了正确的设计结构,所以会发现接下去的工作比一开始就埋头写程序要简单得多。而这正是我们想达到的目的。让代码做到我们想做的事情,这是所有程序项目最终的目标。但切不要急功冒进,否则只有得不偿失。根据我的经验,最后先拿出一套较为全面的方案,使其尽可能设想周全,能满足尽可能多的要求。给我的感觉,编程更象一门艺术,不能只是作为技术活来看待。所有付出最终都会得到回报。作为真正的程序员,这并非可有可无的一种素质。全面的思考、周密的准备、良好的构造不仅使程序更易构建与调试,也使其更易理解和维护,而那正是一套软件赢利的必要条件。 54 | 构建好系统,并令其运行起来后,必须进行实际检验,以前做的那些需求分析和系统规格便可派上用场了。全面地考察自己的程序,确定提出的所有要求均已满足。现在一切似乎都该结束了?是吗? 55 | 56 | 1.12.6 阶段4:校订 57 | 58 | 事实上,整个开发周期还没有结束,现在进入的是传统意义上称为“维护”的一个阶段。“维护”是一个比较暧昧的称呼,可用它表示从“保持它按设想的轨道运行”、“加入客户从前忘了声明的功能”或者更传统的“除掉暴露出来的一切臭虫”等等意思。所以大家对“维护”这个词产生了许多误解,有的人认为:凡是需要“维护”的东西,必定不是好的,或者是有缺陷的!因为这个词说明你实际构建的是一个非常“原始”的程序,以后需要频繁地作出改动、添加新的代码或者防止它的落后、退化等。因此,我们需要用一个更合理的词语来称呼以后需要继续的工作。 59 | 60 | 这个词便是“校订”。换言之,“你第一次做的东西并不完善,所以需为自己留下一个深入学习、认知的空间,再回过头去作一些改变”。对于要解决的问题,随着对它的学习和了解愈加深入,可能需要作出大量改动。进行这些工作的一个动力是随着不断的改革优化,终于能够从自己的努力中得到回报,无论这需要经历一个较短还是较长的时期。 61 | 62 | 什么时候才叫“达到理想的状态”呢?这并不仅仅意味着程序必须按要求的那样工作,并能适应各种指定的“使用条件”,它也意味着代码的内部结构应当尽善尽美。至少,我们应能感觉出整个结构都能良好地协调运作。没有笨拙的语法,没有臃肿的对象,也没有一些华而不实的东西。除此以外,必须保证程序结构有很强的生命力。由于多方面的原因,以后对程序的改动是必不可少。但必须确定改动能够方便和清楚地进行。这里没有花巧可言。不仅需要理解自己构建的是什么,也要理解程序如何不断地进化。幸运的是,面向对象的程序设计语言特别适合进行这类连续作出的修改——由对象建立起来的边界可有效保证结构的整体性,并能防范对无关对象进行的无谓干扰、破坏。也可以对自己的程序作一些看似激烈的大变动,同时不会破坏程序的整体性,不会波及到其他代码。事实上,对“校订”的支持是OOP非常重要的一个特点。 63 | 64 | 通过校订,可创建出至少接近自己设想的东西。然后从整体上观察自己的作品,把它与自己的要求比较,看看还短缺什么。然后就可以从容地回过头去,对程序中不恰当的部分进行重新设计和重新实现(注释⑩)。在最终得到一套恰当的方案之前,可能需要解决一些不能回避的问题,或者至少解决问题的一个方面。而且一般要多“校订”几次才行(“设计范式”在这里可起到很大的帮助作用。有关它的讨论,请参考本书第16章)。 65 | 66 | 构建一套系统时,“校订”几乎是不可避免的。我们需要不断地对比自己的需求,了解系统是否自己实际所需要的。有时只有实际看到系统,才能意识到自己需要解决一个不同的问题。若认为这种形式的校订必然会发生,那么最好尽快拿出自己的第一个版本,检查它是否自己希望的,使自己的思想不断趋向成熟。 67 | 68 | 反复的“校订”同“递增开发”有关密不可分的关系。递增开发意味着先从系统的核心入手,将其作为一个框架实现,以后要在这个框架的基础上逐渐建立起系统剩余的部分。随后,将准备提供的各种功能(特性)一个接一个地加入其中。这里最考验技巧的是架设起一个能方便扩充所有目标特性的一个框架(对这个问题,大家可参考第16章的论述)。这样做的好处在于一旦令核心框架运作起来,要加入的每一项特性就象它自身内的一个小项目,而非大项目的一部分。此外,开发或维护阶段合成的新特性可以更方便地加入。OOP之所以提供了对递增开发的支持,是由于假如程序设计得好,每一次递增都可以成为完善的对象或者对象组。 69 | 70 | ⑩:这有点类似“快速造型”。此时应着眼于建立一个简单、明了的版本,使自己能对系统有个清楚的把握。再把这个原型扔掉,并正式地构建一个。快速造型最麻烦的一种情况就是人们不将原型扔掉,而是直接在它的基础上建造。如果再加上程序化设计中“结构”的缺乏,就会导致一个混乱的系统,致使维护成本增加。 71 | 72 | 1.12.7 计划的回报 73 | 74 | 如果没有仔细拟定的设计图,当然不可能建起一所房子。如建立的是一所狗舍,尽管设计图可以不必那么详尽,但仍然需要一些草图,以做到心中有数。软件开发则完全不同,它的“设计图”(计划)必须详尽而完备。在很长的一段时间里,人们在他们的开发过程中并没有太多的结构,但那些大型项目很容易就会遭致失败。通过不断的摸索,人们掌握了数量众多的结构和详细资料。但它们的使用却使人提心吊胆在意——似乎需要把自己的大多数时间花在编写文档上,而没有多少时间来编程(经常如此)。我希望这里为大家讲述的一切能提供一条折衷的道路。需要采取一种最适合自己需要(以及习惯)的方法。不管制订出的计划有多么小,但与完全没有计划相比,一些形式的计划会极大改善你的项目。请记住:根据估计,没有计划的50%以上的项目都会失败! 75 | -------------------------------------------------------------------------------- /1.13 Java还是C++.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这个关键字,它为程序引入了一个全新的类型(clas和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 | ``` java 21 | Light lt = new Light(); 22 | lt.on(); 23 | ``` 24 | 25 | 在这个例子中,类型/类的名称是Light,可向Light对象发出的请求包括包括打开(on)、关闭(off)、变得更明亮(brighten)或者变得更暗淡(dim)。通过简单地声明一个名字(lt),我们为Light对象创建了一个“指针”。然后用new关键字新建类型为Light的一个对象。再用等号将其赋给指针。为了向对象发送一条消息,我们列出指针名(lt),再用一个句点符号(.)把它同消息名称(on)连接起来。从中可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单和直观的。 26 | 27 | -------------------------------------------------------------------------------- /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”相似,只是一个继承的类可访问受保护的成员,但不能访问私有成员。继承的问题不久就要谈到。 -------------------------------------------------------------------------------- /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 | 使用继承时,相当于创建了一个新类。这个新类不仅包含了现有类型的所有成员(尽管private成员被隐藏起来,且不能访问),但更重要的是,它复制了基础类的接口。也就是说,可向基础类的对象发送的所有消息亦可原样发给衍生类的对象。根据可以发送的消息,我们能知道类的类型。这意味着衍生类具有与基础类相同的类型!为真正理解面向对象程序设计的含义,首先必须认识到这种类型的等价关系。 8 | 9 | 由于基础类和衍生类具有相同的接口,所以那个接口必须进行特殊的设计。也就是说,对象接收到一条特定的消息后,必须有一个“方法”能够执行。若只是简单地继承一个类,并不做其他任何事情,来自基础类接口的方法就会直接照搬到衍生类。这意味着衍生类的对象不仅有相同的类型,也有同样的行为,这一后果通常是我们不愿见到的。 10 | 11 | 有两种做法可将新得的衍生类与原来的基础类区分开。第一种做法十分简单:为衍生类添加新函数(功能)。这些新函数并非基础类接口的一部分。进行这种处理时,一般都是意识到基础类不能满足我们的要求,所以需要添加更多的函数。这是一种最简单、最基本的继承用法,大多数时候都可完美地解决我们的问题。然而,事先还是要仔细调查自己的基础类是否真的需要这些额外的函数。 12 | 13 | 1.5.1 改善基础类 14 | 15 | 尽管extends关键字暗示着我们要为接口“扩展”新功能,但实情并非肯定如此。为区分我们的新类,第二个办法是改变基础类一个现有函数的行为。我们将其称作“改善”那个函数。 16 | 17 | 为改善一个函数,只需为衍生类的函数建立一个新定义即可。我们的目标是:“尽管使用的函数接口未变,但它的新版本具有不同的表现”。 18 | 19 | 1.5.2 等价与类似关系 20 | 21 | 针对继承可能会产生这样的一个争论:继承只能改善原基础类的函数吗?若答案是肯定的,则衍生类型就是与基础类完全相同的类型,因为都拥有完全相同的接口。这样造成的结果就是:我们完全能够将衍生类的一个对象换成基础类的一个对象!可将其想象成一种“纯替换”。在某种意义上,这是进行继承的一种理想方式。此时,我们通常认为基础类和衍生类之间存在一种“等价”关系——因为我们可以理直气壮地说:“圆就是一种几何形状”。为了对继承进行测试,一个办法就是看看自己是否能把它们套入这种“等价”关系中,看看是否有意义。 22 | 23 | 但在许多时候,我们必须为衍生类型加入新的接口元素。所以不仅扩展了接口,也创建了一种新类型。这种新类型仍可替换成基础类型,但这种替换并不是完美的,因为不可在基础类里访问新函数。我们将其称作“类似”关系;新类型拥有旧类型的接口,但也包含了其他函数,所以不能说它们是完全等价的。举个例子来说,让我们考虑一下制冷机的情况。假定我们的房间连好了用于制冷的各种控制器;也就是说,我们已拥有必要的“接口”来控制制冷。现在假设机器出了故障,我们把它换成一台新型的冷、热两用空调,冬天和夏天均可使用。冷、热空调“类似”制冷机,但能做更多的事情。由于我们的房间只安装了控制制冷的设备,所以它们只限于同新机器的制冷部分打交道。新机器的接口已得到了扩展,但现有的系统并不知道除原始接口以外的任何东西。 24 | 25 | 认识了等价与类似的区别后,再进行替换时就会有把握得多。尽管大多数时候“纯替换”已经足够,但您会发现在某些情况下,仍然有明显的理由需要在衍生类的基础上增添新功能。通过前面对这两种情况的讨论,相信大家已心中有数该如何做。 26 | -------------------------------------------------------------------------------- /1.6 多态对象的互换使用.md: -------------------------------------------------------------------------------- 1 | # 1.6 多态对象的互换使用 2 | 3 | 4 | 通常,继承最终会以创建一系列类收场,所有类都建立在统一的接口基础上。我们用一幅颠倒的树形图来阐明这一点(注释⑤): 5 | 6 | ⑤:这儿采用了“统一记号法”,本书将主要采用这种方法。 7 | 8 | ![](1-2.gif) 9 | 10 | 对这样的一系列类,我们要进行的一项重要处理就是将衍生类的对象当作基础类的一个对象对待。这一点是非常重要的,因为它意味着我们只需编写单一的代码,令其忽略类型的特定细节,只与基础类打交道。这样一来,那些代码就可与类型信息分开。所以更易编写,也更易理解。此外,若通过继承增添了一种新类型,如“三角形”,那么我们为“几何形状”新类型编写的代码会象在旧类型里一样良好地工作。所以说程序具备了“扩展能力”,具有“扩展性”。 11 | 以上面的例子为基础,假设我们用Java写了这样一个函数: 12 | 13 | ``` java 14 | void doStuff(Shape s) { 15 | s.erase(); 16 | // ... 17 | s.draw(); 18 | } 19 | ``` 20 | 21 | 这个函数可与任何“几何形状”(Shape)通信,所以完全独立于它要描绘(draw)和删除(erase)的任何特定类型的对象。如果我们在其他一些程序里使用doStuff()函数: 22 | 23 | ``` java 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 | ``` java 36 | doStuff(c); 37 | ``` 38 | 39 | 此时,一个Circle(圆)指针传递给一个本来期待Shape(形状)指针的函数。由于圆是一种几何形状,所以doStuff()能正确地进行处理。也就是说,凡是doStuff()能发给一个Shape的消息,Circle也能接收。所以这样做是安全的,不会造成错误。 40 | 我们将这种把衍生类型当作它的基本类型处理的过程叫作“Upcasting”(上溯造型)。其中,“cast”(造型)是指根据一个现成的模型创建;而“Up”(向上)表明继承的方向是从“上面”来的——即基础类位于顶部,而衍生类在下方展开。所以,根据基础类进行造型就是一个从上面继承的过程,即“Upcasting”。 41 | 42 | 在面向对象的程序里,通常都要用到上溯造型技术。这是避免去调查准确类型的一个好办法。请看看doStuff()里的代码: 43 | 44 | ``` java 45 | s.erase(); 46 | // ... 47 | s.draw(); 48 | ``` 49 | 50 | 注意它并未这样表达:“如果你是一个Circle,就这样做;如果你是一个Square,就那样做;等等”。若那样编写代码,就需检查一个Shape所有可能的类型,如圆、矩形等等。这显然是非常麻烦的,而且每次添加了一种新的Shape类型后,都要相应地进行修改。在这儿,我们只需说:“你是一种几何形状,我知道你能将自己删掉,即erase();请自己采取那个行动,并自己去控制所有的细节吧。” 51 | 52 | 1.6.1 动态绑定 53 | 54 | 在doStuff()的代码里,最让人吃惊的是尽管我们没作出任何特殊指示,采取的操作也是完全正确和恰当的。我们知道,为Circle调用draw()时执行的代码与为一个Square或Line调用draw()时执行的代码是不同的。但在将draw()消息发给一个匿名Shape时,根据Shape指针当时连接的实际类型,会相应地采取正确的操作。这当然令人惊讶,因为当Java编译器为doStuff()编译代码时,它并不知道自己要操作的准确类型是什么。尽管我们确实可以保证最终会为Shape调用erase(),为Shape调用draw(),但并不能保证为特定的Circle,Square或者Line调用什么。然而最后采取的操作同样是正确的,这是怎么做到的呢? 55 | 56 | 将一条消息发给对象时,如果并不知道对方的具体类型是什么,但采取的行动同样是正确的,这种情况就叫作“多态性”(Polymorphism)。对面向对象的程序设计语言来说,它们用以实现多态性的方法叫作“动态绑定”。编译器和运行期系统会负责对所有细节的控制;我们只需知道会发生什么事情,而且更重要的是,如何利用它帮助自己设计程序。 57 | 58 | 有些语言要求我们用一个特殊的关键字来允许动态绑定。在C++中,这个关键字是virtual。在Java中,我们则完全不必记住添加一个关键字,因为函数的动态绑定是自动进行的。所以在将一条消息发给对象时,我们完全可以肯定对象会采取正确的行动,即使其中涉及上溯造型之类的处理。 59 | 60 | 1.6.2 抽象的基础类和接口 61 | 62 | 设计程序时,我们经常都希望基础类只为自己的衍生类提供一个接口。也就是说,我们不想其他任何人实际创建基础类的一个对象,只对上溯造型成它,以便使用它们的接口。为达到这个目的,需要把那个类变成“抽象”的——使用abstract关键字。若有人试图创建抽象类的一个对象,编译器就会阻止他们。这种工具可有效强制实行一种特殊的设计。 63 | 64 | 亦可用abstract关键字描述一个尚未实现的方法——作为一个“根”使用,指出:“这是适用于从这个类继承的所有类型的一个接口函数,但目前尚没有对它进行任何形式的实现。”抽象方法也许只能在一个抽象类里创建。继承了一个类后,那个方法就必须实现,否则继承的类也会变成“抽象”类。通过创建一个抽象方法,我们可以将一个方法置入接口中,不必再为那个方法提供可能毫无意义的主体代码。 65 | 66 | interface(接口)关键字将抽象类的概念更延伸了一步,它完全禁止了所有的函数定义。“接口”是一种相当有效和常用的工具。另外如果自己愿意,亦可将多个接口都合并到一起(不能从多个普通class或abstract class中继承)。 67 | -------------------------------------------------------------------------------- /1.9 多线程.md: -------------------------------------------------------------------------------- 1 | # 1.9 多线程 2 | 3 | 在计算机编程中,一个基本的概念就是同时对多个任务加以控制。许多程序设计问题都要求程序能够停下手头的工作,改为处理其他一些问题,再返回主进程。可以通过多种途径达到这个目的。最开始的时候,那些拥有机器低级知识的程序员编写一些“中断服务例程”,主进程的暂停是通过硬件级的中断实现的。尽管这是一种有用的方法,但编出的程序很难移植,由此造成了另一类的代价高昂问题。 4 | 5 | 有些时候,中断对那些实时性很强的任务来说是很有必要的。但还存在其他许多问题,它们只要求将问题划分进入独立运行的程序片断中,使整个程序能更迅速地响应用户的请求。在一个程序中,这些独立运行的片断叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。多线程处理一个常见的例子就是用户界面。利用线程,用户可按下一个按钮,然后程序会立即作出响应,而不是让用户等待程序完成了当前任务以后才开始响应。 6 | 7 | 最开始,线程只是用于分配单个处理器的处理时间的一种工具。但假如操作系统本身支持多个处理器,那么每个线程都可分配给一个不同的处理器,真正进入“并行运算”状态。从程序设计语言的角度看,多线程操作最有价值的特性之一就是程序员不必关心到底使用了多少个处理器。程序在逻辑意义上被分割为数个线程;假如机器本身安装了多个处理器,那么程序会运行得更快,毋需作出任何特殊的调校。 8 | 9 | 根据前面的论述,大家可能感觉线程处理非常简单。但必须注意一个问题:共享资源!如果有多个线程同时运行,而且它们试图访问相同的资源,就会遇到一个问题。举个例子来说,两个进程不能将信息同时发送给一台打印机。为解决这个问题,对那些可共享的资源来说(比如打印机),它们在使用期间必须进入锁定状态。所以一个线程可将资源锁定,在完成了它的任务后,再解开(释放)这个锁,使其他线程可以接着使用同样的资源。 10 | 11 | Java的多线程机制已内建到语言中,这使一个可能较复杂的问题变得简单起来。对多线程处理的支持是在对象这一级支持的,所以一个执行线程可表达为一个对象。Java也提供了有限的资源锁定方案。它能锁定任何对象占用的内存(内存实际是多种共享资源的一种),所以同一时间只能有一个线程使用特定的内存空间。为达到这个目的,需要使用synchronized关键字。其他类型的资源必须由程序员明确锁定,这通常要求程序员创建一个对象,用它代表一把锁,所有线程在访问那个资源时都必须检查这把锁。 12 | -------------------------------------------------------------------------------- /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 | ``` java 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 | ByteArrayInputStream 允许内存中的一个缓冲区作为InputStream使用 从中提取字节的缓冲区/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口 60 | 61 | StringBufferInputStream 将一个String转换成InputStream 一个String(字串)。基础的实施方案实际采用一个 62 | 63 | StringBuffer(字串缓冲)/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口 64 | 65 | FileInputStream 用于从文件读取信息 代表文件名的一个String,或者一个File或FileDescriptor对象/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口 66 | 67 | ``` java 68 | Piped-InputStream 69 | 70 | Produces the data that’s being written to the associated PipedOutput-Stream. Implements the “piping” concept. 71 | 72 | PipedOutputStream 73 | 74 | As a source of data in multithreading. Connect it to a FilterInputStream object to provide a useful interface. 75 | 76 | Sequence-InputStream 77 | 78 | Coverts two or more InputStream objects into a single InputStream. 79 | 80 | Two InputStream objects or an Enumeration for a container of InputStream objects. 81 | 82 | As a source of data. Connect it to a FilterInputStream object to provide a useful interface. 83 | 84 | Filter-InputStream 85 | 86 | Abstract class which is an interface for decorators that provide useful functionality to the other InputStream classes. See Table 10-3. 87 | 88 | See Table 10-3. 89 | 90 | See Table 10-3. 91 | ``` 92 | 93 | 94 | PipedInputString 产生为相关的PipedOutputStream写的数据。实现了“管道化”的概念 PipedOutputStream/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口 95 | 96 | SequenceInputStream 将两个或更多的InputStream对象转换成单个InputStream使用 两个InputStream对象或者一个Enumeration,用于InputStream对象的一个容器/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口 97 | 98 | FilterInputStream 对作为破坏器接口使用的类进行抽象;那个破坏器为其他InputStream类提供了有用的功能。参见表10.3 参见表10.3/参见表10.3 99 | 100 | 10.1.2 OutputStream的类型 101 | 102 | 这一类别包括的类决定了我们的输入往何处去:一个字节数组(但没有String;假定我们可用字节数组创建一个);一个文件;或者一个“管道”。 103 | 104 | 除此以外,FilterOutputStream为“破坏器”类提供了一个基础类,它将属性或者有用的接口同输出流连接起来。这将在以后讨论。 105 | 106 | 表10.2 OutputStream的类型 107 | 108 | ``` java 109 | Class 110 | 111 | Function 112 | 113 | Constructor Arguments 114 | 115 | How to use it 116 | 117 | ByteArray-OutputStream 118 | 119 | Creates a buffer in memory. All the data that you send to the stream is placed in this buffer. 120 | 121 | Optional initial size of the buffer. 122 | 123 | To designate the destination of your data. Connect it to a FilterOutputStream object to provide a useful interface. 124 | 125 | File-OutputStream 126 | 127 | For sending information to a file. 128 | 129 | A String representing the file name, or a File or FileDescriptor object. 130 | 131 | To designate the destination of your data. Connect it to a FilterOutputStream object to provide a useful interface. 132 | 133 | Piped-OutputStream 134 | 135 | Any information you write to this automatically ends up as input for the associated PipedInput-Stream. Implements the “piping” concept. 136 | 137 | PipedInputStream 138 | 139 | To designate the destination of your data for multithreading. Connect it to a FilterOutputStream object to provide a useful interface. 140 | 141 | Filter-OutputStream 142 | 143 | Abstract class which is an interface for decorators that provide useful functionality to the other OutputStream classes. See Table 144 | 10-4. 145 | 146 | See Table 10-4. 147 | 148 | See Table 10-4. 149 | ``` 150 | 151 | 类 功能 构造器参数/如何使用 152 | 153 | ByteArrayOutputStream 在内存中创建一个缓冲区。我们发送给流的所有数据都会置入这个缓冲区。 可选缓冲区的初始大小/ 154 | 用于指出数据的目的地。若将其同FilterOutputStream对象连接到一起,可提供一个有用的接口 155 | 156 | FileOutputStream 将信息发给一个文件 用一个String代表文件名,或选用一个File或FileDescriptor对象/用于指出数据的目的地。若将其同FilterOutputStream对象连接到一起,可提供一个有用的接口 157 | 158 | PipedOutputStream 我们写给它的任何信息都会自动成为相关的PipedInputStream的输出。实现了“管道化”的概念 PipedInputStream/为多线程处理指出自己数据的目的地/将其同FilterOutputStream对象连接到一起,便可提供一个有用的接口 159 | 160 | FilterOutputStream 对作为破坏器接口使用的类进行抽象处理;那个破坏器为其他OutputStream类提供了有用的功能。参见表10.4 参见表10.4/参见表10.4 161 | -------------------------------------------------------------------------------- /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 | IO流库易使我们混淆一些概念。它确实能做许多事情,而且也可以移植。但假如假如事先没有吃透装饰器方案的概念,那么所有的设计都多少带有一点盲目性质。所以不管学它还是教它,都要特别花一些功夫才行。而且它并不完整:没有提供对输出格式化的支持,而其他几乎所有语言的IO包都提供了这方面的支持(这一点没有在Java 1.1里得以纠正,它完全错失了改变库设计方案的机会,反而增添了更特殊的一些情况,使复杂程度进一步提高)。Java 1.1转到那些尚未替换的IO库,而不是增加新库。而且库的设计人员似乎没有很好地指出哪些特性是不赞成的,哪些是首选的,造成库设计中经常都会出现一些令人恼火的反对消息。 7 | 8 | 然而,一旦掌握了装饰器方案,并开始在一些较为灵活的环境使用库,就会认识到这种设计的好处。到那个时候,为此多付出的代码行应该不至于使你觉得太生气。 9 | -------------------------------------------------------------------------------- /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()内部类有一个硬编码的事件集。请修改这个程序,使其能从一个文本文件里动态读取事件以及它们的相关时间。 -------------------------------------------------------------------------------- /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 | 剩下的类用于修改InputStream的内部行为方式:是否进行缓冲,是否跟踪自己读入的数据行,以及是否能够推回一个字符等等。后两种类看起来特别象提供对构建一个编译器的支持(换言之,添加它们为了支持Java编译器的构建),所以在常规编程中一般都用不着它们。 14 | 15 | 也许几乎每次都要缓冲自己的输入,无论连接的是哪个IO设备。所以IO库最明智的做法就是将未缓冲输入作为一种特殊情况处理,同时将缓冲输入接纳为标准做法。 16 | 17 | 表10.3 FilterInputStream的类型 18 | 19 | 20 | Class 21 | 22 | Function 23 | 24 | Constructor Arguments 25 | 26 | How to use it 27 | 28 | Data-InputStream 29 | 30 | Used in concert with DataOutputStream, so you can read primitives (int, char, long, etc.) from a stream in a portable fashion. 31 | 32 | InputStream 33 | 34 | Contains a full interface to allow you to read primitive types. 35 | 36 | 37 | Buffered-InputStream 38 | 39 | Use this to prevent a physical read every time you want more data. You’re saying “Use a buffer.” 40 | 41 | InputStream, with optional buffer size. 42 | 43 | This doesn’t provide an interface per se, just a requirement that a buffer be used. Attach an interface object. 44 | 45 | LineNumber-InputStream 46 | 47 | Keeps track of line numbers in the input stream; you can call getLineNumber( ) and setLineNumber(int). 48 | 49 | InputStream 50 | 51 | This just adds line numbering, so you’ll probably attach an interface object. 52 | 53 | Pushback-InputStream 54 | 55 | Has a one byte push-back buffer so that you can push back the last character read. 56 | 57 | InputStream 58 | 59 | Generally used in the scanner for a compiler and probably included because the Java compiler needed it. You probably won’t use this. 60 | 61 | 62 | 类 功能 构造器参数/如何使用 63 | 64 | DataInputStream 与DataOutputStream联合使用,使自己能以机动方式读取一个流中的基本数据类型(int,char,long等等) InputStream/包含了一个完整的接口,以便读取基本数据类型 65 | 66 | BufferedInputStream 避免每次想要更多数据时都进行物理性的读取,告诉它“请先在缓冲区里找” InputStream,没有可选的缓冲区大小/本身并不能提供一个接口,只是发出使用缓冲区的要求。要求同一个接口对象连接到一起 67 | 68 | LineNumberInputStream 跟踪输入流中的行号;可调用getLineNumber()以及setLineNumber(int) 只是添加对数据行编号的能力,所以可能需要同一个真正的接口对象连接 69 | 70 | PushbackInputStream 有一个字节的后推缓冲区,以便后推读入的上一个字符 InputStream/通常由编译器在扫描器中使用,因为Java编译器需要它。一般不在自己的代码中使用 71 | 72 | 10.2.2 通过FilterOutputStream向OutputStream里写入数据 73 | 74 | 与DataInputStream对应的是DataOutputStream,后者对各个基本数据类型以及String对象进行格式化,并将其置入一个数据“流”中,以便任何机器上的DataInputStream都能正常地读取它们。所有方法都以“wirte”开头,例如writeByte(),writeFloat()等等。 75 | 76 | 若想进行一些真正的格式化输出,比如输出到控制台,请使用PrintStream。利用它可以打印出所有基本数据类型以及String对象,并可采用一种易于查看的格式。这与DataOutputStream正好相反,后者的目标是将那些数据置入一个数据流中,以便DataInputStream能够方便地重新构造它们。System.out静态对象是一个PrintStream。 77 | 78 | PrintStream内两个重要的方法是print()和println()。它们已进行了覆盖处理,可打印出所有数据类型。print()和println()之间的差异是后者在操作完毕后会自动添加一个新行。 79 | 80 | BufferedOutputStream属于一种“修改器”,用于指示数据流使用缓冲技术,使自己不必每次都向流内物理性地写入数据。通常都应将它应用于文件处理和控制器IO。 81 | 表10.4 FilterOutputStream的类型 82 | 83 | 84 | Class 85 | 86 | Function 87 | 88 | Constructor Arguments 89 | 90 | How to use it 91 | 92 | Data-OutputStream 93 | 94 | Used in concert with DataInputStream so you can write primitives (int, char, long, etc.) to a stream in a portable fashion. 95 | 96 | OutputStream 97 | 98 | Contains full interface to allow you to write primitive types. 99 | 100 | PrintStream 101 | 102 | For producing formatted output. While DataOutputStream handles the storage of data, PrintStream handles display. 103 | 104 | OutputStream, with optional boolean indicating that the buffer is flushed with every newline. 105 | 106 | Should be the “final” wrapping for your OutputStream object. You’ll probably use this a lot. 107 | 108 | Buffered-OutputStream 109 | 110 | 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. 111 | 112 | OutputStream, with optional buffer size. 113 | 114 | This doesn’t provide an interface per se, just a requirement that a buffer is used. Attach an interface object. 115 | 116 | 117 | 类 功能 构造器参数/如何使用 118 | 119 | DataOutputStream 与DataInputStream配合使用,以便采用方便的形式将基本数据类型(int,char,long等)写入一个数据流 OutputStream/包含了完整接口,以便我们写入基本数据类型 120 | 121 | PrintStream 用于产生格式化输出。DataOutputStream控制的是数据的“存储”,而PrintStream控制的是“显示” 122 | 123 | OutputStream,可选一个布尔参数,指示缓冲区是否与每个新行一同刷新/对于自己的OutputStream对象,应该用“final”将其封闭在内。可能经常都要用到它 124 | 125 | BufferedOutputStream 用它避免每次发出数据的时候都要进行物理性的写入,要求它“请先在缓冲区里找”。可调用flush(),对缓冲区进行刷新 OutputStream,可选缓冲区大小/本身并不能提供一个接口,只是发出使用缓冲区的要求。需要同一个接口对象连接到一起 126 | -------------------------------------------------------------------------------- /10.3 本身的缺陷:RandomAccessFile.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 | -------------------------------------------------------------------------------- /11-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/11-1.gif -------------------------------------------------------------------------------- /11.2 RTTI语法.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 | ``` java 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 | ``` java 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 | 但为了利用多态性,要求我们拥有对基础类定义的控制权,因为有些时候在程序范围之内,可能发现基础类并未包括我们想要的方法。若基础类来自一个库,或者由别的什么东西控制着,RTTI便是一种很好的解决方案:可继承一个新类型,然后添加自己的额外方法。在代码的其他地方,可以侦测自己的特定类型,并调用那个特殊的方法。这样做不会破坏多态性以及程序的扩展能力,因为新类型的添加不要求查找程序中的switch语句。但在需要新特性的主体中添加新代码时,就必须用RTTI侦测自己特定的类型。 5 | 6 | 从某个特定类的利益的角度出发,在基础类里加入一个特性后,可能意味着从那个基础类衍生的其他所有类都必须获得一些无意义的“鸡肋”。这使得接口变得含义模糊。若有人从那个基础类继承,且必须覆盖抽象方法,这一现象便会使他们陷入困扰。比如现在用一个类结构来表示乐器(Instrument)。假定我们想清洁管弦乐队中所有适当乐器的通气音栓(Spit Valve),此时的一个办法是在基础类Instrument中置入一个ClearSpitValve()方法。但这样做会造成一个误区,因为它暗示着打击乐器和电子乐器中也有音栓。针对这种情况,RTTI提供了一个更合理的解决方案,可将方法置入特定的类中(此时是Wind,即“通气口”)——这样做是可行的。但事实上一种更合理的方案是将prepareInstrument()置入基础类中。初学者刚开始时往往看不到这一点,一般会认定自己必须使用RTTI。 7 | 8 | 最后,RTTI有时能解决效率问题。若代码大量运用了多态性,但其中的一个对象在执行效率上很有问题,便可用RTTI找出那个类型,然后写一段适当的代码,改进其效率。 9 | -------------------------------------------------------------------------------- /11.5 练习.md: -------------------------------------------------------------------------------- 1 | # 11.5 练习 2 | 3 | (1) 写一个方法,向它传递一个对象,循环打印出对象层次结构中的所有类。 4 | 5 | (2) 在ToyTest.java中,将Toy的默认构造器标记成注释信息,解释随之发生的事情。 6 | 7 | (3) 新建一种类型的集合,令其使用一个Vector。捕获置入其中的第一个对象的类型,然后从那时起只允许用户插入那种类型的对 8 | 象。 9 | 10 | (4) 写一个程序,判断一个Char数组属于基本数据类型,还是一个真正的对象。 11 | 12 | (5) 根据本章的说明,实现clearSpitValve()。 13 | 14 | (6) 实现本章介绍的rotate(Shape)方法,令其检查是否已经旋转了一个圆(若已旋转,就不再执行旋转操作)。 -------------------------------------------------------------------------------- /12.1 传递指针.md: -------------------------------------------------------------------------------- 1 | # 12.1 传递指针 2 | 3 | 12.1 传递指针 4 | 将指针传递进入一个方法时,指向的仍然是相同的对象。一个简单的实验可以证明这一点(若执行这个程序时有麻烦,请参考第3章3.1.2小节“赋值”): 5 | //: PassHandles.java 6 | // Passing handles around 7 | package c12; 8 | 9 | public class PassHandles { 10 | static void f(PassHandles h) { 11 | System.out.println("h inside f(): " + h); 12 | } 13 | public static void main(String[] args) { 14 | PassHandles p = new PassHandles(); 15 | System.out.println("p inside main(): " + p); 16 | f(p); 17 | } 18 | } ///:~ 19 | 20 | toString方法会在打印语句里自动调用,而PassHandles直接从Object继承,没有toString的重新定义。因此,这里会采用toString的Object版本,打印出对象的类,接着是那个对象所在的位置(不是指针,而是对象的实际存储位置)。输出结果如下: 21 | p inside main(): PassHandles@1653748 22 | h inside f() : PassHandles@1653748 23 | 可以看到,无论p还是h引用的都是同一个对象。这比复制一个新的PassHandles对象有效多了,使我们能将一个参数发给一个方法。但这样做也带来了另一个重要的问题。 24 | 25 | 12.1.1 别名问题 26 | “别名”意味着多个指针都试图指向同一个对象,就象前面的例子展示的那样。若有人向那个对象里写入一点什么东西,就会产生别名问题。若其他指针的所有者不希望那个对象改变,恐怕就要失望了。这可用下面这个简单的例子说明: 27 | //: Alias1.java 28 | // Aliasing two handles to one object 29 | 30 | public class Alias1 { 31 | int i; 32 | Alias1(int ii) { i = ii; } 33 | public static void main(String[] args) { 34 | Alias1 x = new Alias1(7); 35 | Alias1 y = x; // Assign the handle 36 | System.out.println("x: " + x.i); 37 | System.out.println("y: " + y.i); 38 | System.out.println("Incrementing x"); 39 | x.i++; 40 | System.out.println("x: " + x.i); 41 | System.out.println("y: " + y.i); 42 | } 43 | } ///:~ 44 | 45 | 对下面这行: 46 | Alias1 y = x; // Assign the handle 47 | 它会新建一个Alias1指针,但不是把它分配给由new创建的一个新鲜对象,而是分配给一个现有的指针。所以指针x的内容——即对象x指向的地址——被分配给y,所以无论x还是y都与相同的对象连接起来。这样一来,一旦x的i在下述语句中增值: 48 | x.i++; 49 | y的i值也必然受到影响。从最终的输出就可以看出: 50 | x: 7 51 | y: 7 52 | Incrementing x 53 | x: 8 54 | y: 8 55 | 56 | 此时最直接的一个解决办法就是干脆不这样做:不要有意将多个指针指向同一个作用域内的同一个对象。这样做可使代码更易理解和调试。然而,一旦准备将指针作为一个自变量或参数传递——这是Java设想的正常方法——别名问题就会自动出现,因为创建的本地指针可能修改“外部对象”(在方法作用域之外创建的对象)。下面是一个例子: 57 | //: Alias2.java 58 | // Method calls implicitly alias their 59 | // arguments. 60 | 61 | public class Alias2 { 62 | int i; 63 | Alias2(int ii) { i = ii; } 64 | static void f(Alias2 handle) { 65 | handle.i++; 66 | } 67 | public static void main(String[] args) { 68 | Alias2 x = new Alias2(7); 69 | System.out.println("x: " + x.i); 70 | System.out.println("Calling f(x)"); 71 | f(x); 72 | System.out.println("x: " + x.i); 73 | } 74 | } ///:~ 75 | 76 | 输出如下: 77 | x: 7 78 | Calling f(x) 79 | x: 8 80 | 81 | 方法改变了自己的参数——外部对象。一旦遇到这种情况,必须判断它是否合理,用户是否愿意这样,以及是不是会造成问题。 82 | 通常,我们调用一个方法是为了产生返回值,或者用它改变为其调用方法的那个对象的状态(方法其实就是我们向那个对象“发一条消息”的方式)。很少需要调用一个方法来处理它的参数;这叫作利用方法的“副作用”(Side Effect)。所以倘若创建一个会修改自己参数的方法,必须向用户明确地指出这一情况,并警告使用那个方法可能会有的后果以及它的潜在威胁。由于存在这些混淆和缺陷,所以应该尽量避免改变参数。 83 | 若需在一个方法调用期间修改一个参数,且不打算修改外部参数,就应在自己的方法内部制作一个副本,从而保护那个参数。本章的大多数内容都是围绕这个问题展开的。 84 | -------------------------------------------------------------------------------- /12.5 总结.md: -------------------------------------------------------------------------------- 1 | # 12.5 总结 2 | 3 | 4 | 12.5 总结 5 | 由于Java中的所有东西都是指针,而且由于每个对象都是在内存堆中创建的——只有不再需要的时候,才会当作垃圾收集掉,所以对象的操作方式发生了变化,特别是在传递和返回对象的时候。举个例子来说,在C和C++中,如果想在一个方法里初始化一些存储空间,可能需要请求用户将那片存储区域的地址传递进入方法。否则就必须考虑由谁负责清除那片区域。因此,这些方法的接口和对它们的理解就显得要复杂一些。但在Java中,根本不必关心由谁负责清除,也不必关心在需要一个对象的时候它是否仍然存在。因为系统会为我们照料一切。我们的程序可在需要的时候创建一个对象。而且更进一步地,根本不必担心那个对象的传输机制的细节:只需简单地传递指针即可。有些时候,这种简化非常有价值,但另一些时候却显得有些多余。 6 | 可从两个方面认识这一机制的缺点: 7 | (1) 肯定要为额外的内存管理付出效率上的损失(尽管损失不大),而且对于运行所需的时间,总是存在一丝不确定的因素(因为在内存不够时,垃圾收集器可能会被强制采取行动)。对大多数应用来说,优点显得比缺点重要,而且部分对时间要求非常苛刻的段落可以用native方法写成(参见附录A)。 8 | (2) 别名处理:有时会不慎获得指向同一个对象的两个指针。只有在这两个指针都假定指向一个“明确”的对象时,才有可能产生问题。对这个问题,必须加以足够的重视。而且应该尽可能地“克隆”一个对象,以防止另一个指针被不希望的改动影响。除此以外,可考虑创建“不可变”对象,使它的操作能返回同种类型或不同种类型的一个新对象,从而提高程序的执行效率。但千万不要改变原始对象,使对那个对象别名的其他任何方面都感觉不出变化。 9 | 10 | 有些人认为Java的克隆是一个笨拙的家伙,所以他们实现了自己的克隆方案(注释⑤),永远杜绝调用Object.clone()方法,从而消除了实现Cloneable和捕获CloneNotSupportException异常的需要。这一做法是合理的,而且由于clone()在Java标准库中很少得以支持,所以这显然也是一种“安全”的方法。只要不调用Object.clone(),就不必实现Cloneable或者捕获异常,所以那看起来也是能够接受的。 11 | 12 | ⑤:Doug Lea特别重视这个问题,并把这个方法推荐给了我,他说只需为每个类都创建一个名为duplicate()的函数即可。 13 | 14 | Java中一个有趣的关键字是byvalue(按值),它属于那些“保留但未实现”的关键字之一。在理解了别名和克隆问题以后,大家可以想象byvalue最终有一天会在Java中用于实现一种自动化的本地副本。这样做可以解决更多复杂的克隆问题,并使这种情况下的编写的代码变得更加简单和健壮。 15 | -------------------------------------------------------------------------------- /12.6 练习.md: -------------------------------------------------------------------------------- 1 | # 12.6 练习 2 | 3 | 4 | 12.6 练习 5 | (1) 创建一个myString类,在其中包含了一个String对象,以便用在构造器中用构造器的自变量对其进行初始化。添加一个toString()方法以及一个concatenate()方法,令其将一个String对象追加到我们的内部字串。在myString中实现clone()。创建两个static方法,每个都取得一个myString x指针作为自己的自变量,并调用x.concatenate("test")。但在第二个方法中,请首先调用clone()。测试这两个方法,观察它们不同的结果。 6 | (2) 创建一个名为Battery(电池)的类,在其中包含一个int,用它表示电池的编号(采用独一无二的标识符的形式)。接下来,创建一个名为Toy的类,其中包含了一个Battery数组以及一个toString,用于打印出所有电池。为Toy写一个clone()方法,令其自动关闭所有Battery对象。克隆Toy并打印出结果,完成对它的测试。 7 | (3) 修改CheckCloneable.java,使所有clone()方法都能捕获CloneNotSupportException异常,而不是把它直接传递给调用者。 8 | (4) 修改Compete.java,为Thing2和Thing4类添加更多的成员对象,看看自己是否能判断计时随复杂性变化的规律——是一种简单的线性关系,还是看起来更加复杂。 9 | (5) 从Snake.java开始,创建Snake的一个深层复制版本。 -------------------------------------------------------------------------------- /14.6 总结.md: -------------------------------------------------------------------------------- 1 | # 14.6 总结 2 | 3 | 4 | 14.6 总结 5 | 6 | 何时使用多线程技术,以及何时避免用它,这是我们需要掌握的重要课题。骼它的主要目的是对大量任务进行有序的管理。通过多个任务的混合使用,可以更有效地利用计算机资源,或者对用户来说显得更方便。资源均衡的经典问题是在IO等候期间如何利用CPU。至于用户方面的方便性,最经典的问题就是如何在一个长时间的下载过程中监视并灵敏地反应一个“停止”(stop)按钮的按下。 7 | 多线程的主要缺点包括: 8 | 9 | (1) 等候使用共享资源时造成程序的运行速度变慢。 10 | 11 | (2) 对线程进行管理要求的额外CPU开销。 12 | 13 | (3) 复杂程度无意义的加大,比如用独立的线程来更新数组内每个元素的愚蠢主意。 14 | 15 | (4) 漫长的等待、浪费精力的资源竞争以及死锁等多线程症状。 16 | 17 | 线程另一个优点是它们用“轻度”执行切换(100条指令的顺序)取代了“重度”进程场景切换(1000条指令)。由于一个进程内的所有线程共享相同的内存空间,所以“轻度”场景切换只改变程序的执行和本地变量。而在“重度”场景切换时,一个进程的改变要求必须完整地交换内存空间。 18 | 线程处理看来好象进入了一个全新的领域,似乎要求我们学习一种全新的程序设计语言——或者至少学习一系列新的语言概念。由于大多数微机操作系统都提供了对线程的支持,所以程序设计语言或者库里也出现了对线程的扩展。不管在什么情况下,涉及线程的程序设计: 19 | 20 | (1) 刚开始会让人摸不着头脑,要求改换我们传统的编程思路; 21 | 22 | (2) 其他语言对线程的支持看来是类似的。所以一旦掌握了线程的概念,在其他环境也不会有太大的困难。尽管对线程的支持使Java语言的复杂程度多少有些增加,但请不要责怪Java。毕竟,利用线程可以做许多有益的事情。 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 | 14.7 练习 5 | 6 | (1) 从Thread继承一个类,并(重载)覆盖run()方法。在run()内,打印出一条消息,然后调用sleep()。重复三遍这些操作,然后从run()返回。在构造器中放置一条启动消息,并覆盖finalize(),打印一条关闭消息。创建一个独立的线程类,使它在run()内调用System.gc()和System.runFinalization(),并打印一条消息,表明调用成功。创建这两种类型的几个线程,然后运行它们,看看会发生什么。 7 | 8 | (2) 修改Counter2.java,使线程成为一个内部类,而且不需要明确保存指向Counter2的一个。 9 | 10 | (3) 修改Sharing2.java,在TwoCounter的run()方法内部添加一个synchronized(同步)块,而不是同步整个run()方法。 11 | 12 | (4) 创建两个Thread子类,第一个的run()方法用于最开始的启动,并捕获第二个Thread对象的指针,然后调用wait()。第二个类的run()应在过几秒后为第一个线程调用modifyAll(),使第一个线程能打印出一条消息。 13 | 14 | (5) 在Ticker2内的Counter5.java中,删除yield(),并解释一下结果。用一个sleep()换掉yield(),再解释一下结果。 15 | 16 | (6) 在ThreadGroup1.java中,将对sys.suspend()的调用换成对线程组的一个wait()调用,令其等候2秒钟。为了保证获得正确的结果,必须在一个同步块内取得sys的对象锁。 17 | 18 | (7) 修改Daemons.java,使main()有一个sleep(),而不是一个readLine()。实验不同的睡眠时间,看看会有什么发生。 19 | 20 | (8) 到第7章(中间部分)找到那个GreenhouseControls.java例子,它应该由三个文件构成。在Event.java中,Event类建立在对时间的监视基础上。修改这个Event,使其成为一个线程。然后修改其余的设计,使它们能与新的、以线程为基础的Event正常协作。 -------------------------------------------------------------------------------- /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 | 下面这个程序利用InetAddress.getByName()来产生你的IP地址。为了让它运行起来,事先必须知道计算机的名字。该程序只在Windows 95中进行了测试,但大家可以依次进入自己的“开始”、“设置”、“控制面板”、“网络”,然后进入“标识”卡片。其中,“计算机名称”就是应在命令行输入的内容。 14 | 15 | ``` java 16 | //: WhoAmI.java 17 | // Finds out your network address when you're 18 | // connected to the Internet. 19 | package c15; 20 | import java.net.*; 21 | 22 | public class WhoAmI { 23 | public static void main(String[] args) 24 | throws Exception { 25 | if(args.length != 1) { 26 | System.err.println( 27 | "Usage: WhoAmI MachineName"); 28 | System.exit(1); 29 | } 30 | InetAddress a = 31 | InetAddress.getByName(args[0]); 32 | System.out.println(a); 33 | } 34 | } ///:~ 35 | ``` 36 | 37 | 就我自己的情况来说,机器的名字叫作“Colossus”(来自同名电影,“巨人”的意思。我在这台机器上有一个很大的硬盘)。所以一旦连通我的ISP,就象下面这样执行程序: 38 | 39 | ``` java 40 | java whoAmI Colossus 41 | ``` 42 | 43 | 得到的结果象下面这个样子(当然,这个地址可能每次都是不同的): 44 | 45 | ``` java 46 | Colossus/202.98.41.151 47 | ``` 48 | 49 | 假如我把这个地址告诉一位朋友,他就可以立即登录到我的个人Web服务器,只需指定目标地址 http://202.98.41.151 即可(当然,我此时不能断线)。有些时候,这是向其他人发送信息或者在自己的Web站点正式出台以前进行测试的一种方便手段。 50 | 51 | 15.1.1 服务器和客户机 52 | 53 | 网络最基本的精神就是让两台机器连接到一起,并相互“交谈”或者“沟通”。一旦两台机器都发现了对方,就可以展开一次令人愉快的双向对话。但它们怎样才能“发现”对方呢?这就象在游乐园里那样:一台机器不得不停留在一个地方,侦听其他机器说:“嘿,你在哪里呢?” 54 | 55 | “停留在一个地方”的机器叫作“服务器”(Server);到处“找人”的机器则叫作“客户机”(Client)或者“客户”。它们之间的区别只有在客户机试图同服务器连接的时候才显得非常明显。一旦连通,就变成了一种双向通信,谁来扮演服务器或者客户机便显得不那么重要了。 56 | 57 | 所以服务器的主要任务是侦听建立连接的请求,这是由我们创建的特定服务器对象完成的。而客户机的任务是试着与一台服务器建立连接,这是由我们创建的特定客户机对象完成的。一旦连接建好,那么无论在服务器端还是客户机端,连接只是魔术般地变成了一个IO数据流对象。从这时开始,我们可以象读写一个普通的文件那样对待连接。所以一旦建好连接,我们只需象第10章那样使用自己熟悉的IO命令即可。这正是Java连网最方便的一个地方。 58 | 59 | 1. 在没有网络的前提下测试程序 60 | 61 | 由于多种潜在的原因,我们可能没有一台客户机、服务器以及一个网络来测试自己做好的程序。我们也许是在一个课堂环境中进行练习,或者写出的是一个不十分可靠的网络应用,还能拿到网络上去。IP的设计者注意到了这个问题,并建立了一个特殊的地址——localhost——来满足非网络环境中的测试要求。在Java中产生这个地址最一般的做法是: 62 | 63 | ``` java 64 | InetAddress addr = InetAddress.getByName(null); 65 | ``` 66 | 67 | 如果向getByName()传递一个null(空)值,就默认为使用localhost。我们用InetAddress对特定的机器进行索引,而且必须在进行进一步的操作之前得到这个InetAddress(互联网地址)。我们不可以操纵一个InetAddress的内容(但可把它打印出来,就象下一个例子要演示的那样)。创建InetAddress的唯一途径就是那个类的static(静态)成员方法getByName()(这是最常用的)、getAllByName()或者getLocalHost()。 68 | 69 | 为得到本地主机地址,亦可向其直接传递字串"localhost": 70 | 71 | ``` java 72 | InetAddress.getByName("localhost"); 73 | ``` 74 | 75 | 或者使用它的保留IP地址(四点形式),就象下面这样: 76 | 77 | ``` java 78 | InetAddress.getByName("127.0.0.1"); 79 | ``` 80 | 81 | 这三种方法得到的结果是一样的。 82 | 83 | 15.1.2 端口:机器内独一无二的场所 84 | 85 | 有些时候,一个IP地址并不足以完整标识一个服务器。这是由于在一台物理性的机器中,往往运行着多个服务器(程序)。由IP表达的每台机器也包含了“端口”(Port)。我们设置一个客户机或者服务器的时候,必须选择一个无论客户机还是服务器都认可连接的端口。就象我们去拜会某人时,IP地址是他居住的房子,而端口是他在的那个房间。 86 | 87 | 注意端口并不是机器上一个物理上存在的场所,而是一种软件抽象(主要是为了表述的方便)。客户程序知道如何通过机器的IP地址同它连接,但怎样才能同自己真正需要的那种服务连接呢(一般每个端口都运行着一种服务,一台机器可能提供了多种服务,比如HTTP和FTP等等)?端口编号在这里扮演了重要的角色,它是必需的一种二级定址措施。也就是说,我们请求一个特定的端口,便相当于请求与那个端口编号关联的服务。“报时”便是服务的一个典型例子。通常,每个服务都同一台特定服务器机器上的一个独一 88 | 无二的端口编号关联在一起。客户程序必须事先知道自己要求的那项服务的运行端口号。 89 | 90 | 系统服务保留了使用端口1到端口1024的权力,所以不应让自己设计的服务占用这些以及其他任何已知正在使用的端口。本书的第一个例子将使用端口8080(为追忆我的第一台机器使用的老式8位Intel 8080芯片,那是一部使用CP/M操作系统的机子)。 91 | -------------------------------------------------------------------------------- /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剪贴板。 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /16-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/16-1.gif -------------------------------------------------------------------------------- /16-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/16-2.gif -------------------------------------------------------------------------------- /16-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/16-3.gif -------------------------------------------------------------------------------- /16-4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/16-4.gif -------------------------------------------------------------------------------- /16.1 范式的概念.md: -------------------------------------------------------------------------------- 1 | # 16.1 范式的概念 2 | 3 | 在最开始,可将范式想象成一种特别聪明、能够自我适应的手法,它可以解决特定类型的问题。也就是说,它类似一些需要全面认识某个问题的人。在了解了问题的方方面面以后,最后提出一套最通用、最灵活的解决方案。具体问题或许是以前见到并解决过的。然而,从前的方案也许并不是最完善的,大家会看到它如何在一个范式里具体表达出来。 4 | 5 | 尽管我们称之为“设计范式”,但它们实际上并不局限于设计领域。思考“范式”时,应脱离传统意义上分析、设计以及实施的思考方式。相反,“范式”是在一个程序里具体表达一套完整的思想,所以它有时可能出现在分析阶段或者高级设计阶段。这一点是非常有趣的,因为范式具有以代码形式直接实现的形式,所以可能不希望它在低级设计或者具体实施以前显露出来(而且事实上,除非真正进入那些阶段,否则一般意识不到自己需要一个范式来解决问题)。 6 | 7 | 范式的基本概念亦可看成是程序设计的基本概念:添加一层新的抽象!只要我们抽象了某些东西,就相当于隔离了特定的细节。而且这后面最引人注目的动机就是“将保持不变的东西身上发生的变化孤立出来”。这样做的另一个原因是一旦发现程序的某部分由于这样或那样的原因可能发生变化,我们一般都想防止那些改变在代码内部繁衍出其他变化。这样做不仅可以降低代码的维护代价,也更便于我们理解(结果同样是降低开销)。 8 | 9 | 为设计出功能强大且易于维护的应用项目,通常最困难的部分就是找出我称之为“领头变化”的东西。这意味着需要找出造成系统改变的最重要的东西,或者换一个角度,找出付出代价最高、开销最大的那一部分。一旦发现了“领头变化”,就可以为自己定下一个焦点,围绕它展开自己的设计。 10 | 11 | 所以设计范式的最终目标就是将代码中变化的内容隔离开。如果从这个角度观察,就会发现本书实际已采用了一些设计范式。举个例子来说,继承可以想象成一种设计范式(类似一个由编译器实现的)。在都拥有同样接口(即保持不变的东西)的对象内部,它允许我们表达行为上的差异(即发生变化的东西)。合成亦可想象成一种范式,因为它允许我们修改——动态或静态——用于实现类的对象,所以也能修改类的运作方式。 12 | 13 | 在《Design Patterns》一书中,大家还能看到另一种范式:“迭代器”(即Iterator,Java 1.0和1.1不负责任地把它叫作Enumeration,即“枚举”;Java1.2的集合则改回了“迭代器”的称呼)。当我们在集合里遍历,逐个选择不同的元素时,迭代器可将集合的实施细节有效地隐藏起来。利用迭代器,可以编写出通用的代码,以便对一个序列里的所有元素采取某种操作,同时不必关心这个序列是如何构建的。这样一来,我们的通用代码即可伴随任何能产生迭代器的集合使用。 14 | 15 | 16.1.1 单例 16 | 17 | 或许最简单的设计范式就是“单例”(Singleton),它能提供对象的一个(而且只有一个)实例。单例在Java库中得到了应用,但下面这个例子显得更直接一些: 18 | 19 | ``` java 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 | 《Design Patterns》一书讨论了23种不同的范式,并依据三个标准分类(所有标准都涉及那些可能发生变化的方面)。这三个标准是: 66 | 67 | (1) 创建:对象的创建方式。这通常涉及对象创建细节的隔离,这样便不必依赖具体类型的对象,所以在新添一种对象类型时也不必改动代码。 68 | 69 | (2) 结构:设计对象,满足特定的项目限制。这涉及对象与其他对象的连接方式,以保证系统内的改变不会影响到这些连接。 70 | 71 | (3) 行为:对程序中特定类型的行动进行操纵的对象。这要求我们将希望采取的操作封装起来,比如解释一种语言、实现一个请求、在一个序列中遍历(就象在迭代器中那样)或者实现一种算法。本章提供了“观察器”(Observer)和“访问器”(Visitor)的范式的例子。 72 | 73 | 《Design Patterns》为所有这23种范式都分别使用了一节,随附的还有大量示例,但大多是用C++编写的,少数用Smalltalk编写(如看过这本书,就知道这实际并不是个大问题,因为很容易即可将基本概念从两种语言翻译到Java里)。现在这本书并不打算重复《Design Patterns》介绍的所有范式,因为那是一本独立的书,大家应该单独阅读。相反,本章只准备给出一些例子,让大家先对范式有个大致的印象,并理解它们的重要性到底在哪里。 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(塑料)类。 -------------------------------------------------------------------------------- /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 | ``` java 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 | ``` java 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 | ``` java 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 | 该程序已满足了设计的初衷:它能够正常工作!只要这是个一次性的方案,就会显得非常出色。但是,真正有用的程序应该能够在任 124 | 何时候解决问题。所以必须问自己这样一个问题:“如果情况发生了变化,它还能工作吗?”举个例子来说,厚纸板现在是一种非常有价值的可回收物品,那么如何把它集成到系统中呢(特别是程序很大很复杂的时候)?由于前面在switch语句中的类型检查编码可能散布于整个程序,所以每次加入一种新类型时,都必须找到所有那些编码。若不慎遗漏一个,编译器除了指出存在一个错误之外,不能再提供任何有价值的帮助。 125 | 126 | RTTI在这里使用不当的关键是“每种类型都进行了测试”。如果由于类型的子集需要特殊的对待,所以只寻找那个子集,那么情况就会变得好一些。但假如在一个switch语句中查找每一种类型,那么很可能错过一个重点,使最终的代码很难维护。在下一节中,大家会学习如何逐步对这个程序进行改进,使其显得越来越灵活。这是在程序设计中一种非常有意义的例子。 127 | -------------------------------------------------------------------------------- /16.5 抽象的应用.md: -------------------------------------------------------------------------------- 1 | # 16.5 抽象的应用 2 | 3 | 走到这一步,接下来该考虑一下设计方案剩下的部分了——在哪里使用类?既然归类到垃圾箱的办法非常不雅且过于暴露,为什么不隔离那个过程,把它隐藏到一个类里呢?这就是著名的“如果必须做不雅的事情,至少应将其本地化到一个类里”规则。看起来就象下面这样: 4 | 5 | ![](16-1.gif) 6 | 7 | 现在,只要一种新类型的Trash加入方法,对TrashSorter对象的初始化就必须变动。可以想象,TrashSorter类看起来应该象下面这个样子: 8 | 9 | ``` java 10 | class TrashSorter extends Vector { 11 | void sort(Trash t) { /* ... */ } 12 | } 13 | ``` 14 | 15 | 也就是说,TrashSorter是由一系列指针构成的Vector(系列),而那些指针指向的又是由Trash指针构成的Vector;利用addElement(),可以安装新的TrashSorter,如下所示: 16 | 17 | ``` java 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 | ``` java 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 RTTI真的有害吗.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 | ``` java 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 | 利用keys(),可以得到对所有Class对象的一个“枚举”(Enumeration),而且可用get(),可通过Class对象获取对应的Vector。 61 | 62 | filler()方法非常有趣,因为它利用了ParseTrash.fillBin()的设计——不仅能尝试填充一个Vector,也能用它的addTrash()方法试着填充实现了Fillable(可填充)接口的任何东西。filter()需要做的全部事情就是将一个指针返回给实现了Fillable的一个接口,然后将这个指针作为参数传递给fillBin(),就象下面这样: 63 | 64 | ``` java 65 | ParseTrash.fillBin("Trash.dat", bin.filler()); 66 | ``` 67 | 68 | 为产生这个指针,我们采用了一个“匿名内部类”(已在第7章讲述)。由于根本不需要用一个已命名的类来实现Fillable,只需要属于那个类的一个对象的指针即可,所以这里使用匿名内部类是非常恰当的。 69 | 70 | 对这个设计,要注意的一个地方是尽管没有设计成对归类加以控制,但在fillBin()每次进行归类的时候,都会将一个Trash对象插入bin。 71 | 72 | 通过前面那些例子的学习,DynaTrash类的大多数部分都应当非常熟悉了。这一次,我们不再将新的Trash对象置入类型Vector的一个bin内。由于bin的类型为TypeMap,所以将垃圾(Trash)丢进垃圾筒(Bin)的时候,TypeMap的内部归类机制会立即进行适当的分类。在TypeMap里遍历并对每个独立的Vector进行操作,这是一件相当简单的事情: 73 | 74 | 75 | ``` java 76 | Enumeration keys = bin.keys(); 77 | while(keys.hasMoreElements()) 78 | Trash.sumValue( 79 | bin.get((Class)keys.nextElement())); 80 | ``` 81 | 82 | 就象大家看到的那样,新类型向系统的加入根本不会影响到这些代码,亦不会影响TypeMap中的代码。这显然是解决问题最圆满的方案。尽管它确实严重依赖RTTI,但请注意散列表中的每个键-值对都只查找一种类型。除此以外,在我们增加一种新类型的时候,不会陷入“忘记”向系统加入正确代码的尴尬境地,因为根本就没有什么代码需要添加。 83 | -------------------------------------------------------------------------------- /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 | 由于《Design Patterns》这本书对程序员造成了如此重要的影响,所以他们纷纷开始寻找其他范式。随着的时间的推移,这类范式必然会越来越多。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 | -------------------------------------------------------------------------------- /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里介绍过的那种名字自动填充技术。 -------------------------------------------------------------------------------- /2.1 用指针操纵对象.md: -------------------------------------------------------------------------------- 1 | # 2.1 用指针操纵对象 2 | 3 | 每种编程语言都有自己的数据处理方式。有些时候,程序员必须时刻留意准备处理的是什么类型。您曾利用一些特殊语法直接操作过对象,或处理过一些间接表示的对象吗(C或C++里的指针)? 4 | 5 | 所有这些在Java里都得到了简化,任何东西都可看作对象。因此,我们可采用一种统一的语法,任何地方均可照搬不误。但要注意,尽管将一切都“看作”对象,但操纵的标识符实际是指向一个对象的“指针”(Handle)。在其他Java参考书里,还可看到有的人将其称作一个“引用”,甚至一个“指针”。可将这一情形想象成用遥控板(指针)操纵电视机(对象)。只要握住这个遥控板,就相当于掌握了与电视机连接的通道。但一旦需要“换频道”或者“关小声音”,我们实际操纵的是遥控板(指针),再由遥控板自己操纵电视机(对象)。如果要在房间里四处走走,并想保持对电视机的控制,那么手上拿着的是遥控板,而非电视机。 6 | 7 | 此外,即使没有电视机,遥控板亦可独立存在。也就是说,只是由于拥有一个指针,并不表示必须有一个对象同它连接。所以如果想容纳一个词或句子,可创建一个String指针: 8 | 9 | ``` java 10 | String s; 11 | ``` 12 | 13 | 但这里创建的只是指针,并不是对象。若此时向s发送一条消息,就会获得一个错误(运行期)。这是由于s实际并未与任何东西连接(即“没有电视机”)。因此,一种更安全的做法是:创建一个指针时,记住无论如何都进行初始化: 14 | 15 | ``` java 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浏览器观看。 -------------------------------------------------------------------------------- /2.2 所有对象都必须创建.md: -------------------------------------------------------------------------------- 1 | # 2.2 所有对象都必须创建 2 | 3 | 4 | 创建指针时,我们希望它同一个新对象连接。通常用new关键字达到这一目的。new的意思是:“把我变成这些对象的一种新类型”。所以在上面的例子中,可以说: 5 | 6 | ``` java 7 | String s = new String("asdf"); 8 | ``` 9 | 10 | 它不仅指出“将我变成一个新字串”,也通过提供一个初始字串,指出了“如何生成这个新字串”。 11 | 当然,字串(String)并非唯一的类型。Java配套提供了数量众多的现成类型。对我们来讲,最重要的就是记住能自行创建类型。事实上,这应是Java程序设计的一项基本操作,是继续本书后余部分学习的基础。 12 | 13 | 2.2.1 保存到什么地方 14 | 15 | 程序运行时,我们最好对数据保存到什么地方做到心中有数。特别要注意的是内存的分配。有六个地方都可以保存数据: 16 | 17 | (1) 寄存器。这是最快的保存区域,因为它位于和其他所有保存方式不同的地方:处理器内部。然而,寄存器的数量十分有限,所以寄存器是根据需要由编译器分配。我们对此没有直接的控制权,也不可能在自己的程序里找到寄存器存在的任何踪迹。 18 | 19 | (2) 栈(Stack)。驻留于常规RAM(随机访问存储器)区域,但可通过它的“栈指针”获得处理的直接支持。栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。这是一种特别快、特别有效的数据保存方式,仅次于寄存器。创建程序时,Java编译器必须准确地知道栈内保存的所有数据的“长度”以及“存在时间”。这是由于它必须生成相应的代码,以便向上和向下移动指针。这一限制无疑影响了程序的灵活性,所以尽管有些Java数据要保存在栈里——特别是对象指针,但Java对象并不放到其中。 20 | 21 | (3) 堆。一种常规用途的内存池(也在RAM区域),其中保存了Java对象。和栈不同,“内存堆”或“堆”(Heap)最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间! 22 | 23 | (4) 静态存储。这儿的“静态”(Static)是指“位于固定位置”(尽管也在RAM里)。程序运行期间,静态存储的数据将随时等候调用。可用static关键字指出一个对象的特定元素是静态的。但Java对象本身永远都不会置入静态存储空间。 24 | 25 | (5) 常数存储。常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。有的常数需要严格地保护,所以可考虑将它们置入只读存储器(ROM)。 26 | 27 | (6) 非RAM存储。若数据完全独立于一个程序之外,则程序不运行时仍可存在,并在程序的控制范围之外。其中两个最主要的例子便是“流式对象”和“固定对象”。对于流式对象,对象会变成字节流,通常会发给另一台机器。而对于固定对象,对象保存在磁盘中。即使程序中止运行,它们仍可保持自己的状态不变。对于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体中。一旦需要,甚至能将它们恢复成普通的、基于RAM的对象。Java 1.1提供了对Lightweight persistence的支持。未来的版本甚至可能提供更完整的方案。 28 | 29 | 2.2.2 特殊情况:主要类型 30 | 31 | 有一系列类需特别对待;可将它们想象成“基本”、“主要”或者“主”(Primitive)类型,进行程序设计时要频繁用到它们。之所以要特别对待,是由于用new创建对象(特别是小的、简单的变量)并不是非常有效,因为new将对象置于“堆”里。对于这些类型,Java采纳了与C和C++相同的方法。也就是说,不是用new创建变量,而是创建一个并非指针的“自动”变量。这个变量容纳了具体的值,并置于栈中,能够更高效地存取。 32 | 33 | Java决定了每种主要类型的大小。就象在大多数语言里那样,这些大小并不随着机器结构的变化而变化。这种大小的不可更改正是Java程序具有很强移植能力的原因之一。 34 | 35 | | 主类型 | 大小 | 最小值 | 最大值 | 封装器类型 | 36 | |---------|---------|-----------|----------------|------------| 37 | | boolean | 1-bit | – | – | Boolean | 38 | | char | 16-bit | Unicode 0 | Unicode 216- 1 | Character | 39 | | byte | 8-bit | -128 | +127 | Byte[1] | 40 | | short | 16-bit | -215 | +215 – 1 | Short[1] | 41 | | int | 32-bit | -231 | +231 – 1 | Integer | 42 | | long | 64-bit | -263 | +263 – 1 | Long | 43 | | float | 32-bit | IEEE754 | IEEE754 | Float | 44 | | double | 64-bit | IEEE754 | IEEE754 | Double | 45 | | void | – | – | – | Void[1] | 46 | 47 | 48 | ①:到Java 1.1才有,1.0版没有。 49 | 50 | 数值类型全都是有符号(正负号)的,所以不必费劲寻找没有符号的类型。 51 | 主数据类型也拥有自己的“封装器”(wrapper)类。这意味着假如想让堆内一个非主要对象表示那个主类型,就要使用对应的封装器。例如: 52 | 53 | ``` java 54 | char c = 'x'; 55 | Character C = new Character(c); 56 | ``` 57 | 58 | 也可以直接使用: 59 | 60 | ``` java 61 | Character C = new Character('x'); 62 | ``` 63 | 64 | 这样做的原因将在以后的章节里解释。 65 | 66 | **1. 高精度数字** 67 | 68 | Java 1.1增加了两个类,用于进行高精度的计算:BigInteger和BigDecimal。尽管它们大致可以划分为“封装器”类型,但两者都没有对应的“主类型”。 69 | 70 | 这两个类都有自己特殊的“方法”,对应于我们针对主类型执行的操作。也就是说,能对int或float做的事情,对BigInteger和BigDecimal一样可以做。只是必须使用方法调用,不能使用运算符。此外,由于牵涉更多,所以运算速度会慢一些。我们牺牲了速度,但换来了精度。 71 | 72 | BigInteger支持任意精度的整数。也就是说,我们可精确表示任意大小的整数值,同时在运算过程中不会丢失任何信息。 73 | BigDecimal支持任意精度的定点数字。例如,可用它进行精确的币值计算。 74 | 75 | 至于调用这两个类时可选用的构造器和方法,请自行参考联机帮助文档。 76 | 77 | 2.2.3 Java的数组 78 | 79 | 几乎所有程序设计语言都支持数组。在C和C++里使用数组是非常危险的,因为那些数组只是内存块。若程序访问自己内存块以外的数组,或者在初始化之前使用内存(属于常规编程错误),会产生不可预测的后果(注释②)。 80 | 81 | ②:在C++里,应尽量不要使用数组,换用标准模板库(Standard TemplateLibrary)里更安全的容器。 82 | 83 | Java的一项主要设计目标就是安全性。所以在C和C++里困扰程序员的许多问题都未在Java里重复。一个Java可以保证被初始化,而且不可在它的范围之外访问。由于系统自动进行范围检查,所以必然要付出一些代价:针对每个数组,以及在运行期间对索引的校验,都会造成少量的内存开销。但由此换回的是更高的安全性,以及更高的工作效率。为此付出少许代价是值得的。 84 | 85 | 创建对象数组时,实际创建的是一个指针数组。而且每个指针都会自动初始化成一个特殊值,并带有自己的关键字:null(空)。一旦Java看到null,就知道该指针并未指向一个对象。正式使用前,必须为每个指针都分配一个对象。若试图使用依然为null的一个指针,就会在运行期报告问题。因此,典型的数组错误在Java里就得到了避免。 86 | 87 | 也可以创建主类型数组。同样地,编译器能够担保对它的初始化,因为会将那个数组的内存划分成零。 88 | 89 | 数组问题将在以后的章节里详细讨论。 90 | -------------------------------------------------------------------------------- /2.3 绝对不要清除对象.md: -------------------------------------------------------------------------------- 1 | # 2.3 绝对不要清除对象 2 | 3 | 4 | 在大多数程序设计语言中,变量的“存在时间”(Lifetime)一直是程序员需要着重考虑的问题。变量应持续多长的时间?如果想清除它,那么何时进行?在变量存在时间上纠缠不清会造成大量的程序错误。在下面的小节里,将阐示Java如何帮助我们完成所有清除工作,从而极大了简化了这个问题。 5 | 6 | 2.3.1 作用域 7 | 8 | 大多数程序设计语言都提供了“作用域”(Scope)的概念。对于在作用域里定义的名字,作用域同时决定了它的“可见性”以及“存在时间”。在C,C++和Java里,作用域是由花括号的位置决定的。参考下面这个例子: 9 | 10 | ``` java 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 | ``` java 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 | ``` java 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 | ``` java 9 | class ATypeName {/*类主体置于这里} 10 | ``` 11 | 12 | 这样就引入了一种新类型,接下来便可用new创建这种类型的一个新对象: 13 | 14 | ``` java 15 | ATypeName a = new ATypeName(); 16 | ``` 17 | 18 | 在ATypeName里,类主体只由一条注释构成(星号和斜杠以及其中的内容,本章后面还会详细讲述),所以并不能对它做太多的事情。事实上,除非为其定义了某些方法,否则根本不能指示它做任何事情。 19 | 20 | 2.4.1 字段和方法 21 | 22 | 定义一个类时(我们在Java里的全部工作就是定义类、制作那些类的对象以及将消息发给那些对象),可在自己的类里设置两种类型的元素:数据成员(有时也叫“字段”)以及成员函数(通常叫“方法”)。其中,数据成员是一种对象(通过它的指针与其通信),可以为任何类型。它也可以是主类型(并不是指针)之一。如果是指向对象的一个指针,则必须初始化那个指针,用一种名为“构造器”(第4章会对此详述)的特殊函数将其与一个实际对象连接起来(就象早先看到的那样,使用new关键字)。但若是一种主类型,则可在类定义位置直接初始化(正如后面会看到的那样,指针亦可在定义位置初始化)。 23 | 24 | 每个对象都为自己的数据成员保有存储空间;数据成员不会在对象之间共享。下面是定义了一些数据成员的类示例: 25 | 26 | ``` java 27 | class DataOnly { 28 | int i; 29 | float f; 30 | boolean b; 31 | } 32 | ``` 33 | 34 | 这个类并没有做任何实质性的事情,但我们可创建一个对象: 35 | 36 | ``` java 37 | DataOnly d = new DataOnly(); 38 | ``` 39 | 40 | 可将值赋给数据成员,但首先必须知道如何引用一个对象的成员。为达到引用对象成员的目的,首先要写上对象指针的名字,再跟随一个点号(句点),再跟随对象内部成员的名字。即“对象指针.成员”。例如: 41 | 42 | ``` java 43 | d.i = 47; 44 | d.f = 1.1f; 45 | d.b = false; 46 | ``` 47 | 48 | 一个对象也可能包含了另一个对象,而另一个对象里则包含了我们想修改的数据。对于这个问题,只需保持“连接句点”即可。例如: 49 | 50 | ``` java 51 | myPlane.leftTank.capacity = 100; 52 | ``` 53 | 54 | 除容纳数据之外,DataOnly类再也不能做更多的事情,因为它没有成员函数(方法)。为正确理解工作原理,首先必须知道“自变量”和“返回值”的概念。我们马上就会详加解释。 55 | 56 | **1. 主成员的默认值** 57 | 58 | 若某个主数据类型属于一个类成员,那么即使不明确(显式)进行初始化,也可以保证它们获得一个默认值。 59 | 60 | 主类型 默认值 61 | 62 | ``` java 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 | ``` java 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 | ``` java 11 | 返回类型 方法名( /* 自变量列表*/ ) {/* 方法主体 */} 12 | ``` 13 | 14 | 返回类型是指调用方法之后返回的数值类型。显然,方法名的作用是对具体的方法进行标识和引用。自变量列表列出了想传递给方法的信息类型和名称。 15 | 16 | Java的方法只能作为类的一部分创建。只能针对某个对象调用一个方法(注释③),而且那个对象必须能够执行那个方法调用。若试图为一个对象调用错误的方法,就会在编译期得到一条出错消息。为一个对象调用方法时,需要先列出对象的名字,在后面跟上一个句点,再跟上方法名以及它的参数列表。亦即“对象名.方法名(自变量1,自变量2,自变量3...)。举个例子来说,假设我们有一个方法名叫f(),它没有自变量,返回的是类型为int的一个值。那么,假设有一个名为a的对象,可为其调用方法f(),则代码如下: 17 | 18 | ``` java 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 | ``` java 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 | ``` java 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 构建Java程序.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 | ``` java 27 | import java.util.Vector; 28 | ``` 29 | 30 | 它的作用是告诉编译器我们想使用Java的Vector类。然而,util包含了数量众多的类,我们有时希望使用其中的几个,同时不想全部明确地声明它们。为达到这个目的,可使用“*”通配符。如下所示: 31 | 32 | ``` java 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 | 有些面向对象的语言使用了“类数据”和“类方法”这两个术语。它们意味着数据和方法只是为作为一个整体的类而存在的,并不是为那个类的任何特定对象。有时,您会在其他一些Java书刊里发现这样的称呼。 44 | 45 | 为了将数据成员或方法设为static,只需在定义前置和这个关键字即可。例如,下述代码能生成一个static数据成员,并对其初始化: 46 | 47 | ``` java 48 | class StaticTest { 49 | Static int i = 47; 50 | } 51 | ``` 52 | 53 | 现在,尽管我们制作了两个StaticTest对象,但它们仍然只占据StaticTest.i的一个存储空间。这两个对象都共享同样的i。请考察下述代码: 54 | 55 | ``` java 56 | StaticTest st1 = new StaticTest(); 57 | StaticTest st2 = new StaticTest(); 58 | ``` 59 | 60 | 此时,无论st1.i还是st2.i都有同样的值47,因为它们引用的是同样的内存区域。 61 | 62 | 有两个办法可引用一个static变量。正如上面展示的那样,可通过一个对象命名它,如st2.i。亦可直接用它的类名引用,而这在非静态成员里是行不通的(最好用这个办法引用static变量,因为它强调了那个变量的“静态”本质)。 63 | 64 | ``` java 65 | StaticTest.i++; 66 | ``` 67 | 68 | 其中,++运算符会使变量增值。此时,无论st1.i还是st2.i的值都是48。 69 | 70 | 类似的逻辑也适用于静态方法。既可象对其他任何方法那样通过一个对象引用静态方法,亦可用特殊的语法格式“类名.方法()”加以引用。静态方法的定义是类似的: 71 | 72 | ``` java 73 | class StaticFun { 74 | static void incr() { StaticTest.i++; } 75 | } 76 | ``` 77 | 78 | 从中可看出,StaticFun的方法incr()使静态数据i增值。通过对象,可用典型的方法调用incr(): 79 | 80 | ``` java 81 | StaticFun sf = new StaticFun(); 82 | sf.incr(); 83 | ``` 84 | 85 | 或者,由于incr()是一种静态方法,所以可通过它的类直接调用: 86 | 87 | ``` java 88 | StaticFun.incr(); 89 | ``` 90 | 91 | 尽管是“静态”的,但只要应用于一个数据成员,就会明确改变数据的创建方式(一个类一个成员,以及每个对象一个非静态成员)。若应用于一个方法,就没有那么戏剧化了。对方法来说,static一项重要的用途就是帮助我们在不必创建对象的前提下调用那个方法。正如以后会看到的那样,这一点是至关重要的——特别是在定义程序运行入口方法main()的时候。 92 | 93 | 和其他任何方法一样,static方法也能创建自己类型的命名对象。所以经常把static方法作为一个“领头羊”使用,用它生成一系列自己类型的“实例”。 94 | -------------------------------------------------------------------------------- /2.7 我们的第一个Java程序.md: -------------------------------------------------------------------------------- 1 | # 2.7 我们的第一个Java程序 2 | 3 | 4 | 最后,让我们正式编一个程序(注释⑤)。它能打印出与当前运行的系统有关的资料,并利用了来自Java标准库的System对象的多种方法。注意这里引入了一种额外的注释样式:“//”。它表示到本行结束前的所有内容都是注释: 5 | 6 | ``` java 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 | ``` java 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 | ``` java 44 | public static void main(String[] args) { 45 | ``` 46 | 47 | 其中,关键字“public”意味着方法可由外部世界调用(第5章会详细解释)。main()的自变量是包含了String对象的一个数组。args不会在本程序中用到,但需要在这个地方列出,因为它们保存了在命令行调用的自变量。 48 | 程序的第一行非常有趣: 49 | 50 | ``` java 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 | ``` java 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 | ``` java 9 | /* 这是 10 | * 一段注释, 11 | * 它跨越了多个行 12 | */ 13 | ``` 14 | 15 | 但请记住,进行编译时,/*和*/之间的所有东西都会被忽略,所以上述注释与下面这段注释并没有什么不同: 16 | 17 | ``` java 18 | /* 这是一段注释, 19 | 它跨越了多个行 */ 20 | ``` 21 | 22 | 第二种类型的注释也起源于C++。这种注释叫作“单行注释”,以一个 `“//”` 起头,表示这一行的所有内容都是注释。这种类型的注释更常用,因为它书写时更方便。没有必要在键盘上寻找 `“/”` ,再寻找 `“*”` (只需按同样的键两次),而且不必在注释结尾时加一个结束标记。下面便是这类注释的一个例子: 23 | 24 | ``` java 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 | ``` java 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 | ``` java 61 | /** 62 | *
 63 | * System.out.println(new Date());
 64 | * 
65 | */ 66 | ``` 67 | 68 | 亦可象在其他Web文档里那样运用HTML,对普通文本进行格式化,使其更具条理、更加美观: 69 | 70 | ``` java 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 | ``` java 90 | @see 类名 91 | @see 完整类名 92 | @see 完整类名#方法名 93 | ``` 94 | 95 | 每一格式都会在生成的文档里自动加入一个超链接的“See Also”(参见)条目。注意javadoc不会检查我们指定的超链接,不会验证它们是否有效。 96 | 97 | 2.8.5 类文档标记 98 | 99 | 随同嵌入HTML和@see引用,类文档还可以包括用于版本信息以及作者姓名的标记。类文档亦可用于“接口”目的(本书后面会详细解释)。 100 | 101 | 102 | **1. @version** 103 | 104 | 105 | 格式如下: 106 | 107 | ``` java 108 | @version 版本信息 109 | ``` 110 | 111 | 其中,“版本信息”代表任何适合作为版本说明的资料。若在javadoc命令行使用了“-version”标记,就会从生成的HTML文档里提取出版本信息。 112 | 113 | **2. @author** 114 | 115 | 格式如下: 116 | 117 | ``` java 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 | @param 参数名 说明 136 | 其中,“参数名”是指参数列表内的标识符,而“说明”代表一些可延续到后续行内的说明文字。一旦遇到一个新文档标记,就认为前一个说明结束。可使用任意数量的说明,每个参数一个。 137 | 138 | **2. @return** 139 | 140 | 格式如下: 141 | 142 | ``` java 143 | @return 说明 144 | ``` 145 | 146 | 其中,“说明”是指返回值的含义。它可延续到后面的行内。 147 | 148 | **3. @exception** 149 | 150 | 有关“异常”(Exception)的详细情况,我们会在第9章讲述。简言之,它们是一些特殊的对象,若某个方法失败,就可将它们“扔出”对象。调用一个方法时,尽管只有一个异常对象出现,但一些特殊的方法也许能产生任意数量的、不同类型的异常。所有这些异常都需要说明。所以,异常标记的格式如下: 151 | 152 | ``` java 153 | @exception 完整类名 说明 154 | ``` 155 | 156 | 其中,“完整类名”明确指定了一个异常类的名字,它是在其他某个地方定义好的。而“说明”(同样可以延续到下面的行)告诉我们为什么这种特殊类型的异常会在方法调用中出现。 157 | 158 | **4. @deprecated** 159 | 160 | 这是Java 1.1的新特性。该标记用于指出一些旧功能已由改进过的新功能取代。该标记的作用是建议用户不必再使用一种特定的功能,因为未来改版时可能摒弃这一功能。若将一个方法标记为@deprecated,则使用该方法时会收到编译器的警告。 161 | 162 | 2.8.8 文档示例 163 | 164 | 下面还是我们的第一个Java程序,只不过已加入了完整的文档注释: 165 | 166 | 92页程序 167 | 168 | 第一行: 169 | 170 | ``` java 171 | //: Property.java 172 | ``` 173 | 174 | 采用了我自己的方法:将一个“:”作为特殊的记号,指出这是包含了源文件名字的一个注释行。最后一行也用这样的一条注释结尾,它标志着源代码清单的结束。这样一来,可将代码从本书的正文中方便地提取出来,并用一个编译器检查。这方面的细节在第17章讲述。 175 | -------------------------------------------------------------------------------- /2.9 编码样式.md: -------------------------------------------------------------------------------- 1 | # 2.9 编码样式 2 | 3 | 4 | 一个非正式的Java编程标准是大写一个类名的首字母。若类名由几个单词构成,那么把它们紧靠到一起(也就是说,不要用下划线来分隔名字)。此外,每个嵌入单词的首字母都采用大写形式。例如: 5 | 6 | ``` java 7 | class AllTheColorsOfTheRainbow { // ...} 8 | ``` 9 | 10 | 对于其他几乎所有内容:方法、字段(成员变量)以及对象指针名称,可接受的样式与类样式差不多,只是标识符的第一个字母采用小写。例如: 11 | 12 | ``` java 13 | class AllTheColorsOfTheRainbow { 14 | int anIntegerRepresentingColors; 15 | void changeTheHueOfTheColor(int newHue) { 16 | // ... 17 | } 18 | // ... 19 | } 20 | ``` 21 | 22 | 当然,要注意用户也必须键入所有这些长名字,而且不能输错。 23 | -------------------------------------------------------------------------------- /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,看看会有什么情况出现。 -------------------------------------------------------------------------------- /4.1 用构造器自动初始化.md: -------------------------------------------------------------------------------- 1 | # 4.1 用构造器自动初始化 2 | 3 | 4 | 对于方法的创建,可将其想象成为自己写的每个类都调用一次initialize()。这个名字提醒我们在使用对象之前,应首先进行这样的调用。但不幸的是,这也意味着用户必须记住调用方法。在Java中,由于提供了名为“构造器”的一种特殊方法,所以类的设计者可担保每个对象都会得到正确的初始化。若某个类有一个构造器,那么在创建对象时,Java会自动调用那个构造器——甚至在用户毫不知觉的情况下。所以说这是可以担保的! 5 | 6 | 接着的一个问题是如何命名这个方法。存在两方面的问题。第一个是我们使用的任何名字都可能与打算为某个类成员使用的名字冲突。第二是由于编译器的责任是调用构造器,所以它必须知道要调用是哪个方法。C++采取的方案看来是最简单的,且更有逻辑性,所以也在Java里得到了应用:构造器的名字与类名相同。这样一来,可保证象这样的一个方法会在初始化期间自动调用。 7 | 8 | 下面是带有构造器的一个简单的类(若执行这个程序有问题,请参考第3章的“赋值”小节)。 9 | 10 | ``` java 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 | ``` java 32 | new Rock(); 33 | ``` 34 | 35 | 就会分配相应的存储空间,并调用构造器。这样可保证在我们经手之前,对象得到正确的初始化。 36 | 请注意所有方法首字母小写的编码规则并不适用于构造器。这是由于构造器的名字必须与类名完全相同! 37 | 和其他任何方法一样,构造器也能使用自变量,以便我们指定对象的具体创建方式。可非常方便地改动上述例子,以便构造器使用自己的自变量。如下所示: 38 | 39 | ``` java 40 | class Rock { 41 | Rock(int i) { 42 | System.out.println( 43 | "Creating Rock number " + i); 44 | } 45 | } 46 | 47 | public class SimpleConstructor { 48 | public static void main(String[] args) { 49 | for(int i = 0; i < 10; i++) 50 | new Rock(i); 51 | } 52 | } 53 | ``` 54 | 55 | 56 | 利用构造器的自变量,我们可为一个对象的初始化设定相应的参数。举个例子来说,假设类Tree有一个构造器,它用一个整数自变量标记树的高度,那么就可以象下面这样创建一个Tree对象: 57 | 58 | ``` java 59 | tree t = new Tree(12); // 12英尺高的树 60 | ``` 61 | 62 | 若Tree(int)是我们唯一的构造器,那么编译器不会允许我们以其他任何方式创建一个Tree对象。 63 | 64 | 构造器有助于消除大量涉及类的问题,并使代码更易阅读。例如在前述的代码段中,我们并未看到对initialize()方法的明确调用——那些方法在概念上独立于定义内容。在Java中,定义和初始化属于统一的概念——两者缺一不可。 65 | 构造器属于一种较特殊的方法类型,因为它没有返回值。这与void返回值存在着明显的区别。对于void返回值,尽管方法本身不会自动返回什么,但仍然可以让它返回另一些东西。构造器则不同,它不仅什么也不会自动返回,而且根本不能有任何选择。若存在一个返回值,而且假设我们可以自行选择返回内容,那么编译器多少要知道如何对那个返回值作什么样的处理。 66 | -------------------------------------------------------------------------------- /4.3 清除:收尾和垃圾收集.md: -------------------------------------------------------------------------------- 1 | # 4.3 清除:收尾和垃圾收集 2 | 3 | 4 | 程序员都知道“初始化”的重要性,但通常忘记清除的重要性。毕竟,谁需要来清除一个int呢?但是对于库来说,用完后简单地“释放”一个对象并非总是安全的。当然,Java可用垃圾收集器回收由不再使用的对象占据的内存。现在考虑一种非常特殊且不多见的情况。假定我们的对象分配了一个“特殊”内存区域,没有使用new。垃圾收集器只知道释放那些由new分配的内存,所以不知道如何释放对象的“特殊”内存。为解决这个问题,Java提供了一个名为finalize()的方法,可为我们的类定义它。在理想情况下,它的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存。所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清除或清扫工作。 5 | 6 | 但也是一个潜在的编程陷阱,因为有些程序员(特别是在C++开发背景的)刚开始可能会错误认为它就是在C++中为“破坏器”(Destructor)使用的finalize()——破坏(清除)一个对象的时候,肯定会调用这个函数。但在这里有必要区分一下C++和Java的区别,因为C++的对象肯定会被清除(排开编程错误的因素),而Java对象并非肯定能作为垃圾被“收集”去。或者换句话说: 7 | 8 | 垃圾收集并不等于“破坏”! 9 | 10 | 若能时刻牢记这一点,踩到陷阱的可能性就会大大减少。它意味着在我们不再需要一个对象之前,有些行动是必须采取的,而且必须由自己来采取这些行动。Java并未提供“破坏器”或者类似的概念,所以必须创建一个原始的方法,用它来进行这种清除。例如,假设在对象创建过程中,它会将自己描绘到屏幕上。如果不从屏幕明确删除它的图像,那么它可能永远都不会被清除。若在finalize()里置入某种删除机制,那么假设对象被当作垃圾收掉了,图像首先会将自身从屏幕上移去。但若未被收掉,图像就会保留下来。所以要记住的第二个重点是: 11 | 12 | 我们的对象可能不会当作垃圾被收掉! 13 | 14 | 有时可能发现一个对象的存储空间永远都不会释放,因为自己的程序永远都接近于用光空间的临界点。若程序执行结束,而且垃圾收集器一直都没有释放我们创建的任何对象的存储空间,则随着程序的退出,那些资源会返回给操作系统。这是一件好事情,因为垃圾收集本身也要消耗一些开销。如永远都不用它,那么永远也不用支出这部分开销。 15 | 16 | 4.3.1 finalize()用途何在 17 | 18 | 此时,大家可能已相信了自己应该将finalize()作为一种常规用途的清除方法使用。它有什么好处呢? 19 | 要记住的第三个重点是: 20 | 21 | 垃圾收集只跟内存有关! 22 | 23 | 也就是说,垃圾收集器存在的唯一原因是为了回收程序不再使用的内存。所以对于与垃圾收集有关的任何活动来说,其中最值得注意的是finalize()方法,它们也必须同内存以及它的回收有关。 24 | 25 | 但这是否意味着假如对象包含了其他对象,finalize()就应该明确释放那些对象呢?答案是否定的——垃圾收集器会负责释放所有对象占据的内存,无论这些对象是如何创建的。它将对finalize()的需求限制到特殊的情况。在这种情况下,我们的对象可采用与创建对象时不同的方法分配一些存储空间。但大家或许会注意到,Java中的所有东西都是对象,所以这到底是怎么一回事呢? 26 | 27 | 之所以要使用finalize(),看起来似乎是由于有时需要采取与Java的普通方法不同的一种方法,通过分配内存来做一些具有C风格的事情。这主要可以通过“固有方法”来进行,它是从Java里调用非Java方法的一种方式(固有方法的问题在附录A讨论)。C和C++是目前唯一获得固有方法支持的语言。但由于它们能调用通过其他语言编写的子程序,所以能够有效地调用任何东西。在非Java代码内部,也许能调用C的malloc()系列函数,用它分配存储空间。而且除非调用了free(),否则存储空间不会得到释放,从而造成内存“漏洞”的出现。当然,free()是一个C和C++函数,所以我们需要在finalize()内部的一个固有方法中调用它。 28 | 29 | 读完上述文字后,大家或许已弄清楚了自己不必过多地使用finalize()。这个思想是正确的;它并不是进行普通清除工作的理想场所。那么,普通的清除工作应在何处进行呢? 30 | 31 | 4.3.2 必须执行清除 32 | 33 | 为清除一个对象,那个对象的用户必须在希望进行清除的地点调用一个清除方法。这听起来似乎很容易做到,但却与C++“破坏器”的概念稍有抵触。在C++中,所有对象都会破坏(清除)。或者换句话说,所有对象都“应该”破坏。若将C++对象创建成一个本地对象,比如在栈中创建(在Java中是不可能的),那么清除或破坏工作就会在“结束花括号”所代表的、创建这个对象的作用域的末尾进行。若对象是用new创建的(类似于Java),那么当程序员调用C++的delete命令时(Java没有这个命令),就会调用相应的破坏器。若程序员忘记了,那么永远不会调用破坏器,我们最终得到的将是一个内存“漏洞”,另外还包括对象的其他部分永远不会得到清除。 34 | 35 | 相反,Java不允许我们创建本地(局部)对象——无论如何都要使用new。但在Java中,没有“delete”命令来释放对象,因为垃圾收集器会帮助我们自动释放存储空间。所以如果站在比较简化的立场,我们可以说正是由于存在垃圾收集机制,所以Java没有破坏器。然而,随着以后学习的深入,就会知道垃圾收集器的存在并不能完全消除对破坏器的需要,或者说不能消除对破坏器代表的那种机制的需要(而且绝对不能直接调用finalize(),所以应尽量避免用它)。若希望执行除释放存储空间之外的其他某种形式的清除工作,仍然必须调用Java中的一个方法。它等价于C++的破坏器,只是没后者方便。 36 | 37 | finalize()最有用处的地方之一是观察垃圾收集的过程。下面这个例子向大家展示了垃圾收集所经历的过程,并对前面的陈述进行了总结。 38 | 39 | ``` java 40 | //: Garbage.java 41 | // Demonstration of the garbage 42 | // collector and finalization 43 | 44 | class Chair { 45 | static boolean gcrun = false; 46 | static boolean f = false; 47 | static int created = 0; 48 | static int finalized = 0; 49 | int i; 50 | Chair() { 51 | i = ++created; 52 | if(created == 47) 53 | System.out.println("Created 47"); 54 | } 55 | protected void finalize() { 56 | if(!gcrun) { 57 | gcrun = true; 58 | System.out.println( 59 | "Beginning to finalize after " + 60 | created + " Chairs have been created"); 61 | } 62 | if(i == 47) { 63 | System.out.println( 64 | "Finalizing Chair #47, " + 65 | "Setting flag to stop Chair creation"); 66 | f = true; 67 | } 68 | finalized++; 69 | if(finalized >= created) 70 | System.out.println( 71 | "All " + finalized + " finalized"); 72 | } 73 | } 74 | 75 | public class Garbage { 76 | public static void main(String[] args) { 77 | if(args.length == 0) { 78 | System.err.println("Usage: \n" + 79 | "java Garbage before\n or:\n" + 80 | "java Garbage after"); 81 | return; 82 | } 83 | while(!Chair.f) { 84 | new Chair(); 85 | new String("To take up space"); 86 | } 87 | System.out.println( 88 | "After all Chairs have been created:\n" + 89 | "total created = " + Chair.created + 90 | ", total finalized = " + Chair.finalized); 91 | if(args[0].equals("before")) { 92 | System.out.println("gc():"); 93 | System.gc(); 94 | System.out.println("runFinalization():"); 95 | System.runFinalization(); 96 | } 97 | System.out.println("bye!"); 98 | if(args[0].equals("after")) 99 | System.runFinalizersOnExit(true); 100 | } 101 | } ///:~ 102 | ``` 103 | 104 | 上面这个程序创建了许多Chair对象,而且在垃圾收集器开始运行后的某些时候,程序会停止创建Chair。由于垃圾收集器可能在任何时间运行,所以我们不能准确知道它在何时启动。因此,程序用一个名为gcrun的标记来指出垃圾收集器是否已经开始运行。利用第二个标记f,Chair可告诉main()它应停止对象的生成。这两个标记都是在finalize()内部设置的,它调用于垃圾收集期间。 105 | 106 | 另两个static变量——created以及finalized——分别用于跟踪已创建的对象数量以及垃圾收集器已进行完收尾工作的对象数量。最后,每个Chair都有它自己的(非static)int i,所以能跟踪了解它具体的编号是多少。编号为47的Chair进行完收尾工作后,标记会设为true,最终结束Chair对象的创建过程。 107 | 108 | 所有这些都在main()的内部进行——在下面这个循环里: 109 | 110 | ``` java 111 | while(!Chair.f) { 112 | new Chair(); 113 | new String("To take up space"); 114 | } 115 | ``` 116 | 117 | 大家可能会疑惑这个循环什么时候会停下来,因为内部没有任何改变Chair.f值的语句。然而,finalize()进程会改变这个值,直至最终对编号47的对象进行收尾处理。 118 | 119 | 每次循环过程中创建的String对象只是属于额外的垃圾,用于吸引垃圾收集器——一旦垃圾收集器对可用内存的容量感到“紧张不安”,就会开始关注它。 120 | 121 | 运行这个程序的时候,提供了一个命令行自变量“before”或者“after”。其中,“before”自变量会调用System.gc()方法(强制执行垃圾收集器),同时还会调用System.runFinalization()方法,以便进行收尾工作。这些方法都可在Java 1.0中使用,但通过使用“after”自变量而调用的runFinalizersOnExit()方法却只有Java 1.1及后续版本提供了对它的支持(注释③)。注意可在程序执行的任何时候调用这个方法,而且收尾程序的执行与垃圾收集器是否运行是无关的。 122 | 123 | ③:不幸的是,Java 1.0采用的垃圾收集器方案永远不能正确地调用finalize()。因此,finalize()方法(特别是那些用于关闭文件的)事实上经常都不会得到调用。现在有些文章声称所有收尾模块都会在程序退出的时候得到调用——即使到程序中止的时候,垃圾收集器仍未针对那些对象采取行动。这并不是真实的情况,所以我们根本不能指望finalize()能为所有对象而调用。特别地,finalize()在Java 1.0里几乎毫无用处。 124 | 125 | 前面的程序向我们揭示出:在Java 1.1中,收尾模块肯定会运行这一许诺已成为现实——但前提是我们明确地强制它采取这一操作。若使用一个不是“before”或“after”的自变量(如“none”),那么两个收尾工作都不会进行,而且我们会得到象下面这样的输出: 126 | 127 | ``` java 128 | Created 47 129 | 130 | Created 47 131 | Beginning to finalize after 8694 Chairs have been created 132 | Finalizing Chair #47, Setting flag to stop Chair creation 133 | After all Chairs have been created: 134 | total created = 9834, total finalized = 108 135 | bye! 136 | ``` 137 | 138 | 因此,到程序结束的时候,并非所有收尾模块都会得到调用(注释④)。为强制进行收尾工作,可先调用System.gc(),再调用System.runFinalization()。这样可清除到目前为止没有使用的所有对象。这样做一个稍显奇怪的地方是在调用runFinalization()之前调用gc(),这看起来似乎与Sun公司的文档说明有些抵触,它宣称首先运行收尾模块,再释放存储空间。然而,若在这里首先调用runFinalization(),再调用gc(),收尾模块根本不会执行。 139 | 140 | ④:到你读到本书时,有些Java虚拟机(JVM)可能已开始表现出不同的行为。 141 | 142 | 针对所有对象,Java 1.1有时之所以会默认为跳过收尾工作,是由于它认为这样做的开销太大。不管用哪种方法强制进行垃圾收集,都可能注意到比没有额外收尾工作时较长的时间延迟。 143 | -------------------------------------------------------------------------------- /4.6 总结.md: -------------------------------------------------------------------------------- 1 | # 4.6 总结 2 | 3 | 作为初始化的一种具体操作形式,构造器应使大家明确感受到在语言中进行初始化的重要性。与C++的程序设计一样,判断一个程序效率如何,关键是看是否由于变量的初始化不正确而造成了严重的编程错误(臭虫)。这些形式的错误很难发现,而且类似的问题也适用于不正确的清除或收尾工作。由于构造器使我们能保证正确的初始化和清除(若没有正确的构造器调用,编译器不允许对象创建),所以能获得完全的控制权和安全性。 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()之前调用,再观察结果。 -------------------------------------------------------------------------------- /5.2 Java访问指示符.md: -------------------------------------------------------------------------------- 1 | # 5.2 Java访问指示符 2 | 3 | 4 | 针对类内每个成员的每个定义,Java访问指示符poublic,protected以及private都置于它们的最前面——无论它们是一个数据成员,还是一个方法。每个访问指示符都只控制着对那个特定定义的访问。这与C++存在着显著不同。在C++中,访问指示符控制着它后面的所有定义,直到又一个访问指示符加入为止。 5 | 6 | 通过千丝万缕的联系,程序为所有东西都指定了某种形式的访问。在后面的小节里,大家要学习与各类访问有关的所有知识。首次从默认访问开始。 7 | 8 | 5.2.1 “友好的” 9 | 10 | 如果根本不指定访问指示符,就象本章之前的所有例子那样,这时会出现什么情况呢?默认的访问没有关键字,但它通常称为“友好”(Friendly)访问。这意味着当前包内的其他所有类都能访问“友好的”成员,但对包外的所有类来说,这些成员却是“私有”(Private)的,外界不得访问。由于一个编译单元(一个文件)只能从属于单个包,所以单个编译单元内的所有类相互间都是自动“友好”的。因此,我们也说友好元素拥有“包访问”权限。 11 | 12 | 友好访问允许我们将相关的类都组合到一个包里,使它们相互间方便地进行沟通。将类组合到一个包内以后(这样便允许友好成员的相互访问,亦即让它们“交朋友”),我们便“拥有”了那个包内的代码。只有我们已经拥有的代码才能友好地访问自己拥有的其他代码。我们可认为友好访问使类在一个包内的组合显得有意义,或者说前者是后者的原因。在许多语言中,我们在文件内组织定义的方式往往显得有些牵强。但在Java中,却强制用一种颇有意义的形式进行组织。除此以外,我们有时可能想排除一些类,不想让它们访问当前包内定义的类。 13 | 14 | 对于任何关系,一个非常重要的问题是“谁能访问我们的‘私有’或private代码”。类控制着哪些代码能够访问自己的成员。没有任何秘诀可以“闯入”。另一个包内推荐可以声明一个新类,然后说:“嗨,我是Bob的朋友!”,并指望看到Bob的“protected”(受到保护的)、友好的以及“private”(私有)的成员。为获得对一个访问权限,唯一的方法就是: 15 | 16 | (1) 使成员成为“public”(公共的)。这样所有人从任何地方都可以访问它。 17 | 18 | (2) 变成一个“友好”成员,方法是舍弃所有访问指示符,并将其类置于相同的包内。这样一来,其他类就可以访问成员。 19 | 20 | (3) 正如以后引入“继承”概念后大家会知道的那样,一个继承的类既可以访问一个protected成员,也可以访问一个public成员(但不可访问private成员)。只有在两个类位于相同的包内时,它才可以访问友好成员。但现在不必关心这方面的问题。 21 | 22 | (4) 提供“访问器/变化器”方法(亦称为“获取/设置”方法),以便读取和修改值。这是OOP环境中最正规的一种方法,也是Java Beans的基础——具体情况会在第13章介绍。 23 | 24 | 5.2.2 public:接口访问 25 | 26 | 使用public关键字时,它意味着紧随在public后面的成员声明适用于所有人,特别是适用于使用库的客户程序员。假定我们定义了一个名为dessert的包,其中包含下述单元(若执行该程序时遇到困难,请参考第3章3.1.2小节“赋值”): 27 | 28 | ``` java 29 | //: Cookie.java 30 | // Creates a library 31 | package c05.dessert; 32 | 33 | public class Cookie { 34 | public Cookie() { 35 | System.out.println("Cookie constructor"); 36 | } 37 | void foo() { System.out.println("foo"); } 38 | } ///:~ 39 | ``` 40 | 41 | 请记住,Cookie.java必须驻留在名为dessert的一个子目录内,而这个子目录又必须位于由CLASSPATH指定的C05目录下面(C05代表本书的第5章)。不要错误地以为Java无论如何都会将当前目录作为搜索的起点看待。如果不将一个“.”作为CLASSPATH的一部分使用,Java就不会考虑当前目录。 42 | 现在,假若创建使用了Cookie的一个程序,如下所示: 43 | 44 | ``` java 45 | //: Dinner.java 46 | // Uses the library 47 | import c05.dessert.*; 48 | 49 | public class Dinner { 50 | public Dinner() { 51 | System.out.println("Dinner constructor"); 52 | } 53 | public static void main(String[] args) { 54 | Cookie x = new Cookie(); 55 | //! x.foo(); // Can't access 56 | } 57 | } ///:~ 58 | ``` 59 | 60 | 就可以创建一个Cookie对象,因为它的构造器是public的,而且类也是public的(公共类的概念稍后还会进行更详细的讲述)。然而,foo()成员不可在Dinner.java内访问,因为foo()只有在dessert包内才是“友好”的。 61 | 62 | 1. 默认包 63 | 64 | 大家可能会惊讶地发现下面这些代码得以顺利编译——尽管它看起来似乎已违背了规则: 65 | 66 | ``` java 67 | //: Cake.java 68 | // Accesses a class in a separate 69 | // compilation unit. 70 | 71 | class Cake { 72 | public static void main(String[] args) { 73 | Pie x = new Pie(); 74 | x.f(); 75 | } 76 | } ///:~ 77 | ``` 78 | 79 | 在位于相同目录的第二个文件里: 80 | 81 | ``` java 82 | //: Pie.java 83 | // The other class 84 | 85 | class Pie { 86 | void f() { System.out.println("Pie.f()"); } 87 | } ///:~ 88 | ``` 89 | 90 | 最初可能会把它们看作完全不相干的文件,然而Cake能创建一个Pie对象,并能调用它的f()方法!通常的想法会认为Pie和f()是“友好的”,所以不适用于Cake。它们确实是友好的——这部分结论非常正确。但它们之所以仍能在Cake.java中使用,是由于它们位于相同的目录中,而且没有明确的包名。Java把象这样的文件看作那个目录“默认包”的一部分,所以它们对于目录内的其他文件来说是“友好”的。 91 | 92 | 5.2.3 private:不能接触! 93 | 94 | private关键字意味着除非那个特定的类,而且从那个类的方法里,否则没有人能访问那个成员。同一个包内的其他成员不能访问private成员,这使其显得似乎将类与我们自己都隔离起来。另一方面,也不能由几个合作的人创建一个包。所以private允许我们自由地改变那个成员,同时毋需关心它是否会影响同一个包内的另一个类。默认的“友好”包访问通常已经是一种适当的隐藏方法;请记住,对于包的用户来说,是不能访问一个“友好”成员的。这种效果往往能令人满意,因为默认访问是我们通常采用的方法。对于希望变成public(公共)的成员,我们通常明确地指出,令其可由客户程序员自由调用。而且作为一个结果,最开始的时候通常会认为自己不必频繁使用private关键字,因为完全可以在不用它的前提下发布自己的代码(这与C++是个鲜明的对比)。然而,随着学习的深入,大家就会发现private仍然有非常重要的用途,特别是在涉及多线程处理的时候(详情见第14章)。 95 | 下面是应用了private的一个例子: 96 | 97 | ``` java 98 | //: IceCream.java 99 | // Demonstrates "private" keyword 100 | 101 | class Sundae { 102 | private Sundae() {} 103 | static Sundae makeASundae() { 104 | return new Sundae(); 105 | } 106 | } 107 | 108 | public class IceCream { 109 | public static void main(String[] args) { 110 | //! Sundae x = new Sundae(); 111 | Sundae x = Sundae.makeASundae(); 112 | } 113 | } ///:~ 114 | ``` 115 | 116 | 这个例子向我们证明了使用private的方便:有时可能想控制对象的创建方式,并防止有人直接访问一个特定的构造器(或者所有构造器)。在上面的例子中,我们不可通过它的构造器创建一个Sundae对象;相反,必须调用makeASundae()方法来实现(注释③)。 117 | 118 | ③:此时还会产生另一个影响:由于默认构造器是唯一获得定义的,而且它的属性是private,所以可防止对这个类的继承(这是第6章要重点讲述的主题)。 119 | 120 | 若确定一个类只有一个“助手”方法,那么对于任何方法来说,都可以把它们设为private,从而保证自己不会误在包内其他地方使用它,防止自己更改或删除方法。将一个方法的属性设为private后,可保证自己一直保持这一选项(然而,若一个指针被设为private,并不表明其他对象不能拥有指向同一个对象的public指针。有关“别名”的问题将在第12章详述)。 121 | 122 | 5.2.4 protected:“友好的一种” 123 | 124 | protected(受到保护的)访问指示符要求大家提前有所认识。首先应注意这样一个事实:为继续学习本书一直到继承那一章之前的内容,并不一定需要先理解本小节的内容。但为了保持内容的完整,这儿仍然要对此进行简要说明,并提供相关的例子。 125 | 126 | protected关键字为我们引入了一种名为“继承”的概念,它以现有的类为基础,并在其中加入新的成员,同时不会对现有的类产生影响——我们将这种现有的类称为“基础类”或者“基本类”(Base Class)。亦可改变那个类现有成员的行为。对于从一个现有类的继承,我们说自己的新类“扩展”(extends)了那个现有的类。如下所示: 127 | 128 | ``` java 129 | class Foo extends Bar { 130 | ``` 131 | 132 | 类定义剩余的部分看起来是完全相同的。 133 | 134 | 若新建一个包,并从另一个包内的某个类里继承,则唯一能够访问的成员就是原来那个包的public成员。当然,如果在相同的包里进行继承,那么继承获得的包能够访问所有“友好”的成员。有些时候,基础类的创建者喜欢提供一个特殊的成员,并允许访问衍生类。这正是protected的工作。若往回引用5.2.2小节“public:接口访问”的那个Cookie.java文件,则下面这个类就不能访问“友好”的成员: 135 | 136 | ``` java 137 | //: ChocolateChip.java 138 | // Can't access friendly member 139 | // in another class 140 | import c05.dessert.*; 141 | 142 | public class ChocolateChip extends Cookie { 143 | public ChocolateChip() { 144 | System.out.println( 145 | "ChocolateChip constructor"); 146 | } 147 | public static void main(String[] args) { 148 | ChocolateChip x = new ChocolateChip(); 149 | //! x.foo(); // Can't access foo 150 | } 151 | } ///:~ 152 | ``` 153 | 154 | 对于继承,值得注意的一件有趣的事情是倘若方法foo()存在于类Cookie中,那么它也会存在于从Cookie继承的所有类中。但由于foo()在外部的包里是“友好”的,所以我们不能使用它。当然,亦可将其变成public。但这样一来,由于所有人都能自由访问它,所以可能并非我们所希望的局面。若象下面这样修改类Cookie: 155 | 156 | ``` java 157 | public class Cookie { 158 | public Cookie() { 159 | System.out.println("Cookie constructor"); 160 | } 161 | protected void foo() { 162 | System.out.println("foo"); 163 | } 164 | } 165 | ``` 166 | 167 | 那么仍然能在包dessert里“友好”地访问foo(),但从Cookie继承的其他东西亦可自由地访问它。然而,它并非公共的(public)。 168 | -------------------------------------------------------------------------------- /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 | ``` java 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 | ``` java 9 | public class Widget { 10 | ``` 11 | 12 | 也就是说,假若我们的库名是mylib,那么所有客户程序员都能访问Widget——通过下述语句: 13 | 14 | ``` java 15 | import mylib.Widget; 16 | ``` 17 | 18 | 或者 19 | 20 | ``` java 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 | ``` java 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 | ``` java 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 | 利用Java中的访问指示符,可有效控制类的创建者。那个类的用户可确切知道哪些是自己能够使用的,哪些则是可以忽略的。但更重要的一点是,它可确保没有任何用户能依赖一个类的基础实施机制的任何部分。作为一个类的创建者,我们可自由修改基础的实施细节,这一改变不会对客户程序员产生任何影响,因为他们不能访问类的那一部分。 15 | 有能力改变基础的实施细节后,除了能在以后改进自己的设置之外,也同时拥有了“犯错误”的自由。无论当初计划与设计时有多么仔细,仍然有可能出现一些失误。由于知道自己能相当安全地犯下这种错误,所以可以放心大胆地进行更多、更自由的试验。这对自己编程水平的提高是很有帮助的,使整个项目最终能更快、更好地完成。 16 | 17 | 一个类的公共接口是所有用户都能看见的,所以在进行分析与设计的时候,这是应尽量保证其准确性的最重要的一个部分。但也不必过于紧张,少许的误差仍然是允许的。若最初设计的接口存在少许问题,可考虑添加更多的方法,只要保证不删除客户程序员已在他们的代码里使用的东西。 18 | -------------------------------------------------------------------------------- /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包的一部分改变了什么东西吗? -------------------------------------------------------------------------------- /6.1 合成的语法.md: -------------------------------------------------------------------------------- 1 | # 6.1 合成的语法 2 | 3 | 4 | 就以前的学习情况来看,事实上已进行了多次“合成”操作。为进行合成,我们只需在新类里简单地置入对象指针即可。举个例子来说,假定需要在一个对象里容纳几个String对象、两种基本数据类型以及属于另一个类的一个对象。对于非基本类型的对象来说,只需将指针置于新类即可;而对于基本数据类型来说,则需在自己的类中定义它们。如下所示(若执行该程序时有麻烦,请参见第3章3.1.2小节“赋值”): 5 | 6 | ``` java 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 | ``` java 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 | ``` java 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 | ``` java 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 | ``` java 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类的构造器代码作为注释标注出去。同样解释会发生什么。 -------------------------------------------------------------------------------- /6.2 继承的语法.md: -------------------------------------------------------------------------------- 1 | # 6.2 继承的语法 2 | 3 | 4 | 继承与Java(以及其他OOP语言)非常紧密地结合在一起。我们早在第1章就为大家引入了继承的概念,并在那章之后到本章之前的各章里不时用到,因为一些特殊的场合要求必须使用继承。除此以外,创建一个类时肯定会进行继承,因为若非如此,会从Java的标准根类Object中继承。 5 | 6 | 用于合成的语法是非常简单且直观的。但为了进行继承,必须采用一种全然不同的形式。需要继承的时候,我们会说:“这个新类和那个旧类差不多。”为了在代码里表面这一观念,需要给出类名。但在类主体的起始花括号之前,需要放置一个关键字extends,在后面跟随“基础类”的名字。若采取这种做法,就可自动获得基础类的所有数据成员以及方法。下面是一个例子: 7 | 8 | ``` java 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 | ``` java 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 | ``` java 99 | Art constructor 100 | Drawing constructor 101 | Cartoon constructor 102 | ``` 103 | 104 | 可以看出,构建是在基础类的“外部”进行的,所以基础类会在衍生类访问它之前得到正确的初始化。 105 | 即使没有为Cartoon()创建一个构造器,编译器也会为我们自动合成一个默认构造器,并发出对基础类构造器的调用。 106 | 107 | 1. 含有自变量的构造器 108 | 109 | 上述例子有自己默认的构造器;也就是说,它们不含任何自变量。编译器可以很容易地调用它们,因为不存在具体传递什么自变量的问题。如果类没有默认的自变量,或者想调用含有一个自变量的某个基础类构造器,必须明确地编写对基础类的调用代码。这是用super关键字以及适当的自变量列表实现的,如下所示: 110 | 111 | ``` java 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.3 合成与继承的结合.md: -------------------------------------------------------------------------------- 1 | # 6.3 合成与继承的结合 2 | 3 | 4 | 许多时候都要求将合成与继承两种技术结合起来使用。下面这个例子展示了如何同时采用继承与合成技术,从而创建一个更复杂的类,同时进行必要的构造器初始化工作: 5 | 6 | ``` java 7 | //: PlaceSetting.java 8 | // Combining composition & inheritance 9 | 10 | class Plate { 11 | Plate(int i) { 12 | System.out.println("Plate constructor"); 13 | } 14 | } 15 | 16 | class DinnerPlate extends Plate { 17 | DinnerPlate(int i) { 18 | super(i); 19 | System.out.println( 20 | "DinnerPlate constructor"); 21 | } 22 | } 23 | 24 | class Utensil { 25 | Utensil(int i) { 26 | System.out.println("Utensil constructor"); 27 | } 28 | } 29 | 30 | class Spoon extends Utensil { 31 | Spoon(int i) { 32 | super(i); 33 | System.out.println("Spoon constructor"); 34 | } 35 | } 36 | 37 | class Fork extends Utensil { 38 | Fork(int i) { 39 | super(i); 40 | System.out.println("Fork constructor"); 41 | } 42 | } 43 | 44 | class Knife extends Utensil { 45 | Knife(int i) { 46 | super(i); 47 | System.out.println("Knife constructor"); 48 | } 49 | } 50 | 51 | // A cultural way of doing something: 52 | class Custom { 53 | Custom(int i) { 54 | System.out.println("Custom constructor"); 55 | } 56 | } 57 | 58 | public class PlaceSetting extends Custom { 59 | Spoon sp; 60 | Fork frk; 61 | Knife kn; 62 | DinnerPlate pl; 63 | PlaceSetting(int i) { 64 | super(i + 1); 65 | sp = new Spoon(i + 2); 66 | frk = new Fork(i + 3); 67 | kn = new Knife(i + 4); 68 | pl = new DinnerPlate(i + 5); 69 | System.out.println( 70 | "PlaceSetting constructor"); 71 | } 72 | public static void main(String[] args) { 73 | PlaceSetting x = new PlaceSetting(9); 74 | } 75 | } ///:~ 76 | ``` 77 | 78 | 尽管编译器会强迫我们对基础类进行初始化,并要求我们在构造器最开头做这一工作,但它并不会监视我们是否正确初始化了成员对象。所以对此必须特别加以留意。 79 | 80 | 6.3.1 确保正确的清除 81 | 82 | Java不具备象C++的“破坏器”那样的概念。在C++中,一旦破坏(清除)一个对象,就会自动调用破坏器方法。之所以将其省略,大概是由于在Java中只需简单地忘记对象,不需强行破坏它们。垃圾收集器会在必要的时候自动回收内存。 83 | 84 | 垃圾收集器大多数时候都能很好地工作,但在某些情况下,我们的类可能在自己的存在时期采取一些行动,而这些行动要求必须进行明确的清除工作。正如第4章已经指出的那样,我们并不知道垃圾收集器什么时候才会显身,或者说不知它何时会调用。所以一旦希望为一个类清除什么东西,必须写一个特别的方法,明确、专门地来做这件事情。同时,还要让客户程序员知道他们必须调用这个方法。而在所有这一切的后面,就如第9章(异常控制)要详细解释的那样,必须将这样的清除代码置于一个finally从句中,从而防范任何可能出现的异常事件。 85 | 86 | 下面介绍的是一个计算机辅助设计系统的例子,它能在屏幕上描绘图形: 87 | 88 | ``` java 89 | //: CADSystem.java 90 | // Ensuring proper cleanup 91 | import java.util.*; 92 | 93 | class Shape { 94 | Shape(int i) { 95 | System.out.println("Shape constructor"); 96 | } 97 | void cleanup() { 98 | System.out.println("Shape cleanup"); 99 | } 100 | } 101 | 102 | class Circle extends Shape { 103 | Circle(int i) { 104 | super(i); 105 | System.out.println("Drawing a Circle"); 106 | } 107 | void cleanup() { 108 | System.out.println("Erasing a Circle"); 109 | super.cleanup(); 110 | } 111 | } 112 | 113 | class Triangle extends Shape { 114 | Triangle(int i) { 115 | super(i); 116 | System.out.println("Drawing a Triangle"); 117 | } 118 | void cleanup() { 119 | System.out.println("Erasing a Triangle"); 120 | super.cleanup(); 121 | } 122 | } 123 | 124 | class Line extends Shape { 125 | private int start, end; 126 | Line(int start, int end) { 127 | super(start); 128 | this.start = start; 129 | this.end = end; 130 | System.out.println("Drawing a Line: " + 131 | start + ", " + end); 132 | } 133 | void cleanup() { 134 | System.out.println("Erasing a Line: " + 135 | start + ", " + end); 136 | super.cleanup(); 137 | } 138 | } 139 | 140 | public class CADSystem extends Shape { 141 | private Circle c; 142 | private Triangle t; 143 | private Line[] lines = new Line[10]; 144 | CADSystem(int i) { 145 | super(i + 1); 146 | for(int j = 0; j < 10; j++) 147 | lines[j] = new Line(j, j*j); 148 | c = new Circle(1); 149 | t = new Triangle(1); 150 | System.out.println("Combined constructor"); 151 | } 152 | void cleanup() { 153 | System.out.println("CADSystem.cleanup()"); 154 | t.cleanup(); 155 | c.cleanup(); 156 | for(int i = 0; i < lines.length; i++) 157 | lines[i].cleanup(); 158 | super.cleanup(); 159 | } 160 | public static void main(String[] args) { 161 | CADSystem x = new CADSystem(47); 162 | try { 163 | // Code and exception handling... 164 | } finally { 165 | x.cleanup(); 166 | } 167 | } 168 | } ///:~ 169 | ``` 170 | 171 | 这个系统中的所有东西都属于某种Shape(几何形状)。Shape本身是一种Object(对象),因为它是从根类明确继承的。每个类都重新定义了Shape的cleanup()方法,同时还要用super调用那个方法的基础类版本。尽管对象存在期间调用的所有方法都可负责做一些要求清除的工作,但对于特定的Shape类——Circle(圆)、Triangle(三角形)以及Line(直线),它们都拥有自己的构造器,能完成“作图”(draw)任务。每个类都有它们自己的cleanup()方法,用于将非内存的东西恢复回对象存在之前的景象。 172 | 173 | 在main()中,可看到两个新关键字:try和finally。我们要到第9章才会向大家正式引荐它们。其中,try关键字指出后面跟随的块(由花括号定界)是一个“警戒区”。也就是说,它会受到特别的待遇。其中一种待遇就是:该警戒区后面跟随的finally从句的代码肯定会得以执行——不管try块到底存不存在(通过异常控制技术,try块可有多种不寻常的应用)。在这里,finally从句的意思是“总是为x调用cleanup(),无论会发生什么事情”。这些关键字将在第9章进行全面、完整的解释。 174 | 175 | 在自己的清除方法中,必须注意对基础类以及成员对象清除方法的调用顺序——假若一个子对象要以另一个为基础。通常,应采取与C++编译器对它的“破坏器”采取的同样的形式:首先完成与类有关的所有特殊工作(可能要求基础类元素仍然可见),然后调用基础类清除方法,就象这儿演示的那样。 176 | 177 | 许多情况下,清除可能并不是个问题;只需让垃圾收集器尽它的职责即可。但一旦必须由自己明确清除,就必须特别谨慎,并要求周全的考虑。 178 | 179 | 1. 垃圾收集的顺序 180 | 181 | 不能指望自己能确切知道何时会开始垃圾收集。垃圾收集器可能永远不会得到调用。即使得到调用,它也可能以自己愿意的任何顺序回收对象。除此以外,Java 1.0实现的垃圾收集器机制通常不会调用finalize()方法。除内存的回收以外,其他任何东西都最好不要依赖垃圾收集器进行回收。若想明确地清除什么,请制作自己的清除方法,而且不要依赖finalize()。然而正如以前指出的那样,可强迫Java1.1调用所有收尾模块(Finalizer)。 182 | 183 | 6.3.2 名字的隐藏 184 | 185 | 只有C++程序员可能才会惊讶于名字的隐藏,因为它的工作原理与在C++里是完全不同的。如果Java基础类有一个方法名被“重载”使用多次,在衍生类里对那个方法名的重新定义就不会隐藏任何基础类的版本。所以无论方法在这一级还是在一个基础类中定义,重载都会生效: 186 | 187 | ``` java 188 | //: Hide.java 189 | // Overloading a base-class method name 190 | // in a derived class does not hide the 191 | // base-class versions 192 | 193 | class Homer { 194 | char doh(char c) { 195 | System.out.println("doh(char)"); 196 | return 'd'; 197 | } 198 | float doh(float f) { 199 | System.out.println("doh(float)"); 200 | return 1.0f; 201 | } 202 | } 203 | 204 | class Milhouse {} 205 | 206 | class Bart extends Homer { 207 | void doh(Milhouse m) {} 208 | } 209 | 210 | class Hide { 211 | public static void main(String[] args) { 212 | Bart b = new Bart(); 213 | b.doh(1); // doh(float) used 214 | b.doh('x'); 215 | b.doh(1.0f); 216 | b.doh(new Milhouse()); 217 | } 218 | } ///:~ 219 | ``` 220 | 221 | 正如下一章会讲到的那样,很少会用与基础类里完全一致的签名和返回类型来覆盖同名的方法,否则会使人感到迷惑(这正是C++不允许那样做的原因,所以能够防止产生一些不必要的错误)。 222 | -------------------------------------------------------------------------------- /6.4 到底选择合成还是继承.md: -------------------------------------------------------------------------------- 1 | # 6.4 到底选择合成还是继承 2 | 3 | 4 | 无论合成还是继承,都允许我们将子对象置于自己的新类中。大家或许会奇怪两者间的差异,以及到底该如何选择。 5 | 如果想利用新类内部一个现有类的特性,而不想使用它的接口,通常应选择合成。也就是说,我们可嵌入一个对象,使自己能用它实现新类的特性。但新类的用户会看到我们已定义的接口,而不是来自嵌入对象的接口。考虑到这种效果,我们需在新类里嵌入现有类的private对象。 6 | 7 | 有些时候,我们想让类用户直接访问新类的合成。也就是说,需要将成员对象的属性变为public。成员对象会将自身隐藏起来,所以这是一种安全的做法。而且在用户知道我们准备合成一系列组件时,接口就更容易理解。car(汽车)对象便是一个很好的例子: 8 | 9 | ``` java 10 | //: Car.java 11 | // Composition with public objects 12 | 13 | class Engine { 14 | public void start() {} 15 | public void rev() {} 16 | public void stop() {} 17 | } 18 | 19 | class Wheel { 20 | public void inflate(int psi) {} 21 | } 22 | 23 | class Window { 24 | public void rollup() {} 25 | public void rolldown() {} 26 | } 27 | 28 | class Door { 29 | public Window window = new Window(); 30 | public void open() {} 31 | public void close() {} 32 | } 33 | 34 | public class Car { 35 | public Engine engine = new Engine(); 36 | public Wheel[] wheel = new Wheel[4]; 37 | public Door left = new Door(), 38 | right = new Door(); // 2-door 39 | Car() { 40 | for(int i = 0; i < 4; i++) 41 | wheel[i] = new Wheel(); 42 | } 43 | public static void main(String[] args) { 44 | Car car = new Car(); 45 | car.left.window.rollup(); 46 | car.wheel[0].inflate(72); 47 | } 48 | } ///:~ 49 | ``` 50 | 51 | 由于汽车的装配是故障分析时需要考虑的一项因素(并非只是基础设计简单的一部分),所以有助于客户程序员理解如何使用类,而且类创建者的编程复杂程度也会大幅度降低。 52 | 53 | 如选择继承,就需要取得一个现成的类,并制作它的一个特殊版本。通常,这意味着我们准备使用一个常规用途的类,并根据特定的需求对其进行定制。只需稍加想象,就知道自己不能用一个车辆对象来合成一辆汽车——汽车并不“包含”车辆;相反,它“属于”车辆的一种类别。“属于”关系是用继承来表达的,而“包含”关系是用合成来表达的。 54 | -------------------------------------------------------------------------------- /6.5 protected.md: -------------------------------------------------------------------------------- 1 | # 6.5 protected 2 | 3 | 现在我们已理解了继承的概念,protected这个关键字最后终于有了意义。在理想情况下,private成员随时都是“私有”的,任何人不得访问。但在实际应用中,经常想把某些东西深深地藏起来,但同时允许访问衍生类的成员。protected关键字可帮助我们做到这一点。它的意思是“它本身是私有的,但可由从这个类继承的任何东西或者同一个包内的其他任何东西访问”。也就是说,Java中的protected会成为进入“友好”状态。 4 | 5 | 我们采取的最好的做法是保持成员的private状态——无论如何都应保留对基 础的实施细节进行修改的权利。在这一前提下,可通过protected方法允许类的继承者进行受到控制的访问: 6 | 7 | ``` java 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 | ``` java 8 | 大家要记住这样一个重点:程序开发是一个不断递增或者累积的过程,就象人们学习知识一样。当然可根据要求进行尽可能多的分析,但在一个项目的设计之初,谁都不可能提前获知所有的答案。如果能将自己的项目看作一个有机的、能不断进步的生物,从而不断地发展和改进它,就有望获得更大的成功以及更直接的反馈。 9 | 10 | 尽管继承是一种非常有用的技术,但在某些情况下,特别是在项目稳定下来以后,仍然需要从新的角度考察自己的类结构,将其收缩成一个更灵活的结构。请记住,继承是对一种特殊关系的表达,意味着“这个新类属于那个旧类的一种类型”。我们的程序不应纠缠于一些细树末节,而应着眼于创建和操作各种类型的对象,用它们表达出来自“问题空间”的一个模型。 11 | -------------------------------------------------------------------------------- /6.7 上溯造型.md: -------------------------------------------------------------------------------- 1 | # 6.7 上溯造型 2 | 3 | 4 | 继承最值得注意的地方就是它没有为新类提供方法。继承是对新类和基础类之间的关系的一种表达。可这样总结该关系:“新类属于现有类的一种类型”。 5 | 6 | 这种表达并不仅仅是对继承的一种形象化解释,继承是直接由语言提供支持的。作为一个例子,大家可考虑一个名为Instrument的基础类,它用于表示乐器;另一个衍生类叫作Wind。由于继承意味着基础类的所有方法亦可在衍生出来的类中使用,所以我们发给基础类的任何消息亦可发给衍生类。若Instrument类有一个play()方法,则Wind设备也会有这个方法。这意味着我们能肯定地认为一个Wind对象也是Instrument的一种类型。下面这个例子揭示出编译器如何提供对这一概念的支持: 7 | 8 | ``` java 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 | ``` java 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 | ``` java 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 | -------------------------------------------------------------------------------- /7-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/7-1.gif -------------------------------------------------------------------------------- /7-10.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/7-10.gif -------------------------------------------------------------------------------- /7-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/7-2.gif -------------------------------------------------------------------------------- /7-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/7-3.gif -------------------------------------------------------------------------------- /7-4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/7-4.gif -------------------------------------------------------------------------------- /7-5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/7-5.gif -------------------------------------------------------------------------------- /7-6.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/7-6.gif -------------------------------------------------------------------------------- /7-7.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/7-7.gif -------------------------------------------------------------------------------- /7-8.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/7-8.gif -------------------------------------------------------------------------------- /7-9.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/7-9.gif -------------------------------------------------------------------------------- /7.1 上溯造型.md: -------------------------------------------------------------------------------- 1 | # 7.1 上溯造型 2 | 3 | 4 | 在第6章,大家已知道可将一个对象作为它自己的类型使用,或者作为它的基础类型的一个对象使用。取得一个对象指针,并将其作为基础类型指针使用的行为就叫作“上溯造型”——因为继承树的画法是基础类位于最上方。 5 | 6 | 但这样做也会遇到一个问题,如下例所示(若执行这个程序遇到麻烦,请参考第3章的3.1.2小节“赋值”): 7 | 8 | ``` java 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 | ``` java 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内部类,使其能打开和关闭风扇。 -------------------------------------------------------------------------------- /7.3 覆盖与重载.md: -------------------------------------------------------------------------------- 1 | # 7.3 覆盖与重载 2 | 3 | 4 | 现在让我们用不同的眼光来看看本章的头一个例子。在下面这个程序中,方法play()的接口会在被覆盖的过程中发生变化。这意味着我们实际并没有“覆盖”方法,而是使其“重载”。编译器允许我们对方法进行重载处理,使其不报告出错。但这种行为可能并不是我们所希望的。下面是这个例子: 5 | 6 | ``` java 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 | @Overload 24 | public void play(NoteX n) { 25 | System.out.println("WindX.play(NoteX n)"); 26 | } 27 | } 28 | 29 | public class WindError { 30 | public static void tune(InstrumentX i) { 31 | // ... 32 | i.play(NoteX.MIDDLE_C); 33 | } 34 | public static void main(String[] args) { 35 | WindX flute = new WindX(); 36 | tune(flute); // Not the desired behavior! 37 | } 38 | } ///:~ 39 | ``` 40 | 41 | 这里还向大家引入了另一个易于混淆的概念。在InstrumentX中,play()方法采用了一个int(整数)数值,它的标识符是NoteX。也就是说,即使NoteX是一个类名,也可以把它作为一个标识符使用,编译器不会报告出错。但在WindX中,play()采用一个NoteX指针,它有一个标识符n。即便我们使用“play(NoteX NoteX)”,编译器也不会报告错误。这样一来,看起来就象是程序员有意覆盖play()的功能,但对方法的类型定义却稍微有些不确切。然而,编译器此时假定的是程序员有意进行“重载”,而非“覆盖”。请仔细体会这两个术语的区别。“重载”是指同一样东西在不同的地方具有多种含义;而“覆盖”是指它随时随地都只有一种含义,只是原先的含义完全被后来的含义取代了。请注意如果遵守标准的Java命名规范,自变量标识符就应该是noteX,这样可把它与类名区分开。 42 | 43 | 在tune中,“InstrumentX i”会发出play()消息,同时将某个NoteX成员作为自变量使用(MIDDLE_C)。由于NoteX包含了int定义,重载的play()方法的int版本会得到调用。同时由于它尚未被“覆盖”,所以会使用基础类版本。 44 | 45 | 输出是: 46 | 47 | ``` java 48 | InstrumentX.play() 49 | ``` java 50 | -------------------------------------------------------------------------------- /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 | ``` java 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 | ``` java 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 | ``` java 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 | ![](7-8.gif) 65 | 66 | 尽管这是一种有用和明智的做法(由具体的环境决定),但它也有一个缺点:衍生类中对接口扩展的那一部分不可在基础类中使用。所以一旦上溯造型,就不可再调用新方法: 67 | 68 | ![](7-9.gif) 69 | 70 | 若在此时不进行上溯造型,则不会出现此类问题。但在许多情况下,都需要重新核实对象的准确类型,使自己能访问那个类型的扩展方法。在后面的小节里,我们具体讲述了这是如何实现的。 71 | 72 | 7.8.2 下溯造型与运行期类型标识 73 | 74 | 由于我们在上溯造型(在继承结构中向上移动)期间丢失了具体的类型信息,所以为了获取具体的类型信息——亦即在分级结构中向下移动——我们必须使用 “下溯造型”技术。然而,我们知道一个上溯造型肯定是安全的;基础类不可能再拥有一个比衍生类更大的接口。因此,我们通过基础类接口发送的每一条消息都肯定能够接收到。但在进行下溯造型的时候,我们(举个例子来说)并不真的知道一个几何形状实际是一个圆,它完全可能是一个三角形、方形或者其他形状。 75 | 76 | ![](7-10.gif) 77 | 78 | 为解决这个问题,必须有一种办法能够保证下溯造型正确进行。只有这样,我们才不会冒然造型成一种错误的类型,然后发出一条对象不可能收到的消息。这样做是非常不安全的。 79 | 80 | 在某些语言中(如C++),为了进行保证“类型安全”的下溯造型,必须采取特殊的操作。但在Java中,所有造型都会自动得到检查和核实!所以即使我们只是进行一次普通的括弧造型,进入运行期以后,仍然会毫无留情地对这个造型进行检查,保证它的确是我们希望的那种类型。如果不是,就会得到一个ClassCastException(类造型异常)。在运行期间对类型进行检查的行为叫作“运行期类型标识”(RTTI)。下面这个例子向大家演示了RTTI的行为: 81 | 82 | ``` java 83 | //: RTTI.java 84 | // Downcasting & Run-Time Type 85 | // Identification (RTTI) 86 | import java.util.*; 87 | 88 | class Useful { 89 | public void f() {} 90 | public void g() {} 91 | } 92 | 93 | class MoreUseful extends Useful { 94 | public void f() {} 95 | public void g() {} 96 | public void u() {} 97 | public void v() {} 98 | public void w() {} 99 | } 100 | 101 | public class RTTI { 102 | public static void main(String[] args) { 103 | Useful[] x = { 104 | new Useful(), 105 | new MoreUseful() 106 | }; 107 | x[0].f(); 108 | x[1].g(); 109 | // Compile-time: method not found in Useful: 110 | //! x[1].u(); 111 | ((MoreUseful)x[1]).u(); // Downcast/RTTI 112 | ((MoreUseful)x[0]).u(); // Exception thrown 113 | } 114 | } ///:~ 115 | ``` 116 | 117 | 和在示意图中一样,MoreUseful(更有用的)对Useful(有用的)的接口进行了扩展。但由于它是继承来的,所以也能上溯造型到一个Useful。我们可看到这会在对数组x(位于main()中)进行初始化的时候发生。由于数组中的两个对象都属于Useful类,所以可将f()和g()方法同时发给它们两个。而且假如试图调用u()(它只存在于MoreUseful),就会收到一条编译期出错提示。 118 | 119 | 若想访问一个MoreUseful对象的扩展接口,可试着进行下溯造型。如果它是正确的类型,这一行动就会成功。否则,就会得到一个ClassCastException。我们不必为这个异常编写任何特殊的代码,因为它指出的是一个可能在程序中任何地方发生的一个编程错误。 120 | 121 | RTTI的意义远不仅仅反映在造型处理上。例如,在试图下溯造型之前,可通过一种方法了解自己处理的是什么类型。整个第11章都在讲述Java运行期类型标识的方方面面。 122 | -------------------------------------------------------------------------------- /7.9 总结.md: -------------------------------------------------------------------------------- 1 | # 7.9 总结 2 | 3 | 4 | “多态性”意味着“不同的形式”。在面向对象的程序设计中,我们有相同的外观(基础类的通用接口)以及使用那个外观的不同形式:动态绑定或组织的、不同版本的方法。 5 | 6 | 通过这一章的学习,大家已知道假如不利用数据抽象以及继承技术,就不可能理解、甚至去创建多态性的一个例子。多态性是一种不可独立应用的特性(就象一个switch语句),只可与其他元素协同使用。我们应将其作为类总体关系的一部分来看待。人们经常混淆Java其他的、非面向对象的特性,比如方法重载等,这些特性有时也具有面向对象的某些特征。但不要被愚弄:如果以后没有绑定,就不成其为多态性。 7 | 8 | 为使用多态性乃至面向对象的技术,特别是在自己的程序中,必须将自己的编程视野扩展到不仅包括单独一个类的成员和消息,也要包括类与类之间的一致性以及它们的关系。尽管这要求学习时付出更多的精力,但却是非常值得的,因为只有这样才可真正有效地加快自己的编程速度、更好地组织代码、更容易做出包容面广的程序以及更易对自己的代码进行维护与扩展。 9 | 10 | -------------------------------------------------------------------------------- /8-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/8-1.gif -------------------------------------------------------------------------------- /8.2 集合.md: -------------------------------------------------------------------------------- 1 | # 8.2 集合 2 | 3 | 4 | 现在总结一下我们前面学过的东西:为容纳一组对象,最适宜的选择应当是数组。而且假如容纳的是一系列基本数据类型,更是必须采用数组。在本章剩下的部分,大家将接触到一些更常规的情况。当我们编写程序时,通常并不能确切地知道最终需要多少个对象。有些时候甚至想用更复杂的方式来保存对象。为解决这个问题,Java提供了四种类型的“集合类”:Vector(矢量)、BitSet(位集)、Stack(栈)以及Hashtable(散列表)。与拥有集合功能的其他语言相比,尽管这儿的数量显得相当少,但仍然能用它们解决数量惊人的实际问题。 5 | 6 | 这些集合类具有形形色色的特征。例如,Stack实现了一个LIFO(先入先出)序列,而Hashtable是一种“关联数组”,允许我们将任何对象关联起来。除此以外,所有Java集合类都能自动改变自身的大小。所以,我们在编程时可使用数量众多的对象,同时不必担心会将集合弄得有多大。 7 | 8 | 8.2.1 缺点:类型未知 9 | 10 | 使用Java集合的“缺点”是在将对象置入一个集合时丢失了类型信息。之所以会发生这种情况,是由于当初编写集合时,那个集合的程序员根本不知道用户到底想把什么类型置入集合。若指示某个集合只允许特定的类型,会妨碍它成为一个“常规用途”的工具,为用户带来麻烦。为解决这个问题,集合实际容纳的是类型为Object的一些对象的指针。这种类型当然代表Java中的所有对象,因为它是所有类的根。当然,也要注意这并不包括基本数据类型,因为它们并不是从“任何东西”继承来的。这是一个很好的方案,只是不适用下述场合: 11 | 12 | (1) 将一个对象指针置入集合时,由于类型信息会被抛弃,所以任何类型的对象都可进入我们的集合——即便特别指示它只能容纳特定类型的对象。举个例子来说,虽然指示它只能容纳猫,但事实上任何人都可以把一条狗扔进来。 13 | 14 | (2) 由于类型信息不复存在,所以集合能肯定的唯一事情就是自己容纳的是指向一个对象的指针。正式使用它之前,必须对其进行造型,使其具有正确的类型。 15 | 16 | 值得欣慰的是,Java不允许人们滥用置入集合的对象。假如将一条狗扔进一个猫的集合,那么仍会将集合内的所有东西都看作猫,所以在使用那条狗时会得到一个“异常”错误。在同样的意义上,假若试图将一条狗的指针“造型”到一只猫,那么运行期间仍会得到一个“异常”错误。 17 | 18 | 下面是个例子: 19 | 20 | ``` java 21 | //: CatsAndDogs.java 22 | // Simple collection example (Vector) 23 | import java.util.*; 24 | 25 | class Cat { 26 | private int catNumber; 27 | Cat(int i) { 28 | catNumber = i; 29 | } 30 | void print() { 31 | System.out.println("Cat #" + catNumber); 32 | } 33 | } 34 | 35 | class Dog { 36 | private int dogNumber; 37 | Dog(int i) { 38 | dogNumber = i; 39 | } 40 | void print() { 41 | System.out.println("Dog #" + dogNumber); 42 | } 43 | } 44 | 45 | public class CatsAndDogs { 46 | public static void main(String[] args) { 47 | Vector cats = new Vector(); 48 | for(int i = 0; i < 7; i++) 49 | cats.addElement(new Cat(i)); 50 | // Not a problem to add a dog to cats: 51 | cats.addElement(new Dog(7)); 52 | for(int i = 0; i < cats.size(); i++) 53 | ((Cat)cats.elementAt(i)).print(); 54 | // Dog is detected only at run-time 55 | } 56 | } ///:~ 57 | ``` 58 | 59 | 可以看出,Vector的使用是非常简单的:先创建一个,再用addElement()置入对象,以后用elementAt()取得那些对象(注意Vector有一个size()方法,可使我们知道已添加了多少个元素,以便防止误超边界,造成异常错误)。 60 | 61 | Cat和Dog类都非常浅显——除了都是“对象”之外,它们并无特别之处(倘若不明确指出从什么类继承,就默认为从Object继承。所以我们不仅能用Vector方法将Cat对象置入这个集合,也能添加Dog对象,同时不会在编译期和运行期得到任何出错提示。用Vector方法elementAt()获取原本认为是Cat的对象时,实际获得的是指向一个Object的指针,必须将那个对象造型为Cat。随后,需要将整个表达式用括号封闭起来,在为Cat调用print()方法之前进行强制造型;否则就会出现一个语法错误。在运行期间,如果试图将Dog对象造型为Cat,就会得到一个异常。 62 | 63 | 这些处理的意义都非常深远。尽管显得有些麻烦,但却获得了安全上的保证。我们从此再难偶然造成一些隐藏得深的错误。若程序的一个部分(或几个部分)将对象插入一个集合,但我们只是通过一次异常在程序的某个部分发现一个错误的对象置入了集合,就必须找出插入错误的位置。当然,可通过检查代码达到这个目的,但这或许是最笨的调试工具。另一方面,我们可从一些标准化的集合类开始自己的编程。尽管它们在功能上存在一些不足,且显得有些笨拙,但却能保证没有隐藏的错误。 64 | 65 | 1. 错误有时并不显露出来 66 | 67 | 在某些情况下,程序似乎正确地工作,不造型回我们原来的类型。第一种情况是相当特殊的:String类从编译器获得了额外的帮助,使其能够正常工作。只要编译器期待的是一个String对象,但它没有得到一个,就会自动调用在Object里定义、并且能够由任何Java类覆盖的toString()方法。这个方法能生成满足要求的String对象,然后在我们需要的时候使用。 68 | 69 | 因此,为了让自己类的对象能显示出来,要做的全部事情就是覆盖toString()方法,如下例所示: 70 | 71 | ``` java 72 | //: WorksAnyway.java 73 | // In special cases, things just seem 74 | // to work correctly. 75 | import java.util.*; 76 | 77 | class Mouse { 78 | private int mouseNumber; 79 | Mouse(int i) { 80 | mouseNumber = i; 81 | } 82 | // Magic method: 83 | public String toString() { 84 | return "This is Mouse #" + mouseNumber; 85 | } 86 | void print(String msg) { 87 | if(msg != null) System.out.println(msg); 88 | System.out.println( 89 | "Mouse number " + mouseNumber); 90 | } 91 | } 92 | 93 | class MouseTrap { 94 | static void caughtYa(Object m) { 95 | Mouse mouse = (Mouse)m; // Cast from Object 96 | mouse.print("Caught one!"); 97 | } 98 | } 99 | 100 | public class WorksAnyway { 101 | public static void main(String[] args) { 102 | Vector mice = new Vector(); 103 | for(int i = 0; i < 3; i++) 104 | mice.addElement(new Mouse(i)); 105 | for(int i = 0; i < mice.size(); i++) { 106 | // No cast necessary, automatic call 107 | // to Object.toString(): 108 | System.out.println( 109 | "Free mouse: " + mice.elementAt(i)); 110 | MouseTrap.caughtYa(mice.elementAt(i)); 111 | } 112 | } 113 | } ///:~ 114 | ``` 115 | 116 | 可在Mouse里看到对toString()的重定义代码。在main()的第二个for循环中,可发现下述语句: 117 | 118 | ``` java 119 | System.out.println("Free mouse: " + 120 | mice.elementAt(i)); 121 | ``` 122 | 123 | 在“+”后,编译器预期看到的是一个String对象。elementAt()生成了一个Object,所以为获得希望的String,编译器会默认调用toString()。但不幸的是,只有针对String才能得到象这样的结果;其他任何类型都不会进行这样的转换。 124 | 隐藏造型的第二种方法已在Mousetrap里得到了应用。caughtYa()方法接收的不是一个Mouse,而是一个Object。随后再将其造型为一个Mouse。当然,这样做是非常冒失的,因为通过接收一个Object,任何东西都可以传递给方法。然而,假若造型不正确——如果我们传递了错误的类型——就会在运行期间得到一个异常错误。这当然没有在编译期进行检查好,但仍然能防止问题的发生。注意在使用这个方法时毋需进行造型: 125 | 126 | ``` java 127 | MouseTrap.caughtYa(mice.elementAt(i)); 128 | ``` 129 | 130 | 2. 生成能自动判别类型的Vector 131 | 132 | 大家或许不想放弃刚才那个问题。一个更“健壮”的方案是用Vector创建一个新类,使其只接收我们指定的类型,也只生成我们希望的类型。如下所示: 133 | 134 | ``` java 135 | //: GopherVector.java 136 | // A type-conscious Vector 137 | import java.util.*; 138 | 139 | class Gopher { 140 | private int gopherNumber; 141 | Gopher(int i) { 142 | gopherNumber = i; 143 | } 144 | void print(String msg) { 145 | if(msg != null) System.out.println(msg); 146 | System.out.println( 147 | "Gopher number " + gopherNumber); 148 | } 149 | } 150 | 151 | class GopherTrap { 152 | static void caughtYa(Gopher g) { 153 | g.print("Caught one!"); 154 | } 155 | } 156 | 157 | class GopherVector { 158 | private Vector v = new Vector(); 159 | public void addElement(Gopher m) { 160 | v.addElement(m); 161 | } 162 | public Gopher elementAt(int index) { 163 | return (Gopher)v.elementAt(index); 164 | } 165 | public int size() { return v.size(); } 166 | public static void main(String[] args) { 167 | GopherVector gophers = new GopherVector(); 168 | for(int i = 0; i < 3; i++) 169 | gophers.addElement(new Gopher(i)); 170 | for(int i = 0; i < gophers.size(); i++) 171 | GopherTrap.caughtYa(gophers.elementAt(i)); 172 | } 173 | } ///:~ 174 | ``` 175 | 176 | 这前一个例子类似,只是新的GopherVector类有一个类型为Vector的private成员(从Vector继承有些麻烦,理由稍后便知),而且方法也和Vector类似。然而,它不会接收和产生普通Object,只对Gopher对象感兴趣。 177 | 由于GopherVector只接收一个Gopher(地鼠),所以假如我们使用: 178 | 179 | ``` java 180 | gophers.addElement(new Pigeon()); 181 | ``` 182 | 183 | 就会在编译期间获得一条出错消息。采用这种方式,尽管从编码的角度看显得更令人沉闷,但可以立即判断出是否使用了正确的类型。 184 | 185 | 注意在使用elementAt()时不必进行造型——它肯定是一个Gopher。 186 | 187 | 3. 参数化类型 188 | 189 | 这类问题并不是孤立的——我们许多时候都要在其他类型的基础上创建新类型。此时,在编译期间拥有特定的类型信息是非常有帮助的。这便是“参数化类型”的概念。在C++中,它由语言通过“模板”获得了直接支持。至少,Java保留了关键字generic,期望有一天能够支持参数化类型。但我们现在无法确定这一天何时会来临。 190 | -------------------------------------------------------------------------------- /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 | ``` java 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 | ``` java 64 | for(int i = 0; i < cats.size(); i++) 65 | ((Cat)cats.elementAt(i)).print(); 66 | ``` 67 | 68 | 而是用一个Enumeration遍历整个序列: 69 | 70 | ``` java 71 | while(e.hasMoreElements()) 72 | ((Cat2)e.nextElement()).print(); 73 | ``` 74 | 75 | 使用Enumeration,我们不必关心集合中的元素数量。所有工作均由hasMoreElements()和nextElement()自动照管了。 76 | 下面再看看另一个例子,让我们创建一个常规用途的打印方法: 77 | 78 | ``` java 79 | //: HamsterMaze.java 80 | // Using an Enumeration 81 | import java.util.*; 82 | 83 | class Hamster { 84 | private int hamsterNumber; 85 | Hamster(int i) { 86 | hamsterNumber = i; 87 | } 88 | public String toString() { 89 | return "This is Hamster #" + hamsterNumber; 90 | } 91 | } 92 | 93 | class Printer { 94 | static void printAll(Enumeration e) { 95 | while(e.hasMoreElements()) 96 | System.out.println( 97 | e.nextElement().toString()); 98 | } 99 | } 100 | 101 | public class HamsterMaze { 102 | public static void main(String[] args) { 103 | Vector v = new Vector(); 104 | for(int i = 0; i < 3; i++) 105 | v.addElement(new Hamster(i)); 106 | Printer.printAll(v.elements()); 107 | } 108 | } ///:~ 109 | ``` 110 | 111 | 仔细研究一下打印方法: 112 | 113 | ``` java 114 | static void printAll(Enumeration e) { 115 | while(e.hasMoreElements()) 116 | System.out.println( 117 | e.nextElement().toString()); 118 | } 119 | ``` 120 | 121 | 注意其中没有与序列类型有关的信息。我们拥有的全部东西便是Enumeration。为了解有关序列的情况,一个Enumeration便足够了:可取得下一个对象,亦可知道是否已抵达了末尾。取得一系列对象,然后在其中遍历,从而执行一个特定的操作——这是一个颇有价值的编程概念,本书许多地方都会沿用这一思路。 122 | 123 | 这个看似特殊的例子甚至可以更为通用,因为它使用了常规的toString()方法(之所以称为常规,是由于它属于Object类的一部分)。下面是调用打印的另一个方法(尽管在效率上可能会差一些): 124 | 125 | ``` java 126 | System.out.println("" + e.nextElement()); 127 | ``` 128 | 129 | 它采用了封装到Java内部的“自动转换成字串”技术。一旦编译器碰到一个字串,后面跟随一个“+”,就会希望后面又跟随一个字串,并自动调用toString()。在Java 1.1中,第一个字串是不必要的;所有对象都会转换成字串。亦可对此执行一次造型,获得与调用toString()同样的效果: 130 | 131 | ``` java 132 | System.out.println((String)e.nextElement()) 133 | ``` 134 | 135 | 但我们想做的事情通常并不仅仅是调用Object方法,所以会再度面临类型造型的问题。对于自己感兴趣的类型,必须假定自己已获得了一个Enumeration,然后将结果对象造型成为那种类型(若操作错误,会得到运行期异常)。 136 | -------------------------------------------------------------------------------- /8.5 排序.md: -------------------------------------------------------------------------------- 1 | # 8.5 排序 2 | 3 | Java 1.0和1.1库都缺少的一样东西是算术运算,甚至没有最简单的排序运算方法。因此,我们最好创建一个Vector,利用经典的Quicksort(快速排序)方法对其自身进行排序。 4 | 5 | 编写通用的排序代码时,面临的一个问题是必须根据对象的实际类型来执行比较运算,从而实现正确的排序。当然,一个办法是为每种不同的类型都写一个不同的排序方法。然而,应认识到假若这样做,以后增加新类型时便不易实现代码的重复利用。 6 | 7 | 程序设计一个主要的目标就是“将发生变化的东西同保持不变的东西分隔开”。在这里,保持不变的代码是通用的排序算法,而每次使用时都要变化的是对象的实际比较方法。因此,我们不可将比较代码“硬编码”到多个不同的排序例程内,而是采用“回调”技术。利用回调,经常发生变化的那部分代码会封装到它自己的类内,而总是保持相同的代码则“回调”发生变化的代码。这样一来,不同的对象就可以表达不同的比较方式,同时向它们传递相同的排序代码。 8 | 9 | 下面这个“接口”(Interface)展示了如何比较两个对象,它将那些“要发生变化的东西”封装在内: 10 | 11 | ``` java 12 | //: Compare.java 13 | // Interface for sorting callback: 14 | package c08; 15 | 16 | interface Compare { 17 | boolean lessThan(Object lhs, Object rhs); 18 | boolean lessThanOrEqual(Object lhs, Object rhs); 19 | } ///:~ 20 | ``` 21 | 22 | 对这两种方法来说,lhs代表本次比较中的“左手”对象,而rhs代表“右手”对象。 23 | 24 | 可创建Vector的一个子类,通过Compare实现“快速排序”。对于这种算法,包括它的速度以及原理等等,在此不具体说明。欲知详情,可参考Binstock和Rex编著的《Practical Algorithms for Programmers》,由Addison-Wesley于1995年出版。 25 | 26 | ``` java 27 | //: SortVector.java 28 | // A generic sorting vector 29 | package c08; 30 | import java.util.*; 31 | 32 | public class SortVector extends Vector { 33 | private Compare compare; // To hold the callback 34 | public SortVector(Compare comp) { 35 | compare = comp; 36 | } 37 | public void sort() { 38 | quickSort(0, size() - 1); 39 | } 40 | private void quickSort(int left, int right) { 41 | if(right > left) { 42 | Object o1 = elementAt(right); 43 | int i = left - 1; 44 | int j = right; 45 | while(true) { 46 | while(compare.lessThan( 47 | elementAt(++i), o1)) 48 | ; 49 | while(j > 0) 50 | if(compare.lessThanOrEqual( 51 | elementAt(--j), o1)) 52 | break; // out of while 53 | if(i >= j) break; 54 | swap(i, j); 55 | } 56 | swap(i , right); 57 | quickSort(left, i-1); 58 | quickSort(i+1, right); 59 | } 60 | } 61 | private void swap(int loc1, int loc2) { 62 | Object tmp = elementAt(loc1); 63 | setElementAt(elementAt(loc2), loc1); 64 | setElementAt(tmp, loc2); 65 | } 66 | } ///:~ 67 | ``` 68 | 69 | 现在,大家可以明白“回调”一词的来历,这是由于quickSort()方法“往回调用”了Compare中的方法。从中亦可理解这种技术如何生成通用的、可重复利用(再生)的代码。 70 | 71 | 为使用SortVector,必须创建一个类,令其为我们准备排序的对象实现Compare。此时内部类并不显得特别重要,但对于代码的组织却是有益的。下面是针对String对象的一个例子: 72 | 73 | ``` java 74 | //: StringSortTest.java 75 | // Testing the generic sorting Vector 76 | package c08; 77 | import java.util.*; 78 | 79 | public class StringSortTest { 80 | static class StringCompare implements Compare { 81 | public boolean lessThan(Object l, Object r) { 82 | return ((String)l).toLowerCase().compareTo( 83 | ((String)r).toLowerCase()) < 0; 84 | } 85 | public boolean 86 | lessThanOrEqual(Object l, Object r) { 87 | return ((String)l).toLowerCase().compareTo( 88 | ((String)r).toLowerCase()) <= 0; 89 | } 90 | } 91 | public static void main(String[] args) { 92 | SortVector sv = 93 | new SortVector(new StringCompare()); 94 | sv.addElement("d"); 95 | sv.addElement("A"); 96 | sv.addElement("C"); 97 | sv.addElement("c"); 98 | sv.addElement("b"); 99 | sv.addElement("B"); 100 | sv.addElement("D"); 101 | sv.addElement("a"); 102 | sv.sort(); 103 | Enumeration e = sv.elements(); 104 | while(e.hasMoreElements()) 105 | System.out.println(e.nextElement()); 106 | } 107 | } ///:~ 108 | ``` 109 | 110 | 内部类是“静态”(Static)的,因为它毋需连接一个外部类即可工作。 111 | 112 | 大家可以看到,一旦设置好框架,就可以非常方便地重复使用象这样的一个设计——只需简单地写一个类,将“需要发生变化”的东西封装进去,然后将一个对象传给SortVector即可。 113 | 114 | 比较时将字串强制为小写形式,所以大写A会排列于小写a的旁边,而不会移动一个完全不同的地方。然而,该例也显示了这种方法的一个不足,因为上述测试代码按照出现顺序排列同一个字母的大写和小写形式:A a b B c C d D。但这通常不是一个大问题,因为经常处理的都是更长的字串,所以上述效果不会显露出来(Java 1.2的集合提供了排序功能,已解决了这个问题)。 115 | 116 | 继承(extends)在这儿用于创建一种新类型的Vector——也就是说,SortVector属于一种Vector,并带有一些附加的功能。继承在这里可发挥很大的作用,但了带来了问题。它使一些方法具有了final属性(已在第7章讲述),所以不能覆盖它们。如果想创建一个排好序的Vector,令其只接收和生成String对象,就会遇到麻烦。因为addElement()和elementAt()都具有final属性,而且它们都是我们必须覆盖的方法,否则便无法实现只能接收和产生String对象。 117 | 118 | 但在另一方面,请考虑采用“合成”方法:将一个对象置入一个新类的内部。此时,不是改写上述代码来达到这个目的,而是在新类里简单地使用一个SortVector。在这种情况下,用于实现Compare接口的内部类就可以“匿名”地创建。如下所示: 119 | 120 | ``` java 121 | //: StrSortVector.java 122 | // Automatically sorted Vector that 123 | // accepts and produces only Strings 124 | package c08; 125 | import java.util.*; 126 | 127 | public class StrSortVector { 128 | private SortVector v = new SortVector( 129 | // Anonymous inner class: 130 | new Compare() { 131 | public boolean 132 | lessThan(Object l, Object r) { 133 | return 134 | ((String)l).toLowerCase().compareTo( 135 | ((String)r).toLowerCase()) < 0; 136 | } 137 | public boolean 138 | lessThanOrEqual(Object l, Object r) { 139 | return 140 | ((String)l).toLowerCase().compareTo( 141 | ((String)r).toLowerCase()) <= 0; 142 | } 143 | } 144 | ); 145 | private boolean sorted = false; 146 | public void addElement(String s) { 147 | v.addElement(s); 148 | sorted = false; 149 | } 150 | public String elementAt(int index) { 151 | if(!sorted) { 152 | v.sort(); 153 | sorted = true; 154 | } 155 | return (String)v.elementAt(index); 156 | } 157 | public Enumeration elements() { 158 | if(!sorted) { 159 | v.sort(); 160 | sorted = true; 161 | } 162 | return v.elements(); 163 | } 164 | // Test it: 165 | public static void main(String[] args) { 166 | StrSortVector sv = new StrSortVector(); 167 | sv.addElement("d"); 168 | sv.addElement("A"); 169 | sv.addElement("C"); 170 | sv.addElement("c"); 171 | sv.addElement("b"); 172 | sv.addElement("B"); 173 | sv.addElement("D"); 174 | sv.addElement("a"); 175 | Enumeration e = sv.elements(); 176 | while(e.hasMoreElements()) 177 | System.out.println(e.nextElement()); 178 | } 179 | } ///:~ 180 | ``` 181 | 182 | 这样便可快速再生来自SortVector的代码,从而获得希望的功能。然而,并不是来自SortVector和Vector的所有public方法都能在StrSortVector中出现。若按这种形式再生代码,可在新类里为包含类内的每一个方法都生成一个定义。当然,也可以在刚开始时只添加少数几个,以后根据需要再添加更多的。新类的设计最终会稳定下来。 183 | 184 | 这种方法的好处在于它仍然只接纳String对象,也只产生String对象。而且相应的检查是在编译期间进行的,而非在运行期。当然,只有addElement()和elementAt()才具备这一特性;elements()仍然会产生一个Enumeration(枚举),它在编译期的类型是未定的。当然,对Enumeration以及在StrSortVector中的类型检查会照旧进行;如果真的有什么错误,运行期间会简单地产生一个异常。事实上,我们在编译或运行期间能保证一切都正确无误吗?(也就是说,“代码测试时也许不能保证”,以及“该程序的用户有可能做一些未经我们测试的事情”)。尽管存在其他选择和争论,使用继承都要容易得多,只是在造型时让人深感不便。同样地,一旦为Java加入参数化类型,就有望解决这个问题。 185 | 186 | 大家在这个类中可以看到有一个名为“sorted”的标志。每次调用addElement()时,都可对Vector进行排序,而且将其连续保持在一个排好序的状态。但在开始读取之前,人们总是向一个Vector添加大量元素。所以与其在每个addElement()后排序,不如一直等到有人想读取Vector,再对其进行排序。后者的效率要高得多。这种除非绝对必要,否则就不采取行动的方法叫作“懒惰求值”(还有一种类似的技术叫作“懒惰初始化”——除非真的需要一个字段值,否则不进行初始化)。 187 | -------------------------------------------------------------------------------- /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 | 18 | -------------------------------------------------------------------------------- /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的一个内部类。 -------------------------------------------------------------------------------- /9.6 用finally清除.md: -------------------------------------------------------------------------------- 1 | # 9.6 用finally清除 2 | 3 | 无论一个异常是否在try块中发生,我们经常都想执行一些特定的代码。对一些特定的操作,经常都会遇到这种情况,但在恢复内存时一般都不需要(因为垃圾收集器会自动照料一切)。为达到这个目的,可在所有异常控制器的末尾使用一个finally从句(注释④)。所以完整的异常控制小节象下面这个样子: 4 | 5 | ``` java 6 | try { 7 | // 要保卫的区域: 8 | // 可能“掷”出A,B,或C的危险情况 9 | } catch (A a1) { 10 | // 控制器 A 11 | } catch (B b1) { 12 | // 控制器 B 13 | } catch (C c1) { 14 | // 控制器 C 15 | } finally { 16 | // 每次都会发生的情况 17 | } 18 | ``` 19 | 20 | ④:C++异常控制未提供finally从句,因为它依赖构造器来达到这种清除效果。 21 | 22 | 为演示finally从句,请试验下面这个程序: 23 | 24 | ``` java 25 | //: FinallyWorks.java 26 | // The finally clause is always executed 27 | 28 | public class FinallyWorks { 29 | static int count = 0; 30 | public static void main(String[] args) { 31 | while(true) { 32 | try { 33 | // post-increment is zero first time: 34 | if(count++ == 0) 35 | throw new Exception(); 36 | System.out.println("No exception"); 37 | } catch(Exception e) { 38 | System.out.println("Exception thrown"); 39 | } finally { 40 | System.out.println("in finally clause"); 41 | if(count == 2) break; // out of "while" 42 | } 43 | } 44 | } 45 | } ///:~ 46 | ``` 47 | 48 | 通过该程序,我们亦可知道如何应付Java异常(类似C++的异常)不允许我们恢复至异常产生地方的这一事实。若将自己的try块置入一个循环内,就可建立一个条件,它必须在继续程序之前满足。亦可添加一个static计数器或者另一些设备,允许循环在放弃以前尝试数种不同的方法。这样一来,我们的程序可以变得更加“健壮”。 49 | 50 | 输出如下: 51 | 52 | ``` java 53 | Exception thrown 54 | in finally clause 55 | No exception 56 | in finally clause 57 | ``` 58 | 59 | 无论是否“掷”出一个异常,finally从句都会执行。 60 | 61 | 9.6.1 用finally做什么 62 | 63 | 在没有“垃圾收集”以及“自动调用破坏器”机制的一种语言中(注释⑤),finally显得特别重要,因为程序员可用它担保内存的正确释放——无论在try块内部发生了什么状况。但Java提供了垃圾收集机制,所以内存的释放几乎绝对不会成为问题。另外,它也没有构造器可供调用。既然如此,Java里何时才会用到finally呢? 64 | 65 | ⑤:“破坏器”(Destructor)是“构造器”(Constructor)的反义词。它代表一个特殊的函数,一旦某个对象失去用处,通常就会调用它。我们肯定知道在哪里以及何时调用破坏器。C++提供了自动的破坏器调用机制,但Delphi的Object Pascal版本1及2却不具备这一能力(在这种语言中,破坏器的含义与用法都发生了变化)。 66 | 67 | 除将内存设回原始状态以外,若要设置另一些东西,finally就是必需的。例如,我们有时需要打开一个文件或者建立一个网络连接,或者在屏幕上画一些东西,甚至设置外部世界的一个开关,等等。如下例所示: 68 | 69 | ``` java 70 | //: OnOffSwitch.java 71 | // Why use finally? 72 | 73 | class Switch { 74 | boolean state = false; 75 | boolean read() { return state; } 76 | void on() { state = true; } 77 | void off() { state = false; } 78 | } 79 | 80 | public class OnOffSwitch { 81 | static Switch sw = new Switch(); 82 | public static void main(String[] args) { 83 | try { 84 | sw.on(); 85 | // Code that can throw exceptions... 86 | sw.off(); 87 | } catch(NullPointerException e) { 88 | System.out.println("NullPointerException"); 89 | sw.off(); 90 | } catch(IllegalArgumentException e) { 91 | System.out.println("IOException"); 92 | sw.off(); 93 | } 94 | } 95 | } ///:~ 96 | ``` 97 | 98 | 这里的目标是保证main()完成时开关处于关闭状态,所以将sw.off()置于try块以及每个异常控制器的末尾。但产生的一个异常有可能不是在这里捕获的,这便会错过sw.off()。然而,利用finally,我们可以将来自try块的关闭代码只置于一个地方: 99 | 100 | ``` java 101 | //: WithFinally.java 102 | // Finally Guarantees cleanup 103 | 104 | class Switch2 { 105 | boolean state = false; 106 | boolean read() { return state; } 107 | void on() { state = true; } 108 | void off() { state = false; } 109 | } 110 | 111 | public class WithFinally { 112 | static Switch2 sw = new Switch2(); 113 | public static void main(String[] args) { 114 | try { 115 | sw.on(); 116 | // Code that can throw exceptions... 117 | } catch(NullPointerException e) { 118 | System.out.println("NullPointerException"); 119 | } catch(IllegalArgumentException e) { 120 | System.out.println("IOException"); 121 | } finally { 122 | sw.off(); 123 | } 124 | } 125 | } ///:~ 126 | ``` 127 | 128 | 在这儿,sw.off()已移至一个地方。无论发生什么事情,都肯定会运行它。 129 | 130 | 即使异常不在当前的catch从句集里捕获,finally都会在异常控制机制转到更高级别搜索一个控制器之前得以执行。如下所示: 131 | 132 | ``` java 133 | //: AlwaysFinally.java 134 | // Finally is always executed 135 | 136 | class Ex extends Exception {} 137 | 138 | public class AlwaysFinally { 139 | public static void main(String[] args) { 140 | System.out.println( 141 | "Entering first try block"); 142 | try { 143 | System.out.println( 144 | "Entering second try block"); 145 | try { 146 | throw new Ex(); 147 | } finally { 148 | System.out.println( 149 | "finally in 2nd try block"); 150 | } 151 | } catch(Ex e) { 152 | System.out.println( 153 | "Caught Ex in first try block"); 154 | } finally { 155 | System.out.println( 156 | "finally in 1st try block"); 157 | } 158 | } 159 | } ///:~ 160 | ``` 161 | 162 | 该程序的输出展示了具体发生的事情: 163 | 164 | ``` java 165 | Entering first try block 166 | Entering second try block 167 | finally in 2nd try block 168 | Caught Ex in first try block 169 | finally in 1st try block 170 | ``` 171 | 172 | 若调用了break和continue语句,finally语句也会得以执行。请注意,与作上标签的break和continue一道,finally排除了Java对goto跳转语句的需求。 173 | 174 | 9.6.2 缺点:丢失的异常 175 | 176 | 一般情况下,Java的异常实施方案都显得十分出色。不幸的是,它依然存在一个缺点。尽管异常指出程序里存在一个危机,而且绝不应忽略,但一个异常仍有可能简单地“丢失”。在采用finally从句的一种特殊配置下,便有可能发生这种情况: 177 | 178 | ``` java 179 | //: LostMessage.java 180 | // How an exception can be lost 181 | 182 | class VeryImportantException extends Exception { 183 | public String toString() { 184 | return "A very important exception!"; 185 | } 186 | } 187 | 188 | class HoHumException extends Exception { 189 | public String toString() { 190 | return "A trivial exception"; 191 | } 192 | } 193 | 194 | public class LostMessage { 195 | void f() throws VeryImportantException { 196 | throw new VeryImportantException(); 197 | } 198 | void dispose() throws HoHumException { 199 | throw new HoHumException(); 200 | } 201 | public static void main(String[] args) 202 | throws Exception { 203 | LostMessage lm = new LostMessage(); 204 | try { 205 | lm.f(); 206 | } finally { 207 | lm.dispose(); 208 | } 209 | } 210 | } ///:~ 211 | ``` 212 | 213 | 输出如下: 214 | 215 | ``` java 216 | A trivial exception 217 | at LostMessage.dispose(LostMessage.java:21) 218 | at LostMessage.main(LostMessage.java:29) 219 | ``` 220 | 221 | 可以看到,这里不存在VeryImportantException(非常重要的异常)的迹象,它只是简单地被finally从句中的HoHumException代替了。 222 | 223 | 这是一项相当严重的缺陷,因为它意味着一个异常可能完全丢失。而且就象前例演示的那样,这种丢失显得非常“自然”,很难被人查出蛛丝马迹。而与此相反,C++里如果第二个异常在第一个异常得到控制前产生,就会被当作一个严重的编程错误处理。或许Java以后的版本会纠正这个问题(上述结果是用Java 1.1生成的)。 224 | -------------------------------------------------------------------------------- /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 | ``` java 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 | ``` java 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.9 总结.md: -------------------------------------------------------------------------------- 1 | # 9.9 总结 2 | 3 | 通过先进的错误纠正与恢复机制,我们可以有效地增强代码的健壮程度。对我们编写的每个程序来说,错误恢复都属于一个基本的考虑目标。它在Java中显得尤为重要,因为该语言的一个目标就是创建不同的程序组件,以便其他用户(客户程序员)使用。为构建一套健壮的系统,每个组件都必须非常健壮。 4 | 5 | 在Java里,异常控制的目的是使用尽可能精简的代码创建大型、可靠的应用程序,同时排除程序里那些不能控制的错误。 6 | 7 | 异常的概念很难掌握。但只有很好地运用它,才可使自己的项目立即获得显著的收益。Java强迫遵守异常所有方面的问题,所以无论库设计者还是客户程序员,都能够连续一致地使用它。 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Thinking in Java \(Java 编程思想\) 2 | 3 | 本书来自网络,[http://woquanke.com](http://woquanke.com) 整理成电子书,支持PDF,ePub,Mobi格式,方便大家下载阅读。 4 | 5 | 阅读地址:[https://woquanke.com/books/java](https://woquanke.com/books/java) 6 | 7 | 下载地址:[https://www.gitbook.com/book/quanke/think-in-java/](https://www.gitbook.com/book/quanke/think-in-java/) 8 | 9 | github地址:[https://github.com/quanke/think-in-java](https://github.com/quanke/think-in-java) 10 | 11 | 编辑:[http://woquanke.com](http://woquanke.com) 12 | 13 | 第13章没有编辑,觉得没有意义,Java的GUI先在应用少,有时间在编辑好。。。 14 | 15 | 编辑整理辛苦,还望大神们点一下star ,抚平我虚荣的心 16 | 17 | 更多请关注我的微信公众号: 18 | 19 | ![](/assets/qrcode_for_gh_26893aa0a4ea_258.jpg) 20 | 21 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [Introduction](README.md) 4 | * [写在前面的话](写在前面的话.md) 5 | * [引言](引言.md) 6 | * [第1章 对象入门](第1章 对象入门.md) 7 | * [1.1 抽象的进步](1.1 抽象的进步.md) 8 | * [1.2 对象的接口](1.2 对象的接口.md) 9 | * [1.3 实现方案的隐藏](1.3 实现方案的隐藏.md) 10 | * [1.4 方案的重复使用](1.4 方案的重复使用.md) 11 | * [1.5 继承:重新使用接口](1.5 继承:重新使用接口.md) 12 | * [1.6 多态对象的互换使用](1.6 多态对象的互换使用.md) 13 | * [1.7 对象的创建和存在时间](1.7 对象的创建和存在时间.md) 14 | * [1.8 异常控制:解决错误](1.8 异常控制:解决错误.md) 15 | * [1.9 多线程](1.9 多线程.md) 16 | * [1.10 永久性](1.10 永久性.md) 17 | * [1.11 Java和因特网](1.11 Java和因特网.md) 18 | * [1.12 分析和设计](1.12 分析和设计.md) 19 | * [1.13 Java还是C++](1.13 Java还是C++.md) 20 | * [第2章 一切都是对象](第2章 一切都是对象.md) 21 | * [2.1 用指针操纵对象](2.1 用指针操纵对象.md) 22 | * [2.2 所有对象都必须创建](2.2 所有对象都必须创建.md) 23 | * [2.3 绝对不要清除对象](2.3 绝对不要清除对象.md) 24 | * [2.4 新建数据类型:类](2.4 新建数据类型:类.md) 25 | * [2.5 方法、自变量和返回值](2.5 方法、自变量和返回值.md) 26 | * [2.6 构建Java程序](2.6 构建Java程序.md) 27 | * [2.7 我们的第一个Java程序](2.7 我们的第一个Java程序.md) 28 | * [2.8 注释和嵌入文档](2.8 注释和嵌入文档.md) 29 | * [2.9 编码样式](2.9 编码样式.md) 30 | * [2.10 总结](2.10 总结.md) 31 | * [2.11 练习](2.11 练习.md) 32 | * [第3章 控制程序流程](第3章 控制程序流程.md) 33 | * [3.1 使用Java运算符](3.1 使用Java运算符.md) 34 | * [3.2 执行控制](3.2 执行控制.md) 35 | * [3.3 总结](3.3 总结.md) 36 | * [3.4 练习](3.4 练习.md) 37 | * [第4章 初始化和清除](第4章 初始化和清除.md) 38 | * [4.1 用构造器自动初始化](4.1 用构造器自动初始化.md) 39 | * [4.2 方法重载](4.2 方法重载.md) 40 | * [4.3 清除:收尾和垃圾收集](4.3 清除:收尾和垃圾收集.md) 41 | * [4.4 成员初始化](4.4 成员初始化.md) 42 | * [4.5 数组初始化](4.5 数组初始化.md) 43 | * [4.6 总结](4.6 总结.md) 44 | * [4.7 练习](4.7 练习.md) 45 | * [第5章 隐藏实施过程](第5章 隐藏实施过程.md) 46 | * [5.1 包:库单元](5.1 包:库单元.md) 47 | * [5.2 Java访问指示符](5.2 Java访问指示符.md) 48 | * [5.3 接口与实现](5.3 接口与实现.md) 49 | * [5.4 类访问](5.4 类访问.md) 50 | * [5.5 总结](5.5 总结.md) 51 | * [5.6 练习](5.6 练习.md) 52 | * [第6章 类再生](第6章 类再生.md) 53 | * [6.1 合成的语法](6.1 合成的语法.md) 54 | * [6.2 继承的语法](6.2 继承的语法.md) 55 | * [6.3 合成与继承的结合](6.3 合成与继承的结合.md) 56 | * [6.4 到底选择合成还是继承](6.4 到底选择合成还是继承.md) 57 | * [6.5 protected](6.5 protected.md) 58 | * [6.6 累积开发](6.6 累积开发.md) 59 | * [6.7 上溯造型](6.7 上溯造型.md) 60 | * [6.8 final关键字](6.8 final关键字.md) 61 | * [6.9 初始化和类装载](6.9 初始化和类装载.md) 62 | * [6.10 总结](6.10 总结.md) 63 | * [6.11 练习](6.11 练习.md) 64 | * [第7章 多态性](第7章 多态性.md) 65 | * [7.1 上溯造型](7.1 上溯造型.md) 66 | * [7.2 深入理解](7.2 深入理解.md) 67 | * [7.3 覆盖与重载](7.3 覆盖与重载.md) 68 | * [7.4 抽象类和方法](7.4 抽象类和方法.md) 69 | * [7.5 接口](7.5 接口.md) 70 | * [7.6 内部类](7.6 内部类.md) 71 | * [7.7 构造器和多态性](7.7 构造器和多态性.md) 72 | * [7.8 通过继承进行设计](7.8 通过继承进行设计.md) 73 | * [7.9 总结](7.9 总结.md) 74 | * [7.10 练习](7.10 练习.md) 75 | * [第8章 对象的容纳](第8章 对象的容纳.md) 76 | * [8.1 数组](8.1 数组.md) 77 | * [8.2 集合](8.2 集合.md) 78 | * [8.3 枚举器(迭代器)](8.3 枚举器(迭代器).md) 79 | * [8.4 集合的类型](8.4 集合的类型.md) 80 | * [8.5 排序](8.5 排序.md) 81 | * [8.6 通用集合库](8.6 通用集合库.md) 82 | * [8.7 新集合](8.7 新集合.md) 83 | * [8.8 总结](8.8 总结.md) 84 | * [8.9 练习](8.9 练习.md) 85 | * [第9章 异常差错控制](第9章 异常差错控制.md) 86 | * [9.1 基本异常](9.1 基本异常.md) 87 | * [9.2 异常的捕获](9.2 异常的捕获.md) 88 | * [9.3 标准Java异常](9.3 标准Java异常.md) 89 | * [9.4 创建自己的异常](9.4 创建自己的异常.md) 90 | * [9.5 异常的限制](9.5 异常的限制.md) 91 | * [9.6 用finally清除](9.6 用finally清除.md) 92 | * [9.7 构造器](9.7 构造器.md) 93 | * [9.8 异常匹配](9.8 异常匹配.md) 94 | * [9.9 总结](9.9 总结.md) 95 | * [9.10 练习](9.10 练习.md) 96 | * [第10章 Java IO系统](第10章 Java IO系统.md) 97 | * [10.1 输入和输出](10.1 输入和输出.md) 98 | * [10.2 增添属性和有用的接口](10.2 增添属性和有用的接口.md) 99 | * [10.3 本身的缺陷:RandomAccessFile](10.3 本身的缺陷:RandomAccessFile.md) 100 | * [10.4 File类](10.4 File类.md) 101 | * [10.5 IO流的典型应用](10.5 IO流的典型应用.md) 102 | * [10.6 StreamTokenizer](10.6 StreamTokenizer.md) 103 | * [10.7 Java 1.1的IO流](10.7 Java 1.1的IO流.md) 104 | * [10.8 压缩](10.8 压缩.md) 105 | * [10.9 对象序列化](10.9 对象序列化.md) 106 | * [10.10 总结](10.10 总结.md) 107 | * [10.11 练习](10.11 练习.md) 108 | * [第11章 运行期类型鉴定](第11章 运行期类型鉴定.md) 109 | * [11.1 对RTTI的需要](11.1 对RTTI的需要.md) 110 | * [11.2 RTTI语法](11.2 RTTI语法.md) 111 | * [11.3 反射:运行期类信息](11.3 反射:运行期类信息.md) 112 | * [11.4 总结](11.4 总结.md) 113 | * [11.5 练习](11.5 练习.md) 114 | * [第12章 传递和返回对象](第12章 传递和返回对象.md) 115 | * [12.1 传递指针](12.1 传递指针.md) 116 | * [12.2 制作本地副本](12.2 制作本地副本.md) 117 | * [12.3 克隆的控制](12.3 克隆的控制.md) 118 | * [12.4 只读类](12.4 只读类.md) 119 | * [12.5 总结](12.5 总结.md) 120 | * [12.6 练习](12.6 练习.md) 121 | * [第13章 创建窗口和程序片](第13章 创建窗口和程序片.md) 122 | * [第14章 多线程](第14章 多线程.md) 123 | * [14.1 反应灵敏的用户界面](14.1 反应灵敏的用户界面.md) 124 | * [14.2 共享有限的资源](14.2 共享有限的资源.md) 125 | * [14.3 堵塞](14.3 堵塞.md) 126 | * [14.4 优先级](14.4 优先级.md) 127 | * [14.5 回顾runnable](14.5 回顾runnable.md) 128 | * [14.6 总结](14.6 总结.md) 129 | * [14.7 练习](14.7 练习.md) 130 | * [第15章 网络编程](第15章 网络编程.md) 131 | * [15.1 机器的标识](15.1 机器的标识.md) 132 | * [15.2 套接字](15.2 套接字.md) 133 | * [15.3 服务多个客户](15.3 服务多个客户.md) 134 | * [15.4 数据报](15.4 数据报.md) 135 | * [15.5 一个Web应用](15.5 一个Web应用.md) 136 | * [15.6 Java与CGI的沟通](15.6 Java与CGI的沟通.md) 137 | * [15.7 用JDBC连接数据库](15.7 用JDBC连接数据库.md) 138 | * [15.8 远程方法](15.8 远程方法.md) 139 | * [15.9 总结](15.9 总结.md) 140 | * [15.10 练习](15.10 练习.md) 141 | * [第16章 设计范式](第16章 设计范式.md) 142 | * [16.1 范式的概念](16.1 范式的概念.md) 143 | * [16.2 观察器范式](16.2 观察器范式.md) 144 | * [16.3 模拟垃圾回收站](16.3 模拟垃圾回收站.md) 145 | * [16.4 改进设计](16.4 改进设计.md) 146 | * [16.5 抽象的应用](16.5 抽象的应用.md) 147 | * [16.6 多重派遣](16.6 多重派遣.md) 148 | * [16.7 访问器范式](16.7 访问器范式.md) 149 | * [16.8 RTTI真的有害吗](16.8 RTTI真的有害吗.md) 150 | * [16.9 总结](16.9 总结.md) 151 | * [16.10 练习](16.10 练习.md) 152 | * [第17章 项目](第17章 项目.md) 153 | * [17.1 文字处理](17.1 文字处理.md) 154 | * [17.2 方法查找工具](17.2 方法查找工具.md) 155 | * [17.3 复杂性理论](17.3 复杂性理论.md) 156 | * [17.4 总结](17.4 总结.md) 157 | * [17.5 练习](17.5 练习.md) 158 | * [附录A 使用非JAVA代码](附录A 使用非JAVA代码.md) 159 | * [附录B 对比C++和Java](附录B 对比C++和Java.md) 160 | * [附录C Java编程规则](附录C Java编程规则.md) 161 | * [附录D 性能](附录D 性能.md) 162 | * [附录E 关于垃圾收集的一些话](附录E 关于垃圾收集的一些话.md) 163 | * [附录F 推荐读物](附录F 推荐读物.md) 164 | 165 | -------------------------------------------------------------------------------- /assets/qrcode_for_gh_26893aa0a4ea_258.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanke/think-in-java/0ea8519fd83c898d0e85c69eb54227cad7cc0440/assets/qrcode_for_gh_26893aa0a4ea_258.jpg -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "plugins": [ 4 | 5 | "layout", 6 | 7 | "baidu", 8 | 9 | "edit-link", 10 | 11 | 12 | 13 | "tbfed-pagefooter", 14 | 15 | "ga" 16 | 17 | ], 18 | 19 | "pluginsConfig": { 20 | 21 | "headerPath": "layouts/header.html", 22 | 23 | "footerPath": "layouts/footer.html", 24 | 25 | "baidu": { 26 | 27 | "token": "0dc0e6ff81f835b8c1dbd9c41fb21c2b" 28 | 29 | }, 30 | 31 | 32 | 33 | "tbfed-pagefooter": { 34 | 35 | "copyright":"Copyright © quanke.name 2016", 36 | 37 | "modify_label": "该文件修订时间:", "modify_format": "YYYY-MM-DD HH:mm:ss" } , 38 | 39 | "edit-link": { 40 | 41 | "base": "https://github.com/quanke/think-in-java/edit/master", "label": "Edit This Page" }, 42 | 43 | "ga": { 44 | 45 | "token": "UA-82833335-2" 46 | 47 | } 48 | 49 | }, 50 | 51 | "links": { 52 | 53 | "sidebar": { 54 | 55 | "Home": "http://quanke.name" 56 | 57 | } 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /写在前面的话.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 | 在理解到Java最终的目标是减轻程序员的负担时,我才真正感受到了震憾,尽管它的潜台词好象是说:“除了缩短时间和减小产生健壮代码的难度以外,我们不关心其他任何事情。”在目前这个初级阶段,达到那个目标的后果便是代码不能特别快地运行(尽管有许多保证都说Java终究有一天会运行得多么快),但它确实将开发时间缩短到令人惊讶的地步——几乎只有创建一个等效C++程序一半甚至更短的时间。这段节省下来的时间可以产生更大的效益,但Java并不仅止于此。它甚至更上一层楼,将重要性越来越明显的一切复杂任务都封装在内,比如网络程序和多线程处理等等。Java的各种语言特性和库在任何时候都能使那些任务轻而易举完成。而且最后,它解决了一些真正有些难度的复杂问题:跨平台程序、动态代码改换以及安全保护等等。换在从前,其中任何每一个都能使你头大如斗。所以不管我们见到了什么性能问题,Java的保证仍然是非常有效的:它使程序员显著提高了程序设计的效率! 18 | 19 | 在我看来,编程效率提升后影响最大的就是Web。网络程序设计以前非常困难,而Java使这个问题迎刃而解(而且Java也在不断地进步,使解决这类问题变得越来越容易)。网络程序的设计要求我们相互间更有效率地沟通,而且至少要比电话通信来得便宜(仅仅电子函件就为许多公司带来了好处)。随着我们网上通信越来越频繁,令人震惊的事情会慢慢发生,而且它们令人吃惊的程度绝不亚于当初工业革命给人带来的震憾。 20 | 21 | 在各个方面:创建程序;按计划编制程序;构造用户界面,使程序能与用户沟通;在不同类型的机器上运行程序;以及方便地编写程序,使其能通过因特网通信——Java提高了人与人之间的“通信带宽”。而且我认为通信革命的结果可能并不单单是数量庞大的比特到处传来传去那么简单。我们认为认清真正的革命发生在哪里,因为人和人之间的交流变得更方便了——个体与个体之间,个体与组之间,组与组之间,甚至在星球之间。有人预言下一次大革命的发生就是由于足够多的人和足够多的相互连接造成的,而这种革命是以整个世界为基础发生的。Java可能是、也可能不是促成那次革命的直接因素,但我在这里至少感觉自己在做一些有意义的工作——尝试教会大家一种重要的语言! 22 | 23 | -------------------------------------------------------------------------------- /第10章 Java IO系统.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章 运行期类型鉴定.md: -------------------------------------------------------------------------------- 1 | # 第11章 运行期类型鉴定 2 | 3 | 4 | 运行期类型鉴定(RTTI)的概念初看非常简单——手上只有基础类型的一个指针时,利用它判断一个对象的正确类型。 5 | 然而,对RTTI的需要暴露出了面向对象设计许多有趣(而且经常是令人困惑的)的问题,并把程序的构造问题正式摆上了桌面。 6 | 本章将讨论如何利用Java在运行期间查找对象和类信息。这主要采取两种形式:一种是“传统”RTTI,它假定我们已在编译和运行期拥有所有类型;另一种是Java1.1特有的“反射”机制,利用它可在运行期独立查找类信息。首先讨论“传统”的RTTI,再讨论反射问题。 7 | -------------------------------------------------------------------------------- /第12章 传递和返回对象.md: -------------------------------------------------------------------------------- 1 | # 第12章 传递和返回对象 2 | 3 | 4 | 到目前为止,读者应对对象的“传递”有了一个较为深刻的认识,记住实际传递的只是一个指针。 5 | 6 | 在许多程序设计语言中,我们可用语言的“普通”方式到处传递对象,而且大多数时候都不会遇到问题。但有些时候却不得不采取一些非常做法,使得情况突然变得稍微复杂起来(在C++中则是变得非常复杂)。Java亦不例外,我们十分有必要准确认识在对象传递和赋值时所发生的一切。这正是本章的宗旨。 7 | 8 | 若读者是从某些特殊的程序设计环境中转移过来的,那么一般都会问到:“Java有指针吗?”有些人认为指针的操作很困难,而且十分危险,所以一厢情愿地认为它没有好处。同时由于Java有如此好的口碑,所以应该很轻易地免除自己以前编程中的麻烦,其中不可能夹带有指针这样的“危险品”。然而准确地说,Java是有指针的!事实上,Java中每个对象(除基本数据类型以外)的标识符都属于指针的一种。但它们的使用受到了严格的限制和防范,不仅编译器对它们有“戒心”,运行期系统也不例外。或者换从另一个角度说,Java有指针,但没有传统指针的麻烦。我曾一度将这种指针叫做“指针”,但你可以把它想像成“安全指针”。和预备学校为学生提供的安全剪刀类似——除非特别有意,否则不会伤着自己,只不过有时要慢慢来,要习惯一些沉闷的工作。 9 | -------------------------------------------------------------------------------- /第14章 多线程.md: -------------------------------------------------------------------------------- 1 | # 第14章 多线程 2 | 3 | 4 | 利用对象,可将一个程序分割成相互独立的区域。我们通常也需要将一个程序转换成多个独立运行的子任务。 5 | 6 | 象这样的每个子任务都叫作一个“线程”(Thread)。编写程序时,可将每个线程都想象成独立运行,而且都有自己的专用CPU。一些基础机制实际会为我们自动分割CPU的时间。我们通常不必关心这些细节问题,所以多线程的代码编写是相当简便的。 7 | 8 | 这时理解一些定义对以后的学习狠有帮助。“进程”是指一种“自包容”的运行程序,有自己的地址空间。“多任务”操作系统能同时运行多个进程(程序)——但实际是由于CPU分时机制的作用,使每个进程都能循环获得自己的CPU时间片。但由于轮换速度非常快,使得所有程序好象是在“同时”运行一样。“线程”是进程内部单一的一个顺序控制流。因此,一个进程可能容纳了多个同时执行的线程。 9 | 10 | 多线程的应用范围很广。但在一般情况下,程序的一些部分同特定的事件或资源联系在一起,同时又不想为它而暂停程序其他部分的执行。这样一来,就可考虑创建一个线程,令其与那个事件或资源关联到一起,并让它独立于主程序运行。一个很好的例子便是“Quit”或“退出”按钮——我们并不希望在程序的每一部分代码中都轮询这个按钮,同时又希望该按钮能及时地作出响应(使程序看起来似乎经常都在轮询它)。事实上,多线程最主要的一个用途就是构建一个“反应灵敏”的用户界面。 11 | -------------------------------------------------------------------------------- /第15章 网络编程.md: -------------------------------------------------------------------------------- 1 | # 第15章 网络编程 2 | 3 | 历史上的网络编程都倾向于困难、复杂,而且极易出错。 4 | 5 | 程序员必须掌握与网络有关的大量细节,有时甚至要对硬件有深刻的认识。一般地,我们需要理解连网协议中不同的“层”(Layer)。而且对于每个连网库,一般都包含了数量众多的函数,分别涉及信息块的连接、打包和拆包;这些块的来回运输;以及握手等等。这是一项令人痛苦的工作。 6 | 7 | 但是,连网本身的概念并不是很难。我们想获得位于其他地方某台机器上的信息,并把它们移到这儿;或者相反。这与读写文件非常相似,只是文件存在于远程机器上,而且远程机器有权决定如何处理我们请求或者发送的数据。 8 | 9 | Java最出色的一个地方就是它的“无痛苦连网”概念。有关连网的基层细节已被尽可能地提取出去,并隐藏在JVM以及Java的本机安装系统里进行控制。我们使用的编程模型是一个文件的模型;事实上,网络连接(一个“套接字”)已被封装到系统对象里,所以可象对其他数据流那样采用同样的方法调用。除此以外,在我们处理另一个连网问题——同时控制多个网络连接——的时候,Java内建的多线程机制也是十分方便的。 10 | 11 | 本章将用一系列易懂的例子解释Java的连网支持。 12 | -------------------------------------------------------------------------------- /第16章 设计范式.md: -------------------------------------------------------------------------------- 1 | # 第16章 设计范式 2 | 3 | 本章要向大家介绍重要但却并不是那么传统的“范式”(Pattern)程序设计方法。 4 | 5 | 在向面向对象程序设计的演化过程中,或许最重要的一步就是“设计范式”(Design Pattern)的问世。它在由Gamma,Helm和Johnson编著的《Design Patterns》一书中被定义成一个“里程碑”(该书由Addison-Wesley于1995年出版,注释①)。那本书列出了解决这个问题的23种不同的方法。在本章中,我们准备伴随几个例子揭示出设计范式的基本概念。这或许能激起您阅读《Design Pattern》一书的欲望。事实上,那本书现在已成为几乎所有OOP程序员都必备的参考书。 6 | 7 | ①:但警告大家:书中的例子是用C++写的。 8 | 9 | 本章的后一部分包含了展示设计进化过程的一个例子,首先是比较原始的方案,经过逐渐发展和改进,慢慢成为更符合逻辑、更为恰当的设计。该程序(仿真垃圾分类)一直都在进化,可将这种进化作为自己设计方案的一个原型——先为特定的问题提出一个适当的方案,再逐步改善,使其成为解决那类问题一种最灵活的方案。 10 | -------------------------------------------------------------------------------- /第17章 项目.md: -------------------------------------------------------------------------------- 1 | # 第17章 项目 2 | 3 | 本章包含了一系列项目,它们都以本书介绍的内容为基础,并对早期的章节进行了一定程度的扩充。 4 | 5 | 与以前经历过的项目相比,这儿的大多数项目都明显要复杂得多,它们充分演示了新技术以及类库的运用。 6 | -------------------------------------------------------------------------------- /第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 | -------------------------------------------------------------------------------- /第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章 控制程序流程.md: -------------------------------------------------------------------------------- 1 | # 第3章 控制程序流程 2 | 3 | 4 | 5 | “就象任何有感知的生物一样,程序必须能操纵自己的世界,在执行过程中作出判断与选择。” 6 | 7 | 在Java里,我们利用运算符操纵对象和数据,并用执行控制语句作出选择。Java是建立在C++基础上的,所以对C和C++程序员来说,对Java这方面的大多数语句和运算符都应是非常熟悉的。当然,Java也进行了自己的一些改进与简化工作。 8 | 9 | -------------------------------------------------------------------------------- /第4章 初始化和清除.md: -------------------------------------------------------------------------------- 1 | # 第4章 初始化和清除 2 | 3 | 4 | “随着计算机的进步,‘不安全’的程序设计已成为造成编程代价高昂的罪魁祸首之一。” 5 | 6 | “初始化”和“清除”是这些安全问题的其中两个。许多C程序的错误都是由于程序员忘记初始化一个变量造成的。对于现成的库,若用户不知道如何初始化库的一个组件,就往往会出现这一类的错误。清除是另一个特殊的问题,因为用完一个元素后,由于不再关心,所以很容易把它忘记。这样一来,那个元素占用的资源会一直保留下去,极易产生资源(主要是内存)用尽的后果。 7 | 8 | C++为我们引入了“构造器”的概念。这是一种特殊的方法,在一个对象创建之后自动调用。Java也沿用了这个概念,但新增了自己的“垃圾收集器”,能在资源不再需要的时候自动释放它们。本章将讨论初始化和清除的问题,以及Java如何提供它们的支持。 9 | -------------------------------------------------------------------------------- /第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章 类再生.md: -------------------------------------------------------------------------------- 1 | # 第6章 类再生 2 | 3 | 4 | “Java引人注目的一项特性是代码的重复使用或者再生。但最具革命意义的是,除代码的复制和修改以外,我们还能做多得多的其他事情。” 5 | 6 | 在象C那样的程序化语言里,代码的重复使用早已可行,但效果不是特别显著。与Java的其他地方一样,这个方案解决的也是与类有关的问题。我们通过创建新类来重复使用代码,但却用不着重新创建,可以直接使用别人已建好并调试好的现成类。 7 | 8 | 但这样做必须保证不会干扰原有的代码。在这一章里,我们将介绍两个达到这一目标的方法。第一个最简单:在新类里简单地创建原有类的对象。我们把这种方法叫作“合成”,因为新类由现有类的对象合并而成。我们只是简单地重复利用代码的功能,而不是采用它的形式。 9 | 10 | 第二种方法则显得稍微有些技巧。它创建一个新类,将其作为现有类的一个“类型”。我们可以原样采取现有类的形式,并在其中加入新代码,同时不会对现有的类产生影响。这种魔术般的行为叫作“继承”(Inheritance),涉及的大多数工作都是由编译器完成的。对于面向对象的程序设计,“继承”是最重要的基础概念之一。它对我们下一章要讲述的内容会产生一些额外的影响。 11 | 12 | 对于合成与继承这两种方法,大多数语法和行为都是类似的(因为它们都要根据现有的类型生成新类型)。在本章,我们将深入学习这些代码再生或者重复使用的机制。 13 | 14 | -------------------------------------------------------------------------------- /第7章 多态性.md: -------------------------------------------------------------------------------- 1 | # 第7章 多态性 2 | 3 | 4 | “对于面向对象的程序设计语言,多态性是第三种最基本的特征(前两种是数据抽象和继承。” 5 | 6 | “多态性”(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来,亦即实现了“是什么”与“怎样做”两个模块的分离。利用多态性的概念,代码的组织以及可读性均能获得改善。此外,还能创建“易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地“成长”。 7 | 8 | 通过合并各种特征与行为,封装技术可创建出新的数据类型。通过对具体实施细节的隐藏,可将接口与实施细节分离,使所有细节成为“private”(私有)。这种组织方式使那些有程序化编程背景人感觉颇为舒适。但多态性却涉及对“类型”的分解。通过上一章的学习,大家已知道通过继承可将一个对象当作它自己的类型或者它自己的基础类型对待。这种能力是十分重要的,因为多个类型(从相同的基础类型中衍生出来)可被当作同一种类型对待。而且只需一段代码,即可对所有不同的类型进行同样的处理。利用具有多态性的方法调用,一种类型可将自己与另一种相似的类型区分开,只要它们都是从相同的基础类型中衍生出来的。这种区分是通过各种方法在行为上的差异实现的,可通过基础类实现对那些方法的调用。 9 | 10 | 在这一章中,大家要由浅入深地学习有关多态性的问题(也叫作动态绑定、推迟绑定或者运行期绑定)。同时举一些简单的例子,其中所有无关的部分都已剥除,只保留与多态性有关的代码。 11 | -------------------------------------------------------------------------------- /第8章 对象的容纳.md: -------------------------------------------------------------------------------- 1 | # 第8章 对象的容纳 2 | 3 | 4 | “如果一个程序只含有数量固定的对象,而且已知它们的存在时间,那么这个程序可以说是相当简单的。” 5 | 6 | 通常,我们的程序需要根据程序运行时才知道的一些标准创建新对象。若非程序正式运行,否则我们根本不知道自己到底需要多少数量的对象,甚至不知道它们的准确类型。为了满足常规编程的需要,我们要求能在任何时候、任何地点创建任意数量的对象。所以不可依赖一个已命名的指针来容纳自己的每一个对象,就象下面这样: 7 | 8 | ``` java 9 | MyObject myHandle; 10 | ``` 11 | 12 | 因为根本不知道自己实际需要多少这样的东西。 13 | 14 | 为解决这个非常关键的问题,Java提供了容纳对象(或者对象的指针)的多种方式。其中内建的类型是数组,我们之前已讨论过它,本章准备加深大家对它的认识。此外,Java的工具(实用程序)库提供了一些“集合类”(亦称作“容器类”,但该术语已由AWT使用,所以这里仍采用“集合”这一称呼)。利用这些集合类,我们可以容纳乃至操纵自己的对象。本章的剩余部分会就此进行详细讨论。 15 | -------------------------------------------------------------------------------- /第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 | -------------------------------------------------------------------------------- /附录C Java编程规则.md: -------------------------------------------------------------------------------- 1 | # 附录C Java编程规则 2 | 3 | 4 | 本附录包含了大量有用的建议,帮助大家进行低级程序设计,并提供了代码编写的一般性指导: 5 | 6 | (1) 类名首字母应该大写。字段、方法以及对象(指针)的首字母应小写。对于所有标识符,其中包含的所有单词都应紧靠在一起,而且大写中间单词的首字母。例如: 7 | 8 | ``` java 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 | ``` java 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 -------------------------------------------------------------------------------- /附录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应用程序。 -------------------------------------------------------------------------------- /附录F 推荐读物.md: -------------------------------------------------------------------------------- 1 | # 附录F 推荐读物 2 | 3 | 4 | ■《Java in a Nutshell:A Desktop Quick Reference,第2版》 5 | 作者:David Flanagan 6 | 出版社:O'Reilly & Assoc 7 | 出版时间:1997 8 | 简介:对Java 1.1联机文档的一个简要总结。就个人来说,我更喜欢在线阅览文档,特别是在它们变化得如此快的时候。然而,许多人仍然喜欢印刷出来的文档,这样可以省一些上网费。而且这本书也提供了比联机文档更多的讨论。 9 | 10 | ■《The Java Class Libraries:An Annotated Reference》 11 | 作者:Patrick Chan和Rosanna Lee 12 | 出版社:Addison-Wesley 13 | 出版时间:1997 14 | 简介:作为一种联机参考资源,应向读者提供足够多的说明,使其简单易用。《Thinking in Java》的一名技术审定员说道:“如果我只能有一本Java书,那么肯定选它。”不过我可没有他那么激动。它太大、太贵,而且示例的质量并不能令我满意。但在遇到麻烦的时候,该书还是很有参考价值的。而且与《Java in a Nutshell》相比,它看起来有更大的深度(当然也有更多的文字)。 15 | 16 | ■《Java Network Programming》 17 | 作者:Elliote Rusty Harold 18 | David Flanagan 19 | 出版社:O'Reilly 20 | 出版时间:1997 21 | 简介:在阅读本书前,我可以说根本不理解Java有关网络的问题。后来,我也发现他的Web站点“Cafe au Lait”是个令人激动的、很人个性的以及经常更新的去处,涉及大量有价值的Java开发资源。由于几乎每天更新,所以在这里能看到与Java有关的大量新闻。站点地址是:http://sunsite.unc.edu/javafaq/。 22 | 23 | ■《Core Java,第3版》 24 | 作者:Cornel和Horstmann 25 | 出版社:Prentice-Hall 26 | 出版时间:1997 27 | 简介:对于自己碰到的问题,若在《Thinking in Java》里找不到答案,这就是一个很好的参考地点。注意:Java 1.1的版本是《Core Java 1.1 Volume 1-Fundamentals & Core Java 1.1 Volume 2-Advanced Features》 28 | 29 | ■《JDBC Database Access with Java》 30 | 作者:Hamilton,Cattell和Fisher 31 | 出版社:Addison-Wesley 32 | 出版时间:1997 33 | 简介:如果对SQL和数据库一无所知,这本书就可以作为一个相当好的起点。它也对API进行了详尽的解释,并提供一个“注释参考。与“Java系列”(由JavaSoft授权的唯一一套丛书)的其他所有书籍一样,这本书的缺点也是进行了过份的渲染,只说Java的好话——在这一系列书籍里找不到任何不利于Java的地方。 34 | 35 | ■《Java Programming with CORBA》 36 | 作者:Andreas Vogel和Keith Duddy 37 | 出版社:Jonh Wiley & Sons 38 | 出版时间:1997 39 | 简介:针对三种主要的Java ORB(Visbroker,Orbix,Joe),本书分别用大量代码实例进行了详尽的阐述。 40 | 41 | ■《Design Patterns》 42 | 作者:Gamma,Helm,Johnson和Vlissides 43 | 出版社:Addison-Wesley 44 | 出版时间:1995 45 | 简介:这是一本发起了编程领域方案革命的经典书籍。 46 | 47 | ■《UML Toolkit》 48 | 作者:Hans-Erik Eriksson和Magnus Penker 49 | 出版社:Jonh Wiley & Sons 50 | 出版时间:1997 51 | 简介:解释UML以及如何使用它,并提供Java的实际案例供参考。配套CD-ROM包含了Java代码以及Rational Rose的一个删减版本。本书对UML进行了非常出色的描述,并解释了如何用它构建实际的系统。 52 | 53 | ■《Practical Algorithms for Programmers》 54 | 作者:Binstock和Rex 55 | 出版社:Addison-Wesley 56 | 出版时间:1995 57 | 简介:算法是用C描述的,所以它们很容易就能转换到Java里面。每种算法都有详尽的解释。 --------------------------------------------------------------------------------