diff --git "a/1.1 \346\212\275\350\261\241\347\232\204\350\277\233\346\255\245.md" "b/1.1 \346\212\275\350\261\241\347\232\204\350\277\233\346\255\245.md" index 6764843..47a88a5 100644 --- "a/1.1 \346\212\275\350\261\241\347\232\204\350\277\233\346\255\245.md" +++ "b/1.1 \346\212\275\350\261\241\347\232\204\350\277\233\346\255\245.md" @@ -19,7 +19,7 @@ Alan Kay总结了Smalltalk的五大基本特征。这是第一种成功的面向 (5) 同一类所有对象都能接收相同的消息。这实际是别有含义的一种说法,大家不久便能理解。由于类型为“圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收形状消息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括“圆”。这一特性称为对象的“可替换性”,是OOP最重要的概念之一。 -一些语言设计者认为面向对象的程序设计本身并不足以方便解决所有形式的程序问题,提倡将不同的方法组合成“多形程序设计语言”(注释②)。 +一些语言设计者认为面向对象的程序设计本身并不足以方便解决所有形式的程序问题,提倡将不同的方法组合成“多态程序设计语言”(注释②)。 ②:参见Timothy Budd编著的《Multiparadigm Programming in Leda》,Addison-Wesley 1995年出版。 diff --git "a/1.11 Java\345\222\214\345\233\240\347\211\271\347\275\221.md" "b/1.11 Java\345\222\214\345\233\240\347\211\271\347\275\221.md" index 9327d13..725b050 100644 --- "a/1.11 Java\345\222\214\345\233\240\347\211\271\347\275\221.md" +++ "b/1.11 Java\345\222\214\345\233\240\347\211\271\347\275\221.md" @@ -49,7 +49,7 @@ Web最初采用的“服务器-浏览器”方案可提供交互式内容, 目前讨论得最多的脚本编制语言包括JavaScript(它与Java没有任何关系;之所以叫那个名字,完全是一种市场策略)、VBScript(同Visual Basic很相似)以及Tcl/Tk(来源于流行的跨平台GUI构造语言)。当然还有其他许多语言,也有许多正在开发中。 -JavaScript也许是目常用的,它得到的支持也最全面。无论NetscapeNavigator,Microsoft Internet Explorer,还是Opera,目前都提供了对JavaScript的支持。除此以外,市面上讲述JavaScript的书籍也要比讲述其他语言的书多得多。有些工具还能利用JavaScript自动产生网页。当然,如果你已经有Visual Basic或者Tcl/Tk的深厚功底,当然用它们要简单得多,起码可以避免学习新语言的烦恼(解决Web方面的问题就已经够让人头痛了)。 +JavaScript也许是目前最常用的,它得到的支持也最全面。无论NetscapeNavigator,Microsoft Internet Explorer,还是Opera,目前都提供了对JavaScript的支持。除此以外,市面上讲述JavaScript的书籍也要比讲述其他语言的书多得多。有些工具还能利用JavaScript自动产生网页。当然,如果你已经有Visual Basic或者Tcl/Tk的深厚功底,当然用它们要简单得多,起码可以避免学习新语言的烦恼(解决Web方面的问题就已经够让人头痛了)。 **3. Java** diff --git "a/1.12 \345\210\206\346\236\220\345\222\214\350\256\276\350\256\241.md" "b/1.12 \345\210\206\346\236\220\345\222\214\350\256\276\350\256\241.md" index 771b7ea..0d151e7 100644 --- "a/1.12 \345\210\206\346\236\220\345\222\214\350\256\276\350\256\241.md" +++ "b/1.12 \345\210\206\346\236\220\345\222\214\350\256\276\350\256\241.md" @@ -42,7 +42,7 @@ 1.12.4 阶段2:如何构建? -在这一阶段,必须拿出一套设计方案,并解释其中包含的各类对象在外观上是什么样子,以及相互间是如何沟通的。此时可考虑采用一种特殊的图表工具:“统一建模语言”(UML)。请到http://www.rational.com去下载一份UML规格书。作为第1阶段中的描述工具,UML也是很有帮助的。此外,还可用它在第2阶段中处理一些图表(如流程图)。当然并非一定要使用UML,但它对你会很有帮助,特别是在希望描绘一张详尽的图表,让许多人在一起研究的时候。除UML外,还可选择对对象以及它们的接口进行文字化描述(就象我在《Thinking in C++》里说的那样,但这种方法非常原始,发挥的作用亦较有限。 +在这一阶段,必须拿出一套设计方案,并解释其中包含的各类对象在外观上是什么样子,以及相互间是如何沟通的。此时可考虑采用一种特殊的图表工具:“统一建模语言”(UML)。请到http://www.rational.com 去下载一份UML规格书。作为第1阶段中的描述工具,UML也是很有帮助的。此外,还可用它在第2阶段中处理一些图表(如流程图)。当然并非一定要使用UML,但它对你会很有帮助,特别是在希望描绘一张详尽的图表,让许多人在一起研究的时候。除UML外,还可选择对对象以及它们的接口进行文字化描述(就象我在《Thinking in C++》里说的那样,但这种方法非常原始,发挥的作用亦较有限。 我曾有一次非常成功的咨询经历,那时涉及到一小组人的初始设计。他们以前还没有构建过OOP(面向对象程序设计)项目,将对象画在白板上面。我们谈到各对象相互间该如何沟通(通信),并删除了其中的一部分,以及替换了另一部分对象。这个小组(他们知道这个项目的目的是什么)实际上已经制订出了设计方案;他们自己“拥有”了设计,而不是让设计自然而然地显露出来。我在那里做的事情就是对设计进行指导,提出一些适当的问题,尝试作出一些假设,并从小组中得到反馈,以便修改那些假设。这个过程中最美妙的事情就是整个小组并不是通过学习一些抽象的例子来进行面向对象的设计,而是通过实践一个真正的设计来掌握OOP的窍门,而那个设计正是他们当时手上的工作! diff --git "a/1.13 Java\350\277\230\346\230\257C++.md" "b/1.13 Java\350\277\230\346\230\257C++.md" index f19df24..cdcc0ee 100644 --- "a/1.13 Java\350\277\230\346\230\257C++.md" +++ "b/1.13 Java\350\277\230\346\230\257C++.md" @@ -4,7 +4,7 @@ Java特别象C++;由此很自然地会得出一个结论:C++似乎会被Java 我感觉Java强大之处反映在与C++稍有不同的领域。C++是一种绝对不会试图迎合某个模子的语言。特别是它的形式可以变化多端,以解决不同类型的问题。这主要反映在象Microsoft Visual C++和Borland C++ Builder(我最喜欢这个)那样的工具身上。它们将库、组件模型以及代码生成工具等合成到一起,以开发视窗化的末端用户应用(用于Microsoft Windows操作系统)。但在另一方面,Windows开发人员最常用的是什么呢?是微软的Visual Basic(VB)。当然,我们在这儿暂且不提VB的语法极易使人迷惑的事实——即使一个只有几页长度的程序,产生的代码也十分难于管理。从语言设计的角度看,尽管VB是那样成功和流行,但仍然存在不少的缺点。最好能够同时拥有VB那样的强大功能和易用性,同时不要产生难于管理的代码。而这正是Java最吸引人的地方:作为“下一代的VB”。无论你听到这种主张后有什么感觉,请无论如何都仔细想一想:人们对Java做了大量的工作,使它能方便程序员解决应用级问题(如连网和跨平台UI等),所以它在本质上允许人们创建非常大型和灵活的代码主体。同时,考虑到Java还拥有我迄今为止尚未在其他任何一种语言里见到的最“健壮”的类型检查及错误控制系统,所以Java确实能大大提高我们的编程效率。这一点是勿庸置疑的! -但对于自己某个特定的项目,真的可以不假思索地将C++换成Java吗?除了Web程序片,还有两个问题需要考虑。首先,假如要使用大量现有的库(这样肯定可以提高不少的效率),或者已经有了一个坚实的C或C++代码库,那么换成Java后,反映会阻碍开发进度,而不是加快它的速度。但若想从头开始构建自己的所有代码,那么Java的简单易用就能有效地缩短开发时间。 +但对于自己某个特定的项目,真的可以不假思索地将C++换成Java吗?除了Web程序片,还有两个问题需要考虑。首先,假如要使用大量现有的库(这样肯定可以提高不少的效率),或者已经有了一个坚实的C或C++代码库,那么换成Java后,反而会阻碍开发进度,而不是加快它的速度。但若想从头开始构建自己的所有代码,那么Java的简单易用就能有效地缩短开发时间。 最大的问题是速度。在原始的Java解释器中,解释过的Java会比C慢上20到50倍。尽管经过长时间的发展,这个速度有一定程度的提高,但和C比起来仍然很悬殊。计算机最注重的就是速度;假如在一台计算机上不能明显较快地干活,那么还不如用手做(有人建议在开发期间使用Java,以缩短开发时间。然后用一个工具和支撑库将代码转换成C++,这样可获得更快的执行速度)。 为使Java适用于大多数Web开发项目,关键在于速度上的改善。此时要用到人们称为“刚好及时”(Just-In Time,或JIT)的编译器,甚至考虑更低级的代码编译器(写作本书时,也有两款问世)。当然,低级代码编译器会使编译好的程序不能跨平台执行,但同时也带来了速度上的提升。这个速度甚至接近C和C++。而且Java中的程序交叉编译应当比C和C++中简单得多(理论上只需重编译即可,但实际仍较难实现;其他语言也曾作出类似的保证)。 diff --git "a/1.2 \345\257\271\350\261\241\347\232\204\346\216\245\345\217\243.md" "b/1.2 \345\257\271\350\261\241\347\232\204\346\216\245\345\217\243.md" index 177fdfc..c82fd22 100644 --- "a/1.2 \345\257\271\350\261\241\347\232\204\346\216\245\345\217\243.md" +++ "b/1.2 \345\257\271\350\261\241\347\232\204\346\216\245\345\217\243.md" @@ -17,10 +17,10 @@ Simula是一个很好的例子。正如这个名字所暗示的,它的作用 ![](1-1.gif) -``` +``` java Light lt = new Light(); lt.on(); ``` -在这个例子中,类型/类的名称是Light,可向Light对象发出的请求包括包括打开(on)、关闭(off)、变得更明亮(brighten)或者变得更暗淡(dim)。通过简单地声明一个名字(lt),我们为Light对象创建了一个“句柄”。然后用new关键字新建类型为Light的一个对象。再用等号将其赋给句柄。为了向对象发送一条消息,我们列出句柄名(lt),再用一个句点符号(.)把它同消息名称(on)连接起来。从中可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单和直观的。 +在这个例子中,类型/类的名称是Light,可向Light对象发出的请求包括包括打开(on)、关闭(off)、变得更明亮(brighten)或者变得更暗淡(dim)。通过简单地声明一个名字(lt),我们为Light对象创建了一个“指针”。然后用new关键字新建类型为Light的一个对象。再用等号将其赋给指针。为了向对象发送一条消息,我们列出指针名(lt),再用一个句点符号(.)把它同消息名称(on)连接起来。从中可以看出,使用一些预先定义好的类时,我们在程序里采用的代码是非常简单和直观的。 diff --git "a/1.6 \345\244\232\345\275\242\345\257\271\350\261\241\347\232\204\344\272\222\346\215\242\344\275\277\347\224\250.md" "b/1.6 \345\244\232\346\200\201\345\257\271\350\261\241\347\232\204\344\272\222\346\215\242\344\275\277\347\224\250.md" similarity index 93% rename from "1.6 \345\244\232\345\275\242\345\257\271\350\261\241\347\232\204\344\272\222\346\215\242\344\275\277\347\224\250.md" rename to "1.6 \345\244\232\346\200\201\345\257\271\350\261\241\347\232\204\344\272\222\346\215\242\344\275\277\347\224\250.md" index 3c0a71e..7f3323e 100644 --- "a/1.6 \345\244\232\345\275\242\345\257\271\350\261\241\347\232\204\344\272\222\346\215\242\344\275\277\347\224\250.md" +++ "b/1.6 \345\244\232\346\200\201\345\257\271\350\261\241\347\232\204\344\272\222\346\215\242\344\275\277\347\224\250.md" @@ -1,4 +1,4 @@ -# 1.6 多形对象的互换使用 +# 1.6 多态对象的互换使用 通常,继承最终会以创建一系列类收场,所有类都建立在统一的接口基础上。我们用一幅颠倒的树形图来阐明这一点(注释⑤): @@ -10,7 +10,7 @@ 对这样的一系列类,我们要进行的一项重要处理就是将衍生类的对象当作基础类的一个对象对待。这一点是非常重要的,因为它意味着我们只需编写单一的代码,令其忽略类型的特定细节,只与基础类打交道。这样一来,那些代码就可与类型信息分开。所以更易编写,也更易理解。此外,若通过继承增添了一种新类型,如“三角形”,那么我们为“几何形状”新类型编写的代码会象在旧类型里一样良好地工作。所以说程序具备了“扩展能力”,具有“扩展性”。 以上面的例子为基础,假设我们用Java写了这样一个函数: -``` +``` java void doStuff(Shape s) { s.erase(); // ... @@ -20,7 +20,7 @@ void doStuff(Shape s) { 这个函数可与任何“几何形状”(Shape)通信,所以完全独立于它要描绘(draw)和删除(erase)的任何特定类型的对象。如果我们在其他一些程序里使用doStuff()函数: -``` +``` java Circle c = new Circle(); Triangle t = new Triangle(); Line l = new Line(); @@ -32,16 +32,16 @@ doStuff(l); 那么对doStuff()的调用会自动良好地工作,无论对象的具体类型是什么。 这实际是一个非常有用的编程技巧。请考虑下面这行代码: -``` +``` java doStuff(c); ``` -此时,一个Circle(圆)句柄传递给一个本来期待Shape(形状)句柄的函数。由于圆是一种几何形状,所以doStuff()能正确地进行处理。也就是说,凡是doStuff()能发给一个Shape的消息,Circle也能接收。所以这样做是安全的,不会造成错误。 +此时,一个Circle(圆)指针传递给一个本来期待Shape(形状)指针的函数。由于圆是一种几何形状,所以doStuff()能正确地进行处理。也就是说,凡是doStuff()能发给一个Shape的消息,Circle也能接收。所以这样做是安全的,不会造成错误。 我们将这种把衍生类型当作它的基本类型处理的过程叫作“Upcasting”(上溯造型)。其中,“cast”(造型)是指根据一个现成的模型创建;而“Up”(向上)表明继承的方向是从“上面”来的——即基础类位于顶部,而衍生类在下方展开。所以,根据基础类进行造型就是一个从上面继承的过程,即“Upcasting”。 在面向对象的程序里,通常都要用到上溯造型技术。这是避免去调查准确类型的一个好办法。请看看doStuff()里的代码: -``` +``` java s.erase(); // ... s.draw(); @@ -51,9 +51,9 @@ s.draw(); 1.6.1 动态绑定 -在doStuff()的代码里,最让人吃惊的是尽管我们没作出任何特殊指示,采取的操作也是完全正确和恰当的。我们知道,为Circle调用draw()时执行的代码与为一个Square或Line调用draw()时执行的代码是不同的。但在将draw()消息发给一个匿名Shape时,根据Shape句柄当时连接的实际类型,会相应地采取正确的操作。这当然令人惊讶,因为当Java编译器为doStuff()编译代码时,它并不知道自己要操作的准确类型是什么。尽管我们确实可以保证最终会为Shape调用erase(),为Shape调用draw(),但并不能保证为特定的Circle,Square或者Line调用什么。然而最后采取的操作同样是正确的,这是怎么做到的呢? +在doStuff()的代码里,最让人吃惊的是尽管我们没作出任何特殊指示,采取的操作也是完全正确和恰当的。我们知道,为Circle调用draw()时执行的代码与为一个Square或Line调用draw()时执行的代码是不同的。但在将draw()消息发给一个匿名Shape时,根据Shape指针当时连接的实际类型,会相应地采取正确的操作。这当然令人惊讶,因为当Java编译器为doStuff()编译代码时,它并不知道自己要操作的准确类型是什么。尽管我们确实可以保证最终会为Shape调用erase(),为Shape调用draw(),但并不能保证为特定的Circle,Square或者Line调用什么。然而最后采取的操作同样是正确的,这是怎么做到的呢? -将一条消息发给对象时,如果并不知道对方的具体类型是什么,但采取的行动同样是正确的,这种情况就叫作“多形性”(Polymorphism)。对面向对象的程序设计语言来说,它们用以实现多形性的方法叫作“动态绑定”。编译器和运行期系统会负责对所有细节的控制;我们只需知道会发生什么事情,而且更重要的是,如何利用它帮助自己设计程序。 +将一条消息发给对象时,如果并不知道对方的具体类型是什么,但采取的行动同样是正确的,这种情况就叫作“多态性”(Polymorphism)。对面向对象的程序设计语言来说,它们用以实现多态性的方法叫作“动态绑定”。编译器和运行期系统会负责对所有细节的控制;我们只需知道会发生什么事情,而且更重要的是,如何利用它帮助自己设计程序。 有些语言要求我们用一个特殊的关键字来允许动态绑定。在C++中,这个关键字是virtual。在Java中,我们则完全不必记住添加一个关键字,因为函数的动态绑定是自动进行的。所以在将一条消息发给对象时,我们完全可以肯定对象会采取正确的行动,即使其中涉及上溯造型之类的处理。 diff --git "a/1.7 \345\257\271\350\261\241\347\232\204\345\210\233\345\273\272\345\222\214\345\255\230\345\234\250\346\227\266\351\227\264.md" "b/1.7 \345\257\271\350\261\241\347\232\204\345\210\233\345\273\272\345\222\214\345\255\230\345\234\250\346\227\266\351\227\264.md" index b8f3480..3210b92 100644 --- "a/1.7 \345\257\271\350\261\241\347\232\204\345\210\233\345\273\272\345\222\214\345\255\230\345\234\250\346\227\266\351\227\264.md" +++ "b/1.7 \345\257\271\350\261\241\347\232\204\345\210\233\345\273\272\345\222\214\345\255\230\345\234\250\346\227\266\351\227\264.md" @@ -3,31 +3,31 @@ 1.7 对象的创建和存在时间 -从技术角度说,OOP(面向对象程序设计)只是涉及抽象的数据类型、继承以及多形性,但另一些问题也可能显得非常重要。本节将就这些问题进行探讨。 +从技术角度说,OOP(面向对象程序设计)只是涉及抽象的数据类型、继承以及多态性,但另一些问题也可能显得非常重要。本节将就这些问题进行探讨。 -最重要的问题之一是对象的创建及破坏方式。对象需要的数据位于哪儿,如何控制对象的“存在时间”呢?针对这个问题,解决的方案是各异其趣的。C++认为程序的执行效率是最重要的一个问题,所以它允许程序员作出选择。为获得最快的运行速度,存储以及存在时间可在编写程序时决定,只需将对象放置在堆栈(有时也叫作自动或定域变量)或者静态存储区域即可。这样便为存储空间的分配和释放提供了一个优先级。某些情况下,这种优先级的控制是非常有价值的。然而,我们同时也牺牲了灵活性,因为在编写程序时,必须知道对象的准确的数量、存在时间、以及类型。如果要解决的是一个较常规的问题,如计算机辅助设计、仓储管理或者空中交通控制,这一方法就显得太局限了。 +最重要的问题之一是对象的创建及破坏方式。对象需要的数据位于哪儿,如何控制对象的“存在时间”呢?针对这个问题,解决的方案是各异其趣的。C++认为程序的执行效率是最重要的一个问题,所以它允许程序员作出选择。为获得最快的运行速度,存储以及存在时间可在编写程序时决定,只需将对象放置在栈(有时也叫作自动或定域变量)或者静态存储区域即可。这样便为存储空间的分配和释放提供了一个优先级。某些情况下,这种优先级的控制是非常有价值的。然而,我们同时也牺牲了灵活性,因为在编写程序时,必须知道对象的准确的数量、存在时间、以及类型。如果要解决的是一个较常规的问题,如计算机辅助设计、仓储管理或者空中交通控制,这一方法就显得太局限了。 -第二个方法是在一个内存池中动态创建对象,该内存池亦叫“堆”或者“内存堆”。若采用这种方式,除非进入运行期,否则根本不知道到底需要多少个对象,也不知道它们的存在时间有多长,以及准确的类型是什么。这些参数都在程序正式运行时才决定的。若需一个新对象,只需在需要它的时候在内存堆里简单地创建它即可。由于存储空间的管理是运行期间动态进行的,所以在内存堆里分配存储空间的时间比在堆栈里创建的时间长得多(在堆栈里创建存储空间一般只需要一个简单的指令,将堆栈指针向下或向下移动即可)。由于动态创建方法使对象本来就倾向于复杂,所以查找存储空间以及释放它所需的额外开销不会为对象的创建造成明显的影响。除此以外,更大的灵活性对于常规编程问题的解决是至关重要的。 +第二个方法是在一个内存池中动态创建对象,该内存池亦叫“堆”或者“内存堆”。若采用这种方式,除非进入运行期,否则根本不知道到底需要多少个对象,也不知道它们的存在时间有多长,以及准确的类型是什么。这些参数都在程序正式运行时才决定的。若需一个新对象,只需在需要它的时候在内存堆里简单地创建它即可。由于存储空间的管理是运行期间动态进行的,所以在内存堆里分配存储空间的时间比在栈里创建的时间长得多(在栈里创建存储空间一般只需要一个简单的指令,将栈指针向下或向下移动即可)。由于动态创建方法使对象本来就倾向于复杂,所以查找存储空间以及释放它所需的额外开销不会为对象的创建造成明显的影响。除此以外,更大的灵活性对于常规编程问题的解决是至关重要的。 -C++允许我们决定是在写程序时创建对象,还是在运行期间创建,这种控制方法更加灵活。大家或许认为既然它如此灵活,那么无论如何都应在内存堆里创建对象,而不是在堆栈中创建。但还要考虑另外一个问题,亦即对象的“存在时间”或者“生存时间”(Lifetime)。若在堆栈或者静态存储空间里创建一个对象,编译器会判断对象的持续时间有多长,到时会自动“破坏”或者“清除”它。程序员可用两种方法来破坏一个对象:用程序化的方式决定何时破坏对象,或者利用由运行环境提供的一种“垃圾收集器”特性,自动寻找那些不再使用的对象,并将其清除。当然,垃圾收集器显得方便得多,但要求所有应用程序都必须容忍垃圾收集器的存在,并能默许随垃圾收集带来的额外开销。但这并不符合C++语言的设计宗旨,所以未能包括到C++里。但Java确实提供了一个垃圾收集器(Smalltalk也有这样的设计;尽管Delphi默认为没有垃圾收集器,但可选择安装;而C++亦可使用一些由其他公司开发的垃圾收集产品)。 +C++允许我们决定是在写程序时创建对象,还是在运行期间创建,这种控制方法更加灵活。大家或许认为既然它如此灵活,那么无论如何都应在内存堆里创建对象,而不是在栈中创建。但还要考虑另外一个问题,亦即对象的“存在时间”或者“生存时间”(Lifetime)。若在栈或者静态存储空间里创建一个对象,编译器会判断对象的持续时间有多长,到时会自动“破坏”或者“清除”它。程序员可用两种方法来破坏一个对象:用程序化的方式决定何时破坏对象,或者利用由运行环境提供的一种“垃圾收集器”特性,自动寻找那些不再使用的对象,并将其清除。当然,垃圾收集器显得方便得多,但要求所有应用程序都必须容忍垃圾收集器的存在,并能默许随垃圾收集带来的额外开销。但这并不符合C++语言的设计宗旨,所以未能包括到C++里。但Java确实提供了一个垃圾收集器(Smalltalk也有这样的设计;尽管Delphi默认为没有垃圾收集器,但可选择安装;而C++亦可使用一些由其他公司开发的垃圾收集产品)。 本节剩下的部分将讨论操纵对象时要考虑的另一些因素。 -1.7.1 集合与继承器 +1.7.1 集合与迭代器 针对一个特定问题的解决,如果事先不知道需要多少个对象,或者它们的持续时间有多长,那么也不知道如何保存那些对象。既然如此,怎样才能知道那些对象要求多少空间呢?事先上根本无法提前知道,除非进入运行期。 -在面向对象的设计中,大多数问题的解决办法似乎都有些轻率——只是简单地创建另一种类型的对象。用于解决特定问题的新型对象容纳了指向其他对象的句柄。当然,也可以用数组来做同样的事情,那是大多数语言都具有的一种功能。但不能只看到这一点。这种新对象通常叫作“集合”(亦叫作一个“容器”,但AWT在不同的场合应用了这个术语,所以本书将一直沿用“集合”的称呼。在需要的时候,集合会自动扩充自己,以便适应我们在其中置入的任何东西。所以我们事先不必知道要在一个集合里容下多少东西。只需创建一个集合,以后的工作让它自己负责好了。 +在面向对象的设计中,大多数问题的解决办法似乎都有些轻率——只是简单地创建另一种类型的对象。用于解决特定问题的新型对象容纳了指向其他对象的指针。当然,也可以用数组来做同样的事情,那是大多数语言都具有的一种功能。但不能只看到这一点。这种新对象通常叫作“集合”(亦叫作一个“容器”,但AWT在不同的场合应用了这个术语,所以本书将一直沿用“集合”的称呼。在需要的时候,集合会自动扩充自己,以便适应我们在其中置入的任何东西。所以我们事先不必知道要在一个集合里容下多少东西。只需创建一个集合,以后的工作让它自己负责好了。 -幸运的是,设计优良的OOP语言都配套提供了一系列集合。在C++中,它们是以“标准模板库”(STL)的形式提供的。Object Pascal用自己的“可视组件库”(VCL)提供集合。Smalltalk提供了一套非常完整的集合。而Java也用自己的标准库提供了集合。在某些库中,一个常规集合便可满足人们的大多数要求;而在另一些库中(特别是C++的库),则面向不同的需求提供了不同类型的集合。例如,可以用一个矢量统一对所有元素的访问方式;一个链接列表则用于保证所有元素的插入统一。所以我们能根据自己的需要选择适当的类型。其中包括集、队列、散列表、树、堆栈等等。 +幸运的是,设计优良的OOP语言都配套提供了一系列集合。在C++中,它们是以“标准模板库”(STL)的形式提供的。Object Pascal用自己的“可视组件库”(VCL)提供集合。Smalltalk提供了一套非常完整的集合。而Java也用自己的标准库提供了集合。在某些库中,一个常规集合便可满足人们的大多数要求;而在另一些库中(特别是C++的库),则面向不同的需求提供了不同类型的集合。例如,可以用一个矢量统一对所有元素的访问方式;一个链接列表则用于保证所有元素的插入统一。所以我们能根据自己的需要选择适当的类型。其中包括集、队列、散列表、树、堆、栈等等。 所有集合都提供了相应的读写功能。将某样东西置入集合时,采用的方式是十分明显的。有一个叫作“推”(Push)、“添加”(Add)或其他类似名字的函数用于做这件事情。但将数据从集合中取出的时候,方式却并不总是那么明显。如果是一个数组形式的实体,比如一个矢量(Vector),那么也许能用索引运算符或函数。但在许多情况下,这样做往往会无功而返。此外,单选定函数的功能是非常有限的。如果想对集合中的一系列元素进行操纵或比较,而不是仅仅面向一个,这时又该怎么办呢? -办法就是使用一个“继续器”(Iterator),它属于一种对象,负责选择集合内的元素,并把它们提供给继承器的用户。作为一个类,它也提供了一级抽象。利用这一级抽象,可将集合细节与用于访问那个集合的代码隔离开。通过继承器的作用,集合被抽象成一个简单的序列。继承器允许我们遍历那个序列,同时毋需关心基础结构是什么——换言之,不管它是一个矢量、一个链接列表、一个堆栈,还是其他什么东西。这样一来,我们就可以灵活地改变基础数据,不会对程序里的代码造成干扰。Java最开始(在1.0和1.1版中)提供的是一个标准继承器,名为Enumeration(枚举),为它的所有集合类提供服务。Java 1.2新增一个更复杂的集合库,其中包含了一个名为Iterator的继承器,可以做比老式的Enumeration更多的事情。 +办法就是使用一个“继续器”(Iterator),它属于一种对象,负责选择集合内的元素,并把它们提供给迭代器的用户。作为一个类,它也提供了一级抽象。利用这一级抽象,可将集合细节与用于访问那个集合的代码隔离开。通过迭代器的作用,集合被抽象成一个简单的序列。迭代器允许我们遍历那个序列,同时毋需关心基础结构是什么——换言之,不管它是一个矢量、一个链接列表、一个堆、栈,还是其他什么东西。这样一来,我们就可以灵活地改变基础数据,不会对程序里的代码造成干扰。Java最开始(在1.0和1.1版中)提供的是一个标准迭代器,名为Enumeration(枚举),为它的所有集合类提供服务。Java 1.2新增一个更复杂的集合库,其中包含了一个名为Iterator的迭代器,可以做比老式的Enumeration更多的事情。 -从设计角度出发,我们需要的是一个全功能的序列。通过对它的操纵,应该能解决自己的问题。如果一种类型的序列即可满足我们的所有要求,那么完全没有必要再换用不同的类型。有两方面的原因促使我们需要对集合作出选择。首先,集合提供了不同的接口类型以及外部行为。堆栈的接口与行为与队列的不同,而队列的接口与行为又与一个集(Set)或列表的不同。利用这个特征,我们解决问题时便有更大的灵活性。 +从设计角度出发,我们需要的是一个全功能的序列。通过对它的操纵,应该能解决自己的问题。如果一种类型的序列即可满足我们的所有要求,那么完全没有必要再换用不同的类型。有两方面的原因促使我们需要对集合作出选择。首先,集合提供了不同的接口类型以及外部行为。堆、栈的接口与行为与队列的不同,而队列的接口与行为又与一个集(Set)或列表的不同。利用这个特征,我们解决问题时便有更大的灵活性。 -其次,不同的集合在进行特定操作时往往有不同的效率。最好的例子便是矢量(Vector)和列表(List)的区别。它们都属于简单的序列,拥有完全一致的接口和外部行为。但在执行一些特定的任务时,需要的开销却是完全不同的。对矢量内的元素进行的随机访问(存取)是一种常时操作;无论我们选择的选择是什么,需要的时间量都是相同的。但在一个链接列表中,若想到处移动,并随机挑选一个元素,就需付出“惨重”的代价。而且假设某个元素位于列表较远的地方,找到它所需的时间也会长许多。但在另一方面,如果想在序列中部插入一个元素,用列表就比用矢量划算得多。这些以及其他操作都有不同的执行效率,具体取决于序列的基础结构是什么。在设计阶段,我们可以先从一个列表开始。最后调整性能的时候,再根据情况把它换成矢量。由于抽象是通过继承器进行的,所以能在两者方便地切换,对代码的影响则显得微不足道。 +其次,不同的集合在进行特定操作时往往有不同的效率。最好的例子便是矢量(Vector)和列表(List)的区别。它们都属于简单的序列,拥有完全一致的接口和外部行为。但在执行一些特定的任务时,需要的开销却是完全不同的。对矢量内的元素进行的随机访问(存取)是一种常时操作;无论我们选择的选择是什么,需要的时间量都是相同的。但在一个链接列表中,若想到处移动,并随机挑选一个元素,就需付出“惨重”的代价。而且假设某个元素位于列表较远的地方,找到它所需的时间也会长许多。但在另一方面,如果想在序列中部插入一个元素,用列表就比用矢量划算得多。这些以及其他操作都有不同的执行效率,具体取决于序列的基础结构是什么。在设计阶段,我们可以先从一个列表开始。最后调整性能的时候,再根据情况把它换成矢量。由于抽象是通过迭代器进行的,所以能在两者方便地切换,对代码的影响则显得微不足道。 最后,记住集合只是一个用来放置对象的储藏所。如果那个储藏所能满足我们的所有需要,就完全没必要关心它具体是如何实现的(这是大多数类型对象的一个基本概念)。如果在一个编程环境中工作,它由于其他因素(比如在Windows下运行,或者由垃圾收集器带来了开销)产生了内在的开销,那么矢量和链接列表之间在系统开销上的差异就或许不是一个大问题。我们可能只需要一种类型的序列。甚至可以想象有一个“完美”的集合抽象,它能根据自己的使用方式自动改变基层的实现方式。 @@ -38,9 +38,9 @@ C++允许我们决定是在写程序时创建对象,还是在运行期间创 单根结构中的所有对象都有一个通用接口,所以它们最终都属于相同的类型。另一种方案(就象C++那样)是我们不能保证所有东西都属于相同的基本类型。从向后兼容的角度看,这一方案可与C模型更好地配合,而且可以认为它的限制更少一些。但假期我们想进行纯粹的面向对象编程,那么必须构建自己的结构,以期获得与内建到其他OOP语言里的同样的便利。需添加我们要用到的各种新类库,还要使用另一些不兼容的接口。理所当然地,这也需要付出额外的精力使新接口与自己的设计方案配合(可能还需要多重继承)。为得到C++额外的“灵活性”,付出这样的代价值得吗?当然,如果真的需要——如果早已是C专家,如果对C有难舍的情结——那么就真的很值得。但假如你是一名新手,首次接触这类设计,象Java那样的替换方案也许会更省事一些。 单根结构中的所有对象(比如所有Java对象)都可以保证拥有一些特定的功能。在自己的系统中,我们知道对每个对象都能进行一些基本操作。一个单根结构,加上所有对象都在内存堆中创建,可以极大简化参数的传递(这在C++里是一个复杂的概念)。 -利用单根结构,我们可以更方便地实现一个垃圾收集器。与此有关的必要支持可安装于基础类中,而垃圾收集器可将适当的消息发给系统内的任何对象。如果没有这种单根结构,而且系统通过一个句柄来操纵对象,那么实现垃圾收集器的途径会有很大的不同,而且会面临许多障碍。 +利用单根结构,我们可以更方便地实现一个垃圾收集器。与此有关的必要支持可安装于基础类中,而垃圾收集器可将适当的消息发给系统内的任何对象。如果没有这种单根结构,而且系统通过一个指针来操纵对象,那么实现垃圾收集器的途径会有很大的不同,而且会面临许多障碍。 -由于运行期的类型信息肯定存在于所有对象中,所以永远不会遇到判断不出一个对象的类型的情况。这对系统级的操作来说显得特别重要,比如违例控制;而且也能在程序设计时获得更大的灵活性。 +由于运行期的类型信息肯定存在于所有对象中,所以永远不会遇到判断不出一个对象的类型的情况。这对系统级的操作来说显得特别重要,比如异常控制;而且也能在程序设计时获得更大的灵活性。 但大家也可能产生疑问,既然你把好处说得这么天花乱坠,为什么C++没有采用单根结构呢?事实上,这是早期在效率与控制上权衡的一种结果。单根结构会带来程序设计上的一些限制。而且更重要的是,它加大了新程序与原有C代码兼容的难度。尽管这些限制仅在特定的场合会真的造成问题,但为了获得最大的灵活程度,C++最终决定放弃采用单根结构这一做法。而Java不存在上述的问题,它是全新设计的一种语言,不必与现有的语言保持所谓的“向后兼容”。所以很自然地,与其他大多数面向对象的程序设计语言一样,单根结构在Java的设计方案中很快就落实下来。 @@ -51,11 +51,11 @@ C++允许我们决定是在写程序时创建对象,还是在运行期间创 1.下溯造型与模板/通用性 为了使这些集合能够重复使用,或者“再生”,Java提供了一种通用类型,以前曾把它叫作“Object”。单根结构意味着、所有东西归根结底都是一个对象”!所以容纳了Object的一个集合实际可以容纳任何东西。这使我们对它的重复使用变得非常简便。 -为使用这样的一个集合,只需添加指向它的对象句柄即可,以后可以通过句柄重新使用对象。但由于集合只能容纳Object,所以在我们向集合里添加对象句柄时,它会上溯造型成Object,这样便丢失了它的身份或者标识信息。再次使用它的时候,会得到一个Object句柄,而非指向我们早先置入的那个类型的句柄。所以怎样才能归还它的本来面貌,调用早先置入集合的那个对象的有用接口呢? +为使用这样的一个集合,只需添加指向它的对象指针即可,以后可以通过指针重新使用对象。但由于集合只能容纳Object,所以在我们向集合里添加对象指针时,它会上溯造型成Object,这样便丢失了它的身份或者标识信息。再次使用它的时候,会得到一个Object指针,而非指向我们早先置入的那个类型的指针。所以怎样才能归还它的本来面貌,调用早先置入集合的那个对象的有用接口呢? 在这里,我们再次用到了造型(Cast)。但这一次不是在分级结构中上溯造型成一种更“通用”的类型。而是下溯造型成一种更“特殊”的类型。这种造型方法叫作“下溯造型”(Downcasting)。举个例子来说,我们知道在上溯造型的时候,Circle(圆)属于Shape(几何形状)的一种类型,所以上溯造型是安全的。但我们不知道一个Object到底是Circle还是Shape,所以很难保证下溯造型的安全进行,除非确切地知道自己要操作的是什么。 -但这也不是绝对危险的,因为假如下溯造型成错误的东西,会得到我们称为“违例”(Exception)的一种运行期错误。我们稍后即会对此进行解释。但在从一个集合提取对象句柄时,必须用某种方式准确地记住它们是什么,以保证下溯造型的正确进行。 +但这也不是绝对危险的,因为假如下溯造型成错误的东西,会得到我们称为“异常”(Exception)的一种运行期错误。我们稍后即会对此进行解释。但在从一个集合提取对象指针时,必须用某种方式准确地记住它们是什么,以保证下溯造型的正确进行。 下溯造型和运行期检查都要求花额外的时间来运行程序,而且程序员必须付出额外的精力。既然如此,我们能不能创建一个“智能”集合,令其知道自己容纳的类型呢?这样做可消除下溯造型的必要以及潜在的错误。答案是肯定的,我们可以采用“参数化类型”,它们是编译器能自动定制的类,可与特定的类型配合。例如,通过使用一个参数化集合,编译器可对那个集合进行定制,使其只接受Shape,而且只提取Shape。 参数化类型是C++一个重要的组成部分,这部分是C++没有单根结构的缘故。在C++中,用于实现参数化类型的关键字是template(模板)。Java目前尚未提供参数化类型,因为由于使用的是单根结构,所以使用它显得有些笨拙。但这并不能保证以后的版本不会实现,因为“generic”这个词已被Java“保留到将来实现”(在Ada语言中,“generic”被用来实现它的模板)。Java采取的这种关键字保留机制其实经常让人摸不着头脑,很难断定以后会发生什么事情。 @@ -74,7 +74,7 @@ C++允许我们决定是在写程序时创建对象,还是在运行期间创 2.垃圾收集器对效率及灵活性的影响 -既然这是如此好的一种手段,为什么在C++里没有得到充分的发挥呢?我们当然要为这种编程的方便性付出一定的代价,代价就是运行期的开销。正如早先提到的那样,在C++中,我们可在堆栈中创建对象。在这种情况下,对象会得以自动清除(但不具有在运行期间随心所欲创建对象的灵活性)。在堆栈中创建对象是为对象分配存储空间最有效的一种方式,也是释放那些空间最有效的一种方式。在内存堆(Heap)中创建对象可能要付出昂贵得多的代价。如果总是从同一个基础类继承,并使所有函数调用都具有“同质多形”特征,那么也不可避免地需要付出一定的代价。但垃圾收集器是一种特殊的问题,因为我们永远不能确定它什么时候启动或者要花多长的时间。这意味着在Java程序执行期间,存在着一种不连贯的因素。所以在某些特殊的场合,我们必须避免用它——比如在一个程序的执行必须保持稳定、连贯的时候(通常把它们叫作“实时程序”,尽管并不是所有实时编程问题都要这方面的要求——注释⑦)。 +既然这是如此好的一种手段,为什么在C++里没有得到充分的发挥呢?我们当然要为这种编程的方便性付出一定的代价,代价就是运行期的开销。正如早先提到的那样,在C++中,我们可在栈中创建对象。在这种情况下,对象会得以自动清除(但不具有在运行期间随心所欲创建对象的灵活性)。在栈中创建对象是为对象分配存储空间最有效的一种方式,也是释放那些空间最有效的一种方式。在内存堆(Heap)中创建对象可能要付出昂贵得多的代价。如果总是从同一个基础类继承,并使所有函数调用都具有“同质多态”特征,那么也不可避免地需要付出一定的代价。但垃圾收集器是一种特殊的问题,因为我们永远不能确定它什么时候启动或者要花多长的时间。这意味着在Java程序执行期间,存在着一种不连贯的因素。所以在某些特殊的场合,我们必须避免用它——比如在一个程序的执行必须保持稳定、连贯的时候(通常把它们叫作“实时程序”,尽管并不是所有实时编程问题都要这方面的要求——注释⑦)。 ⑦:根据本书一些技术性读者的反馈,有一个现成的实时Java系统(www.newmonics.com)确实能够保证垃圾收集器的效能。 diff --git "a/1.8 \350\277\235\344\276\213\346\216\247\345\210\266\357\274\232\350\247\243\345\206\263\351\224\231\350\257\257.md" "b/1.8 \350\277\235\344\276\213\346\216\247\345\210\266\357\274\232\350\247\243\345\206\263\351\224\231\350\257\257.md" deleted file mode 100644 index c8e0d8b..0000000 --- "a/1.8 \350\277\235\344\276\213\346\216\247\345\210\266\357\274\232\350\247\243\345\206\263\351\224\231\350\257\257.md" +++ /dev/null @@ -1,9 +0,0 @@ -# 1.8 违例控制:解决错误 - - -从最古老的程序设计语言开始,错误控制一直都是设计者们需要解决的一个大问题。由于很难设计出一套完美的错误控制方案,许多语言干脆将问题简单地忽略掉,将其转嫁给库设计人员。对大多数错误控制方案来说,最主要的一个问题是它们严重依赖程序员的警觉性,而不是依赖语言本身的强制标准。如果程序员不够警惕——若比较匆忙,这几乎是肯定会发生的——程序所依赖的错误控制方案便会失效。 - -“违例控制”将错误控制方案内置到程序设计语言中,有时甚至内建到操作系统内。这里的“违例”(Exception)属于一个特殊的对象,它会从产生错误的地方“扔”或“掷”出来。随后,这个违例会被设计用于控制特定类型错误的“违例控制器”捕获。在情况变得不对劲的时候,可能有几个违例控制器并行捕获对应的违例对象。由于采用的是独立的执行路径,所以不会干扰我们的常规执行代码。这样便使代码的编写变得更加简单,因为不必经常性强制检查代码。除此以外,“掷”出的一个违例不同于从函数返回的错误值,也不同于由函数设置的一个标志。那些错误值或标志的作用是指示一个错误状态,是可以忽略的。但违例不能被忽略,所以肯定能在某个地方得到处置。最后,利用违例能够可靠地从一个糟糕的环境中恢复。此时一般不需要退出,我们可以采取某些处理,恢复程序的正常执行。显然,这样编制出来的程序显得更加可靠。 - -Java的违例控制机制与大多数程序设计语言都有所不同。因为在Java中,违例控制模块是从一开始就封装好的,所以必须使用它!如果没有自己写一些代码来正确地控制违例,就会得到一条编译期出错提示。这样可保证程序的连贯性,使错误控制变得更加容易。 -注意违例控制并不属于一种面向对象的特性,尽管在面向对象的程序设计语言中,违例通常是用一个对象表示的。早在面向对象语言问世以前,违例控制就已经存在了。 diff --git "a/10.1 \350\276\223\345\205\245\345\222\214\350\276\223\345\207\272.md" "b/10.1 \350\276\223\345\205\245\345\222\214\350\276\223\345\207\272.md" index 7ed188a..efeca77 100644 --- "a/10.1 \350\276\223\345\205\245\345\222\214\350\276\223\345\207\272.md" +++ "b/10.1 \350\276\223\345\205\245\345\222\214\350\276\223\345\207\272.md" @@ -20,7 +20,7 @@ InputStream的作用是标志那些从不同起源地产生输入的类。这些 除此以外,FilterInputStream也属于InputStream的一种类型,用它可为“破坏器”类提供一个基础类,以便将属性或者有用的接口同输入流连接到一起。这将在以后讨论。 -``` +``` java Class Function @@ -54,7 +54,7 @@ A String representing the file name, or a File or FileDescriptor object. As a source of data. Connect it to a FilterInputStream object to provide a useful interface. ``` -类 功能 构建器参数/如何使用 +类 功能 构造器参数/如何使用 ByteArrayInputStream 允许内存中的一个缓冲区作为InputStream使用 从中提取字节的缓冲区/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口 @@ -64,7 +64,7 @@ StringBuffer(字串缓冲)/作为一个数据源使用。通过将其同 FileInputStream 用于从文件读取信息 代表文件名的一个String,或者一个File或FileDescriptor对象/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口 -``` +``` java Piped-InputStream Produces the data that’s being written to the associated PipedOutput-Stream. Implements the “piping” concept. @@ -105,7 +105,7 @@ FilterInputStream 对作为破坏器接口使用的类进行抽象;那个破 表10.2 OutputStream的类型 -``` +``` java Class Function @@ -148,7 +148,7 @@ See Table 10-4. See Table 10-4. ``` -类 功能 构建器参数/如何使用 +类 功能 构造器参数/如何使用 ByteArrayOutputStream 在内存中创建一个缓冲区。我们发送给流的所有数据都会置入这个缓冲区。 可选缓冲区的初始大小/ 用于指出数据的目的地。若将其同FilterOutputStream对象连接到一起,可提供一个有用的接口 diff --git "a/10.10 \346\200\273\347\273\223.md" "b/10.10 \346\200\273\347\273\223.md" index a6cf3cf..3aac871 100644 --- "a/10.10 \346\200\273\347\273\223.md" +++ "b/10.10 \346\200\273\347\273\223.md" @@ -2,7 +2,7 @@ Java IO流库能满足我们的许多基本要求:可以通过控制台、文件、内存块甚至因特网(参见第15章)进行读写。可以创建新的输入和输出对象类型(通过从InputStream和OutputStream继承)。向一个本来预期为收到字串的方法传递一个对象时,由于Java已限制了“自动类型转换”,所以会自动调用toString()方法。而我们可以重新定义这个toString(),扩展一个数据流能接纳的对象种类。 -在IO数据流库的联机文档和设计过程中,仍有些问题没有解决。比如当我们打开一个文件以便输出时,完全可以指定一旦有人试图覆盖该文件就“掷”出一个违例——有的编程系统允许我们自行指定想打开一个输出文件,但唯一的前提是它尚不存在。但在Java中,似乎必须用一个File对象来判断某个文件是否存在,因为假如将其作为FileOutputStream或者FileWriter打开,那么肯定会被覆盖。若同时指定文件和目录路径,File类设计上的一个缺陷就会暴露出来,因为它会说“不要试图在单个类里做太多的事情”! +在IO数据流库的联机文档和设计过程中,仍有些问题没有解决。比如当我们打开一个文件以便输出时,完全可以指定一旦有人试图覆盖该文件就“掷”出一个异常——有的编程系统允许我们自行指定想打开一个输出文件,但唯一的前提是它尚不存在。但在Java中,似乎必须用一个File对象来判断某个文件是否存在,因为假如将其作为FileOutputStream或者FileWriter打开,那么肯定会被覆盖。若同时指定文件和目录路径,File类设计上的一个缺陷就会暴露出来,因为它会说“不要试图在单个类里做太多的事情”! IO流库易使我们混淆一些概念。它确实能做许多事情,而且也可以移植。但假如假如事先没有吃透装饰器方案的概念,那么所有的设计都多少带有一点盲目性质。所以不管学它还是教它,都要特别花一些功夫才行。而且它并不完整:没有提供对输出格式化的支持,而其他几乎所有语言的IO包都提供了这方面的支持(这一点没有在Java 1.1里得以纠正,它完全错失了改变库设计方案的机会,反而增添了更特殊的一些情况,使复杂程度进一步提高)。Java 1.1转到那些尚未替换的IO库,而不是增加新库。而且库的设计人员似乎没有很好地指出哪些特性是不赞成的,哪些是首选的,造成库设计中经常都会出现一些令人恼火的反对消息。 然而,一旦掌握了装饰器方案,并开始在一些较为灵活的环境使用库,就会认识到这种设计的好处。到那个时候,为此多付出的代码行应该不至于使你觉得太生气。 diff --git "a/10.11 \347\273\203\344\271\240.md" "b/10.11 \347\273\203\344\271\240.md" index 96d3a1a..6212288 100644 --- "a/10.11 \347\273\203\344\271\240.md" +++ "b/10.11 \347\273\203\344\271\240.md" @@ -10,7 +10,7 @@ (5) 修改练习2,在文件中查找指定的单词。打印出包含了欲找单词的所有文本行。 -(6) 在Blips.java中复制文件,将其重命名为BlipCheck.java。然后将类Blip2重命名为BlipCheck(在进程中将其标记为public)。删除文件中的//!记号,并执行程序。接下来,将BlipCheck的默认构建器变成注释信息。运行它,并解释为什么仍然能够工作。 +(6) 在Blips.java中复制文件,将其重命名为BlipCheck.java。然后将类Blip2重命名为BlipCheck(在进程中将其标记为public)。删除文件中的//!记号,并执行程序。接下来,将BlipCheck的默认构造器变成注释信息。运行它,并解释为什么仍然能够工作。 (7) 在Blip3.java中,将接在“You must do this:”字样后的两行变成注释,然后运行程序。解释得到的结果为什么会与执行了那两行代码不同。 diff --git "a/10.2 \345\242\236\346\267\273\345\261\236\346\200\247\345\222\214\346\234\211\347\224\250\347\232\204\346\216\245\345\217\243.md" "b/10.2 \345\242\236\346\267\273\345\261\236\346\200\247\345\222\214\346\234\211\347\224\250\347\232\204\346\216\245\345\217\243.md" index b357947..65b8986 100644 --- "a/10.2 \345\242\236\346\267\273\345\261\236\346\200\247\345\222\214\346\234\211\347\224\250\347\232\204\346\216\245\345\217\243.md" +++ "b/10.2 \345\242\236\346\267\273\345\261\236\346\200\247\345\222\214\346\234\211\347\224\250\347\232\204\346\216\245\345\217\243.md" @@ -59,7 +59,7 @@ InputStream Generally used in the scanner for a compiler and probably included because the Java compiler needed it. You probably won’t use this. -类 功能 构建器参数/如何使用 +类 功能 构造器参数/如何使用 DataInputStream 与DataOutputStream联合使用,使自己能以机动方式读取一个流中的基本数据类型(int,char,long等等) InputStream/包含了一个完整的接口,以便读取基本数据类型 @@ -114,7 +114,7 @@ OutputStream, with optional buffer size. This doesn’t provide an interface per se, just a requirement that a buffer is used. Attach an interface object. -类 功能 构建器参数/如何使用 +类 功能 构造器参数/如何使用 DataOutputStream 与DataInputStream配合使用,以便采用方便的形式将基本数据类型(int,char,long等)写入一个数据流 OutputStream/包含了完整接口,以便我们写入基本数据类型 diff --git "a/10.3 \346\234\254\350\272\253\347\232\204\347\274\272\351\231\267\357\274\232RandomAccessFile.md" "b/10.3 \346\234\254\350\272\253\347\232\204\347\274\272\351\231\267\357\274\232RandomAccessFile.md" index ff85b96..73e12f4 100644 --- "a/10.3 \346\234\254\350\272\253\347\232\204\347\274\272\351\231\267\357\274\232RandomAccessFile.md" +++ "b/10.3 \346\234\254\350\272\253\347\232\204\347\274\272\351\231\267\357\274\232RandomAccessFile.md" @@ -4,6 +4,6 @@ RandomAccessFile用于包含了已知长度记录的文件,以便我们能用s 首先,我们有点难以相信RandomAccessFile不属于InputStream或者OutputStream分层结构的一部分。除了恰巧实现了DataInput以及DataOutput(这两者亦由DataInputStream和DataOutputStream实现)接口之外,它们与那些分层结构并无什么关系。它甚至没有用到现有InputStream或OutputStream类的功能——采用的是一个完全不相干的类。该类属于全新的设计,含有自己的全部(大多数为固有)方法。之所以要这样做,是因为RandomAccessFile拥有与其他IO类型完全不同的行为,因为我们可在一个文件里向前或向后移动。不管在哪种情况下,它都是独立运作的,作为Object的一个“直接继承人”使用。 -从根本上说,RandomAccessFile类似DataInputStream和DataOutputStream的联合使用。其中,getFilePointer()用于了解当前在文件的什么地方,seek()用于移至文件内的一个新地点,而length()用于判断文件的最大长度。此外,构建器要求使用另一个自变量(与C的fopen()完全一样),指出自己只是随机读("r"),还是读写兼施("rw")。这里没有提供对“只写文件”的支持。也就是说,假如是从DataInputStream继承的,那么RandomAccessFile也有可能能很好地工作。 +从根本上说,RandomAccessFile类似DataInputStream和DataOutputStream的联合使用。其中,getFilePointer()用于了解当前在文件的什么地方,seek()用于移至文件内的一个新地点,而length()用于判断文件的最大长度。此外,构造器要求使用另一个自变量(与C的fopen()完全一样),指出自己只是随机读("r"),还是读写兼施("rw")。这里没有提供对“只写文件”的支持。也就是说,假如是从DataInputStream继承的,那么RandomAccessFile也有可能能很好地工作。 还有更难对付的。很容易想象我们有时要在其他类型的数据流中搜索,比如一个ByteArrayInputStream,但搜索方法只有RandomAccessFile才会提供。而后者只能针对文件才能操作,不能针对数据流操作。此时,BufferedInputStream确实允许我们标记一个位置(使用mark(),它的值容纳于单个内部变量中),并用reset()重设那个位置。但这些做法都存在限制,并不是特别有用。 diff --git "a/10.4 File\347\261\273.md" "b/10.4 File\347\261\273.md" index 6402c8a..8d92a03 100644 --- "a/10.4 File\347\261\273.md" +++ "b/10.4 File\347\261\273.md" @@ -9,7 +9,7 @@ File类有一个欺骗性的名字——通常会认为它对付的是一个文 下面是用于这个例子的代码(或在执行该程序时遇到困难,请参考第3章3.1.2小节“赋值”): -``` +``` java //: DirList.java // Displays directory listing package c10; @@ -45,7 +45,7 @@ class DirFilter implements FilenameFilter { DirFilter类“实现”了interface FilenameFilter(关于接口的问题,已在第7章进行了详述)。下面让我们看看FilenameFilter接口有多么简单: -``` +``` java public interface FilenameFilter { boolean accept(文件目录, 字串名); } @@ -53,7 +53,7 @@ boolean accept(文件目录, 字串名); 它指出这种类型的所有对象都提供了一个名为accept()的方法。之所以要创建这样的一个类,背后的全部原因就是把accept()方法提供给list()方法,使list()能够“回调”accept(),从而判断应将哪些文件名包括到列表中。因此,通常将这种技术称为“回调”,有时也称为“算子”(也就是说,DirFilter是一个算子,因为它唯一的作用就是容纳一个方法)。由于list()采用一个FilenameFilter对象作为自己的自变量使用,所以我们能传递实现了FilenameFilter的任何类的一个对象,用它决定(甚至在运行期)list()方法的行为方式。回调的目的是在代码的行为上提供更大的灵活性。 -通过DirFilter,我们看出尽管一个“接口”只包含了一系列方法,但并不局限于只能写那些方法(但是,至少必须提供一个接口内所有方法的定义。在这种情况下,DirFilter构建器也会创建)。 +通过DirFilter,我们看出尽管一个“接口”只包含了一系列方法,但并不局限于只能写那些方法(但是,至少必须提供一个接口内所有方法的定义。在这种情况下,DirFilter构造器也会创建)。 accept()方法必须接纳一个File对象,用它指示用于寻找一个特定文件的目录;并接纳一个String,其中包含了要寻找之文件的名字。可决定使用或忽略这两个参数之一,但有时至少要使用文件名。记住list()方法准备为目录对象中的每个文件名调用 @@ -66,9 +66,9 @@ list()方法返回的是一个数组。可查询这个数组的长度,然后 1. 匿名内部类 -下例用一个匿名内部类(已在第7章讲述)来重写显得非常理想。首先创建了一个filter()方法,它返回指向FilenameFilter的一个句柄: +下例用一个匿名内部类(已在第7章讲述)来重写显得非常理想。首先创建了一个filter()方法,它返回指向FilenameFilter的一个指针: -``` +``` java //: DirList2.java // Uses Java 1.1 anonymous inner classes import java.io.*; @@ -108,7 +108,7 @@ public class DirList2 { 之所以认为这样做更好,是由于FilenameFilter类现在同DirList2紧密地结合在一起。然而,我们可采取进一步的操作,将匿名内部类定义成list()的一个参数,使其显得更加精简。如下所示: -``` +``` java //: DirList3.java // Building the anonymous inner class "in-place" import java.io.*; @@ -146,7 +146,7 @@ main()现在的自变量是final,因为匿名内部类直接使用args[0]。 经常都需要文件名以排好序的方式提供。由于Java 1.0和Java 1.1都没有提供对排序的支持(从Java 1.2开始提供),所以必须用第8章创建的SortVector将这一能力直接加入自己的程序。就象下面这样: -``` +``` java //: SortedDirList.java // Displays sorted directory listing import java.io.*; @@ -195,7 +195,7 @@ public class SortedDirList { } ///:~ ``` -这里进行了另外少许改进。不再是将path(路径)和list(列表)创建为main()的本地变量,它们变成了类的成员,使它们的值能在对象“生存”期间方便地访问。事实上,main()现在只是对类进行测试的一种方式。大家可以看到,一旦列表创建完毕,类的构建器就会自动开始对列表进行排序。 +这里进行了另外少许改进。不再是将path(路径)和list(列表)创建为main()的本地变量,它们变成了类的成员,使它们的值能在对象“生存”期间方便地访问。事实上,main()现在只是对类进行测试的一种方式。大家可以看到,一旦列表创建完毕,类的构造器就会自动开始对列表进行排序。 这种排序不要求区分大小写,所以最终不会得到一组全部单词都以大写字母开头的列表,跟着是全部以小写字母开头的列表。然而,我们注意到在以相同字母开头的一组文件名中,大写字母是排在前面的——这对标准的排序来说仍是一种不合格的行为。Java 1.2已成功解决了这个问题。 @@ -203,7 +203,7 @@ public class SortedDirList { File类并不仅仅是对现有目录路径、文件或者文件组的一个表示。亦可用一个File对象新建一个目录,甚至创建一个完整的目录路径——假如它尚不存在的话。亦可用它了解文件的属性(长度、上一次修改日期、读/写属性等),检查一个File对象到底代表一个文件还是一个目录,以及删除一个文件等等。下列程序完整展示了如何运用File类剩下的这些方法: -``` +``` java //: MakeDirectories.java // Demonstrates the use of the File class to // create directories and manipulate files. diff --git "a/10.5 IO\346\265\201\347\232\204\345\205\270\345\236\213\345\272\224\347\224\250.md" "b/10.5 IO\346\265\201\347\232\204\345\205\270\345\236\213\345\272\224\347\224\250.md" index 2a840cb..69900a5 100644 --- "a/10.5 IO\346\265\201\347\232\204\345\205\270\345\236\213\345\272\224\347\224\250.md" +++ "b/10.5 IO\346\265\201\347\232\204\345\205\270\345\236\213\345\272\224\347\224\250.md" @@ -2,7 +2,7 @@ 尽管库内存在大量IO流类,可通过多种不同的方式组合到一起,但实际上只有几种方式才会经常用到。然而,必须小心在意才能得到正确的组合。下面这个相当长的例子展示了典型IO配置的创建与使用,可在写自己的代码时将其作为一个参考使用。注意每个配置都以一个注释形式的编号起头,并提供了适当的解释信息。 -``` +``` java //: IOStreamDemo.java // Typical IO Stream Configurations import java.io.*; @@ -139,7 +139,7 @@ public class IOStreamDemo { 1. 缓冲的输入文件 -为打开一个文件以便输入,需要使用一个FileInputStream,同时将一个String或File对象作为文件名使用。为提高速度,最好先对文件进行缓冲处理,从而获得用于一个BufferedInputStream的构建器的结果句柄。为了以格式化的形式读取输入数据,我们将那个结果句柄赋给用于一个DataInputStream的构建器。DataInputStream是我们的最终(final)对象,并是我们进行读取操作的接口。 +为打开一个文件以便输入,需要使用一个FileInputStream,同时将一个String或File对象作为文件名使用。为提高速度,最好先对文件进行缓冲处理,从而获得用于一个BufferedInputStream的构造器的结果指针。为了以格式化的形式读取输入数据,我们将那个结果指针赋给用于一个DataInputStream的构造器。DataInputStream是我们的最终(final)对象,并是我们进行读取操作的接口。 在这个例子中,只用到了readLine()方法,但理所当然任何DataInputStream方法都可以采用。一旦抵达文件末尾,readLine()就会返回一个null(空),以便中止并退出while循环。 @@ -147,13 +147,13 @@ public class IOStreamDemo { 2. 从内存输入 -这一部分采用已经包含了完整文件内容的String s2,并用它创建一个StringBufferInputStream(字串缓冲输入流)——作为构建器的参数,要求使用一个String,而非一个StringBuffer)。随后,我们用read()依次读取每个字符,并将其发送至控制台。注意read()将下一个字节返回为int,所以必须将其造型为一个char,以便正确地打印。 +这一部分采用已经包含了完整文件内容的String s2,并用它创建一个StringBufferInputStream(字串缓冲输入流)——作为构造器的参数,要求使用一个String,而非一个StringBuffer)。随后,我们用read()依次读取每个字符,并将其发送至控制台。注意read()将下一个字节返回为int,所以必须将其造型为一个char,以便正确地打印。 3. 格式化内存输入 StringBufferInputStream的接口是有限的,所以通常需要将其封装到一个DataInputStream内,从而增强它的能力。然而,若选择用readByte()每次读出一个字符,那么所有值都是有效的,所以不可再用返回值来侦测何时结束输入。相反,可用available()方法判断有多少字符可用。下面这个例子展示了如何从文件中一次读出一个字符: -``` +``` java //: TestEOF.java // Testing for the end of file while reading // a byte at a time. @@ -177,11 +177,11 @@ public class TestEOF { 注意取决于当前从什么媒体读入,avaiable()的工作方式也是有所区别的。它在字面上意味着“可以不受阻塞读取的字节数量”。对一个文件来说,它意味着整个文件。但对一个不同种类的数据流来说,它却可能有不同的含义。因此在使用时应考虑周全。 -为了在这样的情况下侦测输入的结束,也可以通过捕获一个违例来实现。然而,若真的用违例来控制数据流,却显得有些大材小用。 +为了在这样的情况下侦测输入的结束,也可以通过捕获一个异常来实现。然而,若真的用异常来控制数据流,却显得有些大材小用。 4. 行的编号与文件输出 -这个例子展示了如何LineNumberInputStream来跟踪输入行的编号。在这里,不可简单地将所有构建器都组合起来,因为必须保持LineNumberInputStream的一个句柄(注意这并非一种继承环境,所以不能简单地将in4造型到一个LineNumberInputStream)。因此,li容纳了指向LineNumberInputStream的句柄,然后在它的基础上创建一个DataInputStream,以便读入数据。 +这个例子展示了如何LineNumberInputStream来跟踪输入行的编号。在这里,不可简单地将所有构造器都组合起来,因为必须保持LineNumberInputStream的一个指针(注意这并非一种继承环境,所以不能简单地将in4造型到一个LineNumberInputStream)。因此,li容纳了指向LineNumberInputStream的指针,然后在它的基础上创建一个DataInputStream,以便读入数据。 这个例子也展示了如何将格式化数据写入一个文件。首先创建了一个FileOutputStream,用它同一个文件连接。考虑到效率方面的原因,它生成了一个BufferedOutputStream。这几乎肯定是我们一般的做法,但却必须明确地这样做。随后为了进行格式化,它转换成一个PrintStream。用这种方式创建的数据文件可作为一个原始的文本文件读取。 标志DataInputStream何时结束的一个方法是readLine()。一旦没有更多的字串可以读取,它就会返回null。每个行都会伴随自己的行号打印到文件里。该行号可通过li查询。 @@ -204,7 +204,7 @@ writeDouble()将double数字保存到数据流中,并用补充的readDouble() 正如早先指出的那样,RandomAccessFile与IO层次结构的剩余部分几乎是完全隔离的,尽管它也实现了DataInput和DataOutput接口。所以不可将其与InputStream及OutputStream子类的任何部分关联起来。尽管也许能将一个ByteArrayInputStream当作一个随机访问元素对待,但只能用RandomAccessFile打开一个文件。必须假定RandomAccessFile已得到了正确的缓冲,因为我们不能自行选择。 -可以自行选择的是第二个构建器参数:可决定以“只读”(r)方式或“读写”(rw)方式打开一个RandomAccessFile文件。 +可以自行选择的是第二个构造器参数:可决定以“只读”(r)方式或“读写”(rw)方式打开一个RandomAccessFile文件。 使用RandomAccessFile的时候,类似于组合使用DataInputStream和DataOutputStream(因为它实现了等同的接口)。除此以外,还可看到程序中使用了seek(),以便在文件中到处移动,对某个值作出修改。 @@ -216,7 +216,7 @@ writeDouble()将double数字保存到数据流中,并用补充的readDouble() 若想创建一个对象,用它从一个缓冲的DataInputStream中读取一个文件,可将这个过程封装到一个名为InFile的类内。如下所示: -``` +``` java //: InFile.java // Shorthand class for opening an input file package com.bruceeckel.tools; @@ -236,7 +236,7 @@ public class InFile extends DataInputStream { } ///:~ ``` -无论构建器的String版本还是File版本都包括在内,用于共同创建一个FileInputStream。 +无论构造器的String版本还是File版本都包括在内,用于共同创建一个FileInputStream。 就象这个例子展示的那样,现在可以有效减少创建文件时由于重复强调造成的问题。 @@ -244,7 +244,7 @@ public class InFile extends DataInputStream { 亦可用同类型的方法创建一个PrintStream,令其写入一个缓冲文件。下面是对com.bruceeckel.tools的扩展: -``` +``` java //: PrintFile.java // Shorthand class for opening an output file // for human-readable output. @@ -265,13 +265,13 @@ public class PrintFile extends PrintStream { } ///:~ ``` -注意构建器不可能捕获一个由基础类构建器“掷”出的违例。 +注意构造器不可能捕获一个由基础类构造器“掷”出的异常。 9. 快速输出数据文件 最后,利用类似的快捷方式可创建一个缓冲输出文件,用它保存数据(与由人观看的数据格式相反): -``` +``` java //: OutFile.java // Shorthand class for opening an output file // for data storage. @@ -302,7 +302,7 @@ System.err同样是一个PrintStream,但System.in是一个原始的InputStream 典型情况下,我们希望用readLine()每次读取一行输入信息,所以需要将System.in封装到一个DataInputStream中。这是Java 1.0进行行输入时采取的“老”办法。在本章稍后,大家还会看到Java 1.1的解决方案。下面是个简单的例子,作用是回应我们键入的每一行内容: -``` +``` java //: Echo.java // How to read from standard input import java.io.*; diff --git a/10.6 StreamTokenizer.md b/10.6 StreamTokenizer.md index 77b6038..409ddc2 100644 --- a/10.6 StreamTokenizer.md +++ b/10.6 StreamTokenizer.md @@ -5,7 +5,7 @@ StreamTokenizer类用于将任何InputStream分割为一系列“记号”(Token)。这些记号实际是一些断续的文本块,中间用我们选择的任何东西分隔。例如,我们的记号可以是单词,中间用空白(空格)以及标点符号分隔。 下面是一个简单的程序,用于计算各个单词在文本文件中重复出现的次数: -``` +``` java //: SortedWordCount.java // Counts words in a file, outputs // results in sorted form. @@ -112,7 +112,7 @@ public class SortedWordCount { 在countWords()中,每次从数据流中取出一个记号,而ttype信息的作用是判断对每个记号采取什么操作——因为记号可能代表一个行尾、一个数字、一个字串或者一个字符。 -找到一个记号后,会查询Hashtable counts,核实其中是否已经以“键”(Key)的形式包含了一个记号。若答案是肯定的,对应的Counter(计数器)对象就会增值,指出已找到该单词的另一个实例。若答案为否,则新建一个Counter——因为Counter构建器会将它的值初始化为1,正是我们计算单词数量时的要求。 +找到一个记号后,会查询Hashtable counts,核实其中是否已经以“键”(Key)的形式包含了一个记号。若答案是肯定的,对应的Counter(计数器)对象就会增值,指出已找到该单词的另一个实例。若答案为否,则新建一个Counter——因为Counter构造器会将它的值初始化为1,正是我们计算单词数量时的要求。 SortedWordCount并不属于Hashtable(散列表)的一种类型,所以它不会继承。它执行的一种特定类型的操作,所以尽管keys()和values()方法都必须重新揭示出来,但仍不表示应使用那个继承,因为大量Hashtable方法在这里都是不适当的。除此以外,对于另一些方法来说(比如getCounter()——用于获得一个特定字串的计数器;又如sortedKeys()——用于产生一个枚举),它们最终都改变了SortedWordCount接口的形式。 @@ -123,13 +123,13 @@ SortedWordCount并不属于Hashtable(散列表)的一种类型,所以它 尽管并不必要IO库的一部分,但StringTokenizer提供了与StreamTokenizer极相似的功能,所以在这里一并讲述。 -StringTokenizer的作用是每次返回字串内的一个记号。这些记号是一些由制表站、空格以及新行分隔的连续字符。因此,字串“Where is my cat?”的记号分别是“Where”、“is”、“my”和“cat?”。与StreamTokenizer类似,我们可以指示StringTokenizer按照我们的愿望分割输入。但对于StringTokenizer,却需要向构建器传递另一个参数,即我们想使用的分隔字串。通常,如果想进行更复杂的操作,应使用StreamTokenizer。 +StringTokenizer的作用是每次返回字串内的一个记号。这些记号是一些由制表站、空格以及新行分隔的连续字符。因此,字串“Where is my cat?”的记号分别是“Where”、“is”、“my”和“cat?”。与StreamTokenizer类似,我们可以指示StringTokenizer按照我们的愿望分割输入。但对于StringTokenizer,却需要向构造器传递另一个参数,即我们想使用的分隔字串。通常,如果想进行更复杂的操作,应使用StreamTokenizer。 可用nextToken()向StringTokenizer对象请求字串内的下一个记号。该方法要么返回一个记号,要么返回一个空字串(表示没有记号剩下)。 作为一个例子,下述程序将执行一个有限的句法分析,查询键短语序列,了解句子暗示的是快乐亦或悲伤的含义。 -``` +``` java //: AnalyzeSentence.java // Look for particular sequences // within sentences. @@ -208,7 +208,7 @@ public class AnalyzeSentence { } ///:~ ``` -对于准备分析的每个字串,我们进入一个while循环,并将记号从那个字串中取出。请注意第一个if语句,假如记号既不是“I”,也不是“Are”,就会执行continue(返回循环起点,再一次开始)。这意味着除非发现一个“I”或者“Are”,才会真正得到记号。大家可能想用==代替equals()方法,但那样做会出现不正常的表现,因为==比较的是句柄值,而equals()比较的是内容。 +对于准备分析的每个字串,我们进入一个while循环,并将记号从那个字串中取出。请注意第一个if语句,假如记号既不是“I”,也不是“Are”,就会执行continue(返回循环起点,再一次开始)。这意味着除非发现一个“I”或者“Are”,才会真正得到记号。大家可能想用==代替equals()方法,但那样做会出现不正常的表现,因为==比较的是指针值,而equals()比较的是内容。 analyze()方法剩余部分的逻辑是搜索“I am sad”(我很忧伤、“I am nothappy”(我不快乐)或者“Are you sad?”(你悲伤吗?)这样的句法格式。若没有break语句,这方面的代码甚至可能更加散乱。大家应注意对一个典型的解析器来说,通常都有这些记号的一个表格,并能在读取新记号的时候用一小段代码在表格内移动。 diff --git "a/10.7 Java 1.1\347\232\204IO\346\265\201.md" "b/10.7 Java 1.1\347\232\204IO\346\265\201.md" index 4dbd6a3..91380fb 100644 --- "a/10.7 Java 1.1\347\232\204IO\346\265\201.md" +++ "b/10.7 Java 1.1\347\232\204IO\346\265\201.md" @@ -19,7 +19,7 @@ Java 1.0的几乎所有IO流类都有对应的Java 1.1类,用于提供内建 下面这张表格分旧库与新库分别总结了信息发起与接收之间的对应关系。 -``` +``` java Sources & Sinks: Java 1.0 class @@ -77,7 +77,7 @@ PipedWriter 在下面这张表格中,对应关系比上一张表要粗糙一些。之所以会出现这个差别,是由类的组织造成的:尽管BufferedOutputStream是FilterOutputStream的一个子类,但是BufferedWriter并不是FilterWriter的子类(对后者来说,尽管它是一个抽象类,但没有自己的子类或者近似子类的东西,也没有一个“占位符”可用,所以不必费心地寻找)。然而,两个类的接口是非常相似的,而且不管在什么情况下,显然应该尽可能地使用新版本,而不应考虑旧版本(也就是说,除非在一些类中必须生成一个Stream,不可生成Reader或者Writer)。 -``` +``` java Filters: Java 1.0 class @@ -126,7 +126,7 @@ PushBackReader 过滤器:Java 1.0类 对应的Java 1.1类 -``` +``` java FilterInputStream FilterReader FilterOutputStream FilterWriter(没有子类的抽象类) BufferedInputStream BufferedReader(也有readLine()) @@ -134,13 +134,13 @@ BufferedOutputStream BufferedWriter DataInputStream 使用DataInputStream(除非要使用readLine(),那时需要使用一个BufferedReader) PrintStream PrintWriter LineNumberInputStream LineNumberReader -StreamTokenizer StreamTokenizer(用构建器取代Reader) +StreamTokenizer StreamTokenizer(用构造器取代Reader) PushBackInputStream PushBackReader ``` 有一条规律是显然的:若想使用readLine(),就不要再用一个DataInputStream来实现(否则会在编译期得到一条出错消息),而应使用一个BufferedReader。但除这种情况以外,DataInputStream仍是Java 1.1 IO库的“首选”成员。 -为了将向PrintWriter的过渡变得更加自然,它提供了能采用任何OutputStream对象的构建器。PrintWriter提供的格式化支持没有PrintStream那么多;但接口几乎是相同的。 +为了将向PrintWriter的过渡变得更加自然,它提供了能采用任何OutputStream对象的构造器。PrintWriter提供的格式化支持没有PrintStream那么多;但接口几乎是相同的。 10.7.3 未改变的类 @@ -148,7 +148,7 @@ PushBackInputStream PushBackReader 没有对应Java 1.1类的Java 1.0类 -``` +``` java DataOutputStream File RandomAccessFile @@ -161,7 +161,7 @@ SequenceInputStream 为体验新类的效果,下面让我们看看如何修改IOStreamDemo.java示例的相应区域,以便使用Reader和Writer类: -``` +``` java //: NewIODemo.java // Java 1.1 IO typical usage import java.io.*; @@ -265,17 +265,17 @@ public class NewIODemo { 第1节收缩了一点儿,因为假如要做的全部事情就是读取行输入,那么只需要将一个FileReader封装到BufferedReader之内即可。第1b节展示了封装System.in,以便读取控制台输入的新方法。这里的代码量增多了一些,因为System.in是一个DataInputStream,而且BufferedReader需要一个Reader参数,所以要用InputStreamReader来进行转换。 在2节,可以看到如果有一个字串,而且想从中读取数据,只需用一个StringReader替换StringBufferInputStream,剩下的代码是完全相同的。 -``` +``` java 第3节揭示了新IO流库设计中的一个错误。如果有一个字串,而且想从中读取数据,那么不能再以任何形式使用StringBufferInputStream。若编译一个涉及StringBufferInputStream的代码,会得到一条“反对”消息,告诉我们不要用它。此时最好换用一个StringReader。但是,假如要象第3节这样进行格式化的内存输入,就必须使用DataInputStream——没有什么“DataReader”可以代替它——而DataInputStream很不幸地要求用到一个InputStream参数。所以我们没有选择的余地,只好使用编译器不赞成的StringBufferInputStream类。编译器同样会发出反对信息,但我们对此束手无策(注释②)。 StringReader替换StringBufferInputStream,剩下的代码是完全相同的。 ②:到你现在正式使用的时候,这个错误可能已经修正。 -第4节明显是从老式数据流到新数据流的一个直接转换,没有需要特别指出的。在第5节中,我们被强迫使用所有的老式数据流,因为DataOutputStream和DataInputStream要求用到它们,而且没有可供替换的东西。然而,编译期间不会产生任何“反对”信息。若不赞成一种数据流,通常是由于它的构建器产生了一条反对消息,禁止我们使用整个类。但在DataInputStream的情况下,只有readLine()是不赞成使用的,因为我们最好为readLine()使用一个BufferedReader(但为其他所有格式化输入都使用一个DataInputStream)。 +第4节明显是从老式数据流到新数据流的一个直接转换,没有需要特别指出的。在第5节中,我们被强迫使用所有的老式数据流,因为DataOutputStream和DataInputStream要求用到它们,而且没有可供替换的东西。然而,编译期间不会产生任何“反对”信息。若不赞成一种数据流,通常是由于它的构造器产生了一条反对消息,禁止我们使用整个类。但在DataInputStream的情况下,只有readLine()是不赞成使用的,因为我们最好为readLine()使用一个BufferedReader(但为其他所有格式化输入都使用一个DataInputStream)。 若比较第5节和IOStreamDemo.java中的那一小节,会注意到在这个版本中,数据是在文本之前写入的。那是由于Java 1.1本身存在一个错误,如下述代码所示: -``` +``` java //: IOBug.java // Java 1.1 (and higher?) IO Bug import java.io.*; @@ -313,13 +313,13 @@ public class IOBug { } ///:~ ``` -看起来,我们在对一个writeBytes()的调用之后写入的任何东西都不是能够恢复的。这是一个十分有限的错误,希望在你读到本书的时候已获得改正。为检测是否改正,请运行上述程序。若没有得到一个违例,而且值都能正确打印出来,就表明已经改正。 +看起来,我们在对一个writeBytes()的调用之后写入的任何东西都不是能够恢复的。这是一个十分有限的错误,希望在你读到本书的时候已获得改正。为检测是否改正,请运行上述程序。若没有得到一个异常,而且值都能正确打印出来,就表明已经改正。 10.7.5 重导向标准IO Java 1.1在System类中添加了特殊的方法,允许我们重新定向标准输入、输出以及错误IO流。此时要用到下述简单的静态方法调用: -``` +``` java setIn(InputStream) setOut(PrintStream) setErr(PrintStream) @@ -327,7 +327,7 @@ setErr(PrintStream) 如果突然要在屏幕上生成大量输出,而且滚动的速度快于人们的阅读速度,输出的重定向就显得特别有用。在一个命令行程序中,如果想重复测试一个特定的用户输入序列,输入的重定向也显得特别有价值。下面这个简单的例子展示了这些方法的使用: -``` +``` java //: Redirecting.java // Demonstrates the use of redirection for // standard IO in Java 1.1 @@ -370,6 +370,6 @@ class Redirecting { > Note:The constructor java.io.PrintStream(java.io.OutputStream) has been deprecated. -注意:不推荐使用构建器java.io.PrintStream(java.io.OutputStream)。 +注意:不推荐使用构造器java.io.PrintStream(java.io.OutputStream)。 -然而,无论System.setOut()还是System.setErr()都要求用一个PrintStream作为参数使用,所以必须调用PrintStream构建器。所以大家可能会觉得奇怪,既然Java 1.1通过反对构建器而反对了整个PrintStream,为什么库的设计人员在添加这个反对的同时,依然为System添加了新方法,且指明要求用PrintStream,而不是用PrintWriter呢?毕竟,后者是一个崭新和首选的替换措施呀?这真令人费解。 +然而,无论System.setOut()还是System.setErr()都要求用一个PrintStream作为参数使用,所以必须调用PrintStream构造器。所以大家可能会觉得奇怪,既然Java 1.1通过反对构造器而反对了整个PrintStream,为什么库的设计人员在添加这个反对的同时,依然为System添加了新方法,且指明要求用PrintStream,而不是用PrintWriter呢?毕竟,后者是一个崭新和首选的替换措施呀?这真令人费解。 diff --git "a/10.8 \345\216\213\347\274\251.md" "b/10.8 \345\216\213\347\274\251.md" index 8750dd4..a9ffd7c 100644 --- "a/10.8 \345\216\213\347\274\251.md" +++ "b/10.8 \345\216\213\347\274\251.md" @@ -6,7 +6,7 @@ Java 1.1也添加一个类,用以支持对压缩格式的数据流的读写。 Java 1.1压缩类 功能 -``` +``` java CheckedInputStream GetCheckSum()为任何InputStream产生校验和(不仅是解压) CheckedOutputStream GetCheckSum()为任何OutputStream产生校验和(不仅是解压) DeflaterOutputStream 用于压缩类的基础类 @@ -23,7 +23,7 @@ GZIPInputStream 一个DeflaterInputStream,解压用GZIP文件格式保存的 GZIP接口非常简单,所以如果只有单个数据流需要压缩(而不是一系列不同的数据),那么它就可能是最适当选择。下面是对单个文件进行压缩的例子: -``` +``` java //: GZIPcompress.java // Uses Java 1.1 GZIP compression to compress // a file whose name is passed on the command @@ -63,13 +63,13 @@ public class GZIPcompress { } ///:~ ``` -压缩类的用法非常直观——只需将输出流封装到一个GZIPOutputStream或者ZipOutputStream内,并将输入流封装到GZIPInputStream或者ZipInputStream内即可。剩余的全部操作就是标准的IO读写。然而,这是一个很典型的例子,我们不得不混合使用新旧IO流:数据的输入使用Reader类,而GZIPOutputStream的构建器只能接收一个OutputStream对象,不能接收Writer对象。 +压缩类的用法非常直观——只需将输出流封装到一个GZIPOutputStream或者ZipOutputStream内,并将输入流封装到GZIPInputStream或者ZipInputStream内即可。剩余的全部操作就是标准的IO读写。然而,这是一个很典型的例子,我们不得不混合使用新旧IO流:数据的输入使用Reader类,而GZIPOutputStream的构造器只能接收一个OutputStream对象,不能接收Writer对象。 10.8.2 用Zip进行多文件保存 提供了Zip支持的Java 1.1库显得更加全面。利用它可以方便地保存多个文件。甚至有一个独立的类来简化对Zip文件的读操作。这个库采采用的是标准Zip格式,所以能与当前因特网上使用的大量压缩、解压工具很好地协作。下面这个例子采取了与前例相同的形式,但能根据我们需要控制任意数量的命令行参数。除此之外,它展示了如何用Checksum类来计算和校验文件的“校验和”(Checksum)。可选用两种类型的Checksum:Adler32(速度要快一些)和CRC32(慢一些,但更准确)。 -``` +``` java //: ZipCompress.java // Uses Java 1.1 Zip compression to compress // any number of files whose names are passed @@ -148,7 +148,7 @@ public class ZipCompress { 为解压文件,ZipInputStream提供了一个getNextEntry()方法,能在有的前提下返回下一个ZipEntry。作为一个更简洁的方法,可以用ZipFile对象读取文件。该对象有一个entries()方法,可以为ZipEntry返回一个Enumeration(枚举)。 -为读取校验和,必须多少拥有对关联的Checksum对象的访问权限。在这里保留了指向CheckedOutputStream和CheckedInputStream对象的一个句柄。但是,也可以只占有指向Checksum对象的一个句柄。 +为读取校验和,必须多少拥有对关联的Checksum对象的访问权限。在这里保留了指向CheckedOutputStream和CheckedInputStream对象的一个指针。但是,也可以只占有指向Checksum对象的一个指针。 Zip流中一个令人困惑的方法是setComment()。正如前面展示的那样,我们可在写一个文件时设置注释内容,但却没有办法取出ZipInputStream内的注释。看起来,似乎只能通过ZipEntry逐个入口地提供对注释的完全支持。 @@ -163,13 +163,13 @@ Zip格式亦在Java 1.1的JAR(Java ARchive)文件格式中得到了采用。 一个JAR文件由一系列采用Zip压缩格式的文件构成,同时还有一张“详情单”,对所有这些文件进行了描述(可创建自己的详情单文件;否则,jar程序会为我们代劳)。在联机用户文档中,可以找到与JAR详情单更多的资料(详情单的英语是“Manifest”)。 jar实用程序已与Sun的JDK配套提供,可以按我们的选择自动压缩文件。请在命令行调用它: -``` +``` java jar [选项] 说明 [详情单] 输入文件 ``` 其中,“选项”用一系列字母表示(不必输入连字号或其他任何指示符)。如下所示: -``` +``` java c 创建新的或空的压缩档 t 列出目录表 x 解压所有文件 @@ -185,31 +185,31 @@ M 不自动生成详情表文件 下面是调用jar的一些典型方法: -``` +``` java jar cf myJarFile.jar *.class ``` 用于创建一个名为myJarFile.jar的JAR文件,其中包含了当前目录中的所有类文件,同时还有自动产生的详情表文件。 -``` +``` java jar cmf myJarFile.jar myManifestFile.mf *.class ``` 与前例类似,但添加了一个名为myManifestFile.mf的用户自建详情表文件。 -``` +``` java jar tf myJarFile.jar ``` 生成myJarFile.jar内所有文件的一个目录表。 -``` +``` java jar tvf myJarFile.jar ``` 添加“verbose”(详尽)标志,提供与myJarFile.jar中的文件有关的、更详细的资料。 -``` +``` java jar cvf myApp.jar audio classes image ``` @@ -217,7 +217,7 @@ jar cvf myApp.jar audio classes image 如果用O选项创建了一个JAR文件,那个文件就可置入自己的类路径(CLASSPATH)中: -``` +``` java CLASSPATH="lib1.jar;lib2.jar;" ``` diff --git "a/10.9 \345\257\271\350\261\241\345\272\217\345\210\227\345\214\226.md" "b/10.9 \345\257\271\350\261\241\345\272\217\345\210\227\345\214\226.md" index 15e2580..0c04cd3 100644 --- "a/10.9 \345\257\271\350\261\241\345\272\217\345\210\227\345\214\226.md" +++ "b/10.9 \345\257\271\350\261\241\345\272\217\345\210\227\345\214\226.md" @@ -10,11 +10,11 @@ Java 1.1增添了一种有趣的特性,名为“对象序列化”(Object Se 对象的序列化处理非常简单,只需对象实现了Serializable接口即可(该接口仅是一个标记,没有方法)。在Java 1.1中,许多标准库类都发生了改变,以便能够序列化——其中包括用于基本数据类型的全部封装器、所有集合类以及其他许多东西。甚至Class对象也可以序列化(第11章讲述了具体实现过程)。 -为序列化一个对象,首先要创建某些OutputStream对象,然后将其封装到ObjectOutputStream对象内。此时,只需调用writeObject()即可完成对象的序列化,并将其发送给OutputStream。相反的过程是将一个InputStream封装到ObjectInputStream内,然后调用readObject()。和往常一样,我们最后获得的是指向一个上溯造型Object的句柄,所以必须下溯造型,以便能够直接设置。 +为序列化一个对象,首先要创建某些OutputStream对象,然后将其封装到ObjectOutputStream对象内。此时,只需调用writeObject()即可完成对象的序列化,并将其发送给OutputStream。相反的过程是将一个InputStream封装到ObjectInputStream内,然后调用readObject()。和往常一样,我们最后获得的是指向一个上溯造型Object的指针,所以必须下溯造型,以便能够直接设置。 -对象序列化特别“聪明”的一个地方是它不仅保存了对象的“全景图”,而且能追踪对象内包含的所有句柄并保存那些对象;接着又能对每个对象内包含的句柄进行追踪;以此类推。我们有时将这种情况称为“对象网”,单个对象可与之建立连接。而且它还包含了对象的句柄数组以及成员对象。若必须自行操纵一套对象序列化机制,那么在代码里追踪所有这些链接时可能会显得非常麻烦。在另一方面,由于Java对象的序列化似乎找不出什么缺点,所以请尽量不要自己动手,让它用优化的算法自动维护整个对象网。下面这个例子对序列化机制进行了测试。它建立了许多链接对象的一个“Worm”(蠕虫),每个对象都与Worm中的下一段链接,同时又与属于不同类(Data)的对象句柄数组链接: +对象序列化特别“聪明”的一个地方是它不仅保存了对象的“全景图”,而且能追踪对象内包含的所有指针并保存那些对象;接着又能对每个对象内包含的指针进行追踪;以此类推。我们有时将这种情况称为“对象网”,单个对象可与之建立连接。而且它还包含了对象的指针数组以及成员对象。若必须自行操纵一套对象序列化机制,那么在代码里追踪所有这些链接时可能会显得非常麻烦。在另一方面,由于Java对象的序列化似乎找不出什么缺点,所以请尽量不要自己动手,让它用优化的算法自动维护整个对象网。下面这个例子对序列化机制进行了测试。它建立了许多链接对象的一个“Worm”(蠕虫),每个对象都与Worm中的下一段链接,同时又与属于不同类(Data)的对象指针数组链接: -``` +``` java //: Worm.java // Demonstrates object serialization in Java 1.1 import java.io.*; @@ -97,12 +97,12 @@ public class Worm implements Serializable { } ///:~ ``` -更有趣的是,Worm内的Data对象数组是用随机数字初始化的(这样便不用怀疑编译器保留了某种原始信息)。每个Worm段都用一个Char标记。这个Char是在重复生成链接的Worm列表时自动产生的。创建一个Worm时,需告诉构建器希望它有多长。为产生下一个句柄(next),它总是用减去1的长度来调用Worm构建器。最后一个next句柄则保持为null(空),表示已抵达Worm的尾部。 +更有趣的是,Worm内的Data对象数组是用随机数字初始化的(这样便不用怀疑编译器保留了某种原始信息)。每个Worm段都用一个Char标记。这个Char是在重复生成链接的Worm列表时自动产生的。创建一个Worm时,需告诉构造器希望它有多长。为产生下一个指针(next),它总是用减去1的长度来调用Worm构造器。最后一个next指针则保持为null(空),表示已抵达Worm的尾部。 上面的所有操作都是为了加深事情的复杂程度,加大对象序列化的难度。然而,真正的序列化过程却是非常简单的。一旦从另外某个流里创建了ObjectOutputStream,writeObject()就会序列化对象。注意也可以为一个String调用writeObject()。亦可使用与DataOutputStream相同的方法写入所有基本数据类型(它们有相同的接口)。 有两个单独的try块看起来是类似的。第一个读写的是文件,而另一个读写的是一个ByteArray(字节数组)。可利用对任何DataInputStream或者DataOutputStream的序列化来读写特定的对象;正如在关于连网的那一章会讲到的那样,这些对象甚至包括网络。一次循环后的输出结果如下: -``` +``` java Worm constructor: 6 Worm constructor: 5 Worm constructor: 4 @@ -116,7 +116,7 @@ Worm storage, w3 = :a(262):b(100):c(396):d(480):e(316):f(398) 可以看出,装配回原状的对象确实包含了原来那个对象里包含的所有链接。 -注意在对一个Serializable(可序列化)对象进行重新装配的过程中,不会调用任何构建器(甚至默认构建器)。整个对象都是通过从InputStream中取得数据恢复的。 +注意在对一个Serializable(可序列化)对象进行重新装配的过程中,不会调用任何构造器(甚至默认构造器)。整个对象都是通过从InputStream中取得数据恢复的。 作为Java 1.1特性的一种,我们注意到对象的序列化并不属于新的Reader和Writer层次结构的一部分,而是沿用老式的InputStream和OutputStream结构。所以在一些特殊的场合下,不得不混合使用两种类型的层次结构。 @@ -125,7 +125,7 @@ Worm storage, w3 = :a(262):b(100):c(396):d(480):e(316):f(398) 读者或许会奇怪为什么需要一个对象从它的序列化状态中恢复。举个例子来说,假定我们序列化一个对象,并通过网络将其作为文件传送给另一台机器。此时,位于另一台机器的程序可以只用文件目录来重新构造这个对象吗? 回答这个问题的最好方法就是做一个实验。下面这个文件位于本章的子目录下: -``` +``` java //: Alien.java // A serializable class import java.io.*; @@ -137,7 +137,7 @@ public class Alien implements Serializable { 用于创建和序列化一个Alien对象的文件位于相同的目录下: -``` +``` java //: FreezeAlien.java // Create a serialized output file import java.io.*; @@ -154,10 +154,10 @@ public class FreezeAlien { } ///:~ ``` -该程序并不是捕获和控制违例,而是将违例简单、直接地传递到main()外部,这样便能在命令行报告它们。 +该程序并不是捕获和控制异常,而是将异常简单、直接地传递到main()外部,这样便能在命令行报告它们。 程序编译并运行后,将结果产生的file.x复制到名为xfiles的子目录,代码如下: -``` +``` java //: ThawAlien.java // Try to recover a serialized file without the // class of object that's stored in that file. @@ -176,8 +176,8 @@ public class ThawAlien { } } ///:~ -``` -该程序能打开文件,并成功读取mystery对象中的内容。然而,一旦尝试查找与对象有关的任何资料——这要求Alien的Class对象——Java虚拟机(JVM)便找不到Alien.class(除非它正好在类路径内,而本例理应相反)。这样就会得到一个名叫ClassNotFoundException的违例(同样地,若非能够校验Alien存在的证据,否则它等于消失)。 +``` java +该程序能打开文件,并成功读取mystery对象中的内容。然而,一旦尝试查找与对象有关的任何资料——这要求Alien的Class对象——Java虚拟机(JVM)便找不到Alien.class(除非它正好在类路径内,而本例理应相反)。这样就会得到一个名叫ClassNotFoundException的异常(同样地,若非能够校验Alien存在的证据,否则它等于消失)。 恢复了一个序列化的对象后,如果想对其做更多的事情,必须保证JVM能在本地类路径或者因特网的其他什么地方找到相关的.class文件。 @@ -189,7 +189,7 @@ public class ThawAlien { 下面这个例子展示了Externalizable接口方法的简单应用。注意Blip1和Blip2几乎完全一致,除了极微小的差别(自己研究一下代码,看看是否能发现): -``` +``` java //: Blips.java // Simple use of Externalizable & a pitfall import java.io.*; @@ -254,7 +254,7 @@ public class Blips { 该程序输出如下: -``` +``` java Constructing objects: Blip1 Constructor Blip2 Constructor @@ -266,13 +266,13 @@ Blip1 Constructor Blip1.readExternal ``` -未恢复Blip2对象的原因是那样做会导致一个违例。你找出了Blip1和Blip2之间的区别吗?Blip1的构建器是“公共的”(public),Blip2的构建器则不然,这样便会在恢复时造成违例。试试将Blip2的构建器属性变成“public”,然后删除//!注释标记,看看是否能得到正确的结果。 +未恢复Blip2对象的原因是那样做会导致一个异常。你找出了Blip1和Blip2之间的区别吗?Blip1的构造器是“公共的”(public),Blip2的构造器则不然,这样便会在恢复时造成异常。试试将Blip2的构造器属性变成“public”,然后删除//!注释标记,看看是否能得到正确的结果。 -恢复b1后,会调用Blip1默认构建器。这与恢复一个Serializable(可序列化)对象不同。在后者的情况下,对象完全以它保存下来的二进制位为基础恢复,不存在构建器调用。而对一个Externalizable对象,所有普通的默认构建行为都会发生(包括在字段定义时的初始化),而且会调用readExternal()。必须注意这一事实——特别注意所有默认的构建行为都会进行——否则很难在自己的Externalizable对象中产生正确的行为。 +恢复b1后,会调用Blip1默认构造器。这与恢复一个Serializable(可序列化)对象不同。在后者的情况下,对象完全以它保存下来的二进制位为基础恢复,不存在构造器调用。而对一个Externalizable对象,所有普通的默认构建行为都会发生(包括在字段定义时的初始化),而且会调用readExternal()。必须注意这一事实——特别注意所有默认的构建行为都会进行——否则很难在自己的Externalizable对象中产生正确的行为。 下面这个例子揭示了保存和恢复一个Externalizable对象必须做的全部事情: -``` +``` java //: Blip3.java // Reconstructing an externalizable object import java.io.*; @@ -331,7 +331,7 @@ class Blip3 implements Externalizable { } ///:~ ``` -其中,字段s和i只在第二个构建器中初始化,不关默认构建器的事。这意味着假如不在readExternal中初始化s和i,它们就会成为null(因为在对象创建的第一步中已将对象的存储空间清除为1)。若注释掉跟随于“You must do this”后面的两行代码,并运行程序,就会发现当对象恢复以后,s是null,而i是零。 +其中,字段s和i只在第二个构造器中初始化,不关默认构造器的事。这意味着假如不在readExternal中初始化s和i,它们就会成为null(因为在对象创建的第一步中已将对象的存储空间清除为1)。若注释掉跟随于“You must do this”后面的两行代码,并运行程序,就会发现当对象恢复以后,s是null,而i是零。 若从一个Externalizable对象继承,通常需要调用writeExternal()和readExternal()的基础类版本,以便正确地保存和恢复基础类组件。 @@ -347,7 +347,7 @@ class Blip3 implements Externalizable { 例如,假设一个Login对象包含了与一个特定的登录会话有关的信息。校验登录的合法性时,一般都想将数据保存下来,但不包括密码。为做到这一点,最简单的办法是实现Serializable,并将password字段设为transient。下面是具体的代码: -``` +``` java //: Logon.java // Demonstrates the "transient" keyword import java.io.*; @@ -402,7 +402,7 @@ class Logon implements Serializable { 可以看到,其中的date和username字段保持原始状态(未设成transient),所以会自动序列化。然而,password被设为transient,所以不会自动保存到磁盘;另外,自动序列化机制也不会作恢复它的尝试。输出如下: -``` +``` java logon a = logon info: username: Hulk date: Sun Mar 23 18:25:53 PST 1997 @@ -414,7 +414,7 @@ logon a = logon info: password: (n/a) ``` -一旦对象恢复成原来的样子,password字段就会变成null。注意必须用toString()检查password是否为null,因为若用过载的“+”运算符来装配一个String对象,而且那个运算符遇到一个null句柄,就会造成一个名为NullPointerException的违例(新版Java可能会提供避免这个问题的代码)。 +一旦对象恢复成原来的样子,password字段就会变成null。注意必须用toString()检查password是否为null,因为若用重载的“+”运算符来装配一个String对象,而且那个运算符遇到一个null指针,就会造成一个名为NullPointerException的异常(新版Java可能会提供避免这个问题的代码)。 我们也发现date字段被保存到磁盘,并从磁盘恢复,没有重新生成。 @@ -425,7 +425,7 @@ logon a = logon info: 若不是特别在意要实现Externalizable接口,还有另一种方法可供选用。我们可以实现Serializable接口,并添加(注意是“添加”,而非“覆盖”或者“实现”)名为writeObject()和readObject()的方法。一旦对象被序列化或者重新装配,就会分别调用那两个方法。也就是说,只要提供了这两个方法,就会优先使用它们,而不考虑默认的序列化机制。 这些方法必须含有下列准确的签名: -``` +``` java private void writeObject(ObjectOutputStream stream) throws IOException; @@ -443,7 +443,7 @@ private void 还存在另一个问题。在我们的writeObject()内部,可以调用defaultWriteObject(),从而决定采取默认的writeObject()行动。类似地,在readObject()内部,可以调用defaultReadObject()。下面这个简单的例子演示了如何对一个Serializable对象的存储与恢复进行控制: -``` +``` java //: SerialCtl.java // Controlling serialization by adding your own // writeObject() and readObject() methods. @@ -496,13 +496,13 @@ public class SerialCtl implements Serializable { ``` -在这个例子中,一个String保持原始状态,其他设为transient(临时),以便证明非临时字段会被defaultWriteObject()方法自动保存,而transient字段必须在程序中明确保存和恢复。字段是在构建器内部初始化的,而不是在定义的时候,这证明了它们不会在重新装配的时候被某些自动化机制初始化。 +在这个例子中,一个String保持原始状态,其他设为transient(临时),以便证明非临时字段会被defaultWriteObject()方法自动保存,而transient字段必须在程序中明确保存和恢复。字段是在构造器内部初始化的,而不是在定义的时候,这证明了它们不会在重新装配的时候被某些自动化机制初始化。 -若准备通过默认机制写入对象的非transient部分,那么必须调用defaultWriteObject(),令其作为writeObject()中的第一个操作;并调用defaultReadObject(),令其作为readObject()的第一个操作。这些都是不常见的调用方法。举个例子来说,当我们为一个ObjectOutputStream调用defaultWriteObject()的时候,而且没有为其传递参数,就需要采取这种操作,使其知道对象的句柄以及如何写入所有非transient的部分。这种做法非常不便。 +若准备通过默认机制写入对象的非transient部分,那么必须调用defaultWriteObject(),令其作为writeObject()中的第一个操作;并调用defaultReadObject(),令其作为readObject()的第一个操作。这些都是不常见的调用方法。举个例子来说,当我们为一个ObjectOutputStream调用defaultWriteObject()的时候,而且没有为其传递参数,就需要采取这种操作,使其知道对象的指针以及如何写入所有非transient的部分。这种做法非常不便。 transient对象的存储与恢复采用了我们更熟悉的代码。现在考虑一下会发生一些什么事情。在main()中会创建一个SerialCtl对象,随后会序列化到一个ObjectOutputStream里(注意这种情况下使用的是一个缓冲区,而非文件——与ObjectOutputStream完全一致)。正式的序列化操作是在下面这行代码里发生的: -``` +``` java o.writeObject(sc); ``` @@ -514,11 +514,11 @@ o.writeObject(sc); 10.9.3 利用“持久性” -一个比较诱人的想法是用序列化技术保存程序的一些状态信息,从而将程序方便地恢复到以前的状态。但在具体实现以前,有些问题是必须解决的。如果两个对象都有指向第三个对象的句柄,该如何对这两个对象序列化呢?如果从两个对象序列化后的状态恢复它们,第三个对象的句柄只会出现在一个对象身上吗?如果将这两个对象序列化成独立的文件,然后在代码的不同部分重新装配它们,又会得到什么结果呢? +一个比较诱人的想法是用序列化技术保存程序的一些状态信息,从而将程序方便地恢复到以前的状态。但在具体实现以前,有些问题是必须解决的。如果两个对象都有指向第三个对象的指针,该如何对这两个对象序列化呢?如果从两个对象序列化后的状态恢复它们,第三个对象的指针只会出现在一个对象身上吗?如果将这两个对象序列化成独立的文件,然后在代码的不同部分重新装配它们,又会得到什么结果呢? 下面这个例子对上述问题进行了很好的说明: -``` +``` java //: MyWorld.java import java.io.*; import java.util.*; @@ -585,11 +585,11 @@ public class MyWorld { } ///:~ ``` -这里一件有趣的事情是也许是能针对一个字节数组应用对象的序列化,从而实现对任何Serializable(可序列化)对象的一个“全面复制”(全面复制意味着复制的是整个对象网,而不仅是基本对象和它的句柄)。复制问题将在第12章进行全面讲述。 +这里一件有趣的事情是也许是能针对一个字节数组应用对象的序列化,从而实现对任何Serializable(可序列化)对象的一个“全面复制”(全面复制意味着复制的是整个对象网,而不仅是基本对象和它的指针)。复制问题将在第12章进行全面讲述。 Animal对象包含了类型为House的字段。在main()中,会创建这些Animal的一个Vector,并对其序列化两次,分别送入两个不同的数据流内。这些数据重新装配并打印出来后,可看到下面这样的结果(对象在每次运行时都会处在不同的内存位置,所以每次运行的结果有区别): -``` +``` java animals: [Bosco the dog[Animal@1cc76c], House@1cc769 , Ralph the hamster[Animal@1cc76d], House@1cc769 , Fronk the cat[Animal@1cc76e], House@1cc769 @@ -617,7 +617,7 @@ animals3: [Bosco the dog[Animal@1cca52], House@1cca5c 下面这个例子是一套假想的计算机辅助设计(CAD)系统,对这一方法进行了很好的演示。此外,它还为我们引入了static字段的问题——如留意联机文档,就会发现Class是“Serializable”(可序列化)的,所以只需简单地序列化Class对象,就能实现static字段的保存。这无论如何都是一种明智的做法。 -``` +``` java //: CADState.java // Saving and restoring the state of a // pretend CAD system. @@ -749,13 +749,13 @@ public class CADState { Shape(几何形状)类“实现了可序列化”(implements Serializable),所以从Shape继承的任何东西也都会自动“可序列化”。每个Shape都包含了数据,而且每个衍生的Shape类都包含了一个特殊的static字段,用于决定所有那些类型的Shape的颜色(如将一个static字段置入基础类,结果只会产生一个字段,因为static字段未在衍生类中复制)。可对基础类中的方法进行覆盖处理,以便为不同的类型设置颜色(static方法不会动态绑定,所以这些都是普通的方法)。每次调用randomFactory()方法时,它都会创建一个不同的Shape(Shape值采用随机值)。 -Circle(圆)和Square(矩形)属于对Shape的直接扩展;唯一的差别是Circle在定义时会初始化颜色,而Square在构建器中初始化。Line(直线)的问题将留到以后讨论。 +Circle(圆)和Square(矩形)属于对Shape的直接扩展;唯一的差别是Circle在定义时会初始化颜色,而Square在构造器中初始化。Line(直线)的问题将留到以后讨论。 在main()中,一个Vector用于容纳Class对象,而另一个用于容纳形状。若不提供相应的命令行参数,就会创建shapeTypes Vector,并添加Class对象。然后创建shapes Vector,并添加Shape对象。接下来,所有static color值都会设成GREEN,而且所有东西都会序列化到文件CADState.out。 若提供了一个命令行参数(假设CADState.out),便会打开那个文件,并用它恢复程序的状态。无论在哪种情况下,结果产生的Shape的Vector都会打印出来。下面列出它某一次运行的结果: -``` +``` java >java CADState [class Circle color[3] xPos[-51] yPos[-99] dim[38] , class Square color[3] xPos[2] yPos[61] dim[-46] @@ -782,8 +782,8 @@ Circle(圆)和Square(矩形)属于对Shape的直接扩展;唯一的差 , class Circle color[1] xPos[17] yPos[90] dim[-76] ] -``` -从中可以看出,xPos,yPos以及dim的值都已成功保存和恢复出来。但在获取static信息时却出现了问题。所有“3”都已进入,但没有正常地出来。Circle有一个1值(定义为RED),而Square有一个0值(记住,它们是在构建器里初始化的)。看上去似乎static根本没有得到初始化!实情正是如此——尽管类Class是“可以序列化的”,但却不能按我们希望的工作。所以假如想序列化static值,必须亲自动手。 +``` java +从中可以看出,xPos,yPos以及dim的值都已成功保存和恢复出来。但在获取static信息时却出现了问题。所有“3”都已进入,但没有正常地出来。Circle有一个1值(定义为RED),而Square有一个0值(记住,它们是在构造器里初始化的)。看上去似乎static根本没有得到初始化!实情正是如此——尽管类Class是“可以序列化的”,但却不能按我们希望的工作。所以假如想序列化static值,必须亲自动手。 这正是Line中的serializeStaticState()和deserializeStaticState()两个static方法的用途。可以看到,这两个方法都是作为存储和恢复进程的一部分明确调用的(注意写入序列化文件和从中读回的顺序不能改变)。所以为了使CADState.java正确运行起来,必须采用下述三种方法之一: diff --git "a/11.1 \345\257\271RTTI\347\232\204\351\234\200\350\246\201.md" "b/11.1 \345\257\271RTTI\347\232\204\351\234\200\350\246\201.md" index 1d58fb3..ad4706e 100644 --- "a/11.1 \345\257\271RTTI\347\232\204\351\234\200\350\246\201.md" +++ "b/11.1 \345\257\271RTTI\347\232\204\351\234\200\350\246\201.md" @@ -1,17 +1,17 @@ # 11.1 对RTTI的需要 -请考虑下面这个熟悉的类结构例子,它利用了多形性。常规类型是Shape类,而特别衍生出来的类型是Circle,Square和Triangle。 +请考虑下面这个熟悉的类结构例子,它利用了多态性。常规类型是Shape类,而特别衍生出来的类型是Circle,Square和Triangle。 ![](11-1.gif) -这是一个典型的类结构示意图,基础类位于顶部,衍生类向下延展。面向对象编程的基本目标是用大量代码控制基础类型(这里是Shape)的句柄,所以假如决定添加一个新类(比如Rhomboid,从Shape衍生),从而对程序进行扩展,那么不会影响到原来的代码。在这个例子中,Shape接口中的动态绑定方法是draw(),所以客户程序员要做的是通过一个普通Shape句柄调用draw()。draw()在所有衍生类里都会被覆盖。而且由于它是一个动态绑定方法,所以即使通过一个普通的Shape句柄调用它,也有表现出正确的行为。这正是多形性的作用。 +这是一个典型的类结构示意图,基础类位于顶部,衍生类向下延展。面向对象编程的基本目标是用大量代码控制基础类型(这里是Shape)的指针,所以假如决定添加一个新类(比如Rhomboid,从Shape衍生),从而对程序进行扩展,那么不会影响到原来的代码。在这个例子中,Shape接口中的动态绑定方法是draw(),所以客户程序员要做的是通过一个普通Shape指针调用draw()。draw()在所有衍生类里都会被覆盖。而且由于它是一个动态绑定方法,所以即使通过一个普通的Shape指针调用它,也有表现出正确的行为。这正是多态性的作用。 -所以,我们一般创建一个特定的对象(Circle,Square,或者Triangle),把它上溯造型到一个Shape(忽略对象的特殊类型),以后便在程序的剩余部分使用匿名Shape句柄。 +所以,我们一般创建一个特定的对象(Circle,Square,或者Triangle),把它上溯造型到一个Shape(忽略对象的特殊类型),以后便在程序的剩余部分使用匿名Shape指针。 -作为对多形性和上溯造型的一个简要回顾,可以象下面这样为上述例子编码(若执行这个程序时出现困难,请参考第3章3.1.2小节“赋值”): +作为对多态性和上溯造型的一个简要回顾,可以象下面这样为上述例子编码(若执行这个程序时出现困难,请参考第3章3.1.2小节“赋值”): -``` +``` java //: Shapes.java package c11; import java.util.*; @@ -55,13 +55,13 @@ public class Shapes { 每个衍生类都覆盖了基础类draw方法,所以具有不同的行为。在main()中创建了特定类型的Shape,然后将其添加到一个Vector。这里正是上溯造型发生的地方,因为Vector只容纳了对象。由于Java中的所有东西(除基本数据类型外)都是对象,所以Vector也能容纳Shape对象。但在上溯造型至Object的过程中,任何特殊的信息都会丢失,其中甚至包括对象是几何形状这一事实。对Vector来说,它们只是Object。 -用nextElement()将一个元素从Vector提取出来的时候,情况变得稍微有些复杂。由于Vector只容纳Object,所以nextElement()会自然地产生一个Object句柄。但我们知道它实际是个Shape句柄,而且希望将Shape消息发给那个对象。所以需要用传统的"(Shape)"方式造型成一个Shape。这是RTTI最基本的形式,因为在Java中,所有造型都会在运行期间得到检查,以确保其正确性。那正是RTTI的意义所在:在运行期,对象的类型会得到鉴定。 +用nextElement()将一个元素从Vector提取出来的时候,情况变得稍微有些复杂。由于Vector只容纳Object,所以nextElement()会自然地产生一个Object指针。但我们知道它实际是个Shape指针,而且希望将Shape消息发给那个对象。所以需要用传统的"(Shape)"方式造型成一个Shape。这是RTTI最基本的形式,因为在Java中,所有造型都会在运行期间得到检查,以确保其正确性。那正是RTTI的意义所在:在运行期,对象的类型会得到鉴定。 在目前这种情况下,RTTI造型只实现了一部分:Object造型成Shape,而不是造型成Circle,Square或者Triangle。那是由于我们目前能够肯定的唯一事实就是Vector里充斥着几何形状,而不知它们的具体类别。在编译期间,我们肯定的依据是我们自己的规则;而在编译期间,却是通过造型来肯定这一点。 -现在的局面会由多形性控制,而且会为Shape调用适当的方法,以便判断句柄到底是提供Circle,Square,还是提供给Triangle。而且在一般情况下,必须保证采用多形性方案。因为我们希望自己的代码尽可能少知道一些与对象的具体类型有关的情况,只将注意力放在某一类对象(这里是Shape)的常规信息上。只有这样,我们的代码才更易实现、理解以及修改。所以说多形性是面向对象程序设计的一个常规目标。 +现在的局面会由多态性控制,而且会为Shape调用适当的方法,以便判断指针到底是提供Circle,Square,还是提供给Triangle。而且在一般情况下,必须保证采用多态性方案。因为我们希望自己的代码尽可能少知道一些与对象的具体类型有关的情况,只将注意力放在某一类对象(这里是Shape)的常规信息上。只有这样,我们的代码才更易实现、理解以及修改。所以说多态性是面向对象程序设计的一个常规目标。 -然而,若碰到一个特殊的程序设计问题,只有在知道常规句柄的确切类型后,才能最容易地解决这个问题,这个时候又该怎么办呢?举个例子来说,我们有时候想让自己的用户将某一具体类型的几何形状(如三角形)全都变成紫色,以便突出显示它们,并快速找出这一类型的所有形状。此时便要用到RTTI技术,用它查询某个Shape句柄引用的准确类型是什么。 +然而,若碰到一个特殊的程序设计问题,只有在知道常规指针的确切类型后,才能最容易地解决这个问题,这个时候又该怎么办呢?举个例子来说,我们有时候想让自己的用户将某一具体类型的几何形状(如三角形)全都变成紫色,以便突出显示它们,并快速找出这一类型的所有形状。此时便要用到RTTI技术,用它查询某个Shape指针引用的准确类型是什么。 11.1.1 Class对象 @@ -73,7 +73,7 @@ public class Shapes { 若这种说法多少让你产生了一点儿迷惑,或者并没有真正理解它,下面这个示范程序或许能提供进一步的帮助: -``` +``` java //: SweetShop.java // Examination of the way the class loader works @@ -116,15 +116,15 @@ public class SweetShop { 对每个类来说(Candy,Gum和Cookie),它们都有一个static从句,用于在类首次载入时执行。相应的信息会打印出来,告诉我们载入是什么时候进行的。在main()中,对象的创建代码位于打印语句之间,以便侦测载入时间。 特别有趣的一行是: -``` +``` java Class.forName("Gum"); ``` -该方法是Class(即全部Class所从属的)的一个static成员。而Class对象和其他任何对象都是类似的,所以能够获取和控制它的一个句柄(装载模块就是干这件事的)。为获得Class的一个句柄,一个办法是使用forName()。它的作用是取得包含了目标类文本名字的一个String(注意拼写和大小写)。最后返回的是一个Class句柄。 +该方法是Class(即全部Class所从属的)的一个static成员。而Class对象和其他任何对象都是类似的,所以能够获取和控制它的一个指针(装载模块就是干这件事的)。为获得Class的一个指针,一个办法是使用forName()。它的作用是取得包含了目标类文本名字的一个String(注意拼写和大小写)。最后返回的是一个Class指针。 该程序在某个JVM中的输出如下: -``` +``` java inside main Loading Candy After creating Candy @@ -137,7 +137,7 @@ After creating Cookie 可以看到,每个Class只有在它需要的时候才会载入,而static初始化工作是在类载入时执行的。 非常有趣的是,另一个JVM的输出变成了另一个样子: -``` +``` java Loading Candy Loading Cookie inside main @@ -151,15 +151,15 @@ After creating Cookie 1. 类标记 -在Java 1.1中,可以采用第二种方式来产生Class对象的句柄:使用“类标记”。对上述程序来说,看起来就象下面这样: +在Java 1.1中,可以采用第二种方式来产生Class对象的指针:使用“类标记”。对上述程序来说,看起来就象下面这样: Gum.class; 这样做不仅更加简单,而且更安全,因为它会在编译期间得到检查。由于它取消了对方法调用的需要,所以执行的效率也会更高。 -类标记不仅可以应用于普通类,也可以应用于接口、数组以及基本数据类型。除此以外,针对每种基本数据类型的封装器类,它还存在一个名为TYPE的标准字段。TYPE字段的作用是为相关的基本数据类型产生Class对象的一个句柄,如下所示: +类标记不仅可以应用于普通类,也可以应用于接口、数组以及基本数据类型。除此以外,针对每种基本数据类型的封装器类,它还存在一个名为TYPE的标准字段。TYPE字段的作用是为相关的基本数据类型产生Class对象的一个指针,如下所示: ……等价于…… -``` +``` java ... is equivalent to ... boolean.class @@ -203,7 +203,7 @@ Void.TYPE 迄今为止,我们已知的RTTI形式包括: -(1) 经典造型,如"(Shape)",它用RTTI确保造型的正确性,并在遇到一个失败的造型后产生一个ClassCastException违例。 +(1) 经典造型,如"(Shape)",它用RTTI确保造型的正确性,并在遇到一个失败的造型后产生一个ClassCastException异常。 (2) 代表对象类型的Class对象。可查询Class对象,获取有用的运行期资料。 @@ -211,16 +211,16 @@ Void.TYPE RTTI在Java中存在三种形式。关键字instanceof告诉我们对象是不是一个特定类型的实例(Instance即“实例”)。它会返回一个布尔值,以便以问题的形式使用,就象下面这样: -``` +``` java if(x instanceof Dog) ((Dog)x).bark(); ``` -将x造型至一个Dog前,上面的if语句会检查对象x是否从属于Dog类。进行造型前,如果没有其他信息可以告诉自己对象的类型,那么instanceof的使用是非常重要的——否则会得到一个ClassCastException违例。 +将x造型至一个Dog前,上面的if语句会检查对象x是否从属于Dog类。进行造型前,如果没有其他信息可以告诉自己对象的类型,那么instanceof的使用是非常重要的——否则会得到一个ClassCastException异常。 我们最一般的做法是查找一种类型(比如要变成紫色的三角形),但下面这个程序却演示了如何用instanceof标记出所有对象。 -``` +``` java //: PetCount.java // Using instanceof package c11.petcount; @@ -293,13 +293,13 @@ public class PetCount { 在Java 1.0中,对instanceof有一个比较小的限制:只可将其与一个已命名的类型比较,不能同Class对象作对比。在上述例子中,大家可能觉得将所有那些instanceof表达式写出来是件很麻烦的事情。实际情况正是这样。但在Java 1.0中,没有办法让这一工作自动进行——不能创建Class的一个Vector,再将其与之比较。大家最终会意识到,如编写了数量众多的instanceof表达式,整个设计都可能出现问题。 -当然,这个例子只是一个构想——最好在每个类型里添加一个static数据成员,然后在构建器中令其增值,以便跟踪计数。编写程序时,大家可能想象自己拥有类的源码控制权,能够自由改动它。但由于实际情况并非总是这样,所以RTTI显得特别方便。 +当然,这个例子只是一个构想——最好在每个类型里添加一个static数据成员,然后在构造器中令其增值,以便跟踪计数。编写程序时,大家可能想象自己拥有类的源码控制权,能够自由改动它。但由于实际情况并非总是这样,所以RTTI显得特别方便。 1. 使用类标记 PetCount.java示例可用Java 1.1的类标记重写一遍。得到的结果显得更加明确易懂: -``` +``` java //: PetCount2.java // Using Java 1.1 class literals package c11.petcount2; @@ -383,7 +383,7 @@ public class PetCount2 { 在这里,typenames(类型名)数组已被删除,改为从Class对象里获取类型名称。注意为此而额外做的工作:例如,类名不是Getbil,而是c11.petcount2.Getbil,其中已包含了包的名字。也要注意系统是能够区分类和接口的。 -也可以看到,petTypes的创建模块不需要用一个try块包围起来,因为它会在编译期得到检查,不会象Class.forName()那样“掷”出任何违例。 +也可以看到,petTypes的创建模块不需要用一个try块包围起来,因为它会在编译期得到检查,不会象Class.forName()那样“掷”出任何异常。 Pet动态创建好以后,可以看到随机数字已得到了限制,位于1和petTypes.length之间,而且不包括零。那是由于零代表的是Pet.class,而且一个普通的Pet对象可能不会有人感兴趣。然而,由于Pet.class是petTypes的一部分,所以所有Pet(宠物)都会算入计数中。 @@ -392,7 +392,7 @@ Pet动态创建好以后,可以看到随机数字已得到了限制,位于1 Java 1.1为Class类添加了isInstance方法。利用它可以动态调用instanceof运算符。而在Java 1.0中,只能静态地调用它(就象前面指出的那样)。因此,所有那些烦人的instanceof语句都可以从PetCount例子中删去了。如下所示: -``` +``` java //: PetCount3.java // Using Java 1.1 isInstance() package c11.petcount3; diff --git "a/11.2 RTTI\350\257\255\346\263\225.md" "b/11.2 RTTI\350\257\255\346\263\225.md" index a1e1556..a200c5d 100644 --- "a/11.2 RTTI\350\257\255\346\263\225.md" +++ "b/11.2 RTTI\350\257\255\346\263\225.md" @@ -3,9 +3,9 @@ Java用Class对象实现自己的RTTI功能——即便我们要做的只是象造型那样的一些工作。Class类也提供了其他大量方式,以方便我们使用RTTI。 -首先必须获得指向适当Class对象的的一个句柄。就象前例演示的那样,一个办法是用一个字串以及Class.forName()方法。这是非常方便的,因为不需要那种类型的一个对象来获取Class句柄。然而,对于自己感兴趣的类型,如果已有了它的一个对象,那么为了取得Class句柄,可调用属于Object根类一部分的一个方法:getClass()。它的作用是返回一个特定的Class句柄,用来表示对象的实际类型。Class提供了几个有趣且较为有用的方法,从下例即可看出: +首先必须获得指向适当Class对象的的一个指针。就象前例演示的那样,一个办法是用一个字串以及Class.forName()方法。这是非常方便的,因为不需要那种类型的一个对象来获取Class指针。然而,对于自己感兴趣的类型,如果已有了它的一个对象,那么为了取得Class指针,可调用属于Object根类一部分的一个方法:getClass()。它的作用是返回一个特定的Class指针,用来表示对象的实际类型。Class提供了几个有趣且较为有用的方法,从下例即可看出: -``` +``` java //: ToyTest.java // Testing class Class @@ -54,19 +54,19 @@ public class ToyTest { } ///:~ ``` -从中可以看出,class FancyToy相当复杂,因为它从Toy中继承,并实现了HasBatteries,Waterproof以及ShootsThings的接口。在main()中创建了一个Class句柄,并用位于相应try块内的forName()初始化成FancyToy。 +从中可以看出,class FancyToy相当复杂,因为它从Toy中继承,并实现了HasBatteries,Waterproof以及ShootsThings的接口。在main()中创建了一个Class指针,并用位于相应try块内的forName()初始化成FancyToy。 Class.getInterfaces方法会返回Class对象的一个数组,用于表示包含在Class对象内的接口。 -若有一个Class对象,也可以用getSuperclass()查询该对象的直接基础类是什么。当然,这种做会返回一个Class句柄,可用它作进一步的查询。这意味着在运行期的时候,完全有机会调查到对象的完整层次结构。 +若有一个Class对象,也可以用getSuperclass()查询该对象的直接基础类是什么。当然,这种做会返回一个Class指针,可用它作进一步的查询。这意味着在运行期的时候,完全有机会调查到对象的完整层次结构。 -若从表面看,Class的newInstance()方法似乎是克隆(clone())一个对象的另一种手段。但两者是有区别的。利用newInstance(),我们可在没有现成对象供“克隆”的情况下新建一个对象。就象上面的程序演示的那样,当时没有Toy对象,只有cy——即y的Class对象的一个句柄。利用它可以实现“虚拟构建器”。换言之,我们表达:“尽管我不知道你的准确类型是什么,但请你无论如何都正确地创建自己。”在上述例子中,cy只是一个Class句柄,编译期间并不知道进一步的类型信息。一旦新建了一个实例后,可以得到Object句柄。但那个句柄指向一个Toy对象。当然,如果要将除Object能够接收的其他任何消息发出去,首先必须进行一些调查研究,再进行造型。除此以外,用newInstance()创建的类必须有一个默认构建器。没有办法用newInstance()创建拥有非默认构建器的对象,所以在Java 1.0中可能存在一些限制。然而,Java 1.1的“反射”API(下一节讨论)却允许我们动态地使用类里的任何构建器。 +若从表面看,Class的newInstance()方法似乎是克隆(clone())一个对象的另一种手段。但两者是有区别的。利用newInstance(),我们可在没有现成对象供“克隆”的情况下新建一个对象。就象上面的程序演示的那样,当时没有Toy对象,只有cy——即y的Class对象的一个指针。利用它可以实现“虚拟构造器”。换言之,我们表达:“尽管我不知道你的准确类型是什么,但请你无论如何都正确地创建自己。”在上述例子中,cy只是一个Class指针,编译期间并不知道进一步的类型信息。一旦新建了一个实例后,可以得到Object指针。但那个指针指向一个Toy对象。当然,如果要将除Object能够接收的其他任何消息发出去,首先必须进行一些调查研究,再进行造型。除此以外,用newInstance()创建的类必须有一个默认构造器。没有办法用newInstance()创建拥有非默认构造器的对象,所以在Java 1.0中可能存在一些限制。然而,Java 1.1的“反射”API(下一节讨论)却允许我们动态地使用类里的任何构造器。 -程序中的最后一个方法是printInfo(),它取得一个Class句柄,通过getName()获得它的名字,并用interface()调查它是不是一个接口。 +程序中的最后一个方法是printInfo(),它取得一个Class指针,通过getName()获得它的名字,并用interface()调查它是不是一个接口。 该程序的输出如下: -``` +``` java Class name: FancyToy is interface? [false] Class name: HasBatteries is interface? [true] Class name: Waterproof is interface? [true] diff --git "a/11.3 \345\217\215\345\260\204\357\274\232\350\277\220\350\241\214\346\234\237\347\261\273\344\277\241\346\201\257.md" "b/11.3 \345\217\215\345\260\204\357\274\232\350\277\220\350\241\214\346\234\237\347\261\273\344\277\241\346\201\257.md" index 5330daf..67c612f 100644 --- "a/11.3 \345\217\215\345\260\204\357\274\232\350\277\220\350\241\214\346\234\237\347\261\273\344\277\241\346\201\257.md" +++ "b/11.3 \345\217\215\345\260\204\357\274\232\350\277\220\350\241\214\346\234\237\347\261\273\344\277\241\346\201\257.md" @@ -3,20 +3,20 @@ 如果不知道一个对象的准确类型,RTTI会帮助我们调查。但却有一个限制:类型必须是在编译期间已知的,否则就不能用RTTI调查它,进而无法展开下一步的工作。换言之,编译器必须明确知道RTTI要处理的所有类。 -从表面看,这似乎并不是一个很大的限制,但假若得到的是一个不在自己程序空间内的对象的句柄,这时又会怎样呢?事实上,对象的类即使在编译期间也不可由我们的程序使用。例如,假设我们从磁盘或者网络获得一系列字节,而且被告知那些字节代表一个类。由于编译器在编译代码时并不知道那个类的情况,所以怎样才能顺利地使用这个类呢? +从表面看,这似乎并不是一个很大的限制,但假若得到的是一个不在自己程序空间内的对象的指针,这时又会怎样呢?事实上,对象的类即使在编译期间也不可由我们的程序使用。例如,假设我们从磁盘或者网络获得一系列字节,而且被告知那些字节代表一个类。由于编译器在编译代码时并不知道那个类的情况,所以怎样才能顺利地使用这个类呢? 在传统的程序设计环境中,出现这种情况的概率或许很小。但当我们转移到一个规模更大的编程世界中,却必须对这个问题加以高度重视。第一个要注意的是基于组件的程序设计。在这种环境下,我们用“快速应用开发”(RAD)模型来构建程序项目。RAD一般是在应用程序构建工具中内建的。这是编制程序的一种可视途径(在屏幕上以窗体的形式出现)。可将代表不同组件的图标拖曳到窗体中。随后,通过设定这些组件的属性或者值,进行正确的配置。设计期间的配置要求任何组件都是可以“例示”的(即可以自由获得它们的实例)。这些组件也要揭示出自己的一部分内容,允许程序员读取和设置各种值。此外,用于控制GUI事件的组件必须揭示出与相应的方法有关的信息,以便RAD环境帮助程序员用自己的代码覆盖这些由事件驱动的方法。“反射”提供了一种特殊的机制,可以侦测可用的方法,并产生方法名。通过Java Beans(第13章将详细介绍),Java 1.1为这种基于组件的程序设计提供了一个基础结构。 在运行期查询类信息的另一个原动力是通过网络创建与执行位于远程系统上的对象。这就叫作“远程方法调用”(RMI),它允许Java程序(版本1.1以上)使用由多台机器发布或分布的对象。这种对象的分布可能是由多方面的原因引起的:可能要做一件计算密集型的工作,想对它进行分割,让处于空闲状态的其他机器分担部分工作,从而加快处理进度。某些情况下,可能需要将用于控制特定类型任务(比如多层客户/服务器架构中的“运作规则”)的代码放置在一台特殊的机器上,使这台机器成为对那些行动进行描述的一个通用储藏所。而且可以方便地修改这个场所,使其对系统内的所有方面产生影响(这是一种特别有用的设计思路,因为机器是独立存在的,所以能轻易修改软件!)。分布式计算也能更充分地发挥某些专用硬件的作用,它们特别擅长执行一些特定的任务——例如矩阵逆转——但对常规编程来说却显得太夸张或者太昂贵了。 -在Java 1.1中,Class类(本章前面已有详细论述)得到了扩展,可以支持“反射”的概念。针对Field,Method以及Constructor类(每个都实现了Memberinterface——成员接口),它们都新增了一个库:java.lang.reflect。这些类型的对象都是JVM在运行期创建的,用于代表未知类里对应的成员。这样便可用构建器创建新对象,用get()和set()方法读取和修改与Field对象关联的字段,以及用invoke()方法调用与Method对象关联的方法。此外,我们可调用方法getFields(),getMethods(),getConstructors(),分别返回用于表示字段、方法以及构建器的对象数组(在联机文档中,还可找到与Class类有关的更多的资料)。因此,匿名对象的类信息可在运行期被完整的揭露出来,而在编译期间不需要知道任何东西。 +在Java 1.1中,Class类(本章前面已有详细论述)得到了扩展,可以支持“反射”的概念。针对Field,Method以及Constructor类(每个都实现了Memberinterface——成员接口),它们都新增了一个库:java.lang.reflect。这些类型的对象都是JVM在运行期创建的,用于代表未知类里对应的成员。这样便可用构造器创建新对象,用get()和set()方法读取和修改与Field对象关联的字段,以及用invoke()方法调用与Method对象关联的方法。此外,我们可调用方法getFields(),getMethods(),getConstructors(),分别返回用于表示字段、方法以及构造器的对象数组(在联机文档中,还可找到与Class类有关的更多的资料)。因此,匿名对象的类信息可在运行期被完整的揭露出来,而在编译期间不需要知道任何东西。 大家要认识的很重要的一点是“反射”并没有什么神奇的地方。通过“反射”同一个未知类型的对象打交道时,JVM只是简单地检查那个对象,并调查它从属于哪个特定的类(就象以前的RTTI那样)。但在这之后,在我们做其他任何事情之前,Class对象必须载入。因此,用于那种特定类型的.class文件必须能由JVM调用(要么在本地机器内,要么可以通过网络取得)。所以RTTI和“反射”之间唯一的区别就是对RTTI来说,编译器会在编译期打开和检查.class文件。换句话说,我们可以用“普通”方式调用一个对象的所有方法;但对“反射”来说,.class文件在编译期间是不可使用的,而是由运行期环境打开和检查。 11.3.1 一个类方法提取器 很少需要直接使用反射工具;之所以在语言中提供它们,仅仅是为了支持其他Java特性,比如对象序列化(第10章介绍)、Java Beans以及RMI(本章后面介绍)。但是,我们许多时候仍然需要动态提取与一个类有关的资料。其中特别有用的工具便是一个类方法提取器。正如前面指出的那样,若检视类定义源码或者联机文档,只能看到在那个类定义中被定义或覆盖的方法,基础类那里还有大量资料拿不到。幸运的是,“反射”做到了这一点,可用它写一个简单的工具,令其自动展示整个接口。下面便是具体的程序: -``` +``` java //: ShowMethods.java // Using Java 1.1 reflection to show all the // methods of a class, even if the methods are @@ -66,10 +66,10 @@ Class方法getMethods()和getConstructors()可以分别返回Method和Constructo 这里便用到了“反射”技术,因为由Class.forName()产生的结果不能在编译期间获知,所以所有方法签名信息都会在运行期间提取。若研究一下联机文档中关于“反射”(Reflection)的那部分文字,就会发现它已提供了足够多的支持,可对一个编译期完全未知的对象进行实际的设置以及发出方法调用。同样地,这也属于几乎完全不用我们操心的一个步骤——Java自己会利用这种支持,所以程序设计环境能够控制Java Beans——但它无论如何都是非常有趣的。 -一个有趣的试验是运行java ShowMehods ShowMethods。这样做可得到一个列表,其中包括一个public默认构建器,尽管我们在代码中看见并没有定义一个构建器。我们看到的是由编译器自动合成的那一个构建器。如果随之将ShowMethods设为一个非public类(即换成“友好”类),合成的默认构建器便不会在输出结果中出现。合成的默认构建器会自动获得与类一样的访问权限。 +一个有趣的试验是运行java ShowMehods ShowMethods。这样做可得到一个列表,其中包括一个public默认构造器,尽管我们在代码中看见并没有定义一个构造器。我们看到的是由编译器自动合成的那一个构造器。如果随之将ShowMethods设为一个非public类(即换成“友好”类),合成的默认构造器便不会在输出结果中出现。合成的默认构造器会自动获得与类一样的访问权限。 ShowMethods的输出仍然有些“不爽”。例如,下面是通过调用java ShowMethods java.lang.String得到的输出结果的一部分: -``` +``` java public boolean java.lang.String.startsWith(java.lang.String,int) public boolean @@ -80,7 +80,7 @@ public boolean 若能去掉象java.lang这样的限定词,结果显然会更令人满意。有鉴于此,可引入上一章介绍的StreamTokenizer类,解决这个问题: -``` +``` java //: ShowMethodsClean.java // ShowMethods with the qualifiers stripped // to make the results easier to read diff --git "a/11.4 \346\200\273\347\273\223.md" "b/11.4 \346\200\273\347\273\223.md" index cc58ef7..27c54df 100644 --- "a/11.4 \346\200\273\347\273\223.md" +++ "b/11.4 \346\200\273\347\273\223.md" @@ -1,8 +1,8 @@ # 11.4 总结 -利用RTTI可根据一个匿名的基础类句柄调查出类型信息。但正是由于这个原因,新手们极易误用它,因为有些时候多形性方法便足够了。对那些以前习惯程序化编程的人来说,极易将他们的程序组织成一系列switch语句。他们可能用RTTI做到这一点,从而在代码开发和维护中损失多形性技术的重要价值。Java的要求是让我们尽可能地采用多形性,只有在极特别的情况下才使用RTTI。 -但为了利用多形性,要求我们拥有对基础类定义的控制权,因为有些时候在程序范围之内,可能发现基础类并未包括我们想要的方法。若基础类来自一个库,或者由别的什么东西控制着,RTTI便是一种很好的解决方案:可继承一个新类型,然后添加自己的额外方法。在代码的其他地方,可以侦测自己的特定类型,并调用那个特殊的方法。这样做不会破坏多形性以及程序的扩展能力,因为新类型的添加不要求查找程序中的switch语句。但在需要新特性的主体中添加新代码时,就必须用RTTI侦测自己特定的类型。 +利用RTTI可根据一个匿名的基础类指针调查出类型信息。但正是由于这个原因,新手们极易误用它,因为有些时候多态性方法便足够了。对那些以前习惯程序化编程的人来说,极易将他们的程序组织成一系列switch语句。他们可能用RTTI做到这一点,从而在代码开发和维护中损失多态性技术的重要价值。Java的要求是让我们尽可能地采用多态性,只有在极特别的情况下才使用RTTI。 +但为了利用多态性,要求我们拥有对基础类定义的控制权,因为有些时候在程序范围之内,可能发现基础类并未包括我们想要的方法。若基础类来自一个库,或者由别的什么东西控制着,RTTI便是一种很好的解决方案:可继承一个新类型,然后添加自己的额外方法。在代码的其他地方,可以侦测自己的特定类型,并调用那个特殊的方法。这样做不会破坏多态性以及程序的扩展能力,因为新类型的添加不要求查找程序中的switch语句。但在需要新特性的主体中添加新代码时,就必须用RTTI侦测自己特定的类型。 从某个特定类的利益的角度出发,在基础类里加入一个特性后,可能意味着从那个基础类衍生的其他所有类都必须获得一些无意义的“鸡肋”。这使得接口变得含义模糊。若有人从那个基础类继承,且必须覆盖抽象方法,这一现象便会使他们陷入困扰。比如现在用一个类结构来表示乐器(Instrument)。假定我们想清洁管弦乐队中所有适当乐器的通气音栓(Spit Valve),此时的一个办法是在基础类Instrument中置入一个ClearSpitValve()方法。但这样做会造成一个误区,因为它暗示着打击乐器和电子乐器中也有音栓。针对这种情况,RTTI提供了一个更合理的解决方案,可将方法置入特定的类中(此时是Wind,即“通气口”)——这样做是可行的。但事实上一种更合理的方案是将prepareInstrument()置入基础类中。初学者刚开始时往往看不到这一点,一般会认定自己必须使用RTTI。 -最后,RTTI有时能解决效率问题。若代码大量运用了多形性,但其中的一个对象在执行效率上很有问题,便可用RTTI找出那个类型,然后写一段适当的代码,改进其效率。 +最后,RTTI有时能解决效率问题。若代码大量运用了多态性,但其中的一个对象在执行效率上很有问题,便可用RTTI找出那个类型,然后写一段适当的代码,改进其效率。 diff --git "a/11.5 \347\273\203\344\271\240.md" "b/11.5 \347\273\203\344\271\240.md" index 1f9c703..1936941 100644 --- "a/11.5 \347\273\203\344\271\240.md" +++ "b/11.5 \347\273\203\344\271\240.md" @@ -2,7 +2,7 @@ (1) 写一个方法,向它传递一个对象,循环打印出对象层次结构中的所有类。 -(2) 在ToyTest.java中,将Toy的默认构建器标记成注释信息,解释随之发生的事情。 +(2) 在ToyTest.java中,将Toy的默认构造器标记成注释信息,解释随之发生的事情。 (3) 新建一种类型的集合,令其使用一个Vector。捕获置入其中的第一个对象的类型,然后从那时起只允许用户插入那种类型的对 象。 diff --git "a/12.1 \344\274\240\351\200\222\345\217\245\346\237\204.md" "b/12.1 \344\274\240\351\200\222\346\214\207\351\222\210.md" similarity index 84% rename from "12.1 \344\274\240\351\200\222\345\217\245\346\237\204.md" rename to "12.1 \344\274\240\351\200\222\346\214\207\351\222\210.md" index 464ee3f..ff77f23 100644 --- "a/12.1 \344\274\240\351\200\222\345\217\245\346\237\204.md" +++ "b/12.1 \344\274\240\351\200\222\346\214\207\351\222\210.md" @@ -1,7 +1,7 @@ -# 12.1 传递句柄 +# 12.1 传递指针 -12.1 传递句柄 -将句柄传递进入一个方法时,指向的仍然是相同的对象。一个简单的实验可以证明这一点(若执行这个程序时有麻烦,请参考第3章3.1.2小节“赋值”): +12.1 传递指针 +将指针传递进入一个方法时,指向的仍然是相同的对象。一个简单的实验可以证明这一点(若执行这个程序时有麻烦,请参考第3章3.1.2小节“赋值”): //: PassHandles.java // Passing handles around package c12; @@ -17,13 +17,13 @@ public class PassHandles { } } ///:~ -toString方法会在打印语句里自动调用,而PassHandles直接从Object继承,没有toString的重新定义。因此,这里会采用toString的Object版本,打印出对象的类,接着是那个对象所在的位置(不是句柄,而是对象的实际存储位置)。输出结果如下: +toString方法会在打印语句里自动调用,而PassHandles直接从Object继承,没有toString的重新定义。因此,这里会采用toString的Object版本,打印出对象的类,接着是那个对象所在的位置(不是指针,而是对象的实际存储位置)。输出结果如下: p inside main(): PassHandles@1653748 h inside f() : PassHandles@1653748 可以看到,无论p还是h引用的都是同一个对象。这比复制一个新的PassHandles对象有效多了,使我们能将一个参数发给一个方法。但这样做也带来了另一个重要的问题。 12.1.1 别名问题 -“别名”意味着多个句柄都试图指向同一个对象,就象前面的例子展示的那样。若有人向那个对象里写入一点什么东西,就会产生别名问题。若其他句柄的所有者不希望那个对象改变,恐怕就要失望了。这可用下面这个简单的例子说明: +“别名”意味着多个指针都试图指向同一个对象,就象前面的例子展示的那样。若有人向那个对象里写入一点什么东西,就会产生别名问题。若其他指针的所有者不希望那个对象改变,恐怕就要失望了。这可用下面这个简单的例子说明: //: Alias1.java // Aliasing two handles to one object @@ -44,7 +44,7 @@ public class Alias1 { 对下面这行: Alias1 y = x; // Assign the handle -它会新建一个Alias1句柄,但不是把它分配给由new创建的一个新鲜对象,而是分配给一个现有的句柄。所以句柄x的内容——即对象x指向的地址——被分配给y,所以无论x还是y都与相同的对象连接起来。这样一来,一旦x的i在下述语句中增值: +它会新建一个Alias1指针,但不是把它分配给由new创建的一个新鲜对象,而是分配给一个现有的指针。所以指针x的内容——即对象x指向的地址——被分配给y,所以无论x还是y都与相同的对象连接起来。这样一来,一旦x的i在下述语句中增值: x.i++; y的i值也必然受到影响。从最终的输出就可以看出: x: 7 @@ -53,7 +53,7 @@ Incrementing x x: 8 y: 8 -此时最直接的一个解决办法就是干脆不这样做:不要有意将多个句柄指向同一个作用域内的同一个对象。这样做可使代码更易理解和调试。然而,一旦准备将句柄作为一个自变量或参数传递——这是Java设想的正常方法——别名问题就会自动出现,因为创建的本地句柄可能修改“外部对象”(在方法作用域之外创建的对象)。下面是一个例子: +此时最直接的一个解决办法就是干脆不这样做:不要有意将多个指针指向同一个作用域内的同一个对象。这样做可使代码更易理解和调试。然而,一旦准备将指针作为一个自变量或参数传递——这是Java设想的正常方法——别名问题就会自动出现,因为创建的本地指针可能修改“外部对象”(在方法作用域之外创建的对象)。下面是一个例子: //: Alias2.java // Method calls implicitly alias their // arguments. diff --git "a/12.2 \345\210\266\344\275\234\346\234\254\345\234\260\345\211\257\346\234\254.md" "b/12.2 \345\210\266\344\275\234\346\234\254\345\234\260\345\211\257\346\234\254.md" index 01f527b..c09883a 100644 --- "a/12.2 \345\210\266\344\275\234\346\234\254\345\234\260\345\211\257\346\234\254.md" +++ "b/12.2 \345\210\266\344\275\234\346\234\254\345\234\260\345\211\257\346\234\254.md" @@ -2,21 +2,21 @@ 12.2 制作本地副本 -稍微总结一下:Java中的所有自变量或参数传递都是通过传递句柄进行的。也就是说,当我们传递“一个对象”时,实际传递的只是指向位于方法外部的那个对象的“一个句柄”。所以一旦要对那个句柄进行任何修改,便相当于修改外部对象。此外: +稍微总结一下:Java中的所有自变量或参数传递都是通过传递指针进行的。也就是说,当我们传递“一个对象”时,实际传递的只是指向位于方法外部的那个对象的“一个指针”。所以一旦要对那个指针进行任何修改,便相当于修改外部对象。此外: ■参数传递过程中会自动产生别名问题 -■不存在本地对象,只有本地句柄 -■句柄有自己的作用域,而对象没有 +■不存在本地对象,只有本地指针 +■指针有自己的作用域,而对象没有 ■对象的“存在时间”在Java里不是个问题 ■没有语言上的支持(如常量)可防止对象被修改(以避免别名的副作用) -若只是从对象中读取信息,而不修改它,传递句柄便是自变量传递中最有效的一种形式。这种做非常恰当;默认的方法一般也是最有效的方法。然而,有时仍需将对象当作“本地的”对待,使我们作出的改变只影响一个本地副本,不会对外面的对象造成影响。许多程序设计语言都支持在方法内自动生成外部对象的一个本地副本(注释①)。尽管Java不具备这种能力,但允许我们达到同样的效果。 +若只是从对象中读取信息,而不修改它,传递指针便是自变量传递中最有效的一种形式。这种做非常恰当;默认的方法一般也是最有效的方法。然而,有时仍需将对象当作“本地的”对待,使我们作出的改变只影响一个本地副本,不会对外面的对象造成影响。许多程序设计语言都支持在方法内自动生成外部对象的一个本地副本(注释①)。尽管Java不具备这种能力,但允许我们达到同样的效果。 ①:在C语言中,通常控制的是少量数据位,默认操作是按值传递。C++也必须遵照这一形式,但按值传递对象并非肯定是一种有效的方式。此外,在C++中用于支持按值传递的代码也较难编写,是件让人头痛的事情。 12.2.1 按值传递 首先要解决术语的问题,最适合“按值传递”的看起来是自变量。“按值传递”以及它的含义取决于如何理解程序的运行方式。最常见的意思是获得要传递的任何东西的一个本地副本,但这里真正的问题是如何看待自己准备传递的东西。对于“按值传递”的含义,目前存在两种存在明显区别的见解: -(1) Java按值传递任何东西。若将基本数据类型传递进入一个方法,会明确得到基本数据类型的一个副本。但若将一个句柄传递进入方法,得到的是句柄的副本。所以人们认为“一切”都按值传递。当然,这种说法也有一个前提:句柄肯定也会被传递。但Java的设计方案似乎有些超前,允许我们忽略(大多数时候)自己处理的是一个句柄。也就是说,它允许我们将句柄假想成“对象”,因为在发出方法调用时,系统会自动照管两者间的差异。 -(2) Java主要按值传递(无自变量),但对象却是按引用传递的。得到这个结论的前提是句柄只是对象的一个“别名”,所以不考虑传递句柄的问题,而是直接指出“我准备传递对象”。由于将其传递进入一个方法时没有获得对象的一个本地副本,所以对象显然不是按值传递的。Sun公司似乎在某种程度上支持这一见解,因为它“保留但未实现”的关键字之一便是byvalue(按值)。但没人知道那个关键字什么时候可以发挥作用。 -尽管存在两种不同的见解,但其间的分歧归根到底是由于对“句柄”的不同解释造成的。我打算在本书剩下的部分里回避这个问题。大家不久就会知道,这个问题争论下去其实是没有意义的——最重要的是理解一个句柄的传递会使调用者的对象发生意外的改变。 +(1) Java按值传递任何东西。若将基本数据类型传递进入一个方法,会明确得到基本数据类型的一个副本。但若将一个指针传递进入方法,得到的是指针的副本。所以人们认为“一切”都按值传递。当然,这种说法也有一个前提:指针肯定也会被传递。但Java的设计方案似乎有些超前,允许我们忽略(大多数时候)自己处理的是一个指针。也就是说,它允许我们将指针假想成“对象”,因为在发出方法调用时,系统会自动照管两者间的差异。 +(2) Java主要按值传递(无自变量),但对象却是按引用传递的。得到这个结论的前提是指针只是对象的一个“别名”,所以不考虑传递指针的问题,而是直接指出“我准备传递对象”。由于将其传递进入一个方法时没有获得对象的一个本地副本,所以对象显然不是按值传递的。Sun公司似乎在某种程度上支持这一见解,因为它“保留但未实现”的关键字之一便是byvalue(按值)。但没人知道那个关键字什么时候可以发挥作用。 +尽管存在两种不同的见解,但其间的分歧归根到底是由于对“指针”的不同解释造成的。我打算在本书剩下的部分里回避这个问题。大家不久就会知道,这个问题争论下去其实是没有意义的——最重要的是理解一个指针的传递会使调用者的对象发生意外的改变。 12.2.2 克隆对象 若需修改一个对象,同时不想改变调用者的对象,就要制作该对象的一个本地副本。这也是本地副本最常见的一种用途。若决定制作一个本地副本,只需简单地使用clone()方法即可。Clone是“克隆”的意思,即制作完全一模一样的副本。这个方法在基础类Object中定义成“protected”(受保护)模式。但在希望克隆的任何衍生类中,必须将其覆盖为“public”模式。例如,标准库类Vector覆盖了clone(),所以能为Vector调用clone(),如下所示: @@ -50,7 +50,7 @@ public class Cloning { } } ///:~ -clone()方法产生了一个Object,后者必须立即重新造型为正确类型。这个例子指出Vector的clone()方法不能自动尝试克隆Vector内包含的每个对象——由于别名问题,老的Vector和克隆的Vector都包含了相同的对象。我们通常把这种情况叫作“简单复制”或者“浅层复制”,因为它只复制了一个对象的“表面”部分。实际对象除包含这个“表面”以外,还包括句柄指向的所有对象,以及那些对象又指向的其他所有对象,由此类推。这便是“对象网”或“对象关系网”的由来。若能复制下所有这张网,便叫作“全面复制”或者“深层复制”。 +clone()方法产生了一个Object,后者必须立即重新造型为正确类型。这个例子指出Vector的clone()方法不能自动尝试克隆Vector内包含的每个对象——由于别名问题,老的Vector和克隆的Vector都包含了相同的对象。我们通常把这种情况叫作“简单复制”或者“浅层复制”,因为它只复制了一个对象的“表面”部分。实际对象除包含这个“表面”以外,还包括指针指向的所有对象,以及那些对象又指向的其他所有对象,由此类推。这便是“对象网”或“对象关系网”的由来。若能复制下所有这张网,便叫作“全面复制”或者“深层复制”。 在输出中可看到浅层复制的结果,注意对v2采取的行动也会影响到v: v: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] v: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] @@ -63,20 +63,20 @@ v: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 尽管克隆方法是在所有类最基本的Object中定义的,但克隆仍然不会在每个类里自动进行。这似乎有些不可思议,因为基础类方法在衍生类里是肯定能用的。但Java确实有点儿反其道而行之;如果想在一个类里使用克隆方法,唯一的办法就是专门添加一些代码,以便保证克隆的正常进行。 1. 使用protected时的技巧 -为避免我们创建的每个类都默认具有克隆能力,clone()方法在基础类Object里得到了“保留”(设为protected)。这样造成的后果就是:对那些简单地使用一下这个类的客户程序员来说,他们不会默认地拥有这个方法;其次,我们不能利用指向基础类的一个句柄来调用clone()(尽管那样做在某些情况下特别有用,比如用多形性的方式克隆一系列对象)。在编译期的时候,这实际是通知我们对象不可克隆的一种方式——而且最奇怪的是,Java库中的大多数类都不能克隆。因此,假如我们执行下述代码: +为避免我们创建的每个类都默认具有克隆能力,clone()方法在基础类Object里得到了“保留”(设为protected)。这样造成的后果就是:对那些简单地使用一下这个类的客户程序员来说,他们不会默认地拥有这个方法;其次,我们不能利用指向基础类的一个指针来调用clone()(尽管那样做在某些情况下特别有用,比如用多态性的方式克隆一系列对象)。在编译期的时候,这实际是通知我们对象不可克隆的一种方式——而且最奇怪的是,Java库中的大多数类都不能克隆。因此,假如我们执行下述代码: Integer x = new Integer(l); x = x.clone(); 那么在编译期,就有一条讨厌的错误消息弹出,告诉我们不可访问clone()——因为Integer并没有覆盖它,而且它对protected版本来说是默认的)。 -但是,假若我们是在一个从Object衍生出来的类中(所有类都是从Object衍生的),就有权调用Object.clone(),因为它是“protected”,而且我们在一个继承器中。基础类clone()提供了一个有用的功能——它进行的是对衍生类对象的真正“按位”复制,所以相当于标准的克隆行动。然而,我们随后需要将自己的克隆操作设为public,否则无法访问。总之,克隆时要注意的两个关键问题是:几乎肯定要调用super.clone(),以及注意将克隆设为public。 +但是,假若我们是在一个从Object衍生出来的类中(所有类都是从Object衍生的),就有权调用Object.clone(),因为它是“protected”,而且我们在一个迭代器中。基础类clone()提供了一个有用的功能——它进行的是对衍生类对象的真正“按位”复制,所以相当于标准的克隆行动。然而,我们随后需要将自己的克隆操作设为public,否则无法访问。总之,克隆时要注意的两个关键问题是:几乎肯定要调用super.clone(),以及注意将克隆设为public。 有时还想在更深层的衍生类中覆盖clone(),否则就直接使用我们的clone()(现在已成为public),而那并不一定是我们所希望的(然而,由于Object.clone()已制作了实际对象的一个副本,所以也有可能允许这种情况)。protected的技巧在这里只能用一次:首次从一个不具备克隆能力的类继承,而且想使一个类变成“能够克隆”。而在从我们的类继承的任何场合,clone()方法都是可以使用的,因为Java不可能在衍生之后反而缩小方法的访问范围。换言之,一旦对象变得可以克隆,从它衍生的任何东西都是能够克隆的,除非使用特殊的机制(后面讨论)令其“关闭”克隆能力。 2. 实现Cloneable接口 为使一个对象的克隆能力功成圆满,还需要做另一件事情:实现Cloneable接口。这个接口使人稍觉奇怪,因为它是空的! interface Cloneable {} 之所以要实现这个空接口,显然不是因为我们准备上溯造型成一个Cloneable,以及调用它的某个方法。有些人认为在这里使用接口属于一种“欺骗”行为,因为它使用的特性打的是别的主意,而非原来的意思。Cloneable interface的实现扮演了一个标记的角色,封装到类的类型中。 -两方面的原因促成了Cloneable interface的存在。首先,可能有一个上溯造型句柄指向一个基础类型,而且不知道它是否真的能克隆那个对象。在这种情况下,可用instanceof关键字(第11章有介绍)调查句柄是否确实同一个能克隆的对象连接: +两方面的原因促成了Cloneable interface的存在。首先,可能有一个上溯造型指针指向一个基础类型,而且不知道它是否真的能克隆那个对象。在这种情况下,可用instanceof关键字(第11章有介绍)调查指针是否确实同一个能克隆的对象连接: if(myHandle instanceof Cloneable) // ... -第二个原因是考虑到我们可能不愿所有对象类型都能克隆。所以Object.clone()会验证一个类是否真的是实现了Cloneable接口。若答案是否定的,则“掷”出一个CloneNotSupportedException违例。所以在一般情况下,我们必须将“implement Cloneable”作为对克隆能力提供支持的一部分。 +第二个原因是考虑到我们可能不愿所有对象类型都能克隆。所以Object.clone()会验证一个类是否真的是实现了Cloneable接口。若答案是否定的,则“掷”出一个CloneNotSupportedException异常。所以在一般情况下,我们必须将“implement Cloneable”作为对克隆能力提供支持的一部分。 12.2.4 成功的克隆 理解了实现clone()方法背后的所有细节后,便可创建出能方便复制的类,以便提供了一个本地副本: @@ -135,10 +135,10 @@ public class LocalCopy { } ///:~ 不管怎样,clone()必须能够访问,所以必须将其设为public(公共的)。其次,作为clone()的初期行动,应调用clone()的基础类版本。这里调用的clone()是Object内部预先定义好的。之所以能调用它,是由于它具有protected(受到保护的)属性,所以能在衍生的类里访问。 -Object.clone()会检查原先的对象有多大,再为新对象腾出足够多的内存,将所有二进制位从原来的对象复制到新对象。这叫作“按位复制”,而且按一般的想法,这个工作应该是由clone()方法来做的。但在Object.clone()正式开始操作前,首先会检查一个类是否Cloneable,即是否具有克隆能力——换言之,它是否实现了Cloneable接口。若未实现,Object.clone()就掷出一个CloneNotSupportedException违例,指出我们不能克隆它。因此,我们最好用一个try-catch块将对super.clone()的调用代码包围(或封装)起来,试图捕获一个应当永不出现的违例(因为这里确实已实现了Cloneable接口)。 -在LocalCopy中,两个方法g()和f()揭示出两种参数传递方法间的差异。其中,g()演示的是按引用传递,它会修改外部对象,并返回对那个外部对象的一个引用。而f()是对自变量进行克隆,所以将其分离出来,并让原来的对象保持独立。随后,它继续做它希望的事情。甚至能返回指向这个新对象的一个句柄,而且不会对原来的对象产生任何副作用。注意下面这个多少有些古怪的语句: +Object.clone()会检查原先的对象有多大,再为新对象腾出足够多的内存,将所有二进制位从原来的对象复制到新对象。这叫作“按位复制”,而且按一般的想法,这个工作应该是由clone()方法来做的。但在Object.clone()正式开始操作前,首先会检查一个类是否Cloneable,即是否具有克隆能力——换言之,它是否实现了Cloneable接口。若未实现,Object.clone()就掷出一个CloneNotSupportedException异常,指出我们不能克隆它。因此,我们最好用一个try-catch块将对super.clone()的调用代码包围(或封装)起来,试图捕获一个应当永不出现的异常(因为这里确实已实现了Cloneable接口)。 +在LocalCopy中,两个方法g()和f()揭示出两种参数传递方法间的差异。其中,g()演示的是按引用传递,它会修改外部对象,并返回对那个外部对象的一个引用。而f()是对自变量进行克隆,所以将其分离出来,并让原来的对象保持独立。随后,它继续做它希望的事情。甚至能返回指向这个新对象的一个指针,而且不会对原来的对象产生任何副作用。注意下面这个多少有些古怪的语句: v = (MyObject)v.clone(); -它的作用正是创建一个本地副本。为避免被这样的一个语句搞混淆,记住这种相当奇怪的编码形式在Java中是完全允许的,因为有一个名字的所有东西实际都是一个句柄。所以句柄v用于克隆一个它所指向的副本,而且最终返回指向基础类型Object的一个句柄(因为它在Object.clone()中是那样被定义的),随后必须将其造型为正确的类型。 +它的作用正是创建一个本地副本。为避免被这样的一个语句搞混淆,记住这种相当奇怪的编码形式在Java中是完全允许的,因为有一个名字的所有东西实际都是一个指针。所以指针v用于克隆一个它所指向的副本,而且最终返回指向基础类型Object的一个指针(因为它在Object.clone()中是那样被定义的),随后必须将其造型为正确的类型。 在main()中,两种不同参数传递方式的区别在于它们分别测试了一个不同的方法。输出结果如下: a == b a = 12 @@ -147,12 +147,12 @@ c != d c = 47 d = 48 -大家要记住这样一个事实:Java对“是否等价”的测试并不对所比较对象的内部进行检查,从而核实它们的值是否相同。==和!=运算符只是简单地对比句柄的内容。若句柄内的地址相同,就认为句柄指向同样的对象,所以认为它们是“等价”的。所以运算符真正检测的是“由于别名问题,句柄是否指向同一个对象?” +大家要记住这样一个事实:Java对“是否等价”的测试并不对所比较对象的内部进行检查,从而核实它们的值是否相同。==和!=运算符只是简单地对比指针的内容。若指针内的地址相同,就认为指针指向同样的对象,所以认为它们是“等价”的。所以运算符真正检测的是“由于别名问题,指针是否指向同一个对象?” 12.2.5 Object.clone()的效果 调用Object.clone()时,实际发生的是什么事情呢?当我们在自己的类里覆盖clone()时,什么东西对于super.clone()来说是最关键的呢?根类中的clone()方法负责建立正确的存储容量,并通过“按位复制”将二进制位从原始对象中复制到新对象的存储空间。也就是说,它并不只是预留存储空间以及复制一个对象——实际需要调查出欲复制之对象的准确大小,然后复制那个对象。由于所有这些工作都是在由根类定义之clone()方法的内部代码中进行的(根类并不知道要从自己这里继承出去什么),所以大家或许已经猜到,这个过程需要用RTTI判断欲克隆的对象的实际大小。采取这种方式,clone()方法便可建立起正确数量的存储空间,并对那个类型进行正确的按位复制。 不管我们要做什么,克隆过程的第一个部分通常都应该是调用super.clone()。通过进行一次准确的复制,这样做可为后续的克隆进程建立起一个良好的基础。随后,可采取另一些必要的操作,以完成最终的克隆。 -为确切了解其他操作是什么,首先要正确理解Object.clone()为我们带来了什么。特别地,它会自动克隆所有句柄指向的目标吗?下面这个例子可完成这种形式的检测: +为确切了解其他操作是什么,首先要正确理解Object.clone()为我们带来了什么。特别地,它会自动克隆所有指针指向的目标吗?下面这个例子可完成这种形式的检测: //: Snake.java // Tests cloning to see if destination of // handles are also cloned. @@ -195,17 +195,17 @@ public class Snake implements Cloneable { } } ///:~ -一条Snake(蛇)由数段构成,每一段的类型都是Snake。所以,这是一个一段段链接起来的列表。所有段都是以循环方式创建的,每做好一段,都会使第一个构建器参数的值递减,直至最终为零。而为给每段赋予一个独一无二的标记,第二个参数(一个Char)的值在每次循环构建器调用时都会递增。 +一条Snake(蛇)由数段构成,每一段的类型都是Snake。所以,这是一个一段段链接起来的列表。所有段都是以循环方式创建的,每做好一段,都会使第一个构造器参数的值递减,直至最终为零。而为给每段赋予一个独一无二的标记,第二个参数(一个Char)的值在每次循环构造器调用时都会递增。 increment()方法的作用是循环递增每个标记,使我们能看到发生的变化;而toString则循环打印出每个标记。输出如下: s = :a:b:c:d:e s2 = :a:b:c:d:e after s.increment, s2 = :a:c:d:e:f 这意味着只有第一段才是由Object.clone()复制的,所以此时进行的是一种“浅层复制”。若希望复制整条蛇——即进行“深层复制”——必须在被覆盖的clone()里采取附加的操作。 -通常可在从一个能克隆的类里调用super.clone(),以确保所有基础类行动(包括Object.clone())能够进行。随着是为对象内每个句柄都明确调用一个clone();否则那些句柄会别名变成原始对象的句柄。构建器的调用也大致相同——首先构造基础类,然后是下一个衍生的构建器……以此类推,直到位于最深层的衍生构建器。区别在于clone()并不是个构建器,所以没有办法实现自动克隆。为了克隆,必须由自己明确进行。 +通常可在从一个能克隆的类里调用super.clone(),以确保所有基础类行动(包括Object.clone())能够进行。随着是为对象内每个指针都明确调用一个clone();否则那些指针会别名变成原始对象的指针。构造器的调用也大致相同——首先构造基础类,然后是下一个衍生的构造器……以此类推,直到位于最深层的衍生构造器。区别在于clone()并不是个构造器,所以没有办法实现自动克隆。为了克隆,必须由自己明确进行。 12.2.6 克隆合成对象 -试图深层复制合成对象时会遇到一个问题。必须假定成员对象中的clone()方法也能依次对自己的句柄进行深层复制,以此类推。这使我们的操作变得复杂。为了能正常实现深层复制,必须对所有类中的代码进行控制,或者至少全面掌握深层复制中需要涉及的类,确保它们自己的深层复制能正确进行。 +试图深层复制合成对象时会遇到一个问题。必须假定成员对象中的clone()方法也能依次对自己的指针进行深层复制,以此类推。这使我们的操作变得复杂。为了能正常实现深层复制,必须对所有类中的代码进行控制,或者至少全面掌握深层复制中需要涉及的类,确保它们自己的深层复制能正确进行。 下面这个例子总结了面对一个合成对象进行深层复制时需要做哪些事情: //: DeepCopy.java // Cloning a composed object @@ -277,7 +277,7 @@ public class DeepCopy { } ///:~ DepthReading和TemperatureReading非常相似;它们都只包含了基本数据类型。所以clone()方法能够非常简单:调用super.clone()并返回结果即可。注意两个类使用的clone()代码是完全一致的。 -OceanReading是由DepthReading和TemperatureReading对象合并而成的。为了对其进行深层复制,clone()必须同时克隆OceanReading内的句柄。为达到这个目标,super.clone()的结果必须造型成一个OceanReading对象(以便访问depth和temperature句柄)。 +OceanReading是由DepthReading和TemperatureReading对象合并而成的。为了对其进行深层复制,clone()必须同时克隆OceanReading内的指针。为达到这个目标,super.clone()的结果必须造型成一个OceanReading对象(以便访问depth和temperature指针)。 12.2.7 用Vector进行深层复制 下面让我们复习一下本章早些时候提出的Vector例子。这一次Int2类是可以克隆的,所以能对Vector进行深层复制: @@ -341,7 +341,7 @@ public class AddingClone { } } ///:~ -Int3自Int2继承而来,并添加了一个新的基本类型成员int j。大家也许认为自己需要再次覆盖clone(),以确保j得到复制,但实情并非如此。将Int2的clone()当作Int3的clone()调用时,它会调用Object.clone(),判断出当前操作的是Int3,并复制Int3内的所有二进制位。只要没有新增需要克隆的句柄,对Object.clone()的一个调用就能完成所有必要的复制——无论clone()是在层次结构多深的一级定义的。 +Int3自Int2继承而来,并添加了一个新的基本类型成员int j。大家也许认为自己需要再次覆盖clone(),以确保j得到复制,但实情并非如此。将Int2的clone()当作Int3的clone()调用时,它会调用Object.clone(),判断出当前操作的是Int3,并复制Int3内的所有二进制位。只要没有新增需要克隆的指针,对Object.clone()的一个调用就能完成所有必要的复制——无论clone()是在层次结构多深的一级定义的。 至此,大家可以总结出对Vector进行深层复制的先决条件:在克隆了Vector后,必须在其中遍历,并克隆由Vector指向的每个对象。为了对Hashtable(散列表)进行深层复制,也必须采取类似的处理。 这个例子剩余的部分显示出克隆已实际进行——证据就是在克隆了对象以后,可以自由改变它,而原来那个对象不受任何影响。 @@ -482,5 +482,5 @@ public class HorrorFlick { 12.2.10 为什么有这个奇怪的设计 之所以感觉这个方案的奇特,因为它事实上的确如此。也许大家会奇怪它为什么要象这样运行,而该方案背后的真正含义是什么呢?后面讲述的是一个未获证实的故事——大概是由于围绕Java的许多买卖使其成为一种设计优良的语言——但确实要花许多口舌才能讲清楚这背后发生的所有事情。 最初,Java只是作为一种用于控制硬件的语言而设计,与因特网并没有丝毫联系。象这样一类面向大众的语言一样,其意义在于程序员可以对任意一个对象进行克隆。这样一来,clone()就放置在根类Object里面,但因为它是一种公用方式,因而我们通常能够对任意一个对象进行克隆。看来这是最灵活的方式了,毕竟它不会带来任何害处。 -正当Java看起来象一种终级因特网程序设计语言的时候,情况却发生了变化。突然地,人们提出了安全问题,而且理所当然,这些问题与使用对象有关,我们不愿望任何人克隆自己的保密对象。所以我们最后看到的是为原来那个简单、直观的方案添加的大量补丁:clone()在Object里被设置成“protected”。必须将其覆盖,并使用“implement Cloneable”,同时解决违例的问题。 +正当Java看起来象一种终级因特网程序设计语言的时候,情况却发生了变化。突然地,人们提出了安全问题,而且理所当然,这些问题与使用对象有关,我们不愿望任何人克隆自己的保密对象。所以我们最后看到的是为原来那个简单、直观的方案添加的大量补丁:clone()在Object里被设置成“protected”。必须将其覆盖,并使用“implement Cloneable”,同时解决异常的问题。 只有在准备调用Object的clone()方法时,才没有必要使用Cloneable接口,因为那个方法会在运行期间得到检查,以确保我们的类实现了Cloneable。但为了保持连贯性(而且由于Cloneable无论如何都是空的),最好还是由自己实现Cloneable。 diff --git "a/12.3 \345\205\213\351\232\206\347\232\204\346\216\247\345\210\266.md" "b/12.3 \345\205\213\351\232\206\347\232\204\346\216\247\345\210\266.md" index f981ff7..59effc2 100644 --- "a/12.3 \345\205\213\351\232\206\347\232\204\346\216\247\345\210\266.md" +++ "b/12.3 \345\205\213\351\232\206\347\232\204\346\216\247\345\210\266.md" @@ -4,11 +4,11 @@ 12.3 克隆的控制 为消除克隆能力,大家也许认为只需将clone()方法简单地设为private(私有)即可,但这样是行不通的,因为不能采用一个基础类方法,并使其在衍生类中更“私有”。所以事情并没有这么简单。此外,我们有必要控制一个对象是否能够克隆。对于我们设计的一个类,实际有许多种方案都是可以采取的: (1) 保持中立,不为克隆做任何事情。也就是说,尽管不可对我们的类克隆,但从它继承的一个类却可根据实际情况决定克隆。只有Object.clone()要对类中的字段进行某些合理的操作时,才可以作这方面的决定。 -(2) 支持clone(),采用实现Cloneable(可克隆)能力的标准操作,并覆盖clone()。在被覆盖的clone()中,可调用super.clone(),并捕获所有违例(这样可使clone()不“掷”出任何违例)。 -(3) 有条件地支持克隆。若类容纳了其他对象的句柄,而那些对象也许能够克隆(集合类便是这样的一个例子),就可试着克隆拥有对方句柄的所有对象;如果它们“掷”出了违例,只需让这些违例通过即可。举个例子来说,假设有一个特殊的Vector,它试图克隆自己容纳的所有对象。编写这样的一个Vector时,并不知道客户程序员会把什么形式的对象置入这个Vector中,所以并不知道它们是否真的能够克隆。 -(4) 不实现Cloneable(),但是将clone()覆盖成protected,使任何字段都具有正确的复制行为。这样一来,从这个类继承的所有东西都能覆盖clone(),并调用super.clone()来产生正确的复制行为。注意在我们实现方案里,可以而且应该调用super.clone()——即使那个方法本来预期的是一个Cloneable对象(否则会掷出一个违例),因为没有人会在我们这种类型的对象上直接调用它。它只有通过一个衍生类调用;对那个衍生类来说,如果要保证它正常工作,需实现Cloneable。 -(5) 不实现Cloneable来试着防止克隆,并覆盖clone(),以产生一个违例。为使这一设想顺利实现,只有令从它衍生出来的任何类都调用重新定义后的clone()里的suepr.clone()。 -(6) 将类设为final,从而防止克隆。若clone()尚未被我们的任何一个上级类覆盖,这一设想便不会成功。若已被覆盖,那么再一次覆盖它,并“掷”出一个CloneNotSupportedException(克隆不支持)违例。为担保克隆被禁止,将类设为final是唯一的办法。除此以外,一旦涉及保密对象或者遇到想对创建的对象数量进行控制的其他情况,应该将所有构建器都设为private,并提供一个或更多的特殊方法来创建对象。采用这种方式,这些方法就可以限制创建的对象数量以及它们的创建条件——一种特殊情况是第16章要介绍的singleton(独子)方案。 +(2) 支持clone(),采用实现Cloneable(可克隆)能力的标准操作,并覆盖clone()。在被覆盖的clone()中,可调用super.clone(),并捕获所有异常(这样可使clone()不“掷”出任何异常)。 +(3) 有条件地支持克隆。若类容纳了其他对象的指针,而那些对象也许能够克隆(集合类便是这样的一个例子),就可试着克隆拥有对方指针的所有对象;如果它们“掷”出了异常,只需让这些异常通过即可。举个例子来说,假设有一个特殊的Vector,它试图克隆自己容纳的所有对象。编写这样的一个Vector时,并不知道客户程序员会把什么形式的对象置入这个Vector中,所以并不知道它们是否真的能够克隆。 +(4) 不实现Cloneable(),但是将clone()覆盖成protected,使任何字段都具有正确的复制行为。这样一来,从这个类继承的所有东西都能覆盖clone(),并调用super.clone()来产生正确的复制行为。注意在我们实现方案里,可以而且应该调用super.clone()——即使那个方法本来预期的是一个Cloneable对象(否则会掷出一个异常),因为没有人会在我们这种类型的对象上直接调用它。它只有通过一个衍生类调用;对那个衍生类来说,如果要保证它正常工作,需实现Cloneable。 +(5) 不实现Cloneable来试着防止克隆,并覆盖clone(),以产生一个异常。为使这一设想顺利实现,只有令从它衍生出来的任何类都调用重新定义后的clone()里的suepr.clone()。 +(6) 将类设为final,从而防止克隆。若clone()尚未被我们的任何一个上级类覆盖,这一设想便不会成功。若已被覆盖,那么再一次覆盖它,并“掷”出一个CloneNotSupportedException(克隆不支持)异常。为担保克隆被禁止,将类设为final是唯一的办法。除此以外,一旦涉及保密对象或者遇到想对创建的对象数量进行控制的其他情况,应该将所有构造器都设为private,并提供一个或更多的特殊方法来创建对象。采用这种方式,这些方法就可以限制创建的对象数量以及它们的创建条件——一种特殊情况是第16章要介绍的singleton(单例)方案。 下面这个例子总结了克隆的各种实现方法,然后在层次结构中将其“关闭”: //: CheckCloneable.java @@ -106,12 +106,12 @@ public class CheckCloneable { } } ///:~ -第一个类Ordinary代表着大家在本书各处最常见到的类:不支持克隆,但在它正式应用以后,却也不禁止对其克隆。但假如有一个指向Ordinary对象的句柄,而且那个对象可能是从一个更深的衍生类上溯造型来的,便不能判断它到底能不能克隆。 -WrongClone类揭示了实现克隆的一种不正确途径。它确实覆盖了Object.clone(),并将那个方法设为public,但却没有实现Cloneable。所以一旦发出对super.clone()的调用(由于对Object.clone()的一个调用造成的),便会无情地掷出CloneNotSupportedException违例。 -在IsCloneable中,大家看到的才是进行克隆的各种正确行动:先覆盖clone(),并实现了Cloneable。但是,这个clone()方法以及本例的另外几个方法并不捕获CloneNotSupportedException违例,而是任由它通过,并传递给调用者。随后,调用者必须用一个try-catch代码块把它包围起来。在我们自己的clone()方法中,通常需要在clone()内部捕获CloneNotSupportedException违例,而不是任由它通过。正如大家以后会理解的那样,对这个例子来说,让它通过是最正确的做法。 -类NoMore试图按照Java设计者打算的那样“关闭”克隆:在衍生类clone()中,我们掷出CloneNotSupportedException违例。TryMore类中的clone()方法正确地调用super.clone(),并解析成NoMore.clone(),后者掷出一个违例并禁止克隆。 -但在已被覆盖的clone()方法中,假若程序员不遵守调用super.clone()的“正确”方法,又会出现什么情况呢?在BackOn中,大家可看到实际会发生什么。这个类用一个独立的方法duplicate()制作当前对象的一个副本,并在clone()内部调用这个方法,而不是调用super.clone()。违例永远不会产生,而且新类是可以克隆的。因此,我们不能依赖“掷”出一个违例的方法来防止产生一个可克隆的类。唯一安全的方法在ReallyNoMore中得到了演示,它设为final,所以不可继承。这意味着假如clone()在final类中掷出了一个违例,便不能通过继承来进行修改,并可有效地禁止克隆(不能从一个拥有任意继承级数的类中明确调用Object.clone();只能调用super.clone(),它只可访问直接基础类)。因此,只要制作一些涉及安全问题的对象,就最好把那些类设为final。 -在类CheckCloneable中,我们看到的第一个类是tryToClone(),它能接纳任何Ordinary对象,并用instanceof检查它是否能够克隆。若答案是肯定的,就将对象造型成为一个IsCloneable,调用clone(),并将结果造型回Ordinary,最后捕获有可能产生的任何违例。请注意用运行期类型鉴定(见第11章)打印出类名,使自己看到发生的一切情况。 +第一个类Ordinary代表着大家在本书各处最常见到的类:不支持克隆,但在它正式应用以后,却也不禁止对其克隆。但假如有一个指向Ordinary对象的指针,而且那个对象可能是从一个更深的衍生类上溯造型来的,便不能判断它到底能不能克隆。 +WrongClone类揭示了实现克隆的一种不正确途径。它确实覆盖了Object.clone(),并将那个方法设为public,但却没有实现Cloneable。所以一旦发出对super.clone()的调用(由于对Object.clone()的一个调用造成的),便会无情地掷出CloneNotSupportedException异常。 +在IsCloneable中,大家看到的才是进行克隆的各种正确行动:先覆盖clone(),并实现了Cloneable。但是,这个clone()方法以及本例的另外几个方法并不捕获CloneNotSupportedException异常,而是任由它通过,并传递给调用者。随后,调用者必须用一个try-catch代码块把它包围起来。在我们自己的clone()方法中,通常需要在clone()内部捕获CloneNotSupportedException异常,而不是任由它通过。正如大家以后会理解的那样,对这个例子来说,让它通过是最正确的做法。 +类NoMore试图按照Java设计者打算的那样“关闭”克隆:在衍生类clone()中,我们掷出CloneNotSupportedException异常。TryMore类中的clone()方法正确地调用super.clone(),并解析成NoMore.clone(),后者掷出一个异常并禁止克隆。 +但在已被覆盖的clone()方法中,假若程序员不遵守调用super.clone()的“正确”方法,又会出现什么情况呢?在BackOn中,大家可看到实际会发生什么。这个类用一个独立的方法duplicate()制作当前对象的一个副本,并在clone()内部调用这个方法,而不是调用super.clone()。异常永远不会产生,而且新类是可以克隆的。因此,我们不能依赖“掷”出一个异常的方法来防止产生一个可克隆的类。唯一安全的方法在ReallyNoMore中得到了演示,它设为final,所以不可继承。这意味着假如clone()在final类中掷出了一个异常,便不能通过继承来进行修改,并可有效地禁止克隆(不能从一个拥有任意继承级数的类中明确调用Object.clone();只能调用super.clone(),它只可访问直接基础类)。因此,只要制作一些涉及安全问题的对象,就最好把那些类设为final。 +在类CheckCloneable中,我们看到的第一个类是tryToClone(),它能接纳任何Ordinary对象,并用instanceof检查它是否能够克隆。若答案是肯定的,就将对象造型成为一个IsCloneable,调用clone(),并将结果造型回Ordinary,最后捕获有可能产生的任何异常。请注意用运行期类型鉴定(见第11章)打印出类名,使自己看到发生的一切情况。 在main()中,我们创建了不同类型的Ordinary对象,并在数组定义中上溯造型成为Ordinary。在这之后的头两行代码创建了一个纯粹的Ordinary对象,并试图对其克隆。然而,这些代码不会得到编译,因为clone()是Object中的一个protected(受到保护的)方法。代码剩余的部分将遍历数组,并试着克隆每个对象,分别报告它们的成功或失败。输出如下: Attempting IsCloneable Cloned IsCloneable @@ -128,11 +128,11 @@ Could not clone ReallyNoMore (1) 实现Cloneable接口 (2) 覆盖clone() (3) 在自己的clone()中调用super.clone() -(4) 在自己的clone()中捕获违例 +(4) 在自己的clone()中捕获异常 这一系列步骤能达到最理想的效果。 -12.3.1 副本构建器 -克隆看起来要求进行非常复杂的设置,似乎还该有另一种替代方案。一个办法是制作特殊的构建器,令其负责复制一个对象。在C++中,这叫作“副本构建器”。刚开始的时候,这好象是一种非常显然的解决方案(如果你是C++程序员,这个方法就更显亲切)。下面是一个实际的例子: +12.3.1 副本构造器 +克隆看起来要求进行非常复杂的设置,似乎还该有另一种替代方案。一个办法是制作特殊的构造器,令其负责复制一个对象。在C++中,这叫作“副本构造器”。刚开始的时候,这好象是一种非常显然的解决方案(如果你是C++程序员,这个方法就更显亲切)。下面是一个实际的例子: //: CopyConstructor.java // A constructor for copying an object // of the same type, as an attempt to create @@ -260,10 +260,10 @@ public class CopyConstructor { } } ///:~ -这个例子第一眼看上去显得有点奇怪。不同水果的质量肯定有所区别,但为什么只是把代表那些质量的数据成员直接置入Fruit(水果)类?有两方面可能的原因。第一个是我们可能想简便地插入或修改质量。注意Fruit有一个protected(受到保护的)addQualities()方法,它允许衍生类来进行这些插入或修改操作(大家或许会认为最合乎逻辑的做法是在Fruit中使用一个protected构建器,用它获取FruitQualities参数,但构建器不能继承,所以不可在第二级或级数更深的类中使用它)。通过将水果的质量置入一个独立的类,可以得到更大的灵活性,其中包括可以在特定Fruit对象的存在期间中途更改质量。 -之所以将FruitQualities设为一个独立的对象,另一个原因是考虑到我们有时希望添加新的质量,或者通过继承与多形性改变行为。注意对GreenZebra来说(这实际是西红柿的一类——我已栽种成功,它们简直令人难以置信),构建器会调用addQualities(),并为其传递一个ZebraQualities对象。该对象是从FruitQualities衍生出来的,所以能与基础类中的FruitQualities句柄联系在一起。当然,一旦GreenZebra使用FruitQualities,就必须将其下溯造型成为正确的类型(就象evaluate()中展示的那样),但它肯定知道类型是ZebraQualities。 +这个例子第一眼看上去显得有点奇怪。不同水果的质量肯定有所区别,但为什么只是把代表那些质量的数据成员直接置入Fruit(水果)类?有两方面可能的原因。第一个是我们可能想简便地插入或修改质量。注意Fruit有一个protected(受到保护的)addQualities()方法,它允许衍生类来进行这些插入或修改操作(大家或许会认为最合乎逻辑的做法是在Fruit中使用一个protected构造器,用它获取FruitQualities参数,但构造器不能继承,所以不可在第二级或级数更深的类中使用它)。通过将水果的质量置入一个独立的类,可以得到更大的灵活性,其中包括可以在特定Fruit对象的存在期间中途更改质量。 +之所以将FruitQualities设为一个独立的对象,另一个原因是考虑到我们有时希望添加新的质量,或者通过继承与多态性改变行为。注意对GreenZebra来说(这实际是西红柿的一类——我已栽种成功,它们简直令人难以置信),构造器会调用addQualities(),并为其传递一个ZebraQualities对象。该对象是从FruitQualities衍生出来的,所以能与基础类中的FruitQualities指针联系在一起。当然,一旦GreenZebra使用FruitQualities,就必须将其下溯造型成为正确的类型(就象evaluate()中展示的那样),但它肯定知道类型是ZebraQualities。 大家也看到有一个Seed(种子)类,Fruit(大家都知道,水果含有自己的种子)包含了一个Seed数组。 -最后,注意每个类都有一个副本构建器,而且每个副本构建器都必须关心为基础类和成员对象调用副本构建器的问题,从而获得“深层复制”的效果。对副本构建器的测试是在CopyConstructor类内进行的。方法ripen()需要获取一个Tomato参数,并对其执行副本构建工作,以便复制对象: +最后,注意每个类都有一个副本构造器,而且每个副本构造器都必须关心为基础类和成员对象调用副本构造器的问题,从而获得“深层复制”的效果。对副本构造器的测试是在CopyConstructor类内进行的。方法ripen()需要获取一个Tomato参数,并对其执行副本构建工作,以便复制对象: t = new Tomato(t); 而slice()需要获取一个更常规的Fruit对象,而且对它进行复制: f = new Fruit(f); @@ -273,7 +273,7 @@ In slice, f is a Fruit In ripen, t is a Tomato In slice, f is a Fruit -从中可以看出一个问题。在slice()内部对Tomato进行了副本构建工作以后,结果便不再是一个Tomato对象,而只是一个Fruit。它已丢失了作为一个Tomato(西红柿)的所有特征。此外,如果采用一个GreenZebra,ripen()和slice()会把它分别转换成一个Tomato和一个Fruit。所以非常不幸,假如想制作对象的一个本地副本,Java中的副本构建器便不是特别适合我们。 +从中可以看出一个问题。在slice()内部对Tomato进行了副本构建工作以后,结果便不再是一个Tomato对象,而只是一个Fruit。它已丢失了作为一个Tomato(西红柿)的所有特征。此外,如果采用一个GreenZebra,ripen()和slice()会把它分别转换成一个Tomato和一个Fruit。所以非常不幸,假如想制作对象的一个本地副本,Java中的副本构造器便不是特别适合我们。 1. 为什么在C++的作用比在Java中大? -副本构建器是C++的一个基本构成部分,因为它能自动产生对象的一个本地副本。但前面的例子确实证明了它不适合在Java中使用,为什么呢?在Java中,我们操控的一切东西都是句柄,而在C++中,却可以使用类似于句柄的东西,也能直接传递对象。这时便要用到C++的副本构建器:只要想获得一个对象,并按值传递它,就可以复制对象。所以它在C++里能很好地工作,但应注意这套机制在Java里是很不通的,所以不要用它。 +副本构造器是C++的一个基本构成部分,因为它能自动产生对象的一个本地副本。但前面的例子确实证明了它不适合在Java中使用,为什么呢?在Java中,我们操控的一切东西都是指针,而在C++中,却可以使用类似于指针的东西,也能直接传递对象。这时便要用到C++的副本构造器:只要想获得一个对象,并按值传递它,就可以复制对象。所以它在C++里能很好地工作,但应注意这套机制在Java里是很不通的,所以不要用它。 diff --git "a/12.4 \345\217\252\350\257\273\347\261\273.md" "b/12.4 \345\217\252\350\257\273\347\261\273.md" index ec238c5..5172c0a 100644 --- "a/12.4 \345\217\252\350\257\273\347\261\273.md" +++ "b/12.4 \345\217\252\350\257\273\347\261\273.md" @@ -4,7 +4,7 @@ 12.4 只读类 尽管在一些特定的场合,由clone()产生的本地副本能够获得我们希望的结果,但程序员(方法的作者)不得不亲自禁止别名处理的副作用。假如想制作一个库,令其具有常规用途,但却不能担保它肯定能在正确的类中得以克隆,这时又该怎么办呢?更有可能的一种情况是,假如我们想让别名发挥积极的作用——禁止不必要的对象复制——但却不希望看到由此造成的副作用,那么又该如何处理呢? 一个办法是创建“不变对象”,令其从属于只读类。可定义一个特殊的类,使其中没有任何方法能造成对象内部状态的改变。在这样的一个类中,别名处理是没有问题的。因为我们只能读取内部状态,所以当多处代码都读取相同的对象时,不会出现任何副作用。 -作为“不变对象”一个简单例子,Java的标准库包含了“封装器”(wrapper)类,可用于所有基本数据类型。大家可能已发现了这一点,如果想在一个象Vector(只采用Object句柄)这样的集合里保存一个int数值,可以将这个int封装到标准库的Integer类内部。如下所示: +作为“不变对象”一个简单例子,Java的标准库包含了“封装器”(wrapper)类,可用于所有基本数据类型。大家可能已发现了这一点,如果想在一个象Vector(只采用Object指针)这样的集合里保存一个int数值,可以将这个int封装到标准库的Integer类内部。如下所示: //: ImmutableInteger.java // The Integer class cannot be changed import java.util.*; @@ -46,7 +46,7 @@ public class MutableInteger { } ///:~ 注意n在这里简化了我们的编码。 -若默认的初始化为零已经足够(便不需要构建器),而且不用考虑把它打印出来(便不需要toString),那么IntValue甚至还能更加简单。如下所示: +若默认的初始化为零已经足够(便不需要构造器),而且不用考虑把它打印出来(便不需要toString),那么IntValue甚至还能更加简单。如下所示: class IntValue { int n; } 将元素取出来,再对其进行造型,这多少显得有些笨拙,但那是Vector的问题,不是IntValue的错。 @@ -172,19 +172,19 @@ public class Stringer { } } ///:~ -q传递进入upcase()时,它实际是q的句柄的一个副本。该句柄连接的对象实际只在一个统一的物理位置处。句柄四处传递的时候,它的句柄会得到复制。 -若观察对upcase()的定义,会发现传递进入的句柄有一个名字s,而且该名字只有在upcase()执行期间才会存在。upcase()完成后,本地句柄s便会消失,而upcase()返回结果——还是原来那个字串,只是所有字符都变成了大写。当然,它返回的实际是结果的一个句柄。但它返回的句柄最终是为一个新对象的,同时原来的q并未发生变化。所有这些是如何发生的呢? +q传递进入upcase()时,它实际是q的指针的一个副本。该指针连接的对象实际只在一个统一的物理位置处。指针四处传递的时候,它的指针会得到复制。 +若观察对upcase()的定义,会发现传递进入的指针有一个名字s,而且该名字只有在upcase()执行期间才会存在。upcase()完成后,本地指针s便会消失,而upcase()返回结果——还是原来那个字串,只是所有字符都变成了大写。当然,它返回的实际是结果的一个指针。但它返回的指针最终是为一个新对象的,同时原来的q并未发生变化。所有这些是如何发生的呢? 1. 隐式常数 若使用下述语句: String s = "asdf"; String x = Stringer.upcase(s); 那么真的希望upcase()方法改变自变量或者参数吗?我们通常是不愿意的,因为作为提供给方法的一种信息,自变量一般是拿给代码的读者看的,而不是让他们修改。这是一个相当重要的保证,因为它使代码更易编写和理解。 -为了在C++中实现这一保证,需要一个特殊关键字的帮助:const。利用这个关键字,程序员可以保证一个句柄(C++叫“指针”或者“引用”)不会被用来修改原始的对象。但这样一来,C++程序员需要用心记住在所有地方都使用const。这显然易使人混淆,也不容易记住。 +为了在C++中实现这一保证,需要一个特殊关键字的帮助:const。利用这个关键字,程序员可以保证一个指针(C++叫“指针”或者“引用”)不会被用来修改原始的对象。但这样一来,C++程序员需要用心记住在所有地方都使用const。这显然易使人混淆,也不容易记住。 2. 覆盖"+"和StringBuffer 利用前面提到的技术,String类的对象被设计成“不可变”。若查阅联机文档中关于String类的内容(本章稍后还要总结它),就会发现类中能够修改String的每个方法实际都创建和返回了一个崭新的String对象,新对象里包含了修改过的信息——原来的String是原封未动的。因此,Java里没有与C++的const对应的特性可用来让编译器支持对象的不可变能力。若想获得这一能力,可以自行设置,就象String那样。 -由于String对象是不可变的,所以能够根据情况对一个特定的String进行多次别名处理。因为它是只读的,所以一个句柄不可能会改变一些会影响其他句柄的东西。因此,只读对象可以很好地解决别名问题。 +由于String对象是不可变的,所以能够根据情况对一个特定的String进行多次别名处理。因为它是只读的,所以一个指针不可能会改变一些会影响其他指针的东西。因此,只读对象可以很好地解决别名问题。 通过修改产生对象的一个崭新版本,似乎可以解决修改对象时的所有问题,就象String那样。但对某些操作来讲,这种方法的效率并不高。一个典型的例子便是为String对象覆盖的运算符“+”。“覆盖”意味着在与一个特定的类使用时,它的含义已发生了变化(用于String的“+”和“+=”是Java中能被覆盖的唯一运算符,Java不允许程序员覆盖其他任何运算符——注释④)。 ④:C++允许程序员随意覆盖运算符。由于这通常是一个复杂的过程(参见《Thinking in C++》,Prentice-Hall于1995年出版),所以Java的设计者认定它是一种“糟糕”的特性,决定不在Java中采用。但具有讽剌意味的是,运算符的覆盖在Java中要比在C++中容易得多。 @@ -222,7 +222,7 @@ public class ImmutableStrings { 方法 自变量,覆盖 用途 -构建器 已被覆盖:默认,String,StringBuffer,char数组,byte数组 创建String对象 +构造器 已被覆盖:默认,String,StringBuffer,char数组,byte数组 创建String对象 length() 无 String中的字符数量 charAt() int Index 位于String内某个位置的char getChars(),getBytes 开始复制的起点和终点,要向其中复制内容的数组,对目标数组的一个索引 将char或byte复制到外部数组内部 @@ -239,14 +239,14 @@ relpace() 要查找的老字符,要用它替换的新字符 返回一个新Str toLowerCase(),toUpperCase() 无 返回一个新String对象,其中所有字符的大小写形式都进行了统一。若不必修改,则沿用老字串 trim() 无 返回一个新的String对象,头尾空白均已删除。若毋需改动,则沿用老字串 valueOf() 已覆盖:object,char[],char[]和偏移以及计数,boolean,char,int,long,float,double 返回一个String,其中包含自变量的一个字符表现形式 -Intern() 无 为每个独一无二的字符顺序都产生一个(而且只有一个)String句柄 +Intern() 无 为每个独一无二的字符顺序都产生一个(而且只有一个)String指针 -可以看到,一旦有必要改变原来的内容,每个String方法都小心地返回了一个新的String对象。另外要注意的一个问题是,若内容不需要改变,则方法只返回指向原来那个String的一个句柄。这样做可以节省存储空间和系统开销。 +可以看到,一旦有必要改变原来的内容,每个String方法都小心地返回了一个新的String对象。另外要注意的一个问题是,若内容不需要改变,则方法只返回指向原来那个String的一个指针。这样做可以节省存储空间和系统开销。 下面列出有关StringBuffer(字串缓冲)类的方法: 方法 自变量,覆盖 用途 -构建器 已覆盖:默认,要创建的缓冲区长度,要根据它创建的String 新建一个StringBuffer对象 +构造器 已覆盖:默认,要创建的缓冲区长度,要根据它创建的String 新建一个StringBuffer对象 toString() 无 根据这个StringBuffer创建一个String length() 无 StringBuffer中的字符数量 capacity() 无 返回目前分配的空间大小 @@ -262,4 +262,4 @@ reverse() 无 反转缓冲内的字符顺序 最常用的一个方法是append()。在计算包含了+和+=运算符的String表达式时,编译器便会用到这个方法。insert()方法采用类似的形式。这两个方法都能对缓冲区进行重要的操作,不需要另建新对象。 12.4.5 字串的特殊性 -现在,大家已知道String类并非仅仅是Java提供的另一个类。String里含有大量特殊的类。通过编译器和特殊的覆盖或过载运算符+和+=,可将引号字符串转换成一个String。在本章中,大家已见识了剩下的一种特殊情况:用同志StringBuffer精心构造的“不可变”能力,以及编译器中出现的一些有趣现象。 +现在,大家已知道String类并非仅仅是Java提供的另一个类。String里含有大量特殊的类。通过编译器和特殊的覆盖或重载运算符+和+=,可将引号字符串转换成一个String。在本章中,大家已见识了剩下的一种特殊情况:用同志StringBuffer精心构造的“不可变”能力,以及编译器中出现的一些有趣现象。 diff --git "a/12.5 \346\200\273\347\273\223.md" "b/12.5 \346\200\273\347\273\223.md" index 2c15421..2adaff9 100644 --- "a/12.5 \346\200\273\347\273\223.md" +++ "b/12.5 \346\200\273\347\273\223.md" @@ -2,12 +2,12 @@ 12.5 总结 -由于Java中的所有东西都是句柄,而且由于每个对象都是在内存堆中创建的——只有不再需要的时候,才会当作垃圾收集掉,所以对象的操作方式发生了变化,特别是在传递和返回对象的时候。举个例子来说,在C和C++中,如果想在一个方法里初始化一些存储空间,可能需要请求用户将那片存储区域的地址传递进入方法。否则就必须考虑由谁负责清除那片区域。因此,这些方法的接口和对它们的理解就显得要复杂一些。但在Java中,根本不必关心由谁负责清除,也不必关心在需要一个对象的时候它是否仍然存在。因为系统会为我们照料一切。我们的程序可在需要的时候创建一个对象。而且更进一步地,根本不必担心那个对象的传输机制的细节:只需简单地传递句柄即可。有些时候,这种简化非常有价值,但另一些时候却显得有些多余。 +由于Java中的所有东西都是指针,而且由于每个对象都是在内存堆中创建的——只有不再需要的时候,才会当作垃圾收集掉,所以对象的操作方式发生了变化,特别是在传递和返回对象的时候。举个例子来说,在C和C++中,如果想在一个方法里初始化一些存储空间,可能需要请求用户将那片存储区域的地址传递进入方法。否则就必须考虑由谁负责清除那片区域。因此,这些方法的接口和对它们的理解就显得要复杂一些。但在Java中,根本不必关心由谁负责清除,也不必关心在需要一个对象的时候它是否仍然存在。因为系统会为我们照料一切。我们的程序可在需要的时候创建一个对象。而且更进一步地,根本不必担心那个对象的传输机制的细节:只需简单地传递指针即可。有些时候,这种简化非常有价值,但另一些时候却显得有些多余。 可从两个方面认识这一机制的缺点: (1) 肯定要为额外的内存管理付出效率上的损失(尽管损失不大),而且对于运行所需的时间,总是存在一丝不确定的因素(因为在内存不够时,垃圾收集器可能会被强制采取行动)。对大多数应用来说,优点显得比缺点重要,而且部分对时间要求非常苛刻的段落可以用native方法写成(参见附录A)。 -(2) 别名处理:有时会不慎获得指向同一个对象的两个句柄。只有在这两个句柄都假定指向一个“明确”的对象时,才有可能产生问题。对这个问题,必须加以足够的重视。而且应该尽可能地“克隆”一个对象,以防止另一个句柄被不希望的改动影响。除此以外,可考虑创建“不可变”对象,使它的操作能返回同种类型或不同种类型的一个新对象,从而提高程序的执行效率。但千万不要改变原始对象,使对那个对象别名的其他任何方面都感觉不出变化。 +(2) 别名处理:有时会不慎获得指向同一个对象的两个指针。只有在这两个指针都假定指向一个“明确”的对象时,才有可能产生问题。对这个问题,必须加以足够的重视。而且应该尽可能地“克隆”一个对象,以防止另一个指针被不希望的改动影响。除此以外,可考虑创建“不可变”对象,使它的操作能返回同种类型或不同种类型的一个新对象,从而提高程序的执行效率。但千万不要改变原始对象,使对那个对象别名的其他任何方面都感觉不出变化。 -有些人认为Java的克隆是一个笨拙的家伙,所以他们实现了自己的克隆方案(注释⑤),永远杜绝调用Object.clone()方法,从而消除了实现Cloneable和捕获CloneNotSupportException违例的需要。这一做法是合理的,而且由于clone()在Java标准库中很少得以支持,所以这显然也是一种“安全”的方法。只要不调用Object.clone(),就不必实现Cloneable或者捕获违例,所以那看起来也是能够接受的。 +有些人认为Java的克隆是一个笨拙的家伙,所以他们实现了自己的克隆方案(注释⑤),永远杜绝调用Object.clone()方法,从而消除了实现Cloneable和捕获CloneNotSupportException异常的需要。这一做法是合理的,而且由于clone()在Java标准库中很少得以支持,所以这显然也是一种“安全”的方法。只要不调用Object.clone(),就不必实现Cloneable或者捕获异常,所以那看起来也是能够接受的。 ⑤:Doug Lea特别重视这个问题,并把这个方法推荐给了我,他说只需为每个类都创建一个名为duplicate()的函数即可。 diff --git "a/12.6 \347\273\203\344\271\240.md" "b/12.6 \347\273\203\344\271\240.md" index 2ade025..29a4871 100644 --- "a/12.6 \347\273\203\344\271\240.md" +++ "b/12.6 \347\273\203\344\271\240.md" @@ -2,8 +2,8 @@ 12.6 练习 -(1) 创建一个myString类,在其中包含了一个String对象,以便用在构建器中用构建器的自变量对其进行初始化。添加一个toString()方法以及一个concatenate()方法,令其将一个String对象追加到我们的内部字串。在myString中实现clone()。创建两个static方法,每个都取得一个myString x句柄作为自己的自变量,并调用x.concatenate("test")。但在第二个方法中,请首先调用clone()。测试这两个方法,观察它们不同的结果。 +(1) 创建一个myString类,在其中包含了一个String对象,以便用在构造器中用构造器的自变量对其进行初始化。添加一个toString()方法以及一个concatenate()方法,令其将一个String对象追加到我们的内部字串。在myString中实现clone()。创建两个static方法,每个都取得一个myString x指针作为自己的自变量,并调用x.concatenate("test")。但在第二个方法中,请首先调用clone()。测试这两个方法,观察它们不同的结果。 (2) 创建一个名为Battery(电池)的类,在其中包含一个int,用它表示电池的编号(采用独一无二的标识符的形式)。接下来,创建一个名为Toy的类,其中包含了一个Battery数组以及一个toString,用于打印出所有电池。为Toy写一个clone()方法,令其自动关闭所有Battery对象。克隆Toy并打印出结果,完成对它的测试。 -(3) 修改CheckCloneable.java,使所有clone()方法都能捕获CloneNotSupportException违例,而不是把它直接传递给调用者。 +(3) 修改CheckCloneable.java,使所有clone()方法都能捕获CloneNotSupportException异常,而不是把它直接传递给调用者。 (4) 修改Compete.java,为Thing2和Thing4类添加更多的成员对象,看看自己是否能判断计时随复杂性变化的规律——是一种简单的线性关系,还是看起来更加复杂。 (5) 从Snake.java开始,创建Snake的一个深层复制版本。 \ No newline at end of file diff --git "a/14.1 \345\217\215\345\272\224\347\201\265\346\225\217\347\232\204\347\224\250\346\210\267\347\225\214\351\235\242.md" "b/14.1 \345\217\215\345\272\224\347\201\265\346\225\217\347\232\204\347\224\250\346\210\267\347\225\214\351\235\242.md" index cdbce24..f222f1f 100644 --- "a/14.1 \345\217\215\345\272\224\347\201\265\346\225\217\347\232\204\347\224\250\346\210\267\347\225\214\351\235\242.md" +++ "b/14.1 \345\217\215\345\272\224\347\201\265\346\225\217\347\232\204\347\224\250\346\210\267\347\225\214\351\235\242.md" @@ -2,7 +2,7 @@ 作为我们的起点,请思考一个需要执行某些CPU密集型计算的程序。由于CPU“全心全意”为那些计算服务,所以对用户的输入十分迟钝,几乎没有什么反应。在这里,我们用一个合成的applet/application(程序片/应用程序)来简单显示出一个计数器的结果: -``` +``` java //: Counter1.java // A non-responsive user interface package c14; @@ -65,7 +65,7 @@ public class Counter1 extends Applet { go()内的部分无限循环是调用sleep()。sleep()必须同一个Thread(线程)对象关联到一起,而且似乎每个应用程序都有部分线程同它关联(事实上,Java本身就是建立在线程基础上的,肯定有一些线程会伴随我们写的应用一起运行)。所以无论我们是否明确使用了线程,都可利用Thread.currentThread()产生由程序使用的当前线程,然后为那个线程调用sleep()。注意,Thread.currentThread()是Thread类的一个静态方法。 -注意sleep()可能“掷”出一个InterruptException(中断违例)——尽管产生这样的违例被认为是中止线程的一种“恶意”手段,而且应该尽可能地杜绝这一做法。再次提醒大家,违例是为异常情况而产生的,而不是为了正常的控制流。在这里包含了对一个“睡眠”线程的中断,以支持未来的一种语言特性。 +注意sleep()可能“掷”出一个InterruptException(中断异常)——尽管产生这样的异常被认为是中止线程的一种“恶意”手段,而且应该尽可能地杜绝这一做法。再次提醒大家,异常是为异常情况而产生的,而不是为了正常的控制流。在这里包含了对一个“睡眠”线程的中断,以支持未来的一种语言特性。 一旦按下start按钮,就会调用go()。研究一下go(),你可能会很自然地(就象我一样)认为它该支持多线程,因为它会进入“睡眠”状态。也就是说,尽管方法本身“睡着”了,CPU仍然应该忙于监视其他按钮“按下”事件。但有一个问题,那就是go()是永远不会返回的,因为它被设计成一个无限循环。这意味着actionPerformed()根本不会返回。由于在第一个按键以后便陷入actionPerformed()中,所以程序不能再对其他任何事件进行控制(如果想出来,必须以某种方式“杀死”进程——最简便的方式就是在控制台窗口按Ctrl+C键)。 @@ -75,11 +75,11 @@ go()内的部分无限循环是调用sleep()。sleep()必须同一个Thread( 14.1.1 从线程继承 -为创建一个线程,最简单的方法就是从Thread类继承。这个类包含了创建和运行线程所需的一切东西。Thread最重要的方法是run()。但为了使用run(),必须对其进行过载或者覆盖,使其能充分按自己的吩咐行事。因此,run()属于那些会与程序中的其他线程“并发”或“同时”执行的代码。 +为创建一个线程,最简单的方法就是从Thread类继承。这个类包含了创建和运行线程所需的一切东西。Thread最重要的方法是run()。但为了使用run(),必须对其进行重载或者覆盖,使其能充分按自己的吩咐行事。因此,run()属于那些会与程序中的其他线程“并发”或“同时”执行的代码。 下面这个例子可创建任意数量的线程,并通过为每个线程分配一个独一无二的编号(由一个静态变量产生),从而对不同的线程进行跟踪。Thread的run()方法在这里得到了覆盖,每通过一次循环,计数就减1——计数为0时则完成循环(此时一旦返回run(),线程就中止运行)。 -``` +``` java //: SimpleThread.java // Very simple Threading example @@ -108,11 +108,11 @@ public class SimpleThread extends Thread { run()方法几乎肯定含有某种形式的循环——它们会一直持续到线程不再需要为止。因此,我们必须规定特定的条件,以便中断并退出这个循环(或者在上述的例子中,简单地从run()返回即可)。run()通常采用一种无限循环的形式。也就是说,通过阻止外部发出对线程的stop()或者destroy()调用,它会永远运行下去(直到程序完成)。 -在main()中,可看到创建并运行了大量线程。Thread包含了一个特殊的方法,叫作start(),它的作用是对线程进行特殊的初始化,然后调用run()。所以整个步骤包括:调用构建器来构建对象,然后用start()配置线程,再调用run()。如果不调用start()——如果适当的话,可在构建器那样做——线程便永远不会启动。 +在main()中,可看到创建并运行了大量线程。Thread包含了一个特殊的方法,叫作start(),它的作用是对线程进行特殊的初始化,然后调用run()。所以整个步骤包括:调用构造器来构建对象,然后用start()配置线程,再调用run()。如果不调用start()——如果适当的话,可在构造器那样做——线程便永远不会启动。 下面是该程序某一次运行的输出(注意每次运行都会不同): -``` +``` java Making 1 Making 2 Making 3 @@ -150,13 +150,13 @@ Thread 3(1) 亦可看出线程并不是按它们创建时的顺序运行的。事实上,CPU处理一个现有线程集的顺序是不确定的——除非我们亲自介入,并用Thread的setPriority()方法调整它们的优先级。 -main()创建Thread对象时,它并未捕获任何一个对象的句柄。普通对象对于垃圾收集来说是一种“公平竞赛”,但线程却并非如此。每个线程都会“注册”自己,所以某处实际存在着对它的一个引用。这样一来,垃圾收集器便只好对它“瞠目以对”了。 +main()创建Thread对象时,它并未捕获任何一个对象的指针。普通对象对于垃圾收集来说是一种“公平竞赛”,但线程却并非如此。每个线程都会“注册”自己,所以某处实际存在着对它的一个引用。这样一来,垃圾收集器便只好对它“瞠目以对”了。 14.1.2 针对用户界面的多线程 现在,我们也许能用一个线程解决在Counter1.java中出现的问题。采用的一个技巧便是在一个线程的run()方法中放置“子任务”——亦即位于go()内的循环。一旦用户按下Start按钮,线程就会启动,但马上结束线程的创建。这样一来,尽管线程仍在运行,但程序的主要工作却能得以继续(等候并响应用户界面的事件)。下面是具体的代码: -``` +``` java //: Counter2.java // A responsive user interface with threads import java.awt.*; @@ -226,17 +226,17 @@ public class Counter2 extends Applet { } ///:~ ``` -现在,Counter2变成了一个相当直接的程序,它的唯一任务就是设置并管理用户界面。但假若用户现在按下Start按钮,却不会真正调用一个方法。此时不是创建类的一个线程,而是创建SeparateSubTask,然后继续Counter2事件循环。注意此时会保存SeparateSubTask的句柄,以便我们按下onOff按钮的时候,能正常地切换位于SeparateSubTask内部的runFlag(运行标志)。随后那个线程便可启动(当它看到标志的时候),然后将自己中止(亦可将SeparateSubTask设为一个内部类来达到这一目的)。 +现在,Counter2变成了一个相当直接的程序,它的唯一任务就是设置并管理用户界面。但假若用户现在按下Start按钮,却不会真正调用一个方法。此时不是创建类的一个线程,而是创建SeparateSubTask,然后继续Counter2事件循环。注意此时会保存SeparateSubTask的指针,以便我们按下onOff按钮的时候,能正常地切换位于SeparateSubTask内部的runFlag(运行标志)。随后那个线程便可启动(当它看到标志的时候),然后将自己中止(亦可将SeparateSubTask设为一个内部类来达到这一目的)。 -SeparateSubTask类是对Thread的一个简单扩展,它带有一个构建器(其中保存了Counter2句柄,然后通过调用start()来运行线程)以及一个run()——本质上包含了Counter1.java的go()内的代码。由于SeparateSubTask知道自己容纳了指向一个Counter2的句柄,所以能够在需要的时候介入,并访问Counter2的TestField(文本字段)。 +SeparateSubTask类是对Thread的一个简单扩展,它带有一个构造器(其中保存了Counter2指针,然后通过调用start()来运行线程)以及一个run()——本质上包含了Counter1.java的go()内的代码。由于SeparateSubTask知道自己容纳了指向一个Counter2的指针,所以能够在需要的时候介入,并访问Counter2的TestField(文本字段)。 按下onOff按钮,几乎立即能得到正确的响应。当然,这个响应其实并不是“立即”发生的,它毕竟和那种由“中断”驱动的系统不同。只有线程拥有CPU的执行时间,并注意到标记已发生改变,计数器才会停止。 1. 用内部类改善代码 -下面说说题外话,请大家注意一下SeparateSubTask和Counter2类之间发生的结合行为。SeparateSubTask同Counter2“亲密”地结合到了一起——它必须持有指向自己“父”Counter2对象的一个句柄,以便自己能回调和操纵它。但两个类并不是真的合并为单独一个类(尽管在下一节中,我们会讲到Java确实提供了合并它们的方法),因为它们各自做的是不同的事情,而且是在不同的时间创建的。但不管怎样,它们依然紧密地结合到一起(更准确地说,应该叫“联合”),所以使程序代码多少显得有些笨拙。在这种情况下,一个内部类可以显著改善代码的“可读性”和执行效率: +下面说说题外话,请大家注意一下SeparateSubTask和Counter2类之间发生的结合行为。SeparateSubTask同Counter2“亲密”地结合到了一起——它必须持有指向自己“父”Counter2对象的一个指针,以便自己能回调和操纵它。但两个类并不是真的合并为单独一个类(尽管在下一节中,我们会讲到Java确实提供了合并它们的方法),因为它们各自做的是不同的事情,而且是在不同的时间创建的。但不管怎样,它们依然紧密地结合到一起(更准确地说,应该叫“联合”),所以使程序代码多少显得有些笨拙。在这种情况下,一个内部类可以显著改善代码的“可读性”和执行效率: -``` +``` java //: Counter2i.java // Counter2 using an inner class for the thread import java.awt.*; @@ -300,9 +300,9 @@ public class Counter2i extends Applet { } ///:~ ``` -这个SeparateSubTask名字不会与前例中的SeparateSubTask冲突——即使它们都在相同的目录里——因为它已作为一个内部类隐藏起来。大家亦可看到内部类被设为private(私有)属性,这意味着它的字段和方法都可获得默认的访问权限(run()除外,它必须设为public,因为它在基础类中是公开的)。除Counter2i之外,其他任何方面都不可访问private内部类。而且由于两个类紧密结合在一起,所以很容易放宽它们之间的访问限制。在SeparateSubTask中,我们可看到invertFlag()方法已被删去,因为Counter2i现在可以直接访问runFlag。 +这个SeparateSubTask名字不会与前例中的SeparateSubTask冲突——即使它们都在相同的目录里——因为它已作为一个内部类隐藏起来。大家亦可看到内部类被设为private(私有)属性,这意味着它的字段和方法都可获得默认的访问权限(run()除外,它必须设为public,因为它在基础类中是公开的)。除Counter2i之外,其他任何方面都不可访问private内部类。而且由于两个类紧密结合在一起,所以很容易放宽它们之间的访问限制。在SeparateSubTask中,我们可看到invertFlag()方法已被删去,因为SeparateSubTask现在可以直接访问runFlag。 -此外,注意SeparateSubTask的构建器已得到了简化——它现在唯一的用外就是启动线程。Counter2i对象的句柄仍象以前那样得以捕获,但不再是通过人工传递和引用外部对象来达到这一目的,此时的内部类机制可以自动照料它。在run()中,可看到对t的访问是直接进行的,似乎它是SeparateSubTask的一个字段。父类中的t字段现在可以变成private,因为SeparateSubTask能在未获任何特殊许可的前提下自由地访问它——而且无论如何都该尽可能地把字段变成“私有”属性,以防来自类外的某种力量不慎地改变它们。 +此外,注意SeparateSubTask的构造器已得到了简化——它现在唯一的用外就是启动线程。Counter2i对象的指针仍象以前那样得以捕获,但不再是通过人工传递和引用外部对象来达到这一目的,此时的内部类机制可以自动照料它。在run()中,可看到对t的访问是直接进行的,似乎它是SeparateSubTask的一个字段。父类中的t字段现在可以变成private,因为SeparateSubTask能在未获任何特殊许可的前提下自由地访问它——而且无论如何都该尽可能地把字段变成“私有”属性,以防来自类外的某种力量不慎地改变它们。 无论在什么时候,只要注意到类相互之间结合得比较紧密,就可考虑利用内部类来改善代码的编写与维护。 @@ -312,7 +312,7 @@ public class Counter2i extends Applet { 对合并后的程序/线程来说,它的用法不是十分明确。当我们启动程序时,会创建一个Runnable(可运行的)对象,但不会自行启动线程。线程的启动必须明确进行。下面这个程序向我们演示了这一点,它再现了Counter2的功能: -``` +``` java //: Counter3.java // Using the Runnable interface to turn the // main class into a thread. @@ -378,13 +378,13 @@ public class Counter3 现在run()位于类内,但它在init()结束以后仍处在“睡眠”状态。若按下启动按钮,线程便会用多少有些暧昧的表达方式创建(若线程尚不存在): -``` +``` java new Thread(Counter3.this); ``` -若某样东西有一个Runnable接口,实际只是意味着它有一个run()方法,但不存在与之相关的任何特殊东西——它不具有任何天生的线程处理能力,这与那些从Thread继承的类是不同的。所以为了从一个Runnable对象产生线程,必须单独创建一个线程,并为其传递Runnable对象;可为其使用一个特殊的构建器,并令其采用一个Runnable作为自己的参数使用。随后便可为那个线程调用start(),如下所示: +若某样东西有一个Runnable接口,实际只是意味着它有一个run()方法,但不存在与之相关的任何特殊东西——它不具有任何天生的线程处理能力,这与那些从Thread继承的类是不同的。所以为了从一个Runnable对象产生线程,必须单独创建一个线程,并为其传递Runnable对象;可为其使用一个特殊的构造器,并令其采用一个Runnable作为自己的参数使用。随后便可为那个线程调用start(),如下所示: -``` +``` java selfThread.start(); ``` @@ -398,9 +398,9 @@ Runnable接口最大的一个优点是所有东西都从属于相同的类。若 现在考虑一下创建多个不同的线程的问题。我们不可用前面的例子来做到这一点,所以必须倒退回去,利用从Thread继承的多个独立类来封装run()。但这是一种更常规的方案,而且更易理解,所以尽管前例揭示了我们经常都能看到的编码样式,但并不推荐在大多数情况下都那样做,因为它只是稍微复杂一些,而且灵活性稍低一些。 -下面这个例子用计数器和切换按钮再现了前面的编码样式。但这一次,一个特定计数器的所有信息(按钮和文本字段)都位于它自己的、从Thread继承的对象内。Ticker中的所有字段都具有private(私有)属性,这意味着Ticker的具体实现方案可根据实际情况任意修改,其中包括修改用于获取和显示信息的数据组件的数量及类型。创建好一个Ticker对象以后,构建器便请求一个AWT容器(Container)的句柄——Ticker用自己的可视组件填充那个容器。采用这种方式,以后一旦改变了可视组件,使用Ticker的代码便不需要另行修改一道。 +下面这个例子用计数器和切换按钮再现了前面的编码样式。但这一次,一个特定计数器的所有信息(按钮和文本字段)都位于它自己的、从Thread继承的对象内。Ticker中的所有字段都具有private(私有)属性,这意味着Ticker的具体实现方案可根据实际情况任意修改,其中包括修改用于获取和显示信息的数据组件的数量及类型。创建好一个Ticker对象以后,构造器便请求一个AWT容器(Container)的指针——Ticker用自己的可视组件填充那个容器。采用这种方式,以后一旦改变了可视组件,使用Ticker的代码便不需要另行修改一道。 -``` +``` java //: Counter4.java // If you separate your thread from the main // class, you can have as many threads as you @@ -491,7 +491,7 @@ Ticker不仅包括了自己的线程处理机制,也提供了控制与显示 在Counter4中,有一个名为s的Ticker对象的数组。为获得最大的灵活性,这个数组的长度是用程序片参数接触Web页而初始化的。下面是网页中长度参数大致的样子,它们嵌于对程序片(applet)的描述内容中: -``` +``` java @@ -501,16 +501,16 @@ Ticker不仅包括了自己的线程处理机制,也提供了控制与显示 我们注意到对数组s长度的判断是在init()内部完成的,它没有作为s的内嵌定义的一部分提供。换言之,不可将下述代码作为类定义的一部分使用(应该位于任何方法的外部): -``` +``` java inst size = Integer.parseInt(getParameter("Size")); Ticker[] s = new Ticker[size] ``` -可把它编译出来,但会在运行期得到一个空指针违例。但若将getParameter()初始化移入init(),则可正常工作。程序片框架会进行必要的启动工作,以便在进入init()前收集好一些参数。 +可把它编译出来,但会在运行期得到一个空指针异常。但若将getParameter()初始化移入init(),则可正常工作。程序片框架会进行必要的启动工作,以便在进入init()前收集好一些参数。 此外,上述代码被同时设置成一个程序片和一个应用(程序)。在它是应用程序的情况下,size参数可从命令行里提取出来(否则就提供一个默认的值)。 -数组的长度建好以后,就可以创建新的Ticker对象;作为Ticker构建器的一部分,用于每个Ticker的按钮和文本字段就会加入程序片。 +数组的长度建好以后,就可以创建新的Ticker对象;作为Ticker构造器的一部分,用于每个Ticker的按钮和文本字段就会加入程序片。 按下Start按钮后,会在整个Ticker数组里遍历,并为每个Ticker调用start()。记住,start()会进行必要的线程初始化工作,然后为那个线程调用run()。 @@ -518,7 +518,7 @@ ToggleL监视器只是简单地切换Ticker中的标记,一旦对应线程以 这个例子的一个好处是它使我们能够方便地创建由单独子任务构成的大型集合,并以监视它们的行为。在这种情况下,我们会发现随着子任务数量的增多,机器显示出来的数字可能会出现更大的分歧,这是由于为线程提供服务的方式造成的。 -亦可试着体验一下sleep(100)在Ticker.run()中的重要作用。若删除sleep(),那么在按下一个切换按钮前,情况仍然会进展良好。按下按钮以后,那个特定的线程就会出现一个失败的runFlag,而且run()会深深地陷入一个无限循环——很难在多任务处理期间中止退出。因此,程序对用户操作的反应灵敏度会大幅度降低。 +亦可试着体验一下sleep(100)在Ticker.run()中的重要作用。若删除sleep(),那么在按下一个切换按钮前,情况仍然会进展良好。按下按钮以后,那个特定的线程就会出现一个false的runFlag,而且run()会深深地陷入一个无限循环——很难在多任务处理期间中止退出。因此,程序对用户操作的反应灵敏度会大幅度降低。 14.1.5 Daemon线程 @@ -529,7 +529,7 @@ Daemon线程完成,程序也会中止运行。相反,假若有任何非Daemo 下面这个例子演示了Daemon线程的用法: -``` +``` java //: Daemons.java // Daemonic behavior import java.io.*; diff --git "a/14.2 \345\205\261\344\272\253\346\234\211\351\231\220\347\232\204\350\265\204\346\272\220.md" "b/14.2 \345\205\261\344\272\253\346\234\211\351\231\220\347\232\204\350\265\204\346\272\220.md" index 957d508..e36826a 100644 --- "a/14.2 \345\205\261\344\272\253\346\234\211\351\231\220\347\232\204\350\265\204\346\272\220.md" +++ "b/14.2 \345\205\261\344\272\253\346\234\211\351\231\220\347\232\204\350\265\204\346\272\220.md" @@ -9,7 +9,7 @@ 现在考虑换成另一种方式来使用本章频繁见到的计数器。在下面的例子中,每个线程都包含了两个计数器,它们在run()里增值以及显示。除此以外,我们使用了Watcher类的另一个线程。它的作用是监视计数器,检查它们是否保持相等。这表面是一项无意义的行动,因为如果查看代码,就会发现计数器肯定是相同的。但实际情况却不一定如此。下面是程序的第一个版本: -``` +``` java //: Sharing1.java // Problems with resource sharing while threading import java.awt.*; @@ -146,7 +146,7 @@ public class Sharing1 extends Applet { } ///:~ ``` -和往常一样,每个计数器都包含了自己的显示组件:两个文本字段以及一个标签。根据它们的初始值,可知道计数是相同的。这些组件在TwoCounter构建器加入Container。由于这个线程是通过用户的一个“按下按钮”操作启动的,所以start()可能被多次调用。但对一个线程来说,对Thread.start()的多次调用是非法的(会产生违例)。在started标记和过载的start()方法中,大家可看到针对这一情况采取的防范措施。 +和往常一样,每个计数器都包含了自己的显示组件:两个文本字段以及一个标签。根据它们的初始值,可知道计数是相同的。这些组件在TwoCounter构造器加入Container。由于这个线程是通过用户的一个“按下按钮”操作启动的,所以start()可能被多次调用。但对一个线程来说,对Thread.start()的多次调用是非法的(会产生异常)。在started标记和重载的start()方法中,大家可看到针对这一情况采取的防范措施。 在run()中,count1和count2的增值与显示方式表面上似乎能保持它们完全一致。随后会调用sleep();若没有这个调用,程序便会出错,因为那会造成CPU难于交换任务。 @@ -158,7 +158,7 @@ Sharing1包含了TwoCounter对象的一个数组,它通过init()进行初始 注意为了让它作为一个程序片在浏览器中运行,Web页需要包含下面这几行: -``` +``` java @@ -169,7 +169,7 @@ Sharing1包含了TwoCounter对象的一个数组,它通过init()进行初始 下面才是最让人“不可思议”的。在TwoCounter.run()中,无限循环只是不断地重复相邻的行: -``` +``` java t1.setText(Integer.toString(count1++)); t2.setText(Integer.toString(count2++)); ``` @@ -187,7 +187,7 @@ t2.setText(Integer.toString(count2++)); 对一种特殊的资源——对象中的内存——Java提供了内建的机制来防止它们的冲突。由于我们通常将数据元素设为从属于private(私有)类,然后只通过方法访问那些内存,所以只需将一个特定的方法设为synchronized(同步的),便可有效地防止冲突。在任何时刻,只可有一个线程调用特定对象的一个synchronized方法(尽管那个线程可以调用多个对象的同步方法)。下面列出简单的synchronized方法: -``` +``` java synchronized void f() { /* ... */ } synchronized void g() { /* ... */ } ``` @@ -202,7 +202,7 @@ synchronized void g() { /* ... */ } 装备了这个新关键字后,我们能够采取的方案就更灵活了:可以只为TwoCounter中的方法简单地使用synchronized关键字。下面这个例子是对前例的改版,其中加入了新的关键字: -``` +``` java //: Sharing2.java // Using the synchronized keyword to prevent // multiple access to a particular resource. @@ -352,7 +352,7 @@ synchronized(syncObject) { 在能进入同步块之前,必须在synchObject上取得锁。如果已有其他线程取得了这把锁,块便不能进入,必须等候那把锁被释放。 可从整个run()中删除synchronized关键字,换成用一个同步块包围两个关键行,从而完成对Sharing2例子的修改。但什么对象应作为锁来使用呢?那个对象已由synchTest()标记出来了——也就是当前对象(this)!所以修改过的run()方法象下面这个样子: -``` +``` java public void run() { while (true) { synchronized(this) { @@ -384,7 +384,7 @@ synchronized(syncObject) { 第一点很容易处理,但第二点需要考虑更多的东西。让我们以前一章提供的BangBean.java为例。在那个例子中,我们忽略了synchronized关键字(那时还没有引入呢),并将造型设为单造型,从而回避了多线程的问题。在下面这个修改过的版本中,我们使其能在多线程环境中工作,并为事件采用了多造型技术: -``` +``` java //: BangBean2.java // You should write your Beans this way so they // can run in a multithreaded environment. @@ -526,12 +526,12 @@ public class BangBean2 extends Canvas 我们注意到,notifyListeners()方法并未设为“同步”。可从多个线程中发出对这个方法的调用。另外,在对notifyListeners()调用的中途,也可能发出对addActionListener()和removeActionListener()的调用。这显然会造成问题,因为它否定了Vector actionListeners。为缓解这个问题,我们在一个synchronized从句中“克隆”了Vector,并对克隆进行了否定。这样便可在不影响notifyListeners()的前提下,对Vector进行操纵。 -paint()方法也没有设为“同步”。与单纯地添加自己的方法相比,决定是否对过载的方法进行同步要困难得多。在这个例子中,无论paint()是否“同步”,它似乎都能正常地工作。但必须考虑的问题包括: +paint()方法也没有设为“同步”。与单纯地添加自己的方法相比,决定是否对重载的方法进行同步要困难得多。在这个例子中,无论paint()是否“同步”,它似乎都能正常地工作。但必须考虑的问题包括: (1) 方法会在对象内部修改“关键”变量的状态吗?为判断一个变量是否“关键”,必须知道它是否会被程序中的其他线程读取或设置(就目前的情况看,读取或设置几乎肯定是通过“同步”方法进行的,所以可以只对它们进行检查)。对paint()的情况来说,不会发生任何修改。 (2) 方法要以这些“关键”变量的状态为基础吗?如果一个“同步”方法修改了一个变量,而我们的方法要用到这个变量,那么一般都愿意把自己的方法也设为“同步”。基于这一前提,大家可观察到cSize由“同步”方法进行了修改,所以paint()应当是“同步”的。但在这里,我们可以问:“假如cSize在paint()执行期间发生了变化,会发生的最糟糕的事情是什么呢?”如果发现情况不算太坏,而且仅仅是暂时的效果,那么最好保持paint()的“不同步”状态,以避免同步方法调用带来的额外开销。 -(3) 要留意的第三条线索是paint()基础类版本是否“同步”,在这里它不是同步的。这并不是一个非常严格的参数,仅仅是一条“线索”。比如在目前的情况下,通过同步方法(好cSize)改变的一个字段已合成到paint()公式里,而且可能已改变了情况。但请注意,synchronized不能继承——也就是说,假如一个方法在基础类中是“同步”的,那么在衍生类过载版本中,它不会自动进入“同步”状态。 +(3) 要留意的第三条线索是paint()基础类版本是否“同步”,在这里它不是同步的。这并不是一个非常严格的参数,仅仅是一条“线索”。比如在目前的情况下,通过同步方法(好cSize)改变的一个字段已合成到paint()公式里,而且可能已改变了情况。但请注意,synchronized不能继承——也就是说,假如一个方法在基础类中是“同步”的,那么在衍生类重载版本中,它不会自动进入“同步”状态。 TestBangBean2中的测试代码已在前一章的基础上进行了修改,已在其中加入了额外的“听众”,从而演示了BangBean2的多造型能力。 diff --git "a/14.3 \345\240\265\345\241\236.md" "b/14.3 \345\240\265\345\241\236.md" index e670808..0c7ef99 100644 --- "a/14.3 \345\240\265\345\241\236.md" +++ "b/14.3 \345\240\265\345\241\236.md" @@ -7,8 +7,8 @@ (2) 可运行(Runnable):意味着一旦时间分片机制有空闲的CPU周期提供给一个线程,那个线程便可立即开始运行。因此,线程可能在、也可能不在运行当中,但一旦条件许可,没有什么能阻止它的运行——它既没有“死”掉,也未被“堵塞”。 -(3) 死(Dead):从自己的run()方法中返回后,一个线程便已“死”掉。亦可调用stop()令其死掉,但会产生一个违例——属于 -Error的一个子类(也就是说,我们通常不捕获它)。记住一个违例的“掷”出应当是一个特殊事件,而不是正常程序运行的一部分。所以不建议你使用stop()(在Java 1.2则是坚决反对)。另外还有一个destroy()方法(它永远不会实现),应该尽可能地避免调用它,因为它非常武断,根本不会解除对象的锁定。 +(3) 死(Dead):从自己的run()方法中返回后,一个线程便已“死”掉。亦可调用stop()令其死掉,但会产生一个异常——属于 +Error的一个子类(也就是说,我们通常不捕获它)。记住一个异常的“掷”出应当是一个特殊事件,而不是正常程序运行的一部分。所以不建议你使用stop()(在Java 1.2则是坚决反对)。另外还有一个destroy()方法(它永远不会实现),应该尽可能地避免调用它,因为它非常武断,根本不会解除对象的锁定。 (4) 堵塞(Blocked):线程可以运行,但有某种东西阻碍了它。若线程处于堵塞状态,调度机制可以简单地跳过它,不给它分配任何CPU时间。除非线程再次进入“可运行”状态,否则不会采取任何操作。 @@ -30,7 +30,7 @@ Error的一个子类(也就是说,我们通常不捕获它)。记住一个 下面这个例子展示了进入堵塞状态的全部五种途径。它们全都存在于名为Blocking.java的一个文件中,但在这儿采用散落的片断进行解释(大家可注意到片断前后的“Continued”以及“Continuing”标志。利用第17章介绍的工具,可将这些片断连结到一起)。首先让我们看看基本的框架: -``` +``` java //: Blocking.java // Demonstrates the various ways a thread // can be blocked. @@ -93,7 +93,7 @@ Blockable类打算成为本例所有类的一个基础类。一个Blockable对 这个程序的第一项测试是用sleep()作出的: -``` +``` java ///:Continuing ///////////// Blocking via sleep() /////////// class Sleeper1 extends Blockable { @@ -134,7 +134,7 @@ Sleeper2通过设置不同步的运行,提供了一种解决方案。只有cha 这个例子接下来的一部分引入了“挂起”或者“暂停”(Suspend)的概述。Thread类提供了一个名为suspend()的方法,可临时中止线程;以及一个名为resume()的方法,用于从暂停处开始恢复线程的执行。显然,我们可以推断出resume()是由暂停线程外部的某个线程调用的。在这种情况下,需要用到一个名为Resumer(恢复器)的独立类。演示暂停/恢复过程的每个类都有一个相关的恢复器。如下所示: -``` +``` java ///:Continuing /////////// Blocking via suspend() /////////// class SuspendResume extends Blockable { @@ -197,19 +197,19 @@ SuspendResume1也提供了一个同步的run()方法。同样地,当我们启 我们也可以看到wait()的两种形式。第一种形式采用一个以毫秒为单位的参数,它具有与sleep()中相同的含义:暂停这一段规定时间。区别在于在wait()中,对象锁已被解除,而且能够自由地退出wait(),因为一个notify()可强行使时间流逝。 第二种形式不采用任何参数,这意味着wait()会持续执行,直到notify()介入为止。而且在一段时间以后,不会自行中止。 -wait()和notify()比较特别的一个地方是这两个方法都属于基础类Object的一部分,不象sleep(),suspend()以及resume()那样属于Thread的一部分。尽管这表面看有点儿奇怪——居然让专门进行线程处理的东西成为通用基础类的一部分——但仔细想想又会释然,因为它们操纵的对象锁也属于每个对象的一部分。因此,我们可将一个wait()置入任何同步方法内部,无论在那个类里是否准备进行涉及线程的处理。事实上,我们能调用wait()的唯一地方是在一个同步的方法或代码块内部。若在一个不同步的方法内调用wait()或者notify(),尽管程序仍然会编译,但在运行它的时候,就会得到一个IllegalMonitorStateException(非法监视器状态违例),而且会出现多少有点莫名其妙的一条消息:“current thread not owner”(当前线程不是所有人”。注意sleep(),suspend()以及resume()都能在不同步的方法内调用,因为它们不需要对锁定进行操作。 +wait()和notify()比较特别的一个地方是这两个方法都属于基础类Object的一部分,不象sleep(),suspend()以及resume()那样属于Thread的一部分。尽管这表面看有点儿奇怪——居然让专门进行线程处理的东西成为通用基础类的一部分——但仔细想想又会释然,因为它们操纵的对象锁也属于每个对象的一部分。因此,我们可将一个wait()置入任何同步方法内部,无论在那个类里是否准备进行涉及线程的处理。事实上,我们能调用wait()的唯一地方是在一个同步的方法或代码块内部。若在一个不同步的方法内调用wait()或者notify(),尽管程序仍然会编译,但在运行它的时候,就会得到一个IllegalMonitorStateException(非法监视器状态异常),而且会出现多少有点莫名其妙的一条消息:“current thread not owner”(当前线程不是所有人”。注意sleep(),suspend()以及resume()都能在不同步的方法内调用,因为它们不需要对锁定进行操作。 -只能为自己的锁定调用wait()和notify()。同样地,仍然可以编译那些试图使用错误锁定的代码,但和往常一样会产生同样的IllegalMonitorStateException违例。我们没办法用其他人的对象锁来愚弄系统,但可要求另一个对象执行相应的操作,对它自己的锁进行操作。所以一种做法是创建一个同步方法,令其为自己的对象调用notify()。但在Notifier中,我们会看到一个同步方法内部的notify(): +只能为自己的锁定调用wait()和notify()。同样地,仍然可以编译那些试图使用错误锁定的代码,但和往常一样会产生同样的IllegalMonitorStateException异常。我们没办法用其他人的对象锁来愚弄系统,但可要求另一个对象执行相应的操作,对它自己的锁进行操作。所以一种做法是创建一个同步方法,令其为自己的对象调用notify()。但在Notifier中,我们会看到一个同步方法内部的notify(): -``` +``` java synchronized(wn2) { wn2.notify(); } ``` -其中,wn2是类型为WaitNotify2的对象。尽管并不属于WaitNotify2的一部分,这个方法仍然获得了wn2对象的锁定。在这个时候,它为wn2调用notify()是合法的,不会得到IllegalMonitorStateException违例。 +其中,wn2是类型为WaitNotify2的对象。尽管并不属于WaitNotify2的一部分,这个方法仍然获得了wn2对象的锁定。在这个时候,它为wn2调用notify()是合法的,不会得到IllegalMonitorStateException异常。 -``` +``` java ///:Continuing /////////// Blocking via wait() /////////// class WaitNotify1 extends Blockable { @@ -268,7 +268,7 @@ class Notifier extends Thread { Sender将数据置入Writer,并“睡眠”随机长短的时间。然而,Receiver本身并没有包括sleep(),suspend()或者wait()方法。但在执行read()的时候,如果没有数据存在,它会自动进入“堵塞”状态。如下所示: -``` +``` java ///:Continuing class Sender extends Blockable { // send private Writer out; @@ -317,11 +317,11 @@ class Receiver extends Blockable { 令人惊讶的是,主要的程序片(Applet)类非常简单,这是大多数工作都已置入Blockable框架的缘故。大概地说,我们创建了一个由Blockable对象构成的数组。而且由于每个对象都是一个线程,所以在按下“start”按钮后,它们会采取自己的行动。还有另一个按钮和actionPerformed()从句,用于中止所有Peeker对象。由于Java 1.2“反对”使用Thread的stop()方法,所以可考虑采用这种折衷形式的中止方式。 -为了在Sender和Receiver之间建立一个连接,我们创建了一个PipedWriter和一个PipedReader。注意PipedReader in必须通过一个构建器参数同PipedWriterout连接起来。在那以后,我们在out内放进去的所有东西都可从in中提取出来——似乎那些东西是通过一个“管道”传输过去的。随后将in和out对象分别传递给Receiver和Sender构建器;后者将它们当作任意类型的Reader和Writer看待(也就是说,它们被“上溯”造型了)。 +为了在Sender和Receiver之间建立一个连接,我们创建了一个PipedWriter和一个PipedReader。注意PipedReader in必须通过一个构造器参数同PipedWriterout连接起来。在那以后,我们在out内放进去的所有东西都可从in中提取出来——似乎那些东西是通过一个“管道”传输过去的。随后将in和out对象分别传递给Receiver和Sender构造器;后者将它们当作任意类型的Reader和Writer看待(也就是说,它们被“上溯”造型了)。 -Blockable句柄b的数组在定义之初并未得到初始化,因为管道化的数据流是不可在定义前设置好的(对try块的需要将成为障碍): +Blockable指针b的数组在定义之初并未得到初始化,因为管道化的数据流是不可在定义前设置好的(对try块的需要将成为障碍): -``` +``` java ///:Continuing /////////// Testing Everything /////////// public class Blocking extends Applet { @@ -405,7 +405,7 @@ public class Blocking extends Applet { 如果一个线程被堵塞,比如在它等候输入的时候,那么一般都不能象在Blocking.java中那样轮询一个标志。但在这些情况下,我们仍然不该使用stop(),而应换用由Thread提供的interrupt()方法,以便中止并退出堵塞的代码。 -``` +``` java //: Interrupt.java // The alternative approach to using stop() // when a thread is blocked @@ -461,11 +461,11 @@ public class Interrupt extends Applet { } ///:~ ``` -Blocked.run()内部的wait()会产生堵塞的线程。当我们按下按钮以后,blocked(堵塞)的句柄就会设为null,使垃圾收集器能够将其清除,然后调用对象的interrupt()方法。如果是首次按下按钮,我们会看到线程正常退出。但在没有可供“杀死”的线程以后,看到的便只是按钮被按下而已。 +Blocked.run()内部的wait()会产生堵塞的线程。当我们按下按钮以后,blocked(堵塞)的指针就会设为null,使垃圾收集器能够将其清除,然后调用对象的interrupt()方法。如果是首次按下按钮,我们会看到线程正常退出。但在没有可供“杀死”的线程以后,看到的便只是按钮被按下而已。 suspend()和resume()方法天生容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被“挂起”的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成令人难堪的死锁。所以我们不应该使用suspend()和resume(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。我们可以修改前面的Counter2.java来实际体验一番。尽管两个版本的效果是差不多的,但大家会注意到代码的组织结构发生了很大的变化——为所有“听众”都使用了匿名的内部类,而且Thread是一个内部类。这使得程序的编写稍微方便一些,因为它取消了Counter2.java中一些额外的记录工作。 -``` +``` java //: Suspend.java // The alternative approach to using suspend() // and resume(), which have been deprecated diff --git "a/14.4 \344\274\230\345\205\210\347\272\247.md" "b/14.4 \344\274\230\345\205\210\347\272\247.md" index 4e2138f..bd36d2b 100644 --- "a/14.4 \344\274\230\345\205\210\347\272\247.md" +++ "b/14.4 \344\274\230\345\205\210\347\272\247.md" @@ -7,7 +7,7 @@ 可用getPriority()方法读取一个线程的优先级,并用setPriority()改变它。在下面这个程序片中,大家会发现计数器的计数速度慢了下来,因为它们关联的线程分配了较低的优先级: -``` +``` java //: Counter5.java // Adjusting the priorities of threads import java.awt.*; @@ -158,7 +158,7 @@ Ticker采用本章前面构造好的形式,但有一个额外的TextField( 也要注意yield()的用法,它将控制权自动返回给调试程序(机制)。若不进行这样的处理,多线程机制仍会工作,但我们会发现它的运行速度慢了下来(试试删去对yield()的调用)。亦可调用sleep(),但假若那样做,计数频率就会改由sleep()的持续时间控制,而不是优先级。 -Counter5中的init()创建了由10个Ticker2构成的一个数组;它们的按钮以及输入字段(文本字段)由Ticker2构建器置入窗体。Counter5增加了新的按钮,用于启动一切,以及用于提高和降低线程组的最大优先级。除此以外,还有一些标签用于显示一个线程可以采用的最大及最小优先级;以及一个特殊的文本字段,用于显示线程组的最大优先级(在下一节里,我们将全面讨论线程组的问题)。最后,父线程组的优先级也作为标签显示出来。 +Counter5中的init()创建了由10个Ticker2构成的一个数组;它们的按钮以及输入字段(文本字段)由Ticker2构造器置入窗体。Counter5增加了新的按钮,用于启动一切,以及用于提高和降低线程组的最大优先级。除此以外,还有一些标签用于显示一个线程可以采用的最大及最小优先级;以及一个特殊的文本字段,用于显示线程组的最大优先级(在下一节里,我们将全面讨论线程组的问题)。最后,父线程组的优先级也作为标签显示出来。 按下“up”(上)或“down”(下)按钮的时候,会先取得Ticker2当前的优先级,然后相应地提高或者降低。 运行该程序时,我们可注意到几件事情。首先,线程组的默认优先级是5。即使在启动线程之前(或者在创建线程之前,这要求对代码进行适当的修改)将最大优先级降到5以下,每个线程都会有一个5的默认优先级。 @@ -171,10 +171,10 @@ Counter5中的init()创建了由10个Ticker2构成的一个数组;它们的按 所有线程都隶属于一个线程组。那可以是一个默认线程组,亦可是一个创建线程时明确指定的组。在创建之初,线程被限制到一个组里,而且不能改变到一个不同的组。每个应用都至少有一个线程从属于系统线程组。若创建多个线程而不指定一个组,它们就会自动归属于系统线程组。 -线程组也必须从属于其他线程组。必须在构建器里指定新线程组从属于哪个线程组。若在创建一个线程组的时候没有指定它的归属,则同样会自动成为系统线程组的一名属下。因此,一个应用程序中的所有线程组最终都会将系统线程组作为自己的“父”。 +线程组也必须从属于其他线程组。必须在构造器里指定新线程组从属于哪个线程组。若在创建一个线程组的时候没有指定它的归属,则同样会自动成为系统线程组的一名属下。因此,一个应用程序中的所有线程组最终都会将系统线程组作为自己的“父”。 之所以要提出“线程组”的概念,很难从字面上找到原因。这多少为我们讨论的主题带来了一些混乱。一般地说,我们认为是由于“安全”或者“保密”方面的理由才使用线程组的。根据Arnold和Gosling的说法:“线程组中的线程可以修改组内的其他线程,包括那些位于分层结构最深处的。一个线程不能修改位于自己所在组或者下属组之外的任何线程”(注释①)。然而,我们很难判断“修改”在这儿的具体含义是什么。下面这个例子展示了位于一个“叶子组”内的线程能修改它所在线程组树的所有线程的优先级,同时还能为这个“树”内的所有线程都调用一个方法。 -``` +``` java ①:《The Java Programming Language》第179页。该书由Arnold和Jams Gosling编著,Addison-Wesley于1996年出版 //: TestAccess.java // How threads can access other threads @@ -226,11 +226,11 @@ class TestThread2 extends TestThread1 { 在main()中,我们创建了几个ThreadGroup(线程组),每个都位于不同的“叶”上:x没有参数,只有它的名字(一个String),所以会自动进入“system”(系统)线程组;y位于x下方,而z位于y下方。注意初始化是按照文字顺序进行的,所以代码合法。 -有两个线程创建之后进入了不同的线程组。其中,TestThread1没有一个run()方法,但有一个f(),用于通知线程以及打印出一些东西,以便我们知道它已被调用。而TestThread2属于TestThread1的一个子类,它的run()非常详尽,要做许多事情。首先,它获得当前线程所在的线程组,然后利用getParent()在继承树中向上移动两级(这样做是有道理的,因为我想把TestThread2在分级结构中向下移动两级)。随后,我们调用方法activeCount(),查询这个线程组以及所有子线程组内有多少个线程,从而创建由指向Thread的句柄构成的一个数组。enumerate()方法将指向所有这些线程的句柄置入数组gAll里。然后在整个数组里遍历,为每个线程都调用f()方法,同时修改优先级。这样一来,位于一个“叶子”线程组里的线程就修改了位于父线程组的线程。 +有两个线程创建之后进入了不同的线程组。其中,TestThread1没有一个run()方法,但有一个f(),用于通知线程以及打印出一些东西,以便我们知道它已被调用。而TestThread2属于TestThread1的一个子类,它的run()非常详尽,要做许多事情。首先,它获得当前线程所在的线程组,然后利用getParent()在继承树中向上移动两级(这样做是有道理的,因为我想把TestThread2在分级结构中向下移动两级)。随后,我们调用方法activeCount(),查询这个线程组以及所有子线程组内有多少个线程,从而创建由指向Thread的指针构成的一个数组。enumerate()方法将指向所有这些线程的指针置入数组gAll里。然后在整个数组里遍历,为每个线程都调用f()方法,同时修改优先级。这样一来,位于一个“叶子”线程组里的线程就修改了位于父线程组的线程。 调试方法list()打印出与一个线程组有关的所有信息,把它们作为标准输出。在我们对线程组的行为进行调查的时候,这样做是相当有好处的。下面是程序的输出: -``` +``` java java.lang.ThreadGroup[name=x,maxpri=10] Thread[one,5,x] java.lang.ThreadGroup[name=y,maxpri=10] @@ -252,7 +252,7 @@ list()不仅打印出ThreadGroup或者Thread的类名,也打印出了线程组 抛开安全问题不谈,线程组最有用的一个地方就是控制:只需用单个命令即可完成对整个线程组的操作。下面这个例子演示了这一点,并对线程组内优先级的限制进行了说明。括号内的注释数字便于大家比较输出结果: -``` +``` java //: ThreadGroup1.java // How thread groups control priorities // of the threads inside them. @@ -326,7 +326,7 @@ public class ThreadGroup1 { 下面的输出结果已进行了适当的编辑,以便用一页能够装下(java.lang.已被删去),而且添加了适当的数字,与前面程序列表中括号里的数字对应: -``` +``` java (1) ThreadGroup[name=system,maxpri=10] Thread[main,5,system] (2) ThreadGroup[name=system,maxpri=9] @@ -366,7 +366,7 @@ All threads started 所有程序都至少有一个线程在运行,而且main()采取的第一项行动便是调用Thread的一个static(静态)方法,名为currentThread()。从这个线程开始,线程组将被创建,而且会为结果调用list()。输出如下: -``` +``` java (1) ThreadGroup[name=system,maxpri=10] Thread[main,5,system] ``` @@ -374,22 +374,22 @@ All threads started 我们可以看到,主线程组的名字是system,而主线程的名字是main,而且它从属于system线程组。 第二个练习显示出system组的最高优先级可以减少,而且main线程可以增大自己的优先级: -``` +``` java (2) ThreadGroup[name=system,maxpri=9] Thread[main,6,system] ``` 第三个练习创建一个新的线程组,名为g1;它自动从属于system线程组,因为并没有明确指定它的归属关系。我们在g1内部放置了一个新线程,名为A。随后,我们试着将这个组的最大优先级设到最高的级别,并将A的优先级也设到最高一级。结果如下: -``` +``` java (3) ThreadGroup[name=g1,maxpri=9] Thread[A,9,g1] -``` +``` java 可以看出,不可能将线程组的最大优先级设为高于它的父线程组。 第四个练习将g1的最大优先级降低两级,然后试着把它升至Thread.MAX_PRIORITY。结果如下: -``` +``` java (4) ThreadGroup[name=g1,maxpri=8] Thread[A,9,g1] ``` @@ -397,7 +397,7 @@ All threads started 同样可以看出,提高最大优先级的企图是失败的。我们只能降低一个线程组的最大优先级,而不能提高它。此外,注意线程A的优先级并未改变,而且它现在高于线程组的最大优先级。也就是说,线程组最大优先级的变化并不能对现有线程造成影响。 第五个练习试着将一个新线程设为最大优先级。如下所示: -``` +``` java (5) ThreadGroup[name=g1,maxpri=8] Thread[A,9,g1] Thread[B,8,g1] @@ -406,7 +406,7 @@ All threads started 因此,新线程不能变到比最大线程组优先级还要高的一级。 这个程序的默认线程优先级是6;若新建一个线程,那就是它的默认优先级,而且不会发生变化,除非对优先级进行了特别的处理。练习六将把线程组的最大优先级降至默认线程优先级以下,看看在这种情况下新建一个线程会发生什么事情: -``` +``` java (6) ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] @@ -416,7 +416,7 @@ All threads started 尽管线程组现在的最大优先级是3,但仍然用默认优先级6来创建新线程。所以,线程组的最大优先级不会影响默认优先级(事实上,似乎没有办法可以设置新线程的默认优先级)。 改变了优先级后,接下来试试将其降低一级,结果如下: -``` +``` java (7) ThreadGroup[name=g1,maxpri=3] Thread[A,9,g1] Thread[B,8,g1] @@ -426,7 +426,7 @@ All threads started 因此,只有在试图改变优先级的时候,才会强迫遵守线程组最大优先级的限制。 我们在(8)和(9)中进行了类似的试验。在这里,我们创建了一个新的线程组,名为g2,将其作为g1的一个子组,并改变了它的最大优先级。大家可以看到,g2的优先级无论如何都不可能高于g1: -``` +``` java (8) ThreadGroup[name=g2,maxpri=3] (9) ThreadGroup[name=g2,maxpri=3] ``` @@ -434,7 +434,7 @@ All threads started 也要注意在g2创建的时候,它会被自动设为g1的线程组最大优先级。 经过所有这些实验以后,整个线程组和线程系统都会被打印出来,如下所示: -``` +``` java (10)ThreadGroup[name=system,maxpri=9] Thread[main,6,system] ThreadGroup[name=g1,maxpri=3] @@ -451,7 +451,7 @@ All threads started 所以由线程组的规则所限,一个子组的最大优先级在任何时候都只能低于或等于它的父组的最大优先级。 -本程序的最后一个部分演示了用于整组线程的方法。程序首先遍历整个线程树,并启动每一个尚未启动的线程。例如,system组随后会被挂起(暂停),最后被中止(尽管用suspend()和stop()对整个线程组进行操作看起来似乎很有趣,但应注意这些方法在Java 1.2里都是被“反对”的)。但在挂起system组的同时,也挂起了main线程,而且整个程序都会关闭。所以永远不会达到让线程中止的那一步。实际上,假如真的中止了main线程,它会“掷”出一个ThreadDeath违例,所以我们通常不这样做。由于ThreadGroup是从Object继承的,其中包含了wait()方法,所以也能调用wait(秒数×1000),令程序暂停运行任意秒数的时间。当然,事前必须在一个同步块里取得对象锁。 +本程序的最后一个部分演示了用于整组线程的方法。程序首先遍历整个线程树,并启动每一个尚未启动的线程。例如,system组随后会被挂起(暂停),最后被中止(尽管用suspend()和stop()对整个线程组进行操作看起来似乎很有趣,但应注意这些方法在Java 1.2里都是被“反对”的)。但在挂起system组的同时,也挂起了main线程,而且整个程序都会关闭。所以永远不会达到让线程中止的那一步。实际上,假如真的中止了main线程,它会“掷”出一个ThreadDeath异常,所以我们通常不这样做。由于ThreadGroup是从Object继承的,其中包含了wait()方法,所以也能调用wait(秒数×1000),令程序暂停运行任意秒数的时间。当然,事前必须在一个同步块里取得对象锁。 ThreadGroup类也提供了suspend()和resume()方法,所以能中止和启动整个线程组和它的所有线程,也能中止和启动它的子组,所有这些只需一个命令即可(再次提醒,suspend()和resume()都是Java 1.2所“反对”的)。 从表面看,线程组似乎有些让人摸不着头脑,但请注意我们很少需要直接使用它们。 diff --git "a/14.5 \345\233\236\351\241\276runnable.md" "b/14.5 \345\233\236\351\241\276runnable.md" index 8622010..e9eb222 100644 --- "a/14.5 \345\233\236\351\241\276runnable.md" +++ "b/14.5 \345\233\236\351\241\276runnable.md" @@ -7,7 +7,7 @@ 当然,如果必须从一个类继承,而且想使类具有线程处理能力,则Runnable是一种正确的方案。本章最后一个例子对这一点进行了剖析,制作了一个RunnableCanvas类,用于为自己描绘不同的颜色(Canvas是“画布”的意思)。这个应用被设计成从命令行获得参数值,以决定颜色网格有多大,以及颜色发生变化之间的sleep()有多长。通过运用这些值,大家能体验到线程一些有趣而且可能令人费解的特性: -``` +``` java //: ColorBoxes.java // Using the Runnable interface import java.awt.*; @@ -76,8 +76,8 @@ public class ColorBoxes extends Frame { } ///:~ ``` -ColorBoxes是一个典型的应用(程序),有一个构建器用于设置GUI。这个构建器采用int grid的一个参数,用它设置GridLayout(网格布局),使每一维里都有一个grid单元。随后,它添加适当数量的CBox对象,用它们填充网格,并为每一个都传递pause值。在main()中,我们可看到如何对pause和grid的默认值进行修改(如果用命令行参数传递)。 -CBox是进行正式工作的地方。它是从Canvas继承的,并实现了Runnable接口,使每个Canvas也能是一个Thread。记住在实现Runnable的时候,并没有实际产生一个Thread对象,只是一个拥有run()方法的类。因此,我们必须明确地创建一个Thread对象,并将Runnable对象传递给构建器,随后调用start()(在构建器里进行)。在CBox里,这个线程的名字叫作t。 +ColorBoxes是一个典型的应用(程序),有一个构造器用于设置GUI。这个构造器采用int grid的一个参数,用它设置GridLayout(网格布局),使每一维里都有一个grid单元。随后,它添加适当数量的CBox对象,用它们填充网格,并为每一个都传递pause值。在main()中,我们可看到如何对pause和grid的默认值进行修改(如果用命令行参数传递)。 +CBox是进行正式工作的地方。它是从Canvas继承的,并实现了Runnable接口,使每个Canvas也能是一个Thread。记住在实现Runnable的时候,并没有实际产生一个Thread对象,只是一个拥有run()方法的类。因此,我们必须明确地创建一个Thread对象,并将Runnable对象传递给构造器,随后调用start()(在构造器里进行)。在CBox里,这个线程的名字叫作t。 请留意数组colors,它对Color类中的所有颜色进行了列举(枚举)。它在newColor()中用于产生一种随机选择的颜色。当前的单元(格)颜色是cColor。 paint()则相当简单——只是将颜色设为cColor,然后用那种颜色填充整张画布(Canvas)。 @@ -92,7 +92,7 @@ paint()则相当简单——只是将颜色设为cColor,然后用那种颜色 有些时候,我们会发现ColorBoxes几乎陷于停顿状态。在我自己的机器上,这一情况在产生了10×10的网格之后发生了。为什么会这样呢?自然地,我们有理由怀疑AWT对它做了什么事情。所以这里有一个例子能够测试那个猜测,它产生了较少的线程。代码经过了重新组织,使一个Vector实现了Runnable,而且那个Vector容纳了数量众多的色块,并随机挑选一些进行更新。随后,我们创建大量这些Vector对象,数量大致取决于我们挑选的网格维数。结果便是我们得到比色块少得多的线程。所以假如有一个速度的加快,我们就能立即知道,因为前例的线程数量太多了。如下所示: -``` +``` java //: ColorBoxes2.java // Balancing thread use import java.awt.*; @@ -185,7 +185,7 @@ CBox2类似CBox——能用一种随机选择的颜色描绘自己。但那就 CBoxVector也可以拥有继承的Thread,并有一个类型为Vector的成员对象。这样设计的好处就是addElement()和elementAt()方法可以获得特定的参数以及返回值类型,而不是只能获得常规Object(它们的名字也可以变得更短)。然而,这里采用的设计表面上看需要较少的代码。除此以外,它会自动保留一个Vector的其他所有行为。由于elementAt()需要大量进行“封闭”工作,用到许多括号,所以随着代码主体的扩充,最终仍有可能需要大量代码。 -和以前一样,在我们实现Runnable的时候,并没有获得与Thread配套提供的所有功能,所以必须创建一个新的Thread,并将自己传递给它的构建器,以便正式“启动”——start()——一些东西。大家在CBoxVector构建器和go()里都可以体会到这一点。run()方法简单地选择Vector里的一个随机元素编号,并为那个元素调用nextColor(),令其挑选一种新的随机颜色。 +和以前一样,在我们实现Runnable的时候,并没有获得与Thread配套提供的所有功能,所以必须创建一个新的Thread,并将自己传递给它的构造器,以便正式“启动”——start()——一些东西。大家在CBoxVector构造器和go()里都可以体会到这一点。run()方法简单地选择Vector里的一个随机元素编号,并为那个元素调用nextColor(),令其挑选一种新的随机颜色。 运行这个程序时,大家会发现它确实变得更快,响应也更迅速(比如在中断它的时候,它能更快地停下来)。而且随着网格尺寸的壮 大,它也不会经常性地陷于“停顿”状态。因此,线程的处理又多了一项新的考虑因素:必须随时检查自己有没有“太多的线程”(无论对什么程序和运行平台)。若线程太多,必须试着使用上面介绍的技术,对程序中的线程数量进行“平衡”。如果在一个多线程的程序中遇到了性能上的问题,那么现在有许多因素需要检查: diff --git "a/14.7 \347\273\203\344\271\240.md" "b/14.7 \347\273\203\344\271\240.md" index 18229cf..d225b99 100644 --- "a/14.7 \347\273\203\344\271\240.md" +++ "b/14.7 \347\273\203\344\271\240.md" @@ -3,13 +3,13 @@ 14.7 练习 -(1) 从Thread继承一个类,并(过载)覆盖run()方法。在run()内,打印出一条消息,然后调用sleep()。重复三遍这些操作,然后从run()返回。在构建器中放置一条启动消息,并覆盖finalize(),打印一条关闭消息。创建一个独立的线程类,使它在run()内调用System.gc()和System.runFinalization(),并打印一条消息,表明调用成功。创建这两种类型的几个线程,然后运行它们,看看会发生什么。 +(1) 从Thread继承一个类,并(重载)覆盖run()方法。在run()内,打印出一条消息,然后调用sleep()。重复三遍这些操作,然后从run()返回。在构造器中放置一条启动消息,并覆盖finalize(),打印一条关闭消息。创建一个独立的线程类,使它在run()内调用System.gc()和System.runFinalization(),并打印一条消息,表明调用成功。创建这两种类型的几个线程,然后运行它们,看看会发生什么。 (2) 修改Counter2.java,使线程成为一个内部类,而且不需要明确保存指向Counter2的一个。 (3) 修改Sharing2.java,在TwoCounter的run()方法内部添加一个synchronized(同步)块,而不是同步整个run()方法。 -(4) 创建两个Thread子类,第一个的run()方法用于最开始的启动,并捕获第二个Thread对象的句柄,然后调用wait()。第二个类的run()应在过几秒后为第一个线程调用modifyAll(),使第一个线程能打印出一条消息。 +(4) 创建两个Thread子类,第一个的run()方法用于最开始的启动,并捕获第二个Thread对象的指针,然后调用wait()。第二个类的run()应在过几秒后为第一个线程调用modifyAll(),使第一个线程能打印出一条消息。 (5) 在Ticker2内的Counter5.java中,删除yield(),并解释一下结果。用一个sleep()换掉yield(),再解释一下结果。 diff --git "a/15.1 \346\234\272\345\231\250\347\232\204\346\240\207\350\257\206.md" "b/15.1 \346\234\272\345\231\250\347\232\204\346\240\207\350\257\206.md" index c1e0f7a..5c662a9 100644 --- "a/15.1 \346\234\272\345\231\250\347\232\204\346\240\207\350\257\206.md" +++ "b/15.1 \346\234\272\345\231\250\347\232\204\346\240\207\350\257\206.md" @@ -12,7 +12,7 @@ 作为运用InetAddress.getByName()一个简单的例子,请考虑假设自己有一家拨号连接因特网服务提供者(ISP),那么会发生什么情况。每次拨号连接的时候,都会分配得到一个临时IP地址。但在连接期间,那个IP地址拥有与因特网上其他IP地址一样的有效性。如果有人按照你的IP地址连接你的机器,他们就有可能使用在你机器上运行的Web或者FTP服务器程序。当然这有个前提,对方必须准确地知道你目前分配到的IP。由于每次拨号连接获得的IP都是随机的,怎样才能准确地掌握你的IP呢? 下面这个程序利用InetAddress.getByName()来产生你的IP地址。为了让它运行起来,事先必须知道计算机的名字。该程序只在Windows 95中进行了测试,但大家可以依次进入自己的“开始”、“设置”、“控制面板”、“网络”,然后进入“标识”卡片。其中,“计算机名称”就是应在命令行输入的内容。 -``` +``` java //: WhoAmI.java // Finds out your network address when you're // connected to the Internet. @@ -36,13 +36,13 @@ public class WhoAmI { 就我自己的情况来说,机器的名字叫作“Colossus”(来自同名电影,“巨人”的意思。我在这台机器上有一个很大的硬盘)。所以一旦连通我的ISP,就象下面这样执行程序: -``` +``` java java whoAmI Colossus ``` 得到的结果象下面这个样子(当然,这个地址可能每次都是不同的): -``` +``` java Colossus/202.98.41.151 ``` @@ -60,7 +60,7 @@ Colossus/202.98.41.151 由于多种潜在的原因,我们可能没有一台客户机、服务器以及一个网络来测试自己做好的程序。我们也许是在一个课堂环境中进行练习,或者写出的是一个不十分可靠的网络应用,还能拿到网络上去。IP的设计者注意到了这个问题,并建立了一个特殊的地址——localhost——来满足非网络环境中的测试要求。在Java中产生这个地址最一般的做法是: -``` +``` java InetAddress addr = InetAddress.getByName(null); ``` @@ -68,13 +68,13 @@ InetAddress addr = InetAddress.getByName(null); 为得到本地主机地址,亦可向其直接传递字串"localhost": -``` +``` java InetAddress.getByName("localhost"); ``` 或者使用它的保留IP地址(四点形式),就象下面这样: -``` +``` java InetAddress.getByName("127.0.0.1"); ``` diff --git "a/15.2 \345\245\227\346\216\245\345\255\227.md" "b/15.2 \345\245\227\346\216\245\345\255\227.md" index 3ce554e..71cb8dd 100644 --- "a/15.2 \345\245\227\346\216\245\345\255\227.md" +++ "b/15.2 \345\245\227\346\216\245\345\255\227.md" @@ -20,7 +20,7 @@ 下面是服务器程序: -``` +``` java //: JabberServer.java // Very simple server that just // echoes whatever the client sends. @@ -71,20 +71,20 @@ public class JabberServer { 可以看到,ServerSocket需要的只是一个端口编号,不需要IP地址(因为它就在这台机器上运行)。调用accept()时,方法会暂时陷入停顿状态(堵塞),直到某个客户尝试同它建立连接。换言之,尽管它在那里等候连接,但其他进程仍能正常运行(参考第14章)。建好一个连接以后,accept()就会返回一个Socket对象,它是那个连接的代表。 -清除套接字的责任在这里得到了很艺术的处理。假如ServerSocket构建器失败,则程序简单地退出(注意必须保证ServerSocket的构建器在失败之后不会留下任何打开的网络套接字)。针对这种情况,main()会“掷”出一个IOException违例,所以不必使用一个try块。若ServerSocket构建器成功执行,则其他所有方法调用都必须到一个try-finally代码块里寻求保护,以确保无论块以什么方式留下,ServerSocket都能正确地关闭。 +清除套接字的责任在这里得到了很艺术的处理。假如ServerSocket构造器失败,则程序简单地退出(注意必须保证ServerSocket的构造器在失败之后不会留下任何打开的网络套接字)。针对这种情况,main()会“掷”出一个IOException异常,所以不必使用一个try块。若ServerSocket构造器成功执行,则其他所有方法调用都必须到一个try-finally代码块里寻求保护,以确保无论块以什么方式留下,ServerSocket都能正确地关闭。 同样的道理也适用于由accept()返回的Socket。若accept()失败,那么我们必须保证Socket不再存在或者含有任何资源,以便不必清除它们。但假若执行成功,则后续的语句必须进入一个try-finally块内,以保障在它们失败的情况下,Socket仍能得到正确的清除。由于套接字使用了重要的非内存资源,所以在这里必须特别谨慎,必须自己动手将它们清除(Java中没有提供“破坏器”来帮助我们做这件事情)。 无论ServerSocket还是由accept()产生的Socket都打印到System.out里。这意味着它们的toString方法会得到自动调用。这样便产生了: -``` +``` java ServerSocket[addr=0.0.0.0,PORT=0,localport=8080] Socket[addr=127.0.0.1,PORT=1077,localport=8080] ``` 大家不久就会看到它们如何与客户程序做的事情配合。 -程序的下一部分看来似乎仅仅是打开文件,以便读取和写入,只是InputStream和OutputStream是从Socket对象创建的。利用两个“转换器”类InputStreamReader和OutputStreamWriter,InputStream和OutputStream对象已经分别转换成为Java 1.1的Reader和Writer对象。也可以直接使用Java1.0的InputStream和OutputStream类,但对输出来说,使用Writer方式具有明显的优势。这一优势是通过PrintWriter表现出来的,它有一个过载的构建器,能获取第二个参数——一个布尔值标志,指向是否在每一次println()结束的时候自动刷新输出(但不适用于print()语句)。每次写入了输出内容后(写进out),它的缓冲区必须刷新,使信息能正式通过网络传递出去。对目前这个例子来说,刷新显得尤为重要,因为客户和服务器在采取下一步操作之前都要等待一行文本内容的到达。若刷新没有发生,那么信息不会进入网络,除非缓冲区满(溢出),这会为本例带来许多问题。 +程序的下一部分看来似乎仅仅是打开文件,以便读取和写入,只是InputStream和OutputStream是从Socket对象创建的。利用两个“转换器”类InputStreamReader和OutputStreamWriter,InputStream和OutputStream对象已经分别转换成为Java 1.1的Reader和Writer对象。也可以直接使用Java1.0的InputStream和OutputStream类,但对输出来说,使用Writer方式具有明显的优势。这一优势是通过PrintWriter表现出来的,它有一个重载的构造器,能获取第二个参数——一个布尔值标志,指向是否在每一次println()结束的时候自动刷新输出(但不适用于print()语句)。每次写入了输出内容后(写进out),它的缓冲区必须刷新,使信息能正式通过网络传递出去。对目前这个例子来说,刷新显得尤为重要,因为客户和服务器在采取下一步操作之前都要等待一行文本内容的到达。若刷新没有发生,那么信息不会进入网络,除非缓冲区满(溢出),这会为本例带来许多问题。 编写网络应用程序时,需要特别注意自动刷新机制的使用。每次刷新缓冲区时,必须创建和发出一个数据包(数据封)。就目前的情况来说,这正是我们所希望的,因为假如包内包含了还没有发出的文本行,服务器和客户机之间的相互“握手”就会停止。换句话说,一行的末尾就是一条消息的末尾。但在其他许多情况下,消息并不是用行分隔的,所以不如不用自动刷新机制,而用内建的缓冲区判决机制来决定何时发送一个数据包。这样一来,我们可以发出较大的数据包,而且处理进程也能加快。 注意和我们打开的几乎所有数据流一样,它们都要进行缓冲处理。本章末尾有一个练习,清楚展现了假如我们不对数据流进行缓冲,那么会得到什么样的后果(速度会变慢)。 @@ -95,7 +95,7 @@ Socket[addr=127.0.0.1,PORT=1077,localport=8080] 下面是客户程序的源码: -``` +``` java //: JabberClient.java // Very simple client that just sends // lines to the server and reads lines @@ -151,19 +151,19 @@ public class JabberClient { 在main()中,大家可看到获得本地主机IP地址的InetAddress的三种途径:使用null,使用localhost,或者直接使用保留地址127.0.0.1。当然,如果想通过网络同一台远程主机连接,也可以换用那台机器的IP地址。打印出InetAddress addr后(通过对toString()方法的自动调用),结果如下: -``` +``` java localhost/127.0.0.1 ``` 通过向getByName()传递一个null,它会默认寻找localhost,并生成特殊的保留地址127.0.0.1。注意在名为socket的套接字创建时,同时使用了InetAddress以及端口号。打印这样的某个Socket对象时,为了真正理解它的含义,请记住一次独一无二的因特网连接是用下述四种数据标识的:clientHost(客户主机)、clientPortNumber(客户端口号)、serverHost(服务主机)以及serverPortNumber(服务端口号)。服务程序启动后,会在本地主机(127.0.0.1)上建立为它分配的端口(8080)。一旦客户程序发出请求,机器上下一个可用的端口就会分配给它(这种情况下是1077),这一行动也在与服务程序相同的机器(127.0.0.1)上进行。现在,为了使数据能在客户及服务程序之间来回传送,每一端都需要知道把数据发到哪里。所以在同一个“已知”服务程序连接的时候,客户会发出一个“返回地址”,使服务器程序知道将自己的数据发到哪儿。我们在服务器端的示范输出中可以体会到这一情况: -``` +``` java Socket[addr=127.0.0.1,port=1077,localport=8080] ``` 这意味着服务器刚才已接受了来自127.0.0.1这台机器的端口1077的连接,同时监听自己的本地端口(8080)。而在客户端: -``` +``` java Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077] ``` @@ -171,7 +171,7 @@ Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077] 大家会注意到每次重新启动客户程序的时候,本地端口的编号都会增加。这个编号从1025(刚好在系统保留的1-1024之外)开始,并会一直增加下去,除非我们重启机器。若重新启动机器,端口号仍然会从1025开始增值(在Unix机器中,一旦超过保留的套按字范围,数字就会再次从最小的可用数字开始)。 -创建好Socket对象后,将其转换成BufferedReader和PrintWriter的过程便与在服务器中相同(同样地,两种情况下都要从一个Socket开始)。在这里,客户通过发出字串"howdy",并在后面跟随一个数字,从而初始化通信。注意缓冲区必须再次刷新(这是自动发生的,通过传递给PrintWriter构建器的第二个参数)。若缓冲区没有刷新,那么整个会话(通信)都会被挂起,因为用于初始化的“howdy”永远不会发送出去(缓冲区不够满,不足以造成发送动作的自动进行)。从服务器返回的每一行都会写入System.out,以验证一切都在正常运转。为中止会话,需要发出一个"END"。若客户程序简单地挂起,那么服务器会“掷”出一个违例。 +创建好Socket对象后,将其转换成BufferedReader和PrintWriter的过程便与在服务器中相同(同样地,两种情况下都要从一个Socket开始)。在这里,客户通过发出字串"howdy",并在后面跟随一个数字,从而初始化通信。注意缓冲区必须再次刷新(这是自动发生的,通过传递给PrintWriter构造器的第二个参数)。若缓冲区没有刷新,那么整个会话(通信)都会被挂起,因为用于初始化的“howdy”永远不会发送出去(缓冲区不够满,不足以造成发送动作的自动进行)。从服务器返回的每一行都会写入System.out,以验证一切都在正常运转。为中止会话,需要发出一个"END"。若客户程序简单地挂起,那么服务器会“掷”出一个异常。 大家在这里可以看到我们采用了同样的措施来确保由Socket代表的网络资源得到正确的清除,这是用一个try-finally块实现的。 diff --git "a/15.3 \346\234\215\345\212\241\345\244\232\344\270\252\345\256\242\346\210\267.md" "b/15.3 \346\234\215\345\212\241\345\244\232\344\270\252\345\256\242\346\210\267.md" index ea6502b..d475a18 100644 --- "a/15.3 \346\234\215\345\212\241\345\244\232\344\270\252\345\256\242\346\210\267.md" +++ "b/15.3 \346\234\215\345\212\241\345\244\232\344\270\252\345\256\242\346\210\267.md" @@ -7,7 +7,7 @@ JabberServer可以正常工作,但每次只能为一个客户程序提供服 对于下面这段服务器代码,大家可发现它与JabberServer.java例子非常相似,只是为一个特定的客户提供服务的所有操作都已移入一个独立的线程类中: -``` +``` java //: MultiJabberServer.java // A server that uses multithreading to handle // any number of clients. @@ -82,15 +82,15 @@ public class MultiJabberServer { 每次有新客户请求建立一个连接时,ServeOneJabber线程都会取得由accept()在main()中生成的Socket对象。然后和往常一样,它创建一个BufferedReader,并用Socket自动刷新PrintWriter对象。最后,它调用Thread的特殊方法start(),令其进行线程的初始化,然后调用run()。这里采取的操作与前例是一样的:从套扫字读入某些东西,然后把它原样反馈回去,直到遇到一个特殊的"END"结束标志为止。 -同样地,套接字的清除必须进行谨慎的设计。就目前这种情况来说,套接字是在ServeOneJabber外部创建的,所以清除工作可以“共享”。若ServeOneJabber构建器失败,那么只需向调用者“掷”出一个违例即可,然后由调用者负责线程的清除。但假如构建器成功,那么必须由ServeOneJabber对象负责线程的清除,这是在它的run()里进行的。 +同样地,套接字的清除必须进行谨慎的设计。就目前这种情况来说,套接字是在ServeOneJabber外部创建的,所以清除工作可以“共享”。若ServeOneJabber构造器失败,那么只需向调用者“掷”出一个异常即可,然后由调用者负责线程的清除。但假如构造器成功,那么必须由ServeOneJabber对象负责线程的清除,这是在它的run()里进行的。 -请注意MultiJabberServer有多么简单。和以前一样,我们创建一个ServerSocket,并调用accept()允许一个新连接的建立。但这一次,accept()的返回值(一个套接字)将传递给用于ServeOneJabber的构建器,由它创建一个新线程,并对那个连接进行控制。连接中断后,线程便可简单地消失。 +请注意MultiJabberServer有多么简单。和以前一样,我们创建一个ServerSocket,并调用accept()允许一个新连接的建立。但这一次,accept()的返回值(一个套接字)将传递给用于ServeOneJabber的构造器,由它创建一个新线程,并对那个连接进行控制。连接中断后,线程便可简单地消失。 -如果ServerSocket创建失败,则再一次通过main()掷出违例。如果成功,则位于外层的try-finally代码块可以担保正确的清除。位于内层的try-catch块只负责防范ServeOneJabber构建器的失败;若构建器成功,则ServeOneJabber线程会将对应的套接字关掉。 +如果ServerSocket创建失败,则再一次通过main()掷出异常。如果成功,则位于外层的try-finally代码块可以担保正确的清除。位于内层的try-catch块只负责防范ServeOneJabber构造器的失败;若构造器成功,则ServeOneJabber线程会将对应的套接字关掉。 为了证实服务器代码确实能为多名客户提供服务,下面这个程序将创建许多客户(使用线程),并同相同的服务器建立连接。每个线程的“存在时间”都是有限的。一旦到期,就留出空间以便创建一个新线程。允许创建的线程的最大数量是由final int maxthreads决定的。大家会注意到这个值非常关键,因为假如把它设得很大,线程便有可能耗尽资源,并产生不可预知的程序错误。 -``` +``` java //: MultiJabberClient.java // Client that tests the MultiJabberServer // by starting up multiple clients. @@ -175,6 +175,6 @@ public class MultiJabberClient { } ///:~ ``` -JabberClientThread构建器获取一个InetAddress,并用它打开一个套接字。大家可能已看出了这样的一个套路:Socket肯定用于创建某种Reader以及/或者Writer(或者InputStream和/或OutputStream)对象,这是运用Socket的唯一方式(当然,我们可考虑编写一、两个类,令其自动完成这些操作,避免大量重复的代码编写工作)。同样地,start()执行线程的初始化,并调用run()。在这里,消息发送给服务器,而来自服务器的信息则在屏幕上回显出来。然而,线程的“存在时间”是有限的,最终都会结束。注意在套接字创建好以后,但在构建器完成之前,假若构建器失败,套接字会被清除。否则,为套接字调用close()的责任便落到了run()方法的头上。 +JabberClientThread构造器获取一个InetAddress,并用它打开一个套接字。大家可能已看出了这样的一个套路:Socket肯定用于创建某种Reader以及/或者Writer(或者InputStream和/或OutputStream)对象,这是运用Socket的唯一方式(当然,我们可考虑编写一、两个类,令其自动完成这些操作,避免大量重复的代码编写工作)。同样地,start()执行线程的初始化,并调用run()。在这里,消息发送给服务器,而来自服务器的信息则在屏幕上回显出来。然而,线程的“存在时间”是有限的,最终都会结束。注意在套接字创建好以后,但在构造器完成之前,假若构造器失败,套接字会被清除。否则,为套接字调用close()的责任便落到了run()方法的头上。 -threadcount跟踪计算目前存在的JabberClientThread对象的数量。它将作为构建器的一部分增值,并在run()退出时减值(run()退出意味着线程中止)。在MultiJabberClient.main()中,大家可以看到线程的数量会得到检查。若数量太多,则多余的暂时不创建。方法随后进入“休眠”状态。这样一来,一旦部分线程最后被中止,多作的那些线程就可以创建了。大家可试验一下逐渐增大MAX_THREADS,看看对于你使用的系统来说,建立多少线程(连接)才会使您的系统资源降低到危险程度。 +threadcount跟踪计算目前存在的JabberClientThread对象的数量。它将作为构造器的一部分增值,并在run()退出时减值(run()退出意味着线程中止)。在MultiJabberClient.main()中,大家可以看到线程的数量会得到检查。若数量太多,则多余的暂时不创建。方法随后进入“休眠”状态。这样一来,一旦部分线程最后被中止,多作的那些线程就可以创建了。大家可试验一下逐渐增大MAX_THREADS,看看对于你使用的系统来说,建立多少线程(连接)才会使您的系统资源降低到危险程度。 diff --git "a/15.4 \346\225\260\346\215\256\346\212\245.md" "b/15.4 \346\225\260\346\215\256\346\212\245.md" index 40c2f53..79d95e8 100644 --- "a/15.4 \346\225\260\346\215\256\346\212\245.md" +++ "b/15.4 \346\225\260\346\215\256\346\212\245.md" @@ -5,18 +5,18 @@ 大家迄今看到的例子使用的都是“传输控制协议”(TCP),亦称作“基于数据流的套接字”。根据该协议的设计宗旨,它具有高度的可靠性,而且能保证数据顺利抵达目的地。换言之,它允许重传那些由于各种原因半路“走失”的数据。而且收到字节的顺序与它们发出来时是一样的。当然,这种控制与可靠性需要我们付出一些代价:TCP具有非常高的开销。 还有另一种协议,名为“用户数据报协议”(UDP),它并不刻意追求数据包会完全发送出去,也不能担保它们抵达的顺序与它们发出时一样。我们认为这是一种“不可靠协议”(TCP当然是“可靠协议”)。听起来似乎很糟,但由于它的速度快得多,所以经常还是有用武之地的。对某些应用来说,比如声音信号的传输,如果少量数据包在半路上丢失了,那么用不着太在意,因为传输的速度显得更重要一些。大多数互联网游戏,如Diablo,采用的也是UDP协议通信,因为网络通信的快慢是游戏是否流畅的决定性因素。也可以想想一台报时服务器,如果某条消息丢失了,那么也真的不必过份紧张。另外,有些应用也许能向服务器传回一条UDP消息,以便以后能够恢复。如果在适当的时间里没有响应,消息就会丢失。 Java对数据报的支持与它对TCP套接字的支持大致相同,但也存在一个明显的区别。对数据报来说,我们在客户和服务器程序都可以放置一个DatagramSocket(数据报套接字),但与ServerSocket不同,前者不会干巴巴地等待建立一个连接的请求。这是由于不再存在“连接”,取而代之的是一个数据报陈列出来。另一项本质的区别的是对TCP套接字来说,一旦我们建好了连接,便不再需要关心谁向谁“说话”——只需通过会话流来回传送数据即可。但对数据报来说,它的数据包必须知道自己来自何处,以及打算去哪里。这意味着我们必须知道每个数据报包的这些信息,否则信息就不能正常地传递。 -DatagramSocket用于收发数据包,而DatagramPacket包含了具体的信息。准备接收一个数据报时,只需提供一个缓冲区,以便安置接收到的数据。数据包抵达时,通过DatagramSocket,作为信息起源地的因特网地址以及端口编号会自动得到初化。所以一个用于接收数据报的DatagramPacket构建器是: +DatagramSocket用于收发数据包,而DatagramPacket包含了具体的信息。准备接收一个数据报时,只需提供一个缓冲区,以便安置接收到的数据。数据包抵达时,通过DatagramSocket,作为信息起源地的因特网地址以及端口编号会自动得到初化。所以一个用于接收数据报的DatagramPacket构造器是: DatagramPacket(buf, buf.length) -其中,buf是一个字节数组。既然buf是个数组,大家可能会奇怪为什么构建器自己不能调查出数组的长度呢?实际上我也有同感,唯一能猜到的原因就是C风格的编程使然,那里的数组不能自己告诉我们它有多大。 +其中,buf是一个字节数组。既然buf是个数组,大家可能会奇怪为什么构造器自己不能调查出数组的长度呢?实际上我也有同感,唯一能猜到的原因就是C风格的编程使然,那里的数组不能自己告诉我们它有多大。 可以重复使用数据报的接收代码,不必每次都建一个新的。每次用它的时候(再生),缓冲区内的数据都会被覆盖。 缓冲区的最大容量仅受限于允许的数据报包大小,这个限制位于比64KB稍小的地方。但在许多应用程序中,我们都宁愿它变得还要小一些,特别是在发送数据的时候。具体选择的数据包大小取决于应用程序的特定要求。 -发出一个数据报时,DatagramPacket不仅需要包含正式的数据,也要包含因特网地址以及端口号,以决定它的目的地。所以用于输出DatagramPacket的构建器是: +发出一个数据报时,DatagramPacket不仅需要包含正式的数据,也要包含因特网地址以及端口号,以决定它的目的地。所以用于输出DatagramPacket的构造器是: DatagramPacket(buf, length, inetAddress, port) 这一次,buf(一个字节数组)已经包含了我们想发出的数据。length可以是buf的长度,但也可以更短一些,意味着我们只想发出那么多的字节。另两个参数分别代表数据包要到达的因特网地址以及目标机器的一个目标端口(注释②)。 ②:我们认为TCP和UDP端口是相互独立的。也就是说,可以在端口8080同时运行一个TCP和UDP服务程序,两者之间不会产生冲突。 -大家也许认为两个构建器创建了两个不同的对象:一个用于接收数据报,另一个用于发送它们。如果是好的面向对象的设计方案,会建议把它们创建成两个不同的类,而不是具有不同的行为的一个类(具体行为取决于我们如何构建对象)。这也许会成为一个严重的问题,但幸运的是,DatagramPacket的使用相当简单,我们不需要在这个问题上纠缠不清。这一点在下例里将有很明确的说明。该例类似于前面针对TCP套接字的MultiJabberServer和MultiJabberClient例子。多个客户都会将数据报发给服务器,后者会将其反馈回最初发出消息的同样的客户。 +大家也许认为两个构造器创建了两个不同的对象:一个用于接收数据报,另一个用于发送它们。如果是好的面向对象的设计方案,会建议把它们创建成两个不同的类,而不是具有不同的行为的一个类(具体行为取决于我们如何构建对象)。这也许会成为一个严重的问题,但幸运的是,DatagramPacket的使用相当简单,我们不需要在这个问题上纠缠不清。这一点在下例里将有很明确的说明。该例类似于前面针对TCP套接字的MultiJabberServer和MultiJabberClient例子。多个客户都会将数据报发给服务器,后者会将其反馈回最初发出消息的同样的客户。 为简化从一个String里创建DatagramPacket的工作(或者从DatagramPacket里创建String),这个例子首先用到了一个工具类,名为Dgram: //: Dgram.java @@ -46,8 +46,8 @@ public class Dgram { } } ///:~ -Dgram的第一个方法采用一个String、一个InetAddress以及一个端口号作为自己的参数,将String的内容复制到一个字节缓冲区,再将缓冲区传递进入DatagramPacket构建器,从而构建一个DatagramPacket。注意缓冲区分配时的"+1"——这对防止截尾现象是非常重要的。String的getByte()方法属于一种特殊操作,能将一个字串包含的char复制进入一个字节缓冲。该方法现在已被“反对”使用;Java 1.1有一个“更好”的办法来做这个工作,但在这里却被当作注释屏蔽掉了,因为它会截掉String的部分内容。所以尽管我们在Java 1.1下编译该程序时会得到一条“反对”消息,但它的行为仍然是正确无误的(这个错误应该在你读到这里的时候修正了)。 -Dgram.toString()方法同时展示了Java 1.0的方法和Java 1.1的方法(两者是不同的,因为有一种新类型的String构建器)。 +Dgram的第一个方法采用一个String、一个InetAddress以及一个端口号作为自己的参数,将String的内容复制到一个字节缓冲区,再将缓冲区传递进入DatagramPacket构造器,从而构建一个DatagramPacket。注意缓冲区分配时的"+1"——这对防止截尾现象是非常重要的。String的getByte()方法属于一种特殊操作,能将一个字串包含的char复制进入一个字节缓冲。该方法现在已被“反对”使用;Java 1.1有一个“更好”的办法来做这个工作,但在这里却被当作注释屏蔽掉了,因为它会截掉String的部分内容。所以尽管我们在Java 1.1下编译该程序时会得到一条“反对”消息,但它的行为仍然是正确无误的(这个错误应该在你读到这里的时候修正了)。 +Dgram.toString()方法同时展示了Java 1.0的方法和Java 1.1的方法(两者是不同的,因为有一种新类型的String构造器)。 下面是用于数据报演示的服务器代码: //: ChatterServer.java @@ -150,7 +150,7 @@ public class ChatterServer { } } ///:~ -ChatterClient被创建成一个线程(Thread),所以可以用多个客户来“骚扰”服务器。从中可以看到,用于接收的DatagramPacket和用于ChatterServer的那个是相似的。在构建器中,创建DatagramPacket时没有附带任何参数(自变量),因为它不需要明确指出自己位于哪个特定编号的端口里。用于这个套接字的因特网地址将成为“这台机器”(比如localhost),而且会自动分配端口编号,这从输出结果即可看出。同用于服务器的那个一样,这个DatagramPacket将同时用于发送和接收。 +ChatterClient被创建成一个线程(Thread),所以可以用多个客户来“骚扰”服务器。从中可以看到,用于接收的DatagramPacket和用于ChatterServer的那个是相似的。在构造器中,创建DatagramPacket时没有附带任何参数(自变量),因为它不需要明确指出自己位于哪个特定编号的端口里。用于这个套接字的因特网地址将成为“这台机器”(比如localhost),而且会自动分配端口编号,这从输出结果即可看出。同用于服务器的那个一样,这个DatagramPacket将同时用于发送和接收。 hostAddress是我们想与之通信的那台机器的因特网地址。在程序中,如果需要创建一个准备传出去的DatagramPacket,那么必须知道一个准确的因特网地址和端口号。可以肯定的是,主机必须位于一个已知的地址和端口号上,使客户能启动与主机的“会话”。 每个线程都有自己独一无二的标识号(尽管自动分配给线程的端口号是也会提供一个唯一的标识符)。在run()中,我们创建了一个String消息,其中包含了线程的标识编号以及该线程准备发送的消息编号。我们用这个字串创建一个数据报,发到主机上的指定地址;端口编号则直接从ChatterServer内的一个常数取得。一旦消息发出,receive()就会暂时被“堵塞”起来,直到服务器回复了这条消息。与消息附在一起的所有信息使我们知道回到这个特定线程的东西正是从始发消息中投递出去的。在这个例子中,尽管是一种“不可靠”协议,但仍然能够检查数据报是否到去过了它们该去的地方(这在localhost和LAN环境中是成立的,但在非本地连接中却可能出现一些错误)。 运行该程序时,大家会发现每个线程都会结束。这意味着发送到服务器的每个数据报包都会回转,并反馈回正确的接收者。如果不是这样,一个或更多的线程就会挂起并进入“堵塞”状态,直到它们的输入被显露出来。 diff --git "a/15.5 \344\270\200\344\270\252Web\345\272\224\347\224\250.md" "b/15.5 \344\270\200\344\270\252Web\345\272\224\347\224\250.md" index 2f19fd5..f9c6b62 100644 --- "a/15.5 \344\270\200\344\270\252Web\345\272\224\347\224\250.md" +++ "b/15.5 \344\270\200\344\270\252Web\345\272\224\347\224\250.md" @@ -21,7 +21,7 @@ 这个非Java应用是用C写成,因为Java不适合作CGI编程;起码启动的时间不能让人满意。它的任务是管理电子函件(E-mail)地址的一个列表。标准输入会接受一个E-mail地址,程序会检查列表中的名字,判断是否存在那个地址。若不存在,就将其加入,并报告操作成功。但假如名字已在列表里了,就需要指出这一点,避免重复加入。大家不必担心自己不能完全理解下列代码的含义。它仅仅是一个演示程序,告诉你如何用其他语言写一个程序,并从Java中调用它。在这里具体采用何种语言并不重要,只要能够从标准输入中读取数据,并能写入标准输出即可。 -``` +``` java //: Listmgr.c // Used by NameCollector.java to manage // the email list file on the server @@ -88,7 +88,7 @@ int main() { 这个程序先启动上述的C程序,再建立必要的连接,以便同它“交谈”。随后,它创建一个数据报套接字,用它“监视”或者“侦听”来自程序片的数据报包。 -``` +``` java //: NameCollector.java // Extracts email names from datagrams and stores // them inside a file, using Java 1.02. @@ -176,13 +176,13 @@ public class NameCollector { } ///:~ ``` -NameCollector中的第一个定义应该是大家所熟悉的:选定端口,创建一个数据报包,然后创建指向一个DatagramSocket的句柄。接下来的三个定义负责与C程序的连接:一个Process对象是C程序由Java程序启动之后返回的,而且那个Process对象产生了InputStream和OutputStream,分别代表C程序的标准输出和标准输入。和Java IO一样,它们理所当然地需要“封装”起来,所以我们最后得到的是一个PrintStream和DataInputStream。 +NameCollector中的第一个定义应该是大家所熟悉的:选定端口,创建一个数据报包,然后创建指向一个DatagramSocket的指针。接下来的三个定义负责与C程序的连接:一个Process对象是C程序由Java程序启动之后返回的,而且那个Process对象产生了InputStream和OutputStream,分别代表C程序的标准输出和标准输入。和Java IO一样,它们理所当然地需要“封装”起来,所以我们最后得到的是一个PrintStream和DataInputStream。 -这个程序的所有工作都是在构建器内进行的。为启动C程序,需要取得当前的Runtime对象。我们用它调用exec(),再由后者返回Process对象。在Process对象中,大家可看到通过一简单的调用即可生成数据流:getOutputStream()和getInputStream()。从这个时候开始,我们需要考虑的全部事情就是将数据传给数据流nameList,并从addResult中取得结果。 +这个程序的所有工作都是在构造器内进行的。为启动C程序,需要取得当前的Runtime对象。我们用它调用exec(),再由后者返回Process对象。在Process对象中,大家可看到通过一简单的调用即可生成数据流:getOutputStream()和getInputStream()。从这个时候开始,我们需要考虑的全部事情就是将数据传给数据流nameList,并从addResult中取得结果。 和往常一样,我们将DatagramSocket同一个端口连接到一起。在无限while循环中,程序会调用receive()——除非一个数据报到来,否则receive()会一起处于“堵塞”状态。数据报出现以后,它的内容会提取到String rcvd里。我们首先将该字串两头的空格剔除(trim),再将其发给C程序。如下所示: -``` +``` java nameList.println(rcvd.trim()); ``` @@ -196,7 +196,7 @@ nameList.println(rcvd.trim()); 正如早先指出的那样,程序片必须用Java 1.0编写,使其能与绝大多数的浏览器适应。也正是由于这个原因,我们产生的类数量应尽可能地少。所以我们在这儿不考虑使用前面设计好的Dgram类,而将数据报的所有维护工作都转到代码行中进行。此外,程序片要用一个线程监视由服务器传回的响应信息,而非实现Runnable接口,用集成到程序片的一个独立线程来做这件事情。当然,这样做对代码的可读性不利,但却能产生一个单类(以及单个服务器请求)程序片: -``` +``` java //: NameSender.java // An applet that sends an email address // as a datagram, using Java 1.02. @@ -331,7 +331,7 @@ public class NameSender extends Applet init()方法用大家熟悉的布局工具设置GUI,然后创建DatagramSocket,它将同时用于数据报的收发。 -action()方法只负责监视我们是否按下了“发送”(send)按钮。记住,我们已被限制在Java 1.0上面,所以不能再用较灵活的内部类了。按钮按下以后,采取的第一项行动便是检查线程pl,看看它是否为null(空)。如果不为null,表明有一个活动线程正在运行。消息首次发出时,会启动一个新线程,用它监视来自服务器的回应。所以假若有个线程正在运行,就意味着这并非用户第一次发送消息。pl句柄被设为null,同时中止原来的监视者(这是最合理的一种做法,因为stop()已被Java 1.2“反对”,这在前一章已解释过了)。 +action()方法只负责监视我们是否按下了“发送”(send)按钮。记住,我们已被限制在Java 1.0上面,所以不能再用较灵活的内部类了。按钮按下以后,采取的第一项行动便是检查线程pl,看看它是否为null(空)。如果不为null,表明有一个活动线程正在运行。消息首次发出时,会启动一个新线程,用它监视来自服务器的回应。所以假若有个线程正在运行,就意味着这并非用户第一次发送消息。pl指针被设为null,同时中止原来的监视者(这是最合理的一种做法,因为stop()已被Java 1.2“反对”,这在前一章已解释过了)。 无论这是否按钮被第一次按下,I2中的文字都会清除。 @@ -345,7 +345,7 @@ action()方法只负责监视我们是否按下了“发送”(send)按钮 当然,程序片必须放到一个Web页里。下面列出完整的Web页源码;稍微研究一下就可看出,我用它从自己开办的邮寄列表(Mailling List)里自动收集名字。 -``` +``` java diff --git "a/15.6 Java\344\270\216CGI\347\232\204\346\262\237\351\200\232.md" "b/15.6 Java\344\270\216CGI\347\232\204\346\262\237\351\200\232.md" index a928b31..3a63900 100644 --- "a/15.6 Java\344\270\216CGI\347\232\204\346\262\237\351\200\232.md" +++ "b/15.6 Java\344\270\216CGI\347\232\204\346\262\237\351\200\232.md" @@ -10,13 +10,13 @@ Java程序可向一个服务器发出一个CGI请求,这与HTML表单页没什 在这个版本中,我们将收集名字和电子函件地址,并用下述形式将其保存到文件中: -``` +``` java First Last ; ``` 这对任何E-mail程序来说都是一种非常方便的格式。由于只需收集两个字段,而且CGI为字段中的编码采用了一种特殊的格式,所以这里没有简便的方法。如果自己动手编制一个原始的HTML页,并加入下述代码行,即可正确地理解这一点: -``` +``` java

Name:

@@ -28,7 +28,7 @@ NAME = "email" VALUE = "" size = "40">

上述代码创建了两个数据输入字段(区),名为name和email。另外还有一个submit(提交)按钮,用于收集数据,并将其发给CGI程序。Listmgr2.exe是驻留在特殊程序目录中的一个可执行文件。在我们的Web服务器上,该目录一般都叫作“cgi-bin”(注释③)。如果在那个目录里找不到该程序,结果就无法出现。填好这个表单,然后按下提交按钮,即可在浏览器的URL地址窗口里看到象下面这样的内容: -``` +``` java http://www.myhome.com/cgi-bin/Listmgr2.exe?name=First+Last&email=email@domain.com&submit=Submit ``` @@ -38,7 +38,7 @@ http://www.myhome.com/cgi-bin/Listmgr2.exe?name=First+Last&email=email@domain.co 到这时,大家也许会对“+”,“=”以及“&”的使用产生疑惑。假如必须在字段里使用这些字符,那么该如何声明呢?例如,我们可能使用“John & MarshaSmith”这个名字,其中的“&”代表“And”。事实上,它会编码成下面这个样子: -``` +``` java John+%26+Marsha+Smith ``` @@ -46,7 +46,7 @@ John+%26+Marsha+Smith 幸运的是,Java有一个工具来帮助我们进行这种编码。这是URLEncoder类的一个静态方法,名为encode()。可用下述程序来试验这个方法: -``` +``` java //: EncodeDemo.java // Demonstration of URLEncoder.encode() import java.net.*; @@ -70,7 +70,7 @@ public class EncodeDemo { 程序片实际要比NameSender.java简单一些。这部分是由于很容易即可发出一个GET请求。此外,也不必等候回复信息。现在有两个字段,而非一个,但大家会发现许多程序片都是熟悉的,请比较NameSender.java。 -``` +``` java //: NameSender2.java // An applet that sends an email address // via a CGI GET, using Java 1.02. @@ -185,7 +185,7 @@ CGI程序(不久即可看到)的名字是Listmgr2.exe。许多Web服务器 和往常一样,程序片设置了自己的用户界面(这次是两个输入字段,不是一个)。唯一显著的区别是在action()方法内产生的。该方法的作用是对按钮按下事件进行控制。名字检查过以后,大家会发现下述代码行: -``` +``` java String emailData = "name=" + URLEncoder.encode( name.getText().trim()) + @@ -212,7 +212,7 @@ CGI程序(不久即可看到)的名字是Listmgr2.exe。许多Web服务器 name和email数据都是它们对应的文字框里提取出来,而且两端多余的空格都用trim()剔去了。为了进入列表,email名字被强制换成小写形式,以便能够准确地对比(防止基于大小写形式的错误判断)。来自每个字段的数据都编码为URL形式,随后采用与HTML页中一样的方式汇编GET字串(这样一来,我们可将Java程序片与现有的任何CGI程序结合使用,以满足常规的HTML GET请求)。 -到这时,一些Java的魔力已经开始发挥作用了:如果想同任何URL连接,只需创建一个URL对象,并将地址传递给构建器即可。构建器会负责建立同服务器的连接(对Web服务器来说,所有连接行动都是根据作为URL使用的字串来判断的)。就目前这种情况来说,URL指向的是当前Web站点的cgi-bin目录(当前Web站点的基础地址是用getDocumentBase()设定的)。一旦Web服务器在URL中看到了一个“cgi-bin”,会接着希望在它后面跟随了cgi-bin目录内的某个程序的名字,那是我们要运行的目标程序。程序名后面是一个问号以及CGI程序会在QUERY_STRING环境变量中查找的一个参数字串(马上就要学到)。 +到这时,一些Java的魔力已经开始发挥作用了:如果想同任何URL连接,只需创建一个URL对象,并将地址传递给构造器即可。构造器会负责建立同服务器的连接(对Web服务器来说,所有连接行动都是根据作为URL使用的字串来判断的)。就目前这种情况来说,URL指向的是当前Web站点的cgi-bin目录(当前Web站点的基础地址是用getDocumentBase()设定的)。一旦Web服务器在URL中看到了一个“cgi-bin”,会接着希望在它后面跟随了cgi-bin目录内的某个程序的名字,那是我们要运行的目标程序。程序名后面是一个问号以及CGI程序会在QUERY_STRING环境变量中查找的一个参数字串(马上就要学到)。 我们发出任何形式的请求后,一般都会得到一个回应的HTML页。但若使用Java的URL对象,我们可以拦截自CGI程序传回的任何东西,只需从URL对象里取得一个InputStream(输入数据流)即可。这是用URL对象的openStream()方法实现,它要封装到一个DataInputStream里。随后就可以读取数据行,若readLine()返回一个null(空值),就表明CGI程序已结束了它的输出。 我们即将看到的CGI程序返回的仅仅是一行,它是用于标志成功与否(以及失败的具体原因)的一个字串。这一行会被捕获并置放第二个Label字段里,使用户看到具体发生了什么事情。 @@ -221,13 +221,13 @@ name和email数据都是它们对应的文字框里提取出来,而且两端 程序亦可将CGI程序的结果作为一个Web页显示出来,就象它们在普通HTML模式中运行那样。可用下述代码做到这一点: -``` +``` java getAppletContext().showDocument(u); ``` 其中,u代表URL对象。这是将我们重新定向于另一个Web页的一个简单例子。那个页凑巧是一个CGI程序的输出,但可以非常方便地进入一个原始的HTML页,所以可以构建这个程序片,令其产生一个由密码保护的网关,通过它进入自己Web站点的特殊部分: -``` +``` java //: ShowHTML.java import java.awt.*; import java.applet.*; @@ -277,12 +277,12 @@ URL类的最大的特点就是有效地保护了我们的安全。可以同一 这个程序同时也非常有趣,因为它演示了C++与Java相比的许多优缺点。大家会看到一些相似的东西;比如class关键字。访问控制使用的是完全相同的关键字public和private,但用法却有所不同。它们控制的是一个块,而非单个方法或字段(也就是说,如果指定private:,后续的每个定义都具有private属性,直到我们再指定public:为止)。另外在创建一个类的时候,所有定义都自动默认为private。 -在这儿使用C++的一个原因是要利用C++“标准模板库”(STL)提供的便利。至少,STL包含了一个vector类。这是一个C++模板,可在编译期间进行配置,令其只容纳一种特定类型的对象(这里是Pair对象)。和Java的Vector不同,如果我们试图将除Pair对象之外的任何东西置入vector,C++的vector模板都会造成一个编译期错误;而Java的Vector能够照单全收。而且从vector里取出什么东西的时候,它会自动成为一个Pair对象,毋需进行造型处理。所以检查在编译期进行,这使程序显得更为“健壮”。此外,程序的运行速度也可以加快,因为没有必要进行运行期间的造型。vector也会过载operator[],所以可以利用非常方便的语法来提取Pair对象。vector模板将在CGI_vector创建时使用;在那时,大家就可以体会到如此简短的一个定义居然蕴藏有那么巨大的能量。 +在这儿使用C++的一个原因是要利用C++“标准模板库”(STL)提供的便利。至少,STL包含了一个vector类。这是一个C++模板,可在编译期间进行配置,令其只容纳一种特定类型的对象(这里是Pair对象)。和Java的Vector不同,如果我们试图将除Pair对象之外的任何东西置入vector,C++的vector模板都会造成一个编译期错误;而Java的Vector能够照单全收。而且从vector里取出什么东西的时候,它会自动成为一个Pair对象,毋需进行造型处理。所以检查在编译期进行,这使程序显得更为“健壮”。此外,程序的运行速度也可以加快,因为没有必要进行运行期间的造型。vector也会重载operator[],所以可以利用非常方便的语法来提取Pair对象。vector模板将在CGI_vector创建时使用;在那时,大家就可以体会到如此简短的一个定义居然蕴藏有那么巨大的能量。 -若提到缺点,就一定不要忘记Pair在下列代码中定义时的复杂程度。与我们在Java代码中看到的相比,Pair的方法定义要多得多。这是由于C++的程序员必须提前知道如何用副本构建器控制复制过程,而且要用过载的operator=完成赋值。正如第12章解释的那样,我们有时也要在Java中考虑同样的事情。但在C++中,几乎一刻都不能放松对这些问题的关注。 +若提到缺点,就一定不要忘记Pair在下列代码中定义时的复杂程度。与我们在Java代码中看到的相比,Pair的方法定义要多得多。这是由于C++的程序员必须提前知道如何用副本构造器控制复制过程,而且要用重载的operator=完成赋值。正如第12章解释的那样,我们有时也要在Java中考虑同样的事情。但在C++中,几乎一刻都不能放松对这些问题的关注。 这个项目首先创建一个可以重复使用的部分,由C++头文件中的Pair和CGI_vector构成。从技术角度看,确实不应把这些东西都塞到一个头文件里。但就目前的例子来说,这样做不会造成任何方面的损害,而且更具有Java风格,所以大家阅读理解代码时要显得轻松一些: -``` +``` java //: CGITools.h // Automatically extracts and decodes data // from CGI GETs and POSTs. Tested with GNU C++ @@ -429,23 +429,23 @@ private: 在#include语句后,可看到有一行是: -``` +``` java using namespace std; ``` C++中的“命名空间”(Namespace)解决了由Java的package负责的一个问题:将库名隐藏起来。std命名空间引用的是标准C++库,而vector就在这个库中,所以这一行是必需的。 -Pair类表面看异常简单,只是容纳了两个(private)字符指针而已——一个用于名字,另一个用于值。默认构建器将这两个指针简单地设为零。这是由于在C++中,对象的内存不会自动置零。第二个构建器调用方法decodeURLString(),在新分配的堆内存中生成一个解码过后的字串。这个内存区域必须由对象负责管理及清除,这与“破坏器”中见到的相同。name()和value()方法为相关的字段产生只读指针。利用empty()方法,我们查询Pair对象它的某个字段是否为空;返回的结果是一个bool——C++内建的基本布尔数据类型。operator bool()使用的是C++“运算符过载”的一种特殊形式。它允许我们控制自动类型转换。如果有一个名为p的Pair对象,而且在一个本来希望是布尔结果的表达式中使用,比如if(p){//...,那么编译器能辨别出它有一个Pair,而且需要的是个布尔值,所以自动调用operator bool(),进行必要的转换。 +Pair类表面看异常简单,只是容纳了两个(private)字符指针而已——一个用于名字,另一个用于值。默认构造器将这两个指针简单地设为零。这是由于在C++中,对象的内存不会自动置零。第二个构造器调用方法decodeURLString(),在新分配的堆内存中生成一个解码过后的字串。这个内存区域必须由对象负责管理及清除,这与“破坏器”中见到的相同。name()和value()方法为相关的字段产生只读指针。利用empty()方法,我们查询Pair对象它的某个字段是否为空;返回的结果是一个bool——C++内建的基本布尔数据类型。operator bool()使用的是C++“运算符重载”的一种特殊形式。它允许我们控制自动类型转换。如果有一个名为p的Pair对象,而且在一个本来希望是布尔结果的表达式中使用,比如if(p){//...,那么编译器能辨别出它有一个Pair,而且需要的是个布尔值,所以自动调用operator bool(),进行必要的转换。 -接下来的三个方法属于常规编码,在C++中创建类时必须用到它们。根据C++类采用的所谓“经典形式”,我们必须定义必要的“原始”构建器,以及一个副本构建器和赋值运算符——operator=(以及破坏器,用于清除内存)。之所以要作这样的定义,是由于编译器会“默默”地调用它们。在对象传入、传出一个函数的时候,需要调用副本构建器;而在分配对象时,需要调用赋值运算符。只有真正掌握了副本构建器和赋值运算符的工作原理,才能在C++里写出真正“健壮”的类,但这需要需要一个比较艰苦的过程(注释⑤)。 +接下来的三个方法属于常规编码,在C++中创建类时必须用到它们。根据C++类采用的所谓“经典形式”,我们必须定义必要的“原始”构造器,以及一个副本构造器和赋值运算符——operator=(以及破坏器,用于清除内存)。之所以要作这样的定义,是由于编译器会“默默”地调用它们。在对象传入、传出一个函数的时候,需要调用副本构造器;而在分配对象时,需要调用赋值运算符。只有真正掌握了副本构造器和赋值运算符的工作原理,才能在C++里写出真正“健壮”的类,但这需要需要一个比较艰苦的过程(注释⑤)。 ⑤:我的《Thinking in C++》(Prentice-Hall,1995)用了一整章的地方来讨论这个主题。若需更多的帮助,请务必看看那一章。 -只要将一个对象按值传入或传出函数,就会自动调用副本构建器Pair(const Pair&)。也就是说,对于准备为其制作一个完整副本的那个对象,我们不准备在函数框架中传递它的地址。这并不是Java提供的一个选项,由于我们只能传递句柄,所以在Java里没有所谓的副本构建器(如果想制作一个本地副本,可以“克隆”那个对象——使用clone(),参见第12章)。类似地,如果在Java里分配一个句柄,它会简单地复制。但C++中的赋值意味着整个对象都会复制。在副本构建器中,我们创建新的存储空间,并复制原始数据。但对于赋值运算符,我们必须在分配新存储空间之前释放老存储空间。我们要见到的也许是C++类最复杂的一种情况,但那正是Java的支持者们论证Java比C++简单得多的有力证据。在Java中,我们可以自由传递句柄,善后工作则由垃圾收集器负责,所以可以轻松许多。 +只要将一个对象按值传入或传出函数,就会自动调用副本构造器Pair(const Pair&)。也就是说,对于准备为其制作一个完整副本的那个对象,我们不准备在函数框架中传递它的地址。这并不是Java提供的一个选项,由于我们只能传递指针,所以在Java里没有所谓的副本构造器(如果想制作一个本地副本,可以“克隆”那个对象——使用clone(),参见第12章)。类似地,如果在Java里分配一个指针,它会简单地复制。但C++中的赋值意味着整个对象都会复制。在副本构造器中,我们创建新的存储空间,并复制原始数据。但对于赋值运算符,我们必须在分配新存储空间之前释放老存储空间。我们要见到的也许是C++类最复杂的一种情况,但那正是Java的支持者们论证Java比C++简单得多的有力证据。在Java中,我们可以自由传递指针,善后工作则由垃圾收集器负责,所以可以轻松许多。 但事情并没有完。Pair类为nm和val使用的是char*,最复杂的情况主要是围绕指针展开的。如果用较时髦的C++ string类来代替 `char*` ,事情就要变得简单得多(当然,并不是所有编译器都提供了对string的支持)。那么,Pair的第一部分看起来就象下面这样: -``` +``` java class Pair { string nm; string val; @@ -471,17 +471,17 @@ public: } ``` -(此外,对这个类decodeURLString()会返回一个string,而不是一个char*)。我们不必定义副本构建器、operator=或者破坏器,因为编译器已帮我们做了,而且做得非常好。但即使有些事情是自动进行的,C++程序员也必须了解副本构建以及赋值的细节。 +(此外,对这个类decodeURLString()会返回一个string,而不是一个char*)。我们不必定义副本构造器、operator=或者破坏器,因为编译器已帮我们做了,而且做得非常好。但即使有些事情是自动进行的,C++程序员也必须了解副本构建以及赋值的细节。 Pair类剩下的部分由两个方法构成:decodeURLString()以及一个“帮助器”方法translateHex()——将由decodeURLString()使用。注意translateHex()并不能防范用户的恶意输入,比如“%1H”。分配好足够的存储空间后(必须由破坏器释放),decodeURLString()就会其中遍历,将所有“+”都换成一个空格;将所有十六进制代码(以一个“%”打头)换成对应的字符。 -CGI_vector用于解析和容纳整个CGI GET命令。它是从STL vector里继承的,后者例示为容纳Pair。C++中的继承是用一个冒号表示,在Java中则要用extends。此外,继承默认为private属性,所以几乎肯定需要用到public关键字,就象这样做的那样。大家也会发现CGI_vector有一个副本构建器以及一个operator=,但它们都声明成private。这样做是为了防止编译器同步两个函数(如果不自己声明它们,两者就会同步)。但这同时也禁止了客户程序员按值或者通过赋值传递一个CGI_vector。 +CGI_vector用于解析和容纳整个CGI GET命令。它是从STL vector里继承的,后者例示为容纳Pair。C++中的继承是用一个冒号表示,在Java中则要用extends。此外,继承默认为private属性,所以几乎肯定需要用到public关键字,就象这样做的那样。大家也会发现CGI_vector有一个副本构造器以及一个operator=,但它们都声明成private。这样做是为了防止编译器同步两个函数(如果不自己声明它们,两者就会同步)。但这同时也禁止了客户程序员按值或者通过赋值传递一个CGI_vector。 -CGI_vector的工作是获取QUERY_STRING,并把它解析成“名称/值”对,这需要在Pair的帮助下完成。它首先将字串复制到本地分配的内存,并用常数指针start跟踪起始地址(稍后会在破坏器中用于释放内存)。随后,它用自己的nextPair()方法将字串解析成原始的“名称/值”对,各个对之间用一个“=”和“&”符号分隔。这些对由nextPair()传递给Pair构建器,所以nextPair()返回的是一个Pair对象。随后用push_back()将该对象加入vector。nextPair()遍历完整个QUERY_STRING后,会返回一个零值。 +CGI_vector的工作是获取QUERY_STRING,并把它解析成“名称/值”对,这需要在Pair的帮助下完成。它首先将字串复制到本地分配的内存,并用常数指针start跟踪起始地址(稍后会在破坏器中用于释放内存)。随后,它用自己的nextPair()方法将字串解析成原始的“名称/值”对,各个对之间用一个“=”和“&”符号分隔。这些对由nextPair()传递给Pair构造器,所以nextPair()返回的是一个Pair对象。随后用push_back()将该对象加入vector。nextPair()遍历完整个QUERY_STRING后,会返回一个零值。 现在基本工具已定义好,它们可以简单地在一个CGI程序中使用,就象下面这样: -``` +``` java //: Listmgr2.cpp // CGI version of Listmgr.c in C++, which // extracts its input via the GET submission @@ -560,7 +560,7 @@ void main() { ``` alreadyInList()函数与前一个版本几乎是完全相同的,只是它假定所有电子函件地址都在一个“<>”内。 -在使用GET方法时(通过在FORM引导命令的METHOD标记内部设置,但这在这里由数据发送的方式控制),Web服务器会收集位于“?”后面的所有信息,并把它们置入环境变量QUERY_STRING(查询字串)里。所以为了读取那些信息,必须获得QUERY_STRING的值,这是用标准的C库函数getnv()完成的。在main()中,注意对QUERY_STRING的解析有多么容易:只需把它传递给用于CGI_vector对象的构建器(名为query),剩下的所有工作都会自动进行。从这时开始,我们就可以从query中取出名称和值,把它们当作数组看待(这是由于operator[]在vector里已经过载了)。在调试代码中,大家可看到这一切是如何运作的;调试代码封装在预处理器引导命令#if defined(DEBUG)和#endif(DEBUG)之间。 +在使用GET方法时(通过在FORM引导命令的METHOD标记内部设置,但这在这里由数据发送的方式控制),Web服务器会收集位于“?”后面的所有信息,并把它们置入环境变量QUERY_STRING(查询字串)里。所以为了读取那些信息,必须获得QUERY_STRING的值,这是用标准的C库函数getnv()完成的。在main()中,注意对QUERY_STRING的解析有多么容易:只需把它传递给用于CGI_vector对象的构造器(名为query),剩下的所有工作都会自动进行。从这时开始,我们就可以从query中取出名称和值,把它们当作数组看待(这是由于operator[]在vector里已经重载了)。在调试代码中,大家可看到这一切是如何运作的;调试代码封装在预处理器引导命令#if defined(DEBUG)和#endif(DEBUG)之间。 现在,我们迫切需要掌握一些与CGI有关的东西。CGI程序用两个方式之一传递它们的输入:在GET执行期间通过QUERY_STRING传递(目前用的这种方式),或者在POST期间通过标准输入。但CGI程序通过标准输出发送自己的输出,这通常是用C程序的printf()命令实现的。那么这个输出到哪里去了呢?它回到了Web服务器,由服务器决定该如何处理它。服务器作出决定的依据是content-type(内容类型)头数据。这意味着假如content-type头不是它看到的第一件东西,就不知道该如何处理收到的数据。因此,我们无论如何也要使所有CGI程序都从content-type头开始输出。 @@ -575,7 +575,7 @@ alreadyInList()函数与前一个版本几乎是完全相同的,只是它假 对一个用来控制POST的CGI程序,由CGITools.h提供的Pair和CGI_vector均可不加丝毫改变地使用。下面这段程序揭示了写这样的一个CGI程序有多么简单。这个例子将采用“纯”C++,所以studio.h库被iostream(IO数据流)代替。对于iostream,我们可以使用两个预先定义好的对象:cin,用于同标准输入连接;以及cout,用于同标准输出连接。有几个办法可从cin中读入数据以及向cout中写入。但下面这个程序准备采用标准方法:用“<<”将信息发给cout,并用一个成员函数(此时是read())从cin中读入数据: -``` +``` java //: POSTtest.cpp // CGI_vector works as easily with POST as it // does with GET. Written in "pure" C++. @@ -609,11 +609,11 @@ void main() { getenv()函数返回指向一个字串的指针,那个字串指示着内容的长度。若指针为零,表明CONTENT_LENGTH环境变量尚未设置,所以肯定某个地方出了问题。否则就必须用ANSI C库函数atoi()将字串转换成一个整数。这个长度将与new一起运用,分配足够的存储空间,以便容纳查询字串(另加它的空中止符)。随后为cin()调用read()。read()函数需要取得指向目标缓冲区的一个指针以及要读入的字节数。随后用空字符(null)中止query_str,指出已经抵达字串的末尾,这就叫作“空中止”。 -到这个时候,我们得到的查询字串与GET查询字串已经没有什么区别,所以把它传递给用于CGI_vector的构建器。随后便和前例一样,我们可以自由vector内不同的字段。 +到这个时候,我们得到的查询字串与GET查询字串已经没有什么区别,所以把它传递给用于CGI_vector的构造器。随后便和前例一样,我们可以自由vector内不同的字段。 为测试这个程序,必须把它编译到主机Web服务器的cgi-bin目录下。然后就可以写一个简单的HTML页进行测试,就象下面这样: -``` +``` java @@ -643,7 +643,7 @@ VALUE = "" size = "40">

为建立与服务器的一个直接连接,必须取得自己创建的URL,然后调用openConnection()创建一个URLConnection。但是,由于URLConnection一般不允许我们把数据发给它,所以必须很可笑地调用setDoOutput(true)函数,同时调用的还包括setDoInput(true)以及setAllowUserInteraction(false)——注释⑥。最后,可调用getOutputStream()来创建一个OutputStream(输出数据流),并把它封装到一个DataOutputStream里,以便能按传统方式同它通信。下面列出的便是一个用于完成上述工作的程序片,必须在从它的各个字段里收集了数据之后再执行它: -``` +``` java //: POSTtest.java // An applet that sends its data via a CGI POST import java.awt.*; diff --git "a/15.7 \347\224\250JDBC\350\277\236\346\216\245\346\225\260\346\215\256\345\272\223.md" "b/15.7 \347\224\250JDBC\350\277\236\346\216\245\346\225\260\346\215\256\345\272\223.md" index 1fb8b7a..70665a8 100644 --- "a/15.7 \347\224\250JDBC\350\277\236\346\216\245\346\225\260\346\215\256\345\272\223.md" +++ "b/15.7 \347\224\250JDBC\350\277\236\346\216\245\346\225\260\346\215\256\345\272\223.md" @@ -19,7 +19,7 @@ 所有这些信息都统一编译到一个字串里,即“数据库URL”。举个例子来说,若想通过ODBC子协议同一个标识为“people”的数据库连接,相应的数据库URL可设为: -``` +``` java String dbUrl = "jdbc:odbc:people" ``` @@ -29,7 +29,7 @@ String dbUrl = "jdbc:odbc:people" 下面这个例子将打开一个联络信息数据库,并根据命令行提供的参数查询一个人的姓(Last Name)。它只选择那些有E-mail地址的人的名字,然后列印出符合查询条件的所有人: -``` +``` java //: Lookup.java // Looks up email addresses in a // local database using JDBC @@ -74,7 +74,7 @@ public class Lookup { 可以看到,数据库URL的创建过程与我们前面讲述的完全一样。在该例中,数据库未设密码保护,所以用户名和密码都是空串。 用DriverManager.getConnection()建好连接后,接下来可根据结果Connection对象创建一个Statement(语句)对象,这是用createStatement()方法实现的。根据结果Statement,我们可调用executeQuery(),向其传递包含了SQL-92标准SQL语句的一个字串(不久就会看到如何自动创建这类语句,所以没必要在这里知道关于SQL更多的东西)。 -executeQuery()方法会返回一个ResultSet(结果集)对象,它与继承器非常相似:next()方法将继承器移至语句中的下一条记录;如果已抵达结果集的末尾,则返回null。我们肯定能从executeQuery()返回一个ResultSet对象,即使查询结果是个空集(也就是说,不会产生一个违例)。注意在试图读取任何记录数据之前,都必须调用一次next()。若结果集为空,那么对next()的这个首次调用就会返回false。对于结果集中的每条记录,都可将字段名作为字串使用(当然还有其他方法),从而选择不同的字段。另外要注意的是字段名的大小写是无关紧要的——SQL数据库不在乎这个问题。为决定返回的类型,可调用getString(),getFloat()等等。到这个时候,我们已经用Java的原始格式得到了自己的数据库数据,接下去可用Java代码做自己想做的任何事情了。 +executeQuery()方法会返回一个ResultSet(结果集)对象,它与迭代器非常相似:next()方法将迭代器移至语句中的下一条记录;如果已抵达结果集的末尾,则返回null。我们肯定能从executeQuery()返回一个ResultSet对象,即使查询结果是个空集(也就是说,不会产生一个异常)。注意在试图读取任何记录数据之前,都必须调用一次next()。若结果集为空,那么对next()的这个首次调用就会返回false。对于结果集中的每条记录,都可将字段名作为字串使用(当然还有其他方法),从而选择不同的字段。另外要注意的是字段名的大小写是无关紧要的——SQL数据库不在乎这个问题。为决定返回的类型,可调用getString(),getFloat()等等。到这个时候,我们已经用Java的原始格式得到了自己的数据库数据,接下去可用Java代码做自己想做的任何事情了。 15.7.1 让示例运行起来 @@ -85,13 +85,13 @@ executeQuery()方法会返回一个ResultSet(结果集)对象,它与继承 上述程序包含了下面这条语句: -``` +``` java Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); ``` 这似乎暗示着一个目录结构,但大家不要被它蒙骗了。在我手上这个JDK 1.1安装版本中,根本不存在叫作JdbcOdbcDriver.class的一个文件。所以假如在看了这个例子后去寻找它,那么必然会徒劳而返。另一些人提供的例子使用的是一个假名字,如“myDriver.ClassName”,但人们从字面上得不到任何帮助。事实上,上述用于装载jdbc-odbc驱动程序(实际是与JDK 1.1配套提供的唯一驱动)的语句在联机文档的多处地方均有出现(特别是在一个标记为“JDBC-ODBC Bridge Driver”的页内)。若上面的装载语句不能工作,那么它的名字可能已随着Java新版本的发布而改变了;此时应到联机文档里寻找新的表述方式。 -若装载语句出错,会在这个时候得到一个违例。为了检验驱动程序装载语句是不是能正常工作,请将该语句后面直到catch从句之间的代码暂时设为注释。如果程序运行时未出现违例,表明驱动程序的装载是正确的。 +若装载语句出错,会在这个时候得到一个异常。为了检验驱动程序装载语句是不是能正常工作,请将该语句后面直到catch从句之间的代码暂时设为注释。如果程序运行时未出现异常,表明驱动程序的装载是正确的。 2. 步骤2:配置数据库 @@ -107,12 +107,12 @@ Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); 为了对配置进行测试,需用一种方式核实数据库是否可由查询它的一个程序“见到”。当然,可以简单地运行上述的JDBC示范程序,并加入下述语句: -``` +``` java Connection c = DriverManager.getConnection( dbUrl, user, password); ``` -若掷出一个违例,表明你的配置有误。 +若掷出一个异常,表明你的配置有误。 然而,此时很有必要使用一个自动化的查询生成工具。我使用的是与Microsoft Office配套提供的Microsoft Query,但你完全可以自行选择一个。查询工具必须知道数据库在什么地方,而Microsoft Query要求我进入ODBC Administrator的“文件DSN”卡片,并在那里新添一个条目。同样指定文本驱动程序以及保存数据库的目录。虽然可将这个条目命名为自己喜欢的任何东西,但最好还是使用与“系统DSN”中相同的名字。 @@ -135,7 +135,7 @@ dbUrl, user, password); 查询结果会向我们展示出是否能得到自己希望的东西。 现在可以按下SQL按钮。不需要我们任何方面的介入,正确的SQL代码会立即弹现出来,以便我们粘贴和复制。对于这个查询,相应的SQL代码如下: -``` +``` java SELECT people.FIRST, people.LAST, people.EMAIL FROM people.csv people WHERE (people.LAST='Eckel') AND @@ -149,7 +149,7 @@ ORDER BY people.FIRST 我们注意到上述代码与程序中使用的代码是有所区别的。那是由于查询工具对所有名字都进行了限定,即便涉及的仅有一个数据表(若真的涉及多个数据表,这种限定可避免来自不同表的同名数据列发生冲突)。由于这个查询只需要用到一个数据表,所以可考虑从大多数名字中删除“people”限定符,就象下面这样: -``` +``` java SELECT FIRST, LAST, EMAIL FROM people.csv people WHERE (LAST='Eckel') AND @@ -159,7 +159,7 @@ ORDER BY FIRST 此外,我们不希望“硬编码”这个程序,从而只能查找一个特定的名字。相反,它应该能查找我们在命令行动态提供的一个名字。所以还要进行必要的修改,并将SQL语句转换成一个动态生成的字串。如下所示: -``` +``` java "SELECT FIRST, LAST, EMAIL " + "FROM people.csv people " + "WHERE " + @@ -176,7 +176,7 @@ SQL还有一种方式可将名字插入一个查询,名为“程序”(Proce 最好的方法是让查找程序一直保持运行,要查找什么东西时只需简单地切换到它,并键入要查找的名字即可。下面这个程序将查找程序作为一个“application/applet”创建,且添加了名字自动填写功能,所以不必键入完整的姓,即可看到数据: -``` +``` java //: VLookup.java // GUI version of Lookup.java import java.awt.*; diff --git "a/15.8 \350\277\234\347\250\213\346\226\271\346\263\225.md" "b/15.8 \350\277\234\347\250\213\346\226\271\346\263\225.md" index aa3a3a5..8d6cc51 100644 --- "a/15.8 \350\277\234\347\250\213\346\226\271\346\263\225.md" +++ "b/15.8 \350\277\234\347\250\213\346\226\271\346\263\225.md" @@ -4,7 +4,7 @@ 15.8.1 远程接口概念 -RMI对接口有着强烈的依赖。在需要创建一个远程对象的时候,我们通过传递一个接口来隐藏基层的实施细节。所以客户得到远程对象的一个句柄时,它们真正得到的是接口句柄。这个句柄正好同一些本地的根代码连接,由后者负责通过网络通信。但我们并不关心这些事情,只需通过自己的接口句柄发送消息即可。 +RMI对接口有着强烈的依赖。在需要创建一个远程对象的时候,我们通过传递一个接口来隐藏基层的实施细节。所以客户得到远程对象的一个指针时,它们真正得到的是接口指针。这个指针正好同一些本地的根代码连接,由后者负责通过网络通信。但我们并不关心这些事情,只需通过自己的接口指针发送消息即可。 创建一个远程接口时,必须遵守下列规则: @@ -12,13 +12,13 @@ RMI对接口有着强烈的依赖。在需要创建一个远程对象的时候 (2) 远程接口必须扩展接口java.rmi.Remote。 -(3) 除与应用程序本身有关的违例之外,远程接口中的每个方法都必须在自己的throws从句中声明java.rmi.RemoteException。 +(3) 除与应用程序本身有关的异常之外,远程接口中的每个方法都必须在自己的throws从句中声明java.rmi.RemoteException。 (4) 作为参数或返回值传递的一个远程对象(不管是直接的,还是在本地对象中嵌入)必须声明为远程接口,不可声明为实施类。 下面是一个简单的远程接口示例,它代表的是一个精确计时服务: -``` +``` java //: PerfectTimeI.java // The PerfectTime remote interface package c15.ptime; @@ -29,17 +29,17 @@ interface PerfectTimeI extends Remote { } ///:~ ``` -它表面上与其他接口是类似的,只是对Remote进行了扩展,而且它的所有方法都会“掷”出RemoteException(远程违例)。记住接口和它所有的方法都是public的。 +它表面上与其他接口是类似的,只是对Remote进行了扩展,而且它的所有方法都会“掷”出RemoteException(远程异常)。记住接口和它所有的方法都是public的。 15.8.2 远程接口的实施 -服务器必须包含一个扩展了UnicastRemoteObject的类,并实现远程接口。这个类也可以含有附加的方法,但客户只能使用远程接口中的方法。这是显然的,因为客户得到的只是指向接口的一个句柄,而非实现它的那个类。 +服务器必须包含一个扩展了UnicastRemoteObject的类,并实现远程接口。这个类也可以含有附加的方法,但客户只能使用远程接口中的方法。这是显然的,因为客户得到的只是指向接口的一个指针,而非实现它的那个类。 -必须为远程对象明确定义构建器,即使只准备定义一个默认构建器,用它调用基础类构建器。必须把它明确地编写出来,因为它必须“掷”出RemoteException违例。 +必须为远程对象明确定义构造器,即使只准备定义一个默认构造器,用它调用基础类构造器。必须把它明确地编写出来,因为它必须“掷”出RemoteException异常。 下面列出远程接口PerfectTime的实施过程: -``` +``` java //: PerfectTime.java // The implementation of the PerfectTime // remote object @@ -84,31 +84,31 @@ public class PerfectTime (2) 创建远程对象的一个或多个实例。在这里,大家可看到创建的是PerfectTime对象。 -(3) 向RMI远程对象注册表注册至少一个远程对象。一个远程对象拥有的方法可生成指向其他远程对象的句柄。这样一来,客户只需到注册表里访问一次,得到第一个远程对象即可。 +(3) 向RMI远程对象注册表注册至少一个远程对象。一个远程对象拥有的方法可生成指向其他远程对象的指针。这样一来,客户只需到注册表里访问一次,得到第一个远程对象即可。 1. 设置注册表 在这儿,大家可看到对静态方法Naming.bind()的一个调用。然而,这个调用要求注册表作为计算机上的一个独立进程运行。注册表服务器的名字是rmiregistry。在32位Windows环境中,可使用: -``` +``` java start rmiregistry ``` 令其在后台运行。在Unix中,使用: -``` +``` java rmiregistry & ``` 和许多网络程序一样,rmiregistry位于机器启动它所在的某个IP地址处,但它也必须监视一个端口。如果象上面那样调用rmiregistry,不使用参数,注册表的端口就会默认为1099。若希望它位于其他某个端口,只需在命令行添加一个参数,指定那个端口编号即可。对这个例子来说,端口将位于2005,所以rmiregistry应该象下面这样启动(对于32位Windows): -``` +``` java start rmiregistry 2005 ``` 对于Unix,则使用下述命令: -``` +``` java rmiregistry 2005 & ``` @@ -116,31 +116,31 @@ rmiregistry 2005 & (1) localhost不能随RMI工作。所以为了在单独一台机器上完成对RMI的测试,必须提供机器的名字。为了在32位Windows环境中调查自己机器的名字,可进入控制面板,选择“网络”,选择“标识”卡片,其中列出了计算机的名字。就我自己的情况来说,我的机器叫作“Colossus”(因为我用几个大容量的硬盘保存各种不同的开发系统——Clossus是“巨人”的意思)。似乎大写形式会被忽略。 -(2) 除非计算机有一个活动的TCP/IP连接,否则RMI不能工作,即使所有组件都只需要在本地机器里互相通信。这意味着在试图运行程序之前,必须连接到自己的ISP(因特网服务提供者),否则会得到一些含义模糊的违例消息。 +(2) 除非计算机有一个活动的TCP/IP连接,否则RMI不能工作,即使所有组件都只需要在本地机器里互相通信。这意味着在试图运行程序之前,必须连接到自己的ISP(因特网服务提供者),否则会得到一些含义模糊的异常消息。 ⑦:为找出这些信息,我不知损伤了多少个脑细胞。 考虑到这些因素,bind()命令变成了下面这个样子: -``` +``` java Naming.bind("//colossus:2005/PerfectTime", pt); ``` 若使用默认端口1099,就没有必要指定一个端口,所以可以使用: -``` +``` java Naming.bind("//colossus/PerfectTime", pt); ``` 在JDK未来的版本中(1.1之后),一旦改正了localhost的问题,就能正常地进行本地测试,去掉IP地址,只使用标识符: Naming.bind("PerfectTime", pt); -服务名是任意的;它在这里正好为PerfectTime,和类名一样,但你可以根据情况任意修改。最重要的是确保它在注册表里是个独一无二的名字,以便客户正常地获取远程对象。若这个名字已在注册表里了,就会得到一个AlreadyBoundException违例。为防止这个问题,可考虑坚持使用rebind(),放弃bind()。这是由于rebind()要么会添加一个新条目,要么将同名的条目替换掉。 +服务名是任意的;它在这里正好为PerfectTime,和类名一样,但你可以根据情况任意修改。最重要的是确保它在注册表里是个独一无二的名字,以便客户正常地获取远程对象。若这个名字已在注册表里了,就会得到一个AlreadyBoundException异常。为防止这个问题,可考虑坚持使用rebind(),放弃bind()。这是由于rebind()要么会添加一个新条目,要么将同名的条目替换掉。 尽管main()退出,我们的对象已经创建并注册,所以会由注册表一直保持活动状态,等候客户到达并发出对它的请求。只要rmiregistry处于运行状态,而且我们没有为名字调用Naming.unbind()方法,对象就肯定位于那个地方。考虑到这个原因,在我们设计自己的代码时,需要先关闭rmiregistry,并在编译远程对象的一个新版本时重新启动它。 并不一定要将rmiregistry作为一个外部进程启动。若事前知道自己的是要求用以注册表的唯一一个应用,就可在程序内部启动它,使用下述代码: -``` +``` java LocateRegistry.createRegistry(2005); ``` @@ -155,14 +155,14 @@ LocateRegistry.createRegistry(2005); 然而,rmic工具与特定的包和类路径有很大的关联。PerfectTime.java位于包c15.Ptime中,即使我们调用与PerfectTime.class同一目录内的rmic,rmic都无法找到文件。这是由于它搜索的是类路径。因此,我们必须同时指定类路径,就象下面这样: -``` +``` java rmic c15.PTime.PerfectTime ``` 执行这个命令时,并不一定非要在包含了PerfectTime.class的目录中,但结果会置于当前目录。 若rmic成功运行,目录里就会多出两个新类: -``` +``` java PerfectTime_Stub.class PerfectTime_Skel.class ``` @@ -173,7 +173,7 @@ PerfectTime_Skel.class RMI全部的宗旨就是尽可能简化远程对象的使用。我们在客户程序中要做的唯一一件额外的事情就是查找并从服务器取回远程接口。自此以后,剩下的事情就是普通的Java编程:将消息发给对象。下面是使用PerfectTime的程序: -``` +``` java //: DisplayPerfectTime.java // Uses remote object PerfectTime package c15.ptime; @@ -200,14 +200,14 @@ public class DisplayPerfectTime { ID字串与那个用Naming注册对象的那个字串是相同的,第一部分指出了URL和端口号。由于我们准备使用一个URL,所以也可以指定因特网上的一台机器。 -从Naming.lookup()返回的必须造型到远程接口,而不是到类。若换用类,会得到一个违例提示。 +从Naming.lookup()返回的必须造型到远程接口,而不是到类。若换用类,会得到一个异常提示。 在下述方法调用中: -``` +``` java t.getPerfectTime( ) ``` -我们可看到一旦获得远程对象的句柄,用它进行的编程与用本地对象的编程是非常相似(仅有一个区别:远程方法会“掷”出一个RemoteException违例)。 +我们可看到一旦获得远程对象的指针,用它进行的编程与用本地对象的编程是非常相似(仅有一个区别:远程方法会“掷”出一个RemoteException异常)。 15.8.5 RMI的替选方案 diff --git "a/16.1 \350\214\203\345\274\217\347\232\204\346\246\202\345\277\265.md" "b/16.1 \350\214\203\345\274\217\347\232\204\346\246\202\345\277\265.md" index 65451d0..74ffa11 100644 --- "a/16.1 \350\214\203\345\274\217\347\232\204\346\246\202\345\277\265.md" +++ "b/16.1 \350\214\203\345\274\217\347\232\204\346\246\202\345\277\265.md" @@ -10,13 +10,13 @@ 所以设计范式的最终目标就是将代码中变化的内容隔离开。如果从这个角度观察,就会发现本书实际已采用了一些设计范式。举个例子来说,继承可以想象成一种设计范式(类似一个由编译器实现的)。在都拥有同样接口(即保持不变的东西)的对象内部,它允许我们表达行为上的差异(即发生变化的东西)。合成亦可想象成一种范式,因为它允许我们修改——动态或静态——用于实现类的对象,所以也能修改类的运作方式。 -在《Design Patterns》一书中,大家还能看到另一种范式:“继承器”(即Iterator,Java 1.0和1.1不负责任地把它叫作Enumeration,即“枚举”;Java1.2的集合则改回了“继承器”的称呼)。当我们在集合里遍历,逐个选择不同的元素时,继承器可将集合的实施细节有效地隐藏起来。利用继承器,可以编写出通用的代码,以便对一个序列里的所有元素采取某种操作,同时不必关心这个序列是如何构建的。这样一来,我们的通用代码即可伴随任何能产生继承器的集合使用。 +在《Design Patterns》一书中,大家还能看到另一种范式:“迭代器”(即Iterator,Java 1.0和1.1不负责任地把它叫作Enumeration,即“枚举”;Java1.2的集合则改回了“迭代器”的称呼)。当我们在集合里遍历,逐个选择不同的元素时,迭代器可将集合的实施细节有效地隐藏起来。利用迭代器,可以编写出通用的代码,以便对一个序列里的所有元素采取某种操作,同时不必关心这个序列是如何构建的。这样一来,我们的通用代码即可伴随任何能产生迭代器的集合使用。 -16.1.1 单子 +16.1.1 单例 -或许最简单的设计范式就是“单子”(Singleton),它能提供对象的一个(而且只有一个)实例。单子在Java库中得到了应用,但下面这个例子显得更直接一些: +或许最简单的设计范式就是“单例”(Singleton),它能提供对象的一个(而且只有一个)实例。单例在Java库中得到了应用,但下面这个例子显得更直接一些: -``` +``` java //: SingletonPattern.java // The Singleton design pattern: you can // never instantiate more than one. @@ -52,11 +52,11 @@ public class SingletonPattern { } ///:~ ``` -创建单子的关键就是防止客户程序员采用除由我们提供的之外的任何一种方式来创建一个对象。必须将所有构建器都设为private(私有),而且至少要创建一个构建器,以防止编译器帮我们自动同步一个默认构建器(它会自做聪明地创建成为“友好的”——friendly,而非private)。 +创建单例的关键就是防止客户程序员采用除由我们提供的之外的任何一种方式来创建一个对象。必须将所有构造器都设为private(私有),而且至少要创建一个构造器,以防止编译器帮我们自动同步一个默认构造器(它会自做聪明地创建成为“友好的”——friendly,而非private)。 -此时应决定如何创建自己的对象。在这儿,我们选择了静态创建的方式。但亦可选择等候客户程序员发出一个创建请求,然后根据他们的要求动态创建。不管在哪种情况下,对象都应该保存为“私有”属性。我们通过公用方法提供访问途径。在这里,getHandle()会产生指向Singleton的一个句柄。剩下的接口(getValue()和setValue())属于普通的类接口。 +此时应决定如何创建自己的对象。在这儿,我们选择了静态创建的方式。但亦可选择等候客户程序员发出一个创建请求,然后根据他们的要求动态创建。不管在哪种情况下,对象都应该保存为“私有”属性。我们通过公用方法提供访问途径。在这里,getHandle()会产生指向Singleton的一个指针。剩下的接口(getValue()和setValue())属于普通的类接口。 -Java也允许通过克隆(Clone)方式来创建一个对象。在这个例子中,将类设为final可禁止克隆的发生。由于Singleton是从Object直接继承的,所以clone()方法会保持protected(受保护)属性,不能够使用它(强行使用会造成编译期错误)。然而,假如我们是从一个类结构中继承,那个结构已经过载了clone()方法,使其具有public属性,并实现了Cloneable,那么为了禁止克隆,需要过载clone(),并掷出一个CloneNotSupportedException(不支持克隆违例),就象第12章介绍的那样。亦可过载clone(),并简单地返回this。那样做会造成一定的混淆,因为客户程序员可能错误地认为对象尚未克隆,仍然操纵的是原来的那个。 +Java也允许通过克隆(Clone)方式来创建一个对象。在这个例子中,将类设为final可禁止克隆的发生。由于Singleton是从Object直接继承的,所以clone()方法会保持protected(受保护)属性,不能够使用它(强行使用会造成编译期错误)。然而,假如我们是从一个类结构中继承,那个结构已经重载了clone()方法,使其具有public属性,并实现了Cloneable,那么为了禁止克隆,需要重载clone(),并掷出一个CloneNotSupportedException(不支持克隆异常),就象第12章介绍的那样。亦可重载clone(),并简单地返回this。那样做会造成一定的混淆,因为客户程序员可能错误地认为对象尚未克隆,仍然操纵的是原来的那个。 注意我们并不限于只能创建一个对象。亦可利用该技术创建一个有限的对象池。但在那种情况下,可能需要解决池内对象的共享问题。如果不幸真的遇到这个问题,可以自己设计一套方案,实现共享对象的登记与撤消登记。 @@ -68,6 +68,6 @@ Java也允许通过克隆(Clone)方式来创建一个对象。在这个例 (2) 结构:设计对象,满足特定的项目限制。这涉及对象与其他对象的连接方式,以保证系统内的改变不会影响到这些连接。 -(3) 行为:对程序中特定类型的行动进行操纵的对象。这要求我们将希望采取的操作封装起来,比如解释一种语言、实现一个请求、在一个序列中遍历(就象在继承器中那样)或者实现一种算法。本章提供了“观察器”(Observer)和“访问器”(Visitor)的范式的例子。 +(3) 行为:对程序中特定类型的行动进行操纵的对象。这要求我们将希望采取的操作封装起来,比如解释一种语言、实现一个请求、在一个序列中遍历(就象在迭代器中那样)或者实现一种算法。本章提供了“观察器”(Observer)和“访问器”(Visitor)的范式的例子。 《Design Patterns》为所有这23种范式都分别使用了一节,随附的还有大量示例,但大多是用C++编写的,少数用Smalltalk编写(如看过这本书,就知道这实际并不是个大问题,因为很容易即可将基本概念从两种语言翻译到Java里)。现在这本书并不打算重复《Design Patterns》介绍的所有范式,因为那是一本独立的书,大家应该单独阅读。相反,本章只准备给出一些例子,让大家先对范式有个大致的印象,并理解它们的重要性到底在哪里。 diff --git "a/16.2 \350\247\202\345\257\237\345\231\250\350\214\203\345\274\217.md" "b/16.2 \350\247\202\345\257\237\345\231\250\350\214\203\345\274\217.md" index 6eefac4..9905a05 100644 --- "a/16.2 \350\247\202\345\257\237\345\231\250\350\214\203\345\274\217.md" +++ "b/16.2 \350\247\202\345\257\237\345\231\250\350\214\203\345\274\217.md" @@ -8,7 +8,7 @@ 下面这个例子类似于第14章的ColorBoxes示例。箱子(Boxes)置于一个屏幕网格中,每个都初始化一种随机的颜色。此外,每个箱子都“实现”(implement)了“观察器”(Observer)接口,而且随一个Observable对象进行了注册。若点击一个箱子,其他所有箱子都会收到一个通知,指出一个改变已经发生。这是由于Observable对象会自动调用每个Observer对象的update()方法。在这个方法内,箱子会检查被点中的那个箱子是否与自己紧邻。若答案是肯定的,那么也修改自己的颜色,保持与点中那个箱子的协调。 -``` +``` java //: BoxObserver.java // Demonstration of Observer pattern using // Java's built-in observer classes. diff --git "a/16.3 \346\250\241\346\213\237\345\236\203\345\234\276\345\233\236\346\224\266\347\253\231.md" "b/16.3 \346\250\241\346\213\237\345\236\203\345\234\276\345\233\236\346\224\266\347\253\231.md" index 44d5076..fd7ac6b 100644 --- "a/16.3 \346\250\241\346\213\237\345\236\203\345\234\276\345\233\236\346\224\266\347\253\231.md" +++ "b/16.3 \346\250\241\346\213\237\345\236\203\345\234\276\345\233\236\346\224\266\347\253\231.md" @@ -4,7 +4,7 @@ 这并不是一种普通的设计,因为它增加了一个新的限制。正是这个限制使问题变得非常有趣——它更象我们在工作中碰到的那些非常麻烦的问题。这个额外的限制是:垃圾抵达垃圾回收站时,它们全都是混合在一起的。程序必须为那些垃圾的分类定出一个模型。这正是RTTI发挥作用的地方:我们有大量不知名的垃圾,程序将正确判断出它们所属的类型。 -``` +``` java //: RecycleA.java // Recycling with RTTI package c16.recyclea; @@ -108,17 +108,17 @@ public class RecycleA { 要注意的第一个地方是package语句: -``` +``` java package c16.recyclea; ``` 这意味着在本书采用的源码目录中,这个文件会被置入从c16(代表第16章的程序)分支出来的recyclea子目录中。第17章的解包工具会负责将其置入正确的子目录。之所以要这样做,是因为本章会多次改写这个特定的例子;它的每个版本都会置入自己的“包”(package)内,避免类名的冲突。 -其中创建了几个Vector对象,用于容纳Trash句柄。当然,Vector实际容纳的是Object(对象),所以它们最终能够容纳任何东西。之所以要它们容纳Trash(或者从Trash衍生出来的其他东西),唯一的理由是我们需要谨慎地避免放入除Trash以外的其他任何东西。如果真的把某些“错误”的东西置入Vector,那么不会在编译期得到出错或警告提示——只能通过运行期的一个违例知道自己已经犯了错误。 +其中创建了几个Vector对象,用于容纳Trash指针。当然,Vector实际容纳的是Object(对象),所以它们最终能够容纳任何东西。之所以要它们容纳Trash(或者从Trash衍生出来的其他东西),唯一的理由是我们需要谨慎地避免放入除Trash以外的其他任何东西。如果真的把某些“错误”的东西置入Vector,那么不会在编译期得到出错或警告提示——只能通过运行期的一个异常知道自己已经犯了错误。 -Trash句柄加入后,它们会丢失自己的特定标识信息,只会成为简单的Object句柄(上溯造型)。然而,由于存在多形性的因素,所以在我们通过Enumeration sorter调用动态绑定方法时,一旦结果Object已经造型回Trash,仍然会发生正确的行为。sumValue()也用一个Enumeration对Vector中的每个对象进行操作。 +Trash指针加入后,它们会丢失自己的特定标识信息,只会成为简单的Object指针(上溯造型)。然而,由于存在多态性的因素,所以在我们通过Enumeration sorter调用动态绑定方法时,一旦结果Object已经造型回Trash,仍然会发生正确的行为。sumValue()也用一个Enumeration对Vector中的每个对象进行操作。 -表面上持,先把Trash的类型上溯造型到一个集合容纳基础类型的句柄,再回过头重新下溯造型,这似乎是一种非常愚蠢的做法。为什么不只是一开始就将垃圾置入适当的容器里呢?(事实上,这正是拨开“回收”一团迷雾的关键)。在这个程序中,我们很容易就可以换成这种做法,但在某些情况下,系统的结构及灵活性都能从下溯造型中得到极大的好处。 +表面上持,先把Trash的类型上溯造型到一个集合容纳基础类型的指针,再回过头重新下溯造型,这似乎是一种非常愚蠢的做法。为什么不只是一开始就将垃圾置入适当的容器里呢?(事实上,这正是拨开“回收”一团迷雾的关键)。在这个程序中,我们很容易就可以换成这种做法,但在某些情况下,系统的结构及灵活性都能从下溯造型中得到极大的好处。 该程序已满足了设计的初衷:它能够正常工作!只要这是个一次性的方案,就会显得非常出色。但是,真正有用的程序应该能够在任 何时候解决问题。所以必须问自己这样一个问题:“如果情况发生了变化,它还能工作吗?”举个例子来说,厚纸板现在是一种非常有价值的可回收物品,那么如何把它集成到系统中呢(特别是程序很大很复杂的时候)?由于前面在switch语句中的类型检查编码可能散布于整个程序,所以每次加入一种新类型时,都必须找到所有那些编码。若不慎遗漏一个,编译器除了指出存在一个错误之外,不能再提供任何有价值的帮助。 diff --git "a/16.4 \346\224\271\350\277\233\350\256\276\350\256\241.md" "b/16.4 \346\224\271\350\277\233\350\256\276\350\256\241.md" index bd92cbc..36ef000 100644 --- "a/16.4 \346\224\271\350\277\233\350\256\276\350\256\241.md" +++ "b/16.4 \346\224\271\350\277\233\350\256\276\350\256\241.md" @@ -11,7 +11,7 @@ 首先考虑Trash对象首次创建的地方,这是main()里的一个switch语句: -``` +``` java for(int i = 0; i < 30; i++) switch((int)(Math.random() * 3)) { case 0 : @@ -28,12 +28,12 @@ } ``` -这些代码显然“过于复杂”,也是新类型加入时必须改动代码的场所之一。如果经常都要加入新类型,那么更好的方案就是建立一个独立的方法,用它获取所有必需的信息,并创建一个句柄,指向正确类型的一个对象——已经上溯造型到一个Trash对象。在《Design Patterns》中,它被粗略地称呼为“创建范式”。要在这里应用的特殊范式是Factory方法的一种变体。在这里,Factory方法属于Trash的一名static(静态)成员。但更常见的一种情况是:它属于衍生类中一个被过载的方法。 -Factory方法的基本原理是我们将创建对象所需的基本信息传递给它,然后返回并等候句柄(已经上溯造型至基础类型)作为返回值出现。从这时开始,就可以按多形性的方式对待对象了。因此,我们根本没必要知道所创建对象的准确类型是什么。事实上,Factory方法会把自己隐藏起来,我们是看不见它的。这样做可防止不慎的误用。如果想在没有多形性的前提下使用对象,必须明确地使用RTTI和指定造型。 +这些代码显然“过于复杂”,也是新类型加入时必须改动代码的场所之一。如果经常都要加入新类型,那么更好的方案就是建立一个独立的方法,用它获取所有必需的信息,并创建一个指针,指向正确类型的一个对象——已经上溯造型到一个Trash对象。在《Design Patterns》中,它被粗略地称呼为“创建范式”。要在这里应用的特殊范式是Factory方法的一种变体。在这里,Factory方法属于Trash的一名static(静态)成员。但更常见的一种情况是:它属于衍生类中一个被重载的方法。 +Factory方法的基本原理是我们将创建对象所需的基本信息传递给它,然后返回并等候指针(已经上溯造型至基础类型)作为返回值出现。从这时开始,就可以按多态性的方式对待对象了。因此,我们根本没必要知道所创建对象的准确类型是什么。事实上,Factory方法会把自己隐藏起来,我们是看不见它的。这样做可防止不慎的误用。如果想在没有多态性的前提下使用对象,必须明确地使用RTTI和指定造型。 -但仍然存在一个小问题,特别是在基础类中使用更复杂的方法(不是在这里展示的那种),且在衍生类里过载(覆盖)了它的前提下。如果在衍生类里请求的信息要求更多或者不同的参数,那么该怎么办呢?“创建更多的对象”解决了这个问题。为实现Factory方法,Trash类使用了一个新的方法,名为factory。为了将创建数据隐藏起来,我们用一个名为Info的新类包含factory方法创建适当的Trash对象时需要的全部信息。下面是Info一种简单的实现方式: +但仍然存在一个小问题,特别是在基础类中使用更复杂的方法(不是在这里展示的那种),且在衍生类里重载(覆盖)了它的前提下。如果在衍生类里请求的信息要求更多或者不同的参数,那么该怎么办呢?“创建更多的对象”解决了这个问题。为实现Factory方法,Trash类使用了一个新的方法,名为factory。为了将创建数据隐藏起来,我们用一个名为Info的新类包含factory方法创建适当的Trash对象时需要的全部信息。下面是Info一种简单的实现方式: -``` +``` java class Info { int type; // Must change this to add another type: @@ -45,12 +45,12 @@ class Info { } } -``` -Info对象唯一的任务就是容纳用于factory()方法的信息。现在,假如出现了一种特殊情况,factory()需要更多或者不同的信息来新建一种类型的Trash对象,那么再也不需要改动factory()了。通过添加新的数据和构建器,我们可以修改Info类,或者采用子类处理更典型的面向对象形式。 +``` java +Info对象唯一的任务就是容纳用于factory()方法的信息。现在,假如出现了一种特殊情况,factory()需要更多或者不同的信息来新建一种类型的Trash对象,那么再也不需要改动factory()了。通过添加新的数据和构造器,我们可以修改Info类,或者采用子类处理更典型的面向对象形式。 用于这个简单示例的factory()方法如下: -``` +``` java static Trash factory(Info i) { switch(i.type) { default: // To quiet the compiler @@ -71,7 +71,7 @@ Info对象唯一的任务就是容纳用于factory()方法的信息。现在, 新对象在main()中的创建现在变得非常简单和清爽: -``` +``` java for(int i = 0; i < 30; i++) bin.addElement( Trash.factory( @@ -80,7 +80,7 @@ Info对象唯一的任务就是容纳用于factory()方法的信息。现在, Math.random() * 100))); ``` -我们在这里创建了一个Info对象,用于将数据传入factory();后者在内存堆中创建某种Trash对象,并返回添加到Vector bin内的句柄。当然,如果改变了参数的数量及类型,仍然需要修改这个语句。但假如Info对象的创建是自动进行的,也可以避免那个麻烦。例如,可将参数的一个Vector传递到Info对象的构建器中(或直接传入一个factory()调用)。这要求在运行期间对参数(自变量)进行分析与检查,但确实提供了非常高的灵活程度。 +我们在这里创建了一个Info对象,用于将数据传入factory();后者在内存堆中创建某种Trash对象,并返回添加到Vector bin内的指针。当然,如果改变了参数的数量及类型,仍然需要修改这个语句。但假如Info对象的创建是自动进行的,也可以避免那个麻烦。例如,可将参数的一个Vector传递到Info对象的构造器中(或直接传入一个factory()调用)。这要求在运行期间对参数(自变量)进行分析与检查,但确实提供了非常高的灵活程度。 大家从这个代码可看出Factory要负责解决的“领头变化”问题:如果向系统添加了新类型(发生了变化),唯一需要修改的代码在Factory内部,所以Factory将那种变化的影响隔离出来了。 @@ -92,11 +92,11 @@ Info对象唯一的任务就是容纳用于factory()方法的信息。现在, 采用这种方案,我们不必用硬编码的方式植入任何创建信息。每个对象都知道如何揭示出适当的信息,以及如何对自身进行克隆。所以一种新类型加入系统的时候,factory()方法不需要任何改变。 -为解决原型的创建问题,一个方法是添加大量方法,用它们支持新对象的创建。但在Java 1.1中,如果拥有指向Class对象的一个句柄,那么它已经提供了对创建新对象的支持。利用Java 1.1的“反射”(已在第11章介绍)技术,即便我们只有指向Class对象的一个句柄,亦可正常地调用一个构建器。这对原型问题的解决无疑是个完美的方案。 +为解决原型的创建问题,一个方法是添加大量方法,用它们支持新对象的创建。但在Java 1.1中,如果拥有指向Class对象的一个指针,那么它已经提供了对创建新对象的支持。利用Java 1.1的“反射”(已在第11章介绍)技术,即便我们只有指向Class对象的一个指针,亦可正常地调用一个构造器。这对原型问题的解决无疑是个完美的方案。 -原型列表将由指向所有想创建的Class对象的一个句柄列表间接地表示。除此之外,假如原型处理失败,则factory()方法会认为由于一个特定的Class对象不在列表中,所以会尝试装载它。通过以这种方式动态装载原型,Trash类根本不需要知道自己要操纵的是什么类型。因此,在我们添加新类型时不需要作出任何形式的修改。于是,我们可在本章剩余的部分方便地重复利用它。 +原型列表将由指向所有想创建的Class对象的一个指针列表间接地表示。除此之外,假如原型处理失败,则factory()方法会认为由于一个特定的Class对象不在列表中,所以会尝试装载它。通过以这种方式动态装载原型,Trash类根本不需要知道自己要操纵的是什么类型。因此,在我们添加新类型时不需要作出任何形式的修改。于是,我们可在本章剩余的部分方便地重复利用它。 -``` +``` java //: Trash.java // Base class for Trash recycling examples package c16.trash; @@ -185,35 +185,35 @@ public abstract class Trash { } ///:~ ``` -基本Trash类和sumValue()还是象往常一样。这个类剩下的部分支持原型范式。大家首先会看到两个内部类(被设为static属性,使其成为只为代码组织目的而存在的内部类),它们描述了可能出现的违例。在它后面跟随的是一个Vector trashTypes,用于容纳Class句柄。 +基本Trash类和sumValue()还是象往常一样。这个类剩下的部分支持原型范式。大家首先会看到两个内部类(被设为static属性,使其成为只为代码组织目的而存在的内部类),它们描述了可能出现的异常。在它后面跟随的是一个Vector trashTypes,用于容纳Class指针。 在Trash.factory()中,Info对象id(Info类的另一个版本,与前面讨论的不同)内部的String包含了要创建的那种Trash的类型名称。这个String会与列表中的Class名比较。若存在相符的,那便是要创建的对象。当然,还有很多方法可以决定我们想创建的对象。之所以要采用这种方法,是因为从一个文件读入的信息可以转换成对象。 -发现自己要创建的Trash(垃圾)种类后,接下来就轮到“反射”方法大显身手了。getConstructor()方法需要取得自己的参数——由Class句柄构成的一个数组。这个数组代表着不同的参数,并按它们正确的顺序排列,以便我们查找的构建器使用。在这儿,该数组是用Java 1.1的数组创建语法动态创建的: +发现自己要创建的Trash(垃圾)种类后,接下来就轮到“反射”方法大显身手了。getConstructor()方法需要取得自己的参数——由Class指针构成的一个数组。这个数组代表着不同的参数,并按它们正确的顺序排列,以便我们查找的构造器使用。在这儿,该数组是用Java 1.1的数组创建语法动态创建的: -``` +``` java new Class[] {double.class} ``` -这个代码假定所有Trash类型都有一个需要double数值的构建器(注意double.class与Double.class是不同的)。若考虑一种更灵活的方案,亦可调用getConstructors(),令其返回可用构建器的一个数组。 -从getConstructors()返回的是指向一个Constructor对象的句柄(该对象是java.lang.reflect的一部分)。我们用方法newInstance()动态地调用构建器。该方法需要获取包含了实际参数的一个Object数组。这个数组同样是按Java 1.1的语法创建的: +这个代码假定所有Trash类型都有一个需要double数值的构造器(注意double.class与Double.class是不同的)。若考虑一种更灵活的方案,亦可调用getConstructors(),令其返回可用构造器的一个数组。 +从getConstructors()返回的是指向一个Constructor对象的指针(该对象是java.lang.reflect的一部分)。我们用方法newInstance()动态地调用构造器。该方法需要获取包含了实际参数的一个Object数组。这个数组同样是按Java 1.1的语法创建的: -``` +``` java new Object[] {new Double(info.data)} ``` 在这种情况下,double必须置入一个封装(容器)类的内部,使其真正成为这个对象数组的一部分。通过调用newInstance(),会提取出double,但大家可能会觉得稍微有些迷惑——参数既可能是double,也可能是Double,但在调用的时候必须用Double传递。幸运的是,这个问题只存在于基本数据类型中间。 -理解了具体的过程后,再来创建一个新对象,并且只为它提供一个Class句柄,事情就变得非常简单了。就目前的情况来说,内部循环中的return永远不会执行,我们在终点就会退出。在这儿,程序动态装载Class对象,并把它加入trashTypes(垃圾类型)列表,从而试图纠正这个问题。若仍然找不到真正有问题的地方,同时装载又是成功的,那么就重复调用factory方法,重新试一遍。 +理解了具体的过程后,再来创建一个新对象,并且只为它提供一个Class指针,事情就变得非常简单了。就目前的情况来说,内部循环中的return永远不会执行,我们在终点就会退出。在这儿,程序动态装载Class对象,并把它加入trashTypes(垃圾类型)列表,从而试图纠正这个问题。若仍然找不到真正有问题的地方,同时装载又是成功的,那么就重复调用factory方法,重新试一遍。 -正如大家会看到的那样,这种设计方案最大的优点就是不需要改动代码。无论在什么情况下,它都能正常地使用(假定所有Trash子类都包含了一个构建器,用以获取单个double参数)。 +正如大家会看到的那样,这种设计方案最大的优点就是不需要改动代码。无论在什么情况下,它都能正常地使用(假定所有Trash子类都包含了一个构造器,用以获取单个double参数)。 1. Trash子类 -为了与原型机制相适应,对Trash每个新子类唯一的要求就是在其中包含了一个构建器,指示它获取一个double参数。Java 1.1的“反射”机制可负责剩下的所有工作。 +为了与原型机制相适应,对Trash每个新子类唯一的要求就是在其中包含了一个构造器,指示它获取一个double参数。Java 1.1的“反射”机制可负责剩下的所有工作。 下面是不同类型的Trash,每种类型都有它们自己的文件里,但都属于Trash包的一部分(同样地,为了方便在本章内重复使用): -``` +``` java //: Aluminum.java // The Aluminum class with prototyping package c16.trash; @@ -230,7 +230,7 @@ public class Aluminum extends Trash { 下面是一种新的Trash类型: -``` +``` java //: Cardboard.java // The Cardboard class with prototyping package c16.trash; @@ -245,13 +245,13 @@ public class Cardboard extends Trash { } ///:~ ``` -可以看出,除构建器以外,这些类根本没有什么特别的地方。 +可以看出,除构造器以外,这些类根本没有什么特别的地方。 2. 从外部文件中解析出Trash 与Trash对象有关的信息将从一个外部文件中读取。针对Trash的每个方面,文件内列出了所有必要的信息——每行都代表一个方面,采用“垃圾(废品)名称:值”的固定格式。例如: -``` +``` java c16.Trash.Glass:54 c16.Trash.Paper:22 c16.Trash.Paper:11 @@ -295,7 +295,7 @@ c16.Trash.Cardboard:22 Trash解析器置入单独的文件中,因为本章将不断地用到它。如下所示: -``` +``` java //: ParseTrash.java // Open a file and parse its contents into // Trash objects, placing each into a Vector @@ -337,9 +337,9 @@ public class ParseTrash { ``` -在RecycleA.java中,我们用一个Vector容纳Trash对象。然而,亦可考虑采用其他集合类型。为做到这一点,fillBin()的第一个版本将获取指向一个Fillable的句柄。后者是一个接口,用于支持一个名为addTrash()的方法: +在RecycleA.java中,我们用一个Vector容纳Trash对象。然而,亦可考虑采用其他集合类型。为做到这一点,fillBin()的第一个版本将获取指向一个Fillable的指针。后者是一个接口,用于支持一个名为addTrash()的方法: -``` +``` java //: Fillable.java // Any object that can be filled with Trash package c16.trash; @@ -349,9 +349,9 @@ public interface Fillable { } ///:~ ``` -支持该接口的所有东西都能伴随fillBin使用。当然,Vector并未实现Fillable,所以它不能工作。由于Vector将在大多数例子中应用,所以最好的做法是添加另一个过载的fillBin()方法,令其以一个Vector作为参数。利用一个适配器(Adapter)类,这个Vector可作为一个Fillable对象使用: +支持该接口的所有东西都能伴随fillBin使用。当然,Vector并未实现Fillable,所以它不能工作。由于Vector将在大多数例子中应用,所以最好的做法是添加另一个重载的fillBin()方法,令其以一个Vector作为参数。利用一个适配器(Adapter)类,这个Vector可作为一个Fillable对象使用: -``` +``` java //: FillableVector.java // Adapter that makes a Vector Fillable package c16.trash; @@ -366,9 +366,9 @@ public class FillableVector implements Fillable { } ///:~ ``` -可以看到,这个类唯一的任务就是负责将Fillable的addTrash()同Vector的addElement()方法连接起来。利用这个类,已过载的fillBin()方法可在ParseTrash.java中伴随一个Vector使用: +可以看到,这个类唯一的任务就是负责将Fillable的addTrash()同Vector的addElement()方法连接起来。利用这个类,已重载的fillBin()方法可在ParseTrash.java中伴随一个Vector使用: -``` +``` java public static void fillBin(String filename, Vector bin) { fillBin(filename, new FillableVector(bin)); @@ -381,7 +381,7 @@ public class FillableVector implements Fillable { 现在,大家可以看到采用原型技术的、修订过的RecycleA.java版本了: -``` +``` java //: RecycleAP.java // Recycling with RTTI and Prototypes package c16.recycleap; @@ -422,4 +422,4 @@ public class RecycleAP { 所有Trash对象——以及ParseTrash及支撑类——现在都成为名为c16.trash的一个包的一部分,所以它们可以简单地导入。 无论打开包含了Trash描述信息的数据文件,还是对那个文件进行解析,所有涉及到的操作均已封装到static(静态)方法ParseTrash.fillBin()里。所以它现在已经不是我们设计过程中要注意的一个重点。在本章剩余的部分,大家经常都会看到无论添加的是什么类型的新类,ParseTrash.fillBin()都会持续工作,不会发生改变,这无疑是一种优良的设计方案。 -提到对象的创建,这一方案确实已将新类型加入系统所需的变动严格地“本地化”了。但在使用RTTI的过程中,却存在着一个严重的问题,这里已明确地显露出来。程序表面上工作得很好,但却永远侦测到不能“硬纸板”(Cardboard)这种新的废品类型——即使列表里确实有一个硬纸板类型!之所以会出现这种情况,完全是由于使用了RTTI的缘故。RTTI只会查找那些我们告诉它查找的东西。RTTI在这里错误的用法是“系统中的每种类型”都进行了测试,而不是仅测试一种类型或者一个类型子集。正如大家以后会看到的那样,在测试每一种类型时可换用其他方式来运用多形性特征。但假如以这种形式过多地使用RTTI,而且又在自己的系统里添加了一种新类型,很容易就会忘记在程序里作出适当的改动,从而埋下以后难以发现的Bug。因此,在这种情况下避免使用RTTI是很有必要的,这并不仅仅是为了表面好看——也是为了产生更易维护的代码。 +提到对象的创建,这一方案确实已将新类型加入系统所需的变动严格地“本地化”了。但在使用RTTI的过程中,却存在着一个严重的问题,这里已明确地显露出来。程序表面上工作得很好,但却永远侦测到不能“硬纸板”(Cardboard)这种新的废品类型——即使列表里确实有一个硬纸板类型!之所以会出现这种情况,完全是由于使用了RTTI的缘故。RTTI只会查找那些我们告诉它查找的东西。RTTI在这里错误的用法是“系统中的每种类型”都进行了测试,而不是仅测试一种类型或者一个类型子集。正如大家以后会看到的那样,在测试每一种类型时可换用其他方式来运用多态性特征。但假如以这种形式过多地使用RTTI,而且又在自己的系统里添加了一种新类型,很容易就会忘记在程序里作出适当的改动,从而埋下以后难以发现的Bug。因此,在这种情况下避免使用RTTI是很有必要的,这并不仅仅是为了表面好看——也是为了产生更易维护的代码。 diff --git "a/16.5 \346\212\275\350\261\241\347\232\204\345\272\224\347\224\250.md" "b/16.5 \346\212\275\350\261\241\347\232\204\345\272\224\347\224\250.md" index 7a9e695..24b010a 100644 --- "a/16.5 \346\212\275\350\261\241\347\232\204\345\272\224\347\224\250.md" +++ "b/16.5 \346\212\275\350\261\241\347\232\204\345\272\224\347\224\250.md" @@ -6,15 +6,15 @@ 现在,只要一种新类型的Trash加入方法,对TrashSorter对象的初始化就必须变动。可以想象,TrashSorter类看起来应该象下面这个样子: -``` +``` java class TrashSorter extends Vector { void sort(Trash t) { /* ... */ } } ``` -也就是说,TrashSorter是由一系列句柄构成的Vector(系列),而那些句柄指向的又是由Trash句柄构成的Vector;利用addElement(),可以安装新的TrashSorter,如下所示: +也就是说,TrashSorter是由一系列指针构成的Vector(系列),而那些指针指向的又是由Trash指针构成的Vector;利用addElement(),可以安装新的TrashSorter,如下所示: -``` +``` java TrashSorter ts = new TrashSorter(); ts.addElement(new Vector()); ``` @@ -27,10 +27,10 @@ ts.addElement(new Vector()); OOP设计一条基本的准则是“为状态的变化使用数据成员,为行为的变化使用多性形”。对于容纳Paper(纸张)的Vector,以及容纳Glass(玻璃)的Vector,大家最开始或许会认为分别用于它们的grab()方法肯定会产生不同的行为。但具体如何却完全取决于类型,而不是其他什么东西。可将其解释成一种不同的状态,而且由于Java有一个类可表示类型(Class),所以可用它判断特定的Tbin要容纳什么类型的Trash。 -用于Tbin的构建器要求我们为其传递自己选择的一个Class。这样做可告诉Vector它希望容纳的是什么类型。随后,grab()方法用Class BinType和RTTI来检查我们传递给它的Trash对象是否与它希望收集的类型相符。 +用于Tbin的构造器要求我们为其传递自己选择的一个Class。这样做可告诉Vector它希望容纳的是什么类型。随后,grab()方法用Class BinType和RTTI来检查我们传递给它的Trash对象是否与它希望收集的类型相符。 下面列出完整的解决方案。设定为注释的编号(如*1*)便于大家对照程序后面列出的说明。 -``` +``` java //: RecycleB.java // Adding more objects to the recycling problem package c16.recycleb; @@ -99,7 +99,7 @@ public class RecycleB { } ///:~ ``` -(1) TbinList容纳一系列Tbin句柄,所以在查找与我们传递给它的Trash对象相符的情况时,sort()能通过Tbin继承。 +(1) TbinList容纳一系列Tbin指针,所以在查找与我们传递给它的Trash对象相符的情况时,sort()能通过Tbin继承。 (2) sortBin()允许我们将一个完整的Tbin传递进去,而且它会在Tbin里遍历,挑选出每种Trash,并将其归类到特定的Tbin中。请注意这些代码的通用性:新类型加入时,它本身不需要任何改动。只要新类型加入(或发生其他事件)时大量代码都不需要变化,就表明我们设计的是一个容易扩展的系统。 diff --git "a/16.6 \345\244\232\351\207\215\346\264\276\351\201\243.md" "b/16.6 \345\244\232\351\207\215\346\264\276\351\201\243.md" index f01d883..513f016 100644 --- "a/16.6 \345\244\232\351\207\215\346\264\276\351\201\243.md" +++ "b/16.6 \345\244\232\351\207\215\346\264\276\351\201\243.md" @@ -3,9 +3,9 @@ 上述设计方案肯定是令人满意的。系统内新类型的加入涉及添加或修改不同的类,但没有必要在系统内对代码作大范围的改动。除此以外,RTTI并不象它在RecycleA.java里那样被不当地使用。然而,我们仍然有可能更深入一步,以最“纯”的角度来看待RTTI, 考虑如何在垃圾分类系统中将它完全消灭。 -为达到这个目标,首先必须认识到:对所有与不同类型有特殊关联的活动来说——比如侦测一种垃圾的具体类型,并把它置入适当的垃圾筒里——这些活动都应当通过多形性以及动态绑定加以控制。 +为达到这个目标,首先必须认识到:对所有与不同类型有特殊关联的活动来说——比如侦测一种垃圾的具体类型,并把它置入适当的垃圾筒里——这些活动都应当通过多态性以及动态绑定加以控制。 -以前的例子都是先按类型排序,再对属于某种特殊类型的一系列元素进行操作。现在一旦需要操作特定的类型,就请先停下来想一想。事实上,多形性(动态绑定的方法调用)整个的宗旨就是帮我们管理与不同类型有特殊关联的信息。既然如此,为什么还要自己去检查类型呢? +以前的例子都是先按类型排序,再对属于某种特殊类型的一系列元素进行操作。现在一旦需要操作特定的类型,就请先停下来想一想。事实上,多态性(动态绑定的方法调用)整个的宗旨就是帮我们管理与不同类型有特殊关联的信息。既然如此,为什么还要自己去检查类型呢? 答案在于大家或许不以为然的一个道理:Java只执行单一派遣。也就是说,假如对多个类型未知的对象执行某项操作,Java只会为那些类型中的一种调用动态绑定机制。这当然不能解决问题,所以最后不得不人工判断某些类型,才能有效地产生自己的动态绑定行为。 @@ -13,15 +13,15 @@ 16.6.1 实现双重派遣 -记住多形性只能通过方法调用才能表现出来,所以假如想使双重派遣正确进行,必须执行两个方法调用:在每种结构中都用一个来判断其中的类型。在Trash结构中,将使用一个新的方法调用addToBin(),它采用的参数是由TypeBin构成的一个数组。那个方法将在数组中遍历,尝试将自己加入适当的垃圾筒,这里正是双重派遣发生的地方。 +记住多态性只能通过方法调用才能表现出来,所以假如想使双重派遣正确进行,必须执行两个方法调用:在每种结构中都用一个来判断其中的类型。在Trash结构中,将使用一个新的方法调用addToBin(),它采用的参数是由TypeBin构成的一个数组。那个方法将在数组中遍历,尝试将自己加入适当的垃圾筒,这里正是双重派遣发生的地方。 ![](16-3.gif) -新建立的分级结构是TypeBin,其中包含了它自己的一个方法,名为add(),而且也应用了多形性。但要注意一个新特点:add()已进行了“过载”处理,可接受不同的垃圾类型作为参数。因此,双重满足机制的一个关键点是它也要涉及到过载。 +新建立的分级结构是TypeBin,其中包含了它自己的一个方法,名为add(),而且也应用了多态性。但要注意一个新特点:add()已进行了“重载”处理,可接受不同的垃圾类型作为参数。因此,双重满足机制的一个关键点是它也要涉及到重载。 程序的重新设计也带来了一个问题:现在的基础类Trash必须包含一个addToBin()方法。为解决这个问题,一个最直接的办法是复制所有代码,并修改基础类。然而,假如没有对源码的控制权,那么还有另一个办法可以考虑:将addToBin()方法置入一个接口内部,保持Trash不变,并继承新的、特殊的类型Aluminum,Paper,Glass以及Cardboard。我们在这里准备采取后一个办法。 这个设计方案中用到的大多数类都必须设为public(公用)属性,所以它们放置于自己的类内。下面列出接口代码: -``` +``` java //: TypedBinMember.java // An interface for adding the double dispatching // method to the trash hierarchy without @@ -36,7 +36,7 @@ interface TypedBinMember { 在Aluminum,Paper,Glass以及Cardboard每个特定的子类型内,都会实现接口TypeBinMember的addToBin()方法,但每种情况下使用的代码“似乎”都是完全一样的: -``` +``` java //: DDAluminum.java // Aluminum for double dispatching package c16.doubledispatch; @@ -99,11 +99,11 @@ public class DDCardboard extends Cardboard } ///:~ ``` -每个addToBin()内的代码会为数组中的每个TypeBin对象调用add()。但请注意参数:this。对Trash的每个子类来说,this的类型都是不同的,所以不能认为代码“完全”一样——尽管以后在Java里加入参数化类型机制后便可认为一样。这是双重派遣的第一个部分,因为一旦进入这个方法内部,便可知道到底是Aluminum,Paper,还是其他什么垃圾类型。在对add()的调用过程中,这种信息是通过this的类型传递的。编译器会分析出对add()正确的过载版本的调用。但由于tb[i]会产生指向基础类型TypeBin的一个句柄,所以最终会调用一个不同的方法——具体什么方法取决于当前选择的TypeBin的类型。那就是第二次派遣。 +每个addToBin()内的代码会为数组中的每个TypeBin对象调用add()。但请注意参数:this。对Trash的每个子类来说,this的类型都是不同的,所以不能认为代码“完全”一样——尽管以后在Java里加入参数化类型机制后便可认为一样。这是双重派遣的第一个部分,因为一旦进入这个方法内部,便可知道到底是Aluminum,Paper,还是其他什么垃圾类型。在对add()的调用过程中,这种信息是通过this的类型传递的。编译器会分析出对add()正确的重载版本的调用。但由于tb[i]会产生指向基础类型TypeBin的一个指针,所以最终会调用一个不同的方法——具体什么方法取决于当前选择的TypeBin的类型。那就是第二次派遣。 下面是TypeBin的基础类: -``` +``` java //: TypedBin.java // Vector that knows how to grab the right type package c16.doubledispatch; @@ -134,13 +134,13 @@ public abstract class TypedBin { } ///:~ ``` -可以看到,过载的add()方法全都会返回false。如果未在衍生类里对方法进行过载,它就会一直返回false,而且调用者(目前是addToBin())会认为当前Trash对象尚未成功加入一个集合,所以会继续查找正确的集合。 +可以看到,重载的add()方法全都会返回false。如果未在衍生类里对方法进行重载,它就会一直返回false,而且调用者(目前是addToBin())会认为当前Trash对象尚未成功加入一个集合,所以会继续查找正确的集合。 -在TypeBin的每一个子类中,都只有一个过载的方法会被过载——具体取决于准备创建的是什么垃圾筒类型。举个例子来说,CardboardBin会过载add(DDCardboard)。过载的方法会将垃圾对象加入它的集合,并返回true。而CardboardBin中剩余的所有add()方法都会继续返回false,因为它们尚未过载。事实上,假如在这里采用了参数化类型机制,Java代码的自动创建就要方便得多(使用C++的“模板”,我们不必费事地为子类编码,或者将addToBin()方法置入Trash里;Java在这方面尚有待改进)。 +在TypeBin的每一个子类中,都只有一个重载的方法会被重载——具体取决于准备创建的是什么垃圾筒类型。举个例子来说,CardboardBin会重载add(DDCardboard)。重载的方法会将垃圾对象加入它的集合,并返回true。而CardboardBin中剩余的所有add()方法都会继续返回false,因为它们尚未重载。事实上,假如在这里采用了参数化类型机制,Java代码的自动创建就要方便得多(使用C++的“模板”,我们不必费事地为子类编码,或者将addToBin()方法置入Trash里;Java在这方面尚有待改进)。 由于对这个例子来说,垃圾的类型已经定制并置入一个不同的目录,所以需要用一个不同的垃圾数据文件令其运转起来。下面是一个示范性的DDTrash.dat: -``` +``` java c16.DoubleDispatch.DDGlass:54 c16.DoubleDispatch.DDPaper:22 c16.DoubleDispatch.DDPaper:11 @@ -179,7 +179,7 @@ c16.DoubleDispatch.DDCardboard:22 下面列出程序剩余的部分: -``` +``` java //: DoubleDispatch.java // Using multiple dispatching to handle more // than one unknown type during a method call. diff --git "a/16.7 \350\256\277\351\227\256\345\231\250\350\214\203\345\274\217.md" "b/16.7 \350\256\277\351\227\256\345\231\250\350\214\203\345\274\217.md" index 8b31dde..312d590 100644 --- "a/16.7 \350\256\277\351\227\256\345\231\250\350\214\203\345\274\217.md" +++ "b/16.7 \350\256\277\351\227\256\345\231\250\350\214\203\345\274\217.md" @@ -3,22 +3,22 @@ 接下来,让我们思考如何将具有完全不同目标的一个设计范式应用到垃圾归类系统。 -对这个范式,我们不再关心在系统中加入新型Trash时的优化。事实上,这个范式使新型Trash的添加显得更加复杂。假定我们有一个基本类结构,它是固定不变的;它或许来自另一个开发者或公司,我们无权对那个结构进行任何修改。然而,我们又希望在那个结构里加入新的多形性方法。这意味着我们一般必须在基础类的接口里添加某些东西。因此,我们目前面临的困境是一方面需要向基础类添加方法,另一方面又不能改动基础类。怎样解决这个问题呢? +对这个范式,我们不再关心在系统中加入新型Trash时的优化。事实上,这个范式使新型Trash的添加显得更加复杂。假定我们有一个基本类结构,它是固定不变的;它或许来自另一个开发者或公司,我们无权对那个结构进行任何修改。然而,我们又希望在那个结构里加入新的多态性方法。这意味着我们一般必须在基础类的接口里添加某些东西。因此,我们目前面临的困境是一方面需要向基础类添加方法,另一方面又不能改动基础类。怎样解决这个问题呢? “访问器”(Visitor)范式使我们能扩展基本类型的接口,方法是创建类型为Visitor的一个独立的类结构,对以后需对基本类型采取的操作进行虚拟。基本类型的任务就是简单地“接收”访问器,然后调用访问器的动态绑定方法。看起来就象下面这样: ![](16-4.gif) -现在,假如v是一个指向Aluminum(铝制品)的Visitable句柄,那么下述代码: +现在,假如v是一个指向Aluminum(铝制品)的Visitable指针,那么下述代码: -``` +``` java PriceVisitor pv = new PriceVisitor(); v.accept(pv); ``` -会造成两个多形性方法调用:第一个会选择accept()的Aluminum版本;第二个则在accept()里——用基础类Visitor句柄v动态调用visit()的特定版本时。 +会造成两个多态性方法调用:第一个会选择accept()的Aluminum版本;第二个则在accept()里——用基础类Visitor指针v动态调用visit()的特定版本时。 -这种配置意味着可采取Visitor的新子类的形式将新的功能添加到系统里,没必要接触Trash结构。这就是“访问器”范式最主要的优点:可为一个类结构添加新的多形性功能,同时不必改动结构——只要安装好了accept()方法。注意这个优点在这儿是有用的,但并不一定是我们在任何情况下的首选方案。所以在最开始的时候,就要判断这到底是不是自己需要的方案。 +这种配置意味着可采取Visitor的新子类的形式将新的功能添加到系统里,没必要接触Trash结构。这就是“访问器”范式最主要的优点:可为一个类结构添加新的多态性功能,同时不必改动结构——只要安装好了accept()方法。注意这个优点在这儿是有用的,但并不一定是我们在任何情况下的首选方案。所以在最开始的时候,就要判断这到底是不是自己需要的方案。 现在注意一件没有做成的事情:访问器方案防止了从主控Trash序列向单独类型序列的归类。所以我们可将所有东西都留在单主控序列中,只需用适当的访问器通过那个序列传递,即可达到希望的目标。尽管这似乎并非访问器范式的本意,但确实让我们达到了很希望达到的一个目标(避免使用RTTI)。 @@ -26,7 +26,7 @@ v.accept(pv); 可以看到,所有这些都是用回收程序一个新的、改进过的版本实现的。而且和DoubleDispatch.java一样,Trash类被保持孤立,并创建一个新接口来添加accept()方法: -``` +``` java //: Visitable.java // An interface to add visitor functionality to // the Trash hierarchy without modifying the @@ -42,7 +42,7 @@ interface Visitable { Aluminum,Paper,Glass以及Cardboard的子类型实现了accept()方法: -``` +``` java //: VAluminum.java // Aluminum for the visitor pattern package c16.trashvisitor; @@ -95,7 +95,7 @@ public class VCardboard extends Cardboard 由于Visitor基础类没有什么需要实在的东西,可将其创建成一个接口: -``` +``` java //: Visitor.java // The base interface for visitors package c16.trashvisitor; @@ -147,7 +147,7 @@ c16.TrashVisitor.VCardboard:22 程序剩余的部分将创建特定的Visitor类型,并通过一个Trash对象列表发送它们: -``` +``` java //: TrashVisitor.java // The "visitor" pattern package c16.trashvisitor; @@ -256,10 +256,10 @@ public class TrashVisitor { 最好,将东西从序列中取出的时候,除了不可避免地向Trash造型以外,再没有运行期的类型验证。若在Java里实现了参数化类型,甚至那个造型操作也可以避免。 -对比之前介绍过的双重派遣方案,区分这两种方案的一个办法是:在双重派遣方案中,每个子类创建时只会过载其中的一个过载方法,即add()。而在这里,每个过载的visit()方法都必须在Visitor的每个子类中进行过载。 +对比之前介绍过的双重派遣方案,区分这两种方案的一个办法是:在双重派遣方案中,每个子类创建时只会重载其中的一个重载方法,即add()。而在这里,每个重载的visit()方法都必须在Visitor的每个子类中进行重载。 1. 更多的结合? 这里还有其他许多代码,Trash结构和Visitor结构之间存在着明显的“结合”(Coupling)关系。然而,在它们所代表的类集内部,也存在着高度的凝聚力:都只做一件事情(Trash描述垃圾或废品,而Visitor描述对垃圾采取什么行动)。作为一套优秀的设计方案,这无疑是个良好的开端。当然就目前的情况来说,只有在我们添加新的Visitor类型时才能体会到它的好处。但在添加新类型的Trash时,它却显得有些碍手碍脚。 -类与类之间低度的结合与类内高度的凝聚无疑是一个重要的设计目标。但只要稍不留神,就可能妨碍我们得到一个本该更出色的设计。从表面看,有些类不可避免地相互间存在着一些“亲密”关系。这种关系通常是成对发生的,可以叫作“对联”(Couplet)——比如集合和继承器(Enumeration)。前面的Trash-Visitor对似乎也是这样的一种“对联”。 +类与类之间低度的结合与类内高度的凝聚无疑是一个重要的设计目标。但只要稍不留神,就可能妨碍我们得到一个本该更出色的设计。从表面看,有些类不可避免地相互间存在着一些“亲密”关系。这种关系通常是成对发生的,可以叫作“对联”(Couplet)——比如集合和迭代器(Enumeration)。前面的Trash-Visitor对似乎也是这样的一种“对联”。 diff --git "a/16.8 RTTI\347\234\237\347\232\204\346\234\211\345\256\263\345\220\227.md" "b/16.8 RTTI\347\234\237\347\232\204\346\234\211\345\256\263\345\220\227.md" index a26f3ee..7ce9083 100644 --- "a/16.8 RTTI\347\234\237\347\232\204\346\234\211\345\256\263\345\220\227.md" +++ "b/16.8 RTTI\347\234\237\347\232\204\346\234\211\345\256\263\345\220\227.md" @@ -7,7 +7,7 @@ 我们的例子同样建立在c16.Trash这个“包”(Package)内的Trash类型结构的基础上(而且那儿使用的Trash.dat文件可以照搬到这里来)。 -``` +``` java //: DynaTrash.java // Using a Hashtable of Vectors and RTTI // to automatically sort trash into @@ -56,23 +56,23 @@ public class DynaTrash { } ///:~ ``` -尽管功能很强,但对TypeMap的定义是非常简单的。它只是包含了一个散列表,同时add()负担了大部分的工作。添加一个新类型时,那种类型的Class对象的句柄会被提取出来。随后,利用这个句柄判断容纳了那类对象的一个Vector是否已存在于散列表中。如答案是肯定的,就提取出那个Vector,并将对象加入其中;反之,就将Class对象及新Vector作为一个“键-值”对加入。 +尽管功能很强,但对TypeMap的定义是非常简单的。它只是包含了一个散列表,同时add()负担了大部分的工作。添加一个新类型时,那种类型的Class对象的指针会被提取出来。随后,利用这个指针判断容纳了那类对象的一个Vector是否已存在于散列表中。如答案是肯定的,就提取出那个Vector,并将对象加入其中;反之,就将Class对象及新Vector作为一个“键-值”对加入。 利用keys(),可以得到对所有Class对象的一个“枚举”(Enumeration),而且可用get(),可通过Class对象获取对应的Vector。 -filler()方法非常有趣,因为它利用了ParseTrash.fillBin()的设计——不仅能尝试填充一个Vector,也能用它的addTrash()方法试着填充实现了Fillable(可填充)接口的任何东西。filter()需要做的全部事情就是将一个句柄返回给实现了Fillable的一个接口,然后将这个句柄作为参数传递给fillBin(),就象下面这样: +filler()方法非常有趣,因为它利用了ParseTrash.fillBin()的设计——不仅能尝试填充一个Vector,也能用它的addTrash()方法试着填充实现了Fillable(可填充)接口的任何东西。filter()需要做的全部事情就是将一个指针返回给实现了Fillable的一个接口,然后将这个指针作为参数传递给fillBin(),就象下面这样: -``` +``` java ParseTrash.fillBin("Trash.dat", bin.filler()); ``` -为产生这个句柄,我们采用了一个“匿名内部类”(已在第7章讲述)。由于根本不需要用一个已命名的类来实现Fillable,只需要属于那个类的一个对象的句柄即可,所以这里使用匿名内部类是非常恰当的。 +为产生这个指针,我们采用了一个“匿名内部类”(已在第7章讲述)。由于根本不需要用一个已命名的类来实现Fillable,只需要属于那个类的一个对象的指针即可,所以这里使用匿名内部类是非常恰当的。 对这个设计,要注意的一个地方是尽管没有设计成对归类加以控制,但在fillBin()每次进行归类的时候,都会将一个Trash对象插入bin。 通过前面那些例子的学习,DynaTrash类的大多数部分都应当非常熟悉了。这一次,我们不再将新的Trash对象置入类型Vector的一个bin内。由于bin的类型为TypeMap,所以将垃圾(Trash)丢进垃圾筒(Bin)的时候,TypeMap的内部归类机制会立即进行适当的分类。在TypeMap里遍历并对每个独立的Vector进行操作,这是一件相当简单的事情: -``` +``` java Enumeration keys = bin.keys(); while(keys.hasMoreElements()) Trash.sumValue( diff --git "a/16.9 \346\200\273\347\273\223.md" "b/16.9 \346\200\273\347\273\223.md" index 8332552..418c108 100644 --- "a/16.9 \346\200\273\347\273\223.md" +++ "b/16.9 \346\200\273\347\273\223.md" @@ -4,9 +4,9 @@ 变化序列的发现并非一件平常事;在程序的初始设计出台以前,那些分析家一般不可能预测到这种变化。除非进入项目设计的后期,否则一些必要的信息是不会显露出来的:有时只有进入设计或最终实现阶段,才能体会到对自己系统一个更深入或更不易察觉需要。添加新类型时(这是“回收”例子最主要的一个重点),可能会意识到只有自己进入维护阶段,而且开始扩充系统时,才需要一个特定的继承结构。 -通过设计范式的学习,大家可体会到最重要的一件事情就是本书一直宣扬的一个观点——多形性是OOP(面向对象程序设计)的全部——已发生了彻底的改变。换句话说,很难“获得”多形性;而一旦获得,就需要尝试将自己的所有设计都造型到一个特定的模子里去。 +通过设计范式的学习,大家可体会到最重要的一件事情就是本书一直宣扬的一个观点——多态性是OOP(面向对象程序设计)的全部——已发生了彻底的改变。换句话说,很难“获得”多态性;而一旦获得,就需要尝试将自己的所有设计都造型到一个特定的模子里去。 -设计范式要表明的观点是“OOP并不仅仅同多形性有关”。应当与OOP有关的是“将发生变化的东西同保持不变的东西分隔开来”。多形性是达到这一目的的特别重要的手段。而且假如编程语言直接支持多形性,那么它就显得尤其有用(由于直接支持,所以不必自己动手编写,从而节省大量的精力和时间)。但设计范式向我们揭示的却是达到基本目标的另一些常规途径。而且一旦熟悉并掌握了它的用法,就会发现自己可以做出更有创新性的设计。 +设计范式要表明的观点是“OOP并不仅仅同多态性有关”。应当与OOP有关的是“将发生变化的东西同保持不变的东西分隔开来”。多态性是达到这一目的的特别重要的手段。而且假如编程语言直接支持多态性,那么它就显得尤其有用(由于直接支持,所以不必自己动手编写,从而节省大量的精力和时间)。但设计范式向我们揭示的却是达到基本目标的另一些常规途径。而且一旦熟悉并掌握了它的用法,就会发现自己可以做出更有创新性的设计。 由于《Design Patterns》这本书对程序员造成了如此重要的影响,所以他们纷纷开始寻找其他范式。随着的时间的推移,这类范式必然会越来越多。JimCoplien(http://www.bell-labs.com/~cope主页作者)向我们推荐了这样的一些站点,上面有许多很有价值的范式说明: diff --git "a/17.1 \346\226\207\345\255\227\345\244\204\347\220\206.md" "b/17.1 \346\226\207\345\255\227\345\244\204\347\220\206.md" index f4a0bcd..64def87 100644 --- "a/17.1 \346\226\207\345\255\227\345\244\204\347\220\206.md" +++ "b/17.1 \346\226\207\345\255\227\345\244\204\347\220\206.md" @@ -18,7 +18,7 @@ 下面是完整的代码,后面会对它进行详细的说明: -``` +``` java //: CodePackager.java // "Packs" and "unpacks" the code in "Thinking // in Java" for cross-platform distribution. @@ -412,11 +412,11 @@ public class CodePackager { 我们注意到package语句已经作为注释标志出来了。由于这是本章的第一个程序,所以package语句是必需的,用它告诉CodePackager已改换到另一章。但是把它放入包里却会成为一个问题。当我们创建一个包的时候,需要将结果程序同一个特定的目录结构联系在一起,这一做法对本书的大多数例子都是适用的。但在这里,CodePackager程序必须在一个专用的目录里编译和运行,所以package语句作为注释标记出去。但对CodePackager来说,它“看起来”依然象一个普通的package语句,因为程序还不是特别复杂,不能侦查到多行注释(没有必要做得这么复杂,这里只要求方便就行)。 -头两个类是“支持/工具”类,作用是使程序剩余的部分在编写时更加连贯,也更便于阅读。第一个是Pr,它类似ANSI C的perror库,两者都能打印出一条错误提示消息(但同时也会退出程序)。第二个类将文件的创建过程封装在内,这个过程已在第10章介绍过了;大家已经知道,这样做很快就会变得非常累赘和麻烦。为解决这个问题,第10章提供的方案致力于新类的创建,但这儿的“静态”方法已经使用过了。在那些方法中,正常的违例会被捕获,并相应地进行处理。这些方法使剩余的代码显得更加清爽,更易阅读。 +头两个类是“支持/工具”类,作用是使程序剩余的部分在编写时更加连贯,也更便于阅读。第一个是Pr,它类似ANSI C的perror库,两者都能打印出一条错误提示消息(但同时也会退出程序)。第二个类将文件的创建过程封装在内,这个过程已在第10章介绍过了;大家已经知道,这样做很快就会变得非常累赘和麻烦。为解决这个问题,第10章提供的方案致力于新类的创建,但这儿的“静态”方法已经使用过了。在那些方法中,正常的异常会被捕获,并相应地进行处理。这些方法使剩余的代码显得更加清爽,更易阅读。 帮助解决问题的第一个类是SourceCodeFile(源码文件),它代表本书一个源码文件包含的所有信息(内容、文件名以及目录)。它同时还包含了一系列String常数,分别代表一个文件的开始与结束;在打包文件内使用的一个标记;当前系统的换行符;文件路径分隔符(注意要用System.getProperty()侦查本地版本是什么);以及一大段版权声明,它是从下面这个Copyright.txt文件里提取出来的: -``` +``` java ////////////////////////////////////////////////// // Copyright (c) Bruce Eckel, 1998 // Source code file from the book "Thinking in Java" @@ -471,22 +471,22 @@ public class CodePackager { 1. 构建一个打包文件 -第一个构建器用于从本书的ASCII文本版里提取出一个文件。发出调用的代码(在列表里较深的地方)会读入并检查每一行,直到找到与一个列表的开头相符的为止。在这个时候,它就会新建一个SourceCodeFile对象,将第一行的内容(已经由调用代码读入了)传递给它,同时还要传递BufferedReader对象,以便在这个缓冲区中提取源码列表剩余的内容。 +第一个构造器用于从本书的ASCII文本版里提取出一个文件。发出调用的代码(在列表里较深的地方)会读入并检查每一行,直到找到与一个列表的开头相符的为止。在这个时候,它就会新建一个SourceCodeFile对象,将第一行的内容(已经由调用代码读入了)传递给它,同时还要传递BufferedReader对象,以便在这个缓冲区中提取源码列表剩余的内容。 -从这时起,大家会发现String方法被频繁运用。为提取出文件名,需调用substring()的过载版本,令其从一个起始偏移开始,一直读到字串的末尾,从而形成一个“子串”。为算出这个起始索引,先要用length()得出startMarker的总长,再用trim()删除字串头尾多余的空格。第一行在文件名后也可能有一些字符;它们是用indexOf()侦测出来的。若没有发现找到我们想寻找的字符,就返回-1;若找到那些字符,就返回它们第一次出现的位置。注意这也是indexOf()的一个过载版本,采用一个字串作为参数,而非一个字符。 +从这时起,大家会发现String方法被频繁运用。为提取出文件名,需调用substring()的重载版本,令其从一个起始偏移开始,一直读到字串的末尾,从而形成一个“子串”。为算出这个起始索引,先要用length()得出startMarker的总长,再用trim()删除字串头尾多余的空格。第一行在文件名后也可能有一些字符;它们是用indexOf()侦测出来的。若没有发现找到我们想寻找的字符,就返回-1;若找到那些字符,就返回它们第一次出现的位置。注意这也是indexOf()的一个重载版本,采用一个字串作为参数,而非一个字符。 解析出并保存好文件名后,第一行会被置入字串contents中(该字串用于保存源码清单的完整正文)。随后,将剩余的代码行读入,并合并进入contents字串。当然事情并没有想象的那么简单,因为特定的情况需加以特别的控制。一种情况是错误检查:若直接遇到一个startMarker(起始标记),表明当前操作的这个代码列表没有设置一个结束标记。这属于一个出错条件,需要退出程序。 -另一种特殊情况与package关键字有关。尽管Java是一种自由形式的语言,但这个程序要求package关键字必须位于行首。若发现package关键字,就通过检查位于开头的空格以及位于末尾的分号,从而提取出包名(注意亦可一次单独的操作实现,方法是使用过载的substring(),令其同时检查起始和结束索引位置)。随后,将包名中的点号替换成特定的文件分隔符——当然,这里要假设文件分隔符仅有一个字符的长度。尽管这个假设可能对目前的所有系统都是适用的,但一旦遇到问题,一定不要忘了检查一下这里。 -默认操作是将每一行都连接到contents里,同时还有换行字符,直到遇到一个endMarker(结束标记)为止。该标记指出构建器应当停止了。若在endMarker之前遇到了文件结尾,就认为存在一个错误。 +另一种特殊情况与package关键字有关。尽管Java是一种自由形式的语言,但这个程序要求package关键字必须位于行首。若发现package关键字,就通过检查位于开头的空格以及位于末尾的分号,从而提取出包名(注意亦可一次单独的操作实现,方法是使用重载的substring(),令其同时检查起始和结束索引位置)。随后,将包名中的点号替换成特定的文件分隔符——当然,这里要假设文件分隔符仅有一个字符的长度。尽管这个假设可能对目前的所有系统都是适用的,但一旦遇到问题,一定不要忘了检查一下这里。 +默认操作是将每一行都连接到contents里,同时还有换行字符,直到遇到一个endMarker(结束标记)为止。该标记指出构造器应当停止了。若在endMarker之前遇到了文件结尾,就认为存在一个错误。 2. 从打包文件中提取 -第二个构建器用于将源码文件从打包文件中恢复(提取)出来。在这儿,作为调用者的方法不必担心会跳过一些中间文本。打包文件包含了所有源码文件,它们相互间紧密地靠在一起。需要传递给该构建器的仅仅是一个BufferedReader,它代表着“信息源”。构建器会从中提取出自己需要的信息。但在每个代码列表开始的地方还有一些配置信息,它们的身份是用packMarker(打包标记)指出的。若packMarker不存在,意味着调用者试图用错误的方法来使用这个构建器。 +第二个构造器用于将源码文件从打包文件中恢复(提取)出来。在这儿,作为调用者的方法不必担心会跳过一些中间文本。打包文件包含了所有源码文件,它们相互间紧密地靠在一起。需要传递给该构造器的仅仅是一个BufferedReader,它代表着“信息源”。构造器会从中提取出自己需要的信息。但在每个代码列表开始的地方还有一些配置信息,它们的身份是用packMarker(打包标记)指出的。若packMarker不存在,意味着调用者试图用错误的方法来使用这个构造器。 一旦发现packMarker,就会将其剥离出来,并提取出目录名(用一个'#'结尾)以及文件名(直到行末)。不管在哪种情况下,旧分隔符都会被替换成本地适用的一个分隔符,这是用String replace()方法实现的。老的分隔符被置于打包文件的开头,在代码列表稍靠后的一部分即可看到是如何把它提取出来的。 -构建器剩下的部分就非常简单了。它读入每一行,把它合并到contents里,直到遇见endMarker为止。 +构造器剩下的部分就非常简单了。它读入每一行,把它合并到contents里,直到遇见endMarker为止。 3. 程序列表的存取 @@ -502,7 +502,7 @@ public class CodePackager { DirMap类可帮助我们实现这一效果,并有效地演示了一个“多重映射”的概述。这是通过一个散列表(Hashtable)实现的,它的“键”是准备创建的子目录,而“值”是包含了那个特定目录中的SourceCodeFile对象的Vector对象。所以,我们在这儿并不是将一个“键”映射(或对应)到一个值,而是通过对应的Vector,将一个键“多重映射”到一系列值。尽管这听起来似乎很复杂,但具体实现时却是非常简单和直接的。大家可以看到,DirMap类的大多数代码都与向文件中的写入有关,而非与“多重映射”有关。与它有关的代码仅极少数而已。 -可通过两种方式建立一个DirMap(目录映射或对应)关系:默认构建器假定我们希望目录从当前位置向下展开,而另一个构建器让我们为起始目录指定一个备用的“绝对”路径。 +可通过两种方式建立一个DirMap(目录映射或对应)关系:默认构造器假定我们希望目录从当前位置向下展开,而另一个构造器让我们为起始目录指定一个备用的“绝对”路径。 add()方法是一个采取的行动比较密集的场所。首先将directory()从我们想添加的SourceCodeFile里提取出来,然后检查散列表(Hashtable),看看其中是否已经包含了那个键。如果没有,就向散列表加入一个新的Vector,并将它同那个键关联到一起。到这时,不管采取的是什么途径,Vector都已经就位了,可以将它提取出来,以便添加SourceCodeFile。由于Vector可象这样同散列表方便地合并到一起,所以我们从两方面都能感觉得非常方便。 @@ -514,11 +514,11 @@ add()方法是一个采取的行动比较密集的场所。首先将directory() 前面介绍的那些类都要在CodePackager中用到。大家首先看到的是用法字串。一旦最终用户不正确地调用了程序,就会打印出介绍正确用法的这个字串。调用这个字串的是usage()方法,同时还要退出程序。main()唯一的任务就是判断我们希望创建一个打包文件,还是希望从一个打包文件中提取什么东西。随后,它负责保证使用的是正确的参数,并调用适当的方法。 -创建一个打包文件时,它默认位于当前目录,所以我们用默认构建器创建DirMap。打开文件后,其中的每一行都会读入,并检查是否符合特殊的条件: +创建一个打包文件时,它默认位于当前目录,所以我们用默认构造器创建DirMap。打开文件后,其中的每一行都会读入,并检查是否符合特殊的条件: -(1) 若行首是一个用于源码列表的起始标记,就新建一个SourceCodeFile对象。构建器会读入源码列表剩下的所有内容。结果产生的句柄将直接加入DirMap。 +(1) 若行首是一个用于源码列表的起始标记,就新建一个SourceCodeFile对象。构造器会读入源码列表剩下的所有内容。结果产生的指针将直接加入DirMap。 -(2) 若行首是一个用于源码列表的结束标记,表明某个地方出现错误,因为结束标记应当只能由SourceCodeFile构建器发现。 +(2) 若行首是一个用于源码列表的结束标记,表明某个地方出现错误,因为结束标记应当只能由SourceCodeFile构造器发现。 提取/释放一个打包文件时,提取出来的内容可进入当前目录,亦可进入另一个备用目录。所以需要相应地创建DirMap对象。打开文件,并将第一行读入。老的文件路径分隔符信息将从这一行中提取出来。随后根据输入来创建第一个SourceCodeFile对象,它会加入DirMap。只要包含了一个文件,新的SourceCodeFile对象就会创建并加入(创建的最后一个用光输入内容后,会简单地返回,然后hasFile()会返回一个错误)。 @@ -534,7 +534,7 @@ add()方法是一个采取的行动比较密集的场所。首先将directory() 下面列出源代码,后面有详细的解释: -``` +``` java //: ClassScanner.java // Scans all files in directory for classes // and identifiers, to check capitalization. @@ -791,7 +791,7 @@ MultiStringMap类是个特殊的工具,允许我们将一组字串与每个键 针对特定目录中的文件,为找出相应的类与标识符,我们使用了两个MultiStringMap:classMap以及identMap。此外在程序启动的时候,它会将标准类名仓库装载到名为classes的Properties对象中。一旦在本地目录发现了一个新类名,也会将其加入classes以及classMap。这样一来,classMap就可用于在本地目录的所有类间遍历,而且可用classes检查当前标记是不是一个类名(它标记着对象或方法定义的开始,所以收集接下去的记号——直到碰到一个分号——并将它们都置入identMap)。 -ClassScanner的默认构建器会创建一个由文件名构成的列表(采用FilenameFilter的JavaFilter实现形式,参见第10章)。随后会为每个文件名都调用scanListing()。 +ClassScanner的默认构造器会创建一个由文件名构成的列表(采用FilenameFilter的JavaFilter实现形式,参见第10章)。随后会为每个文件名都调用scanListing()。 在scanListing()内部,会打开源码文件,并将其转换成一个StreamTokenizer。根据Java帮助文档,将true传递给slashStartComments()和slashSlashComments()的本意应当是剥除那些注释内容,但这样做似乎有些问题(在Java 1.0中几乎无效)。所以相反,那些行被当作注释标记出去,并用另一个方法来提取注释。为达到这个目的,'/'必须作为一个原始字符捕获,而不是让StreamTokeinzer将其当作注释的一部分对待。此时要用ordinaryChar()方法指示StreamTokenizer采取正确的操作。同样的道理也适用于点号('.'),因为我们希望让方法调用分离出单独的标识符。但对下划线来说,它最初是被StreamTokenizer当作一个单独的字符对待的,但此时应把它留作标识符的一部分,因为它在static final值中以TT_EOF等等形式使用。当然,这一点只对目前这个特殊的程序成立。wordChars()方法需要取得我们想添加的一系列字符,把它们留在作为一个单词看待的记号中。最后,在解析单行注释或者放弃一行的时候,我们需要知道一个换行动作什么时候发生。所以通过调用eollsSignificant(true),换行符(EOL)会被显示出来,而不是被StreamTokenizer吸收。 @@ -815,4 +815,4 @@ discardLine()方法是一个简单的工具,用于查找行末位置。注意 程序列表剩下的部分由main()构成,它负责控制命令行参数,并判断我们是准备在标准Java库的基础上构建由一系列类名构成的“仓库”,还是想检查已写好的那些代码的正确性。不管在哪种情况下,都会创建一个ClassScanner对象。 -无论准备构建一个“仓库”,还是准备使用一个现成的,都必须尝试打开现有仓库。通过创建一个File对象并测试是否存在,就可决定是否打开文件并在ClassScanner中装载classes这个Properties列表(使用load())。来自仓库的类将追加到由ClassScanner构建器发现的类后面,而不是将其覆盖。如果仅提供一个命令行参数,就意味着自己想对类名和标识符名字进行一次检查。但假如提供两个参数(第二个是"-a"),就表明自己想构成一个类名仓库。在这种情况下,需要打开一个输出文件,并用Properties.save()方法将列表写入一个文件,同时用一个字串提供文件头信息。 +无论准备构建一个“仓库”,还是准备使用一个现成的,都必须尝试打开现有仓库。通过创建一个File对象并测试是否存在,就可决定是否打开文件并在ClassScanner中装载classes这个Properties列表(使用load())。来自仓库的类将追加到由ClassScanner构造器发现的类后面,而不是将其覆盖。如果仅提供一个命令行参数,就意味着自己想对类名和标识符名字进行一次检查。但假如提供两个参数(第二个是"-a"),就表明自己想构成一个类名仓库。在这种情况下,需要打开一个输出文件,并用Properties.save()方法将列表写入一个文件,同时用一个字串提供文件头信息。 diff --git "a/17.2 \346\226\271\346\263\225\346\237\245\346\211\276\345\267\245\345\205\267.md" "b/17.2 \346\226\271\346\263\225\346\237\245\346\211\276\345\267\245\345\205\267.md" index 7ef77e3..c0b7586 100644 --- "a/17.2 \346\226\271\346\263\225\346\237\245\346\211\276\345\267\245\345\205\267.md" +++ "b/17.2 \346\226\271\346\263\225\346\237\245\346\211\276\345\267\245\345\205\267.md" @@ -4,7 +4,7 @@ 第11章介绍了Java 1.1新的“反射”概念,并利用这个概念查询一个特定类的方法——要么是由所有方法构成的一个完整列表,要么是这个列表的一个子集(名字与我们指定的关键字相符)。那个例子最大的好处就是能自动显示出所有方法,不强迫我们在继承结构中遍历,检查每一级的基础类。所以,它实际是我们节省编程时间的一个有效工具:因为大多数Java方法的名字都规定得非常全面和详尽,所以能有效地找出那些包含了一个特殊关键字的方法名。若找到符合标准的一个名字,便可根据它直接查阅联机帮助文档。 但第11的那个例子也有缺陷,它没有使用AWT,仅是一个纯命令行的应用。在这儿,我们准备制作一个改进的GUI版本,能在我们键入字符的时候自动刷新输出,也允许我们在输出结果中进行剪切和粘贴操作: -``` +``` java //: DisplayMethods.java // Display the methods of any class inside // a window. Dynamically narrows your search. @@ -170,7 +170,7 @@ class StripQualifiers { GUI包含了一个名为name的“文本字段”(TextField),或在其中输入想查找的类名;还包含了另一个文本字段,名为searchFor,可选择性地在其中输入一定的文字,希望在方法列表中查找那些文字。Checkbox(复选框)允许我们指出最终希望在输出中使用完整的名字,还是将前面的各种限定信息删去。最后,结果显示于一个“文本区域”(TextArea)中。 -大家会注意到这个程序未使用任何按钮或其他组件,不能用它们开始一次搜索。这是由于无论文本字段还是复选框都会受到它们的“侦听者(Listener)对象的监视。只要作出一项改变,结果列表便会立即更新。若改变了name字段中的文字,新的文字就会在NameL类中捕获。若文字不为空,则在Class.forName()中用于尝试查找类。当然,在文字键入期间,名字可能会变得不完整,而Class.forName()会失败,这意味着它会“掷”出一个违例。该违例会被捕获,TextArea会随之设为“Nomatch”(没有相符)。但只要键入了一个正确的名字(大小写也算在内),Class.forName()就会成功,而getMethods()和getConstructors()会分别返回由Method和Constructor对象构成的一个数组。这些数组中的每个对象都会通过toString()转变成一个字串(这样便产生了完整的方法或构建器签名),而且两个列表都会合并到n中——一个独立的字串数组。数组n属于DisplayMethods类的一名成员,并在调用reDisplay()时用于显示的更新。 +大家会注意到这个程序未使用任何按钮或其他组件,不能用它们开始一次搜索。这是由于无论文本字段还是复选框都会受到它们的“侦听者(Listener)对象的监视。只要作出一项改变,结果列表便会立即更新。若改变了name字段中的文字,新的文字就会在NameL类中捕获。若文字不为空,则在Class.forName()中用于尝试查找类。当然,在文字键入期间,名字可能会变得不完整,而Class.forName()会失败,这意味着它会“掷”出一个异常。该异常会被捕获,TextArea会随之设为“Nomatch”(没有相符)。但只要键入了一个正确的名字(大小写也算在内),Class.forName()就会成功,而getMethods()和getConstructors()会分别返回由Method和Constructor对象构成的一个数组。这些数组中的每个对象都会通过toString()转变成一个字串(这样便产生了完整的方法或构造器签名),而且两个列表都会合并到n中——一个独立的字串数组。数组n属于DisplayMethods类的一名成员,并在调用reDisplay()时用于显示的更新。 若改变了Checkbox或searchFor组件,它们的“侦听者”会简单地调用reDisplay()。reDisplay()会创建一个临时数组,其中包含了名为rs的字串(rs代表“结果集”——Result Set)。结果集要么直接从n复制(没有find关键字),要么选择性地从包含了find关键字的n中的字串复制。最后会检查strip Checkbox,看看用户是不是希望将名字中多余的部分删除(默认为“是”)。若答案是肯定的,则用StripQualifiers.strip()做这件事情;反之,就将列表简单地显示出来。 diff --git "a/17.3 \345\244\215\346\235\202\346\200\247\347\220\206\350\256\272.md" "b/17.3 \345\244\215\346\235\202\346\200\247\347\220\206\350\256\272.md" index b04829d..b01778c 100644 --- "a/17.3 \345\244\215\346\235\202\346\200\247\347\220\206\350\256\272.md" +++ "b/17.3 \345\244\215\346\235\202\346\200\247\347\220\206\350\256\272.md" @@ -16,7 +16,7 @@ 程序以合成到一起的应用程序/程序片的形式提供: -``` +``` java //: FieldOBeasts.java // Demonstration of complexity theory; simulates // herding behavior in animals. Adapted from @@ -300,7 +300,7 @@ http://www.hmt.com/cwr/boids.html 为了将这个程序作为一个程序片运行,请在HTML文件中设置下述程序片标志: -``` +``` java -``` +``` java diff --git "a/2.1 \347\224\250\345\217\245\346\237\204\346\223\215\347\272\265\345\257\271\350\261\241.md" "b/2.1 \347\224\250\346\214\207\351\222\210\346\223\215\347\272\265\345\257\271\350\261\241.md" similarity index 75% rename from "2.1 \347\224\250\345\217\245\346\237\204\346\223\215\347\272\265\345\257\271\350\261\241.md" rename to "2.1 \347\224\250\346\214\207\351\222\210\346\223\215\347\272\265\345\257\271\350\261\241.md" index bbc8556..e41796f 100644 --- "a/2.1 \347\224\250\345\217\245\346\237\204\346\223\215\347\272\265\345\257\271\350\261\241.md" +++ "b/2.1 \347\224\250\346\214\207\351\222\210\346\223\215\347\272\265\345\257\271\350\261\241.md" @@ -1,18 +1,18 @@ -# 2.1 用句柄操纵对象 +# 2.1 用指针操纵对象 每种编程语言都有自己的数据处理方式。有些时候,程序员必须时刻留意准备处理的是什么类型。您曾利用一些特殊语法直接操作过对象,或处理过一些间接表示的对象吗(C或C++里的指针)? -所有这些在Java里都得到了简化,任何东西都可看作对象。因此,我们可采用一种统一的语法,任何地方均可照搬不误。但要注意,尽管将一切都“看作”对象,但操纵的标识符实际是指向一个对象的“句柄”(Handle)。在其他Java参考书里,还可看到有的人将其称作一个“引用”,甚至一个“指针”。可将这一情形想象成用遥控板(句柄)操纵电视机(对象)。只要握住这个遥控板,就相当于掌握了与电视机连接的通道。但一旦需要“换频道”或者“关小声音”,我们实际操纵的是遥控板(句柄),再由遥控板自己操纵电视机(对象)。如果要在房间里四处走走,并想保持对电视机的控制,那么手上拿着的是遥控板,而非电视机。 +所有这些在Java里都得到了简化,任何东西都可看作对象。因此,我们可采用一种统一的语法,任何地方均可照搬不误。但要注意,尽管将一切都“看作”对象,但操纵的标识符实际是指向一个对象的“指针”(Handle)。在其他Java参考书里,还可看到有的人将其称作一个“引用”,甚至一个“指针”。可将这一情形想象成用遥控板(指针)操纵电视机(对象)。只要握住这个遥控板,就相当于掌握了与电视机连接的通道。但一旦需要“换频道”或者“关小声音”,我们实际操纵的是遥控板(指针),再由遥控板自己操纵电视机(对象)。如果要在房间里四处走走,并想保持对电视机的控制,那么手上拿着的是遥控板,而非电视机。 -此外,即使没有电视机,遥控板亦可独立存在。也就是说,只是由于拥有一个句柄,并不表示必须有一个对象同它连接。所以如果想容纳一个词或句子,可创建一个String句柄: +此外,即使没有电视机,遥控板亦可独立存在。也就是说,只是由于拥有一个指针,并不表示必须有一个对象同它连接。所以如果想容纳一个词或句子,可创建一个String指针: -``` +``` java String s; ``` -但这里创建的只是句柄,并不是对象。若此时向s发送一条消息,就会获得一个错误(运行期)。这是由于s实际并未与任何东西连接(即“没有电视机”)。因此,一种更安全的做法是:创建一个句柄时,记住无论如何都进行初始化: +但这里创建的只是指针,并不是对象。若此时向s发送一条消息,就会获得一个错误(运行期)。这是由于s实际并未与任何东西连接(即“没有电视机”)。因此,一种更安全的做法是:创建一个指针时,记住无论如何都进行初始化: -``` +``` java String s = "asdf"; ``` diff --git "a/2.2 \346\211\200\346\234\211\345\257\271\350\261\241\351\203\275\345\277\205\351\241\273\345\210\233\345\273\272.md" "b/2.2 \346\211\200\346\234\211\345\257\271\350\261\241\351\203\275\345\277\205\351\241\273\345\210\233\345\273\272.md" index 87869d5..c737cf7 100644 --- "a/2.2 \346\211\200\346\234\211\345\257\271\350\261\241\351\203\275\345\277\205\351\241\273\345\210\233\345\273\272.md" +++ "b/2.2 \346\211\200\346\234\211\345\257\271\350\261\241\351\203\275\345\277\205\351\241\273\345\210\233\345\273\272.md" @@ -1,9 +1,9 @@ # 2.2 所有对象都必须创建 -创建句柄时,我们希望它同一个新对象连接。通常用new关键字达到这一目的。new的意思是:“把我变成这些对象的一种新类型”。所以在上面的例子中,可以说: +创建指针时,我们希望它同一个新对象连接。通常用new关键字达到这一目的。new的意思是:“把我变成这些对象的一种新类型”。所以在上面的例子中,可以说: -``` +``` java String s = new String("asdf"); ``` @@ -16,9 +16,9 @@ String s = new String("asdf"); (1) 寄存器。这是最快的保存区域,因为它位于和其他所有保存方式不同的地方:处理器内部。然而,寄存器的数量十分有限,所以寄存器是根据需要由编译器分配。我们对此没有直接的控制权,也不可能在自己的程序里找到寄存器存在的任何踪迹。 -(2) 堆栈。驻留于常规RAM(随机访问存储器)区域,但可通过它的“堆栈指针”获得处理的直接支持。堆栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。这是一种特别快、特别有效的数据保存方式,仅次于寄存器。创建程序时,Java编译器必须准确地知道堆栈内保存的所有数据的“长度”以及“存在时间”。这是由于它必须生成相应的代码,以便向上和向下移动指针。这一限制无疑影响了程序的灵活性,所以尽管有些Java数据要保存在堆栈里——特别是对象句柄,但Java对象并不放到其中。 +(2) 栈(Stack)。驻留于常规RAM(随机访问存储器)区域,但可通过它的“栈指针”获得处理的直接支持。栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。这是一种特别快、特别有效的数据保存方式,仅次于寄存器。创建程序时,Java编译器必须准确地知道栈内保存的所有数据的“长度”以及“存在时间”。这是由于它必须生成相应的代码,以便向上和向下移动指针。这一限制无疑影响了程序的灵活性,所以尽管有些Java数据要保存在栈里——特别是对象指针,但Java对象并不放到其中。 -(3) 堆。一种常规用途的内存池(也在RAM区域),其中保存了Java对象。和堆栈不同,“内存堆”或“堆”(Heap)最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间! +(3) 堆。一种常规用途的内存池(也在RAM区域),其中保存了Java对象。和栈不同,“内存堆”或“堆”(Heap)最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间! (4) 静态存储。这儿的“静态”(Static)是指“位于固定位置”(尽管也在RAM里)。程序运行期间,静态存储的数据将随时等候调用。可用static关键字指出一个对象的特定元素是静态的。但Java对象本身永远都不会置入静态存储空间。 @@ -28,21 +28,21 @@ String s = new String("asdf"); 2.2.2 特殊情况:主要类型 -有一系列类需特别对待;可将它们想象成“基本”、“主要”或者“主”(Primitive)类型,进行程序设计时要频繁用到它们。之所以要特别对待,是由于用new创建对象(特别是小的、简单的变量)并不是非常有效,因为new将对象置于“堆”里。对于这些类型,Java采纳了与C和C++相同的方法。也就是说,不是用new创建变量,而是创建一个并非句柄的“自动”变量。这个变量容纳了具体的值,并置于堆栈中,能够更高效地存取。 +有一系列类需特别对待;可将它们想象成“基本”、“主要”或者“主”(Primitive)类型,进行程序设计时要频繁用到它们。之所以要特别对待,是由于用new创建对象(特别是小的、简单的变量)并不是非常有效,因为new将对象置于“堆”里。对于这些类型,Java采纳了与C和C++相同的方法。也就是说,不是用new创建变量,而是创建一个并非指针的“自动”变量。这个变量容纳了具体的值,并置于栈中,能够更高效地存取。 Java决定了每种主要类型的大小。就象在大多数语言里那样,这些大小并不随着机器结构的变化而变化。这种大小的不可更改正是Java程序具有很强移植能力的原因之一。 | 主类型 | 大小 | 最小值 | 最大值 | 封装器类型 | |---------|---------|-----------|----------------|------------| | boolean | 1-bit | – | – | Boolean | -| char | 16-bit | Unicode 0 | Unicode 216- 1 | Character | -| byte | 8-bit | -128 | +127 | Byte[11] | -| short | 16-bit | -215 | +215 – 1 | Short1 | -| int | 32-bit | -231 | +231 – 1 | Integer | -| long | 64-bit | -263 | +263 – 1 | Long | +| char | 16-bit | Unicode 0 | Unicode 216- 1 | Character | +| byte | 8-bit | -128 | +127 | Byte[1] | +| short | 16-bit | -215 | +215 – 1 | Short[1] | +| int | 32-bit | -231 | +231 – 1 | Integer | +| long | 64-bit | -263 | +263 – 1 | Long | | float | 32-bit | IEEE754 | IEEE754 | Float | | double | 64-bit | IEEE754 | IEEE754 | Double | -| void | – | – | – | Void1 | +| void | – | – | – | Void[1] | ①:到Java 1.1才有,1.0版没有。 @@ -50,14 +50,14 @@ Java决定了每种主要类型的大小。就象在大多数语言里那样, 数值类型全都是有符号(正负号)的,所以不必费劲寻找没有符号的类型。 主数据类型也拥有自己的“封装器”(wrapper)类。这意味着假如想让堆内一个非主要对象表示那个主类型,就要使用对应的封装器。例如: -``` +``` java char c = 'x'; -Character C = new Character('c'); +Character C = new Character(c); ``` 也可以直接使用: -``` +``` java Character C = new Character('x'); ``` @@ -72,7 +72,7 @@ Java 1.1增加了两个类,用于进行高精度的计算:BigInteger和BigDe BigInteger支持任意精度的整数。也就是说,我们可精确表示任意大小的整数值,同时在运算过程中不会丢失任何信息。 BigDecimal支持任意精度的定点数字。例如,可用它进行精确的币值计算。 -至于调用这两个类时可选用的构建器和方法,请自行参考联机帮助文档。 +至于调用这两个类时可选用的构造器和方法,请自行参考联机帮助文档。 2.2.3 Java的数组 @@ -82,7 +82,7 @@ BigDecimal支持任意精度的定点数字。例如,可用它进行精确的 Java的一项主要设计目标就是安全性。所以在C和C++里困扰程序员的许多问题都未在Java里重复。一个Java可以保证被初始化,而且不可在它的范围之外访问。由于系统自动进行范围检查,所以必然要付出一些代价:针对每个数组,以及在运行期间对索引的校验,都会造成少量的内存开销。但由此换回的是更高的安全性,以及更高的工作效率。为此付出少许代价是值得的。 -创建对象数组时,实际创建的是一个句柄数组。而且每个句柄都会自动初始化成一个特殊值,并带有自己的关键字:null(空)。一旦Java看到null,就知道该句柄并未指向一个对象。正式使用前,必须为每个句柄都分配一个对象。若试图使用依然为null的一个句柄,就会在运行期报告问题。因此,典型的数组错误在Java里就得到了避免。 +创建对象数组时,实际创建的是一个指针数组。而且每个指针都会自动初始化成一个特殊值,并带有自己的关键字:null(空)。一旦Java看到null,就知道该指针并未指向一个对象。正式使用前,必须为每个指针都分配一个对象。若试图使用依然为null的一个指针,就会在运行期报告问题。因此,典型的数组错误在Java里就得到了避免。 也可以创建主类型数组。同样地,编译器能够担保对它的初始化,因为会将那个数组的内存划分成零。 diff --git "a/2.3 \347\273\235\345\257\271\344\270\215\350\246\201\346\270\205\351\231\244\345\257\271\350\261\241.md" "b/2.3 \347\273\235\345\257\271\344\270\215\350\246\201\346\270\205\351\231\244\345\257\271\350\261\241.md" index 498dd77..14ed648 100644 --- "a/2.3 \347\273\235\345\257\271\344\270\215\350\246\201\346\270\205\351\231\244\345\257\271\350\261\241.md" +++ "b/2.3 \347\273\235\345\257\271\344\270\215\350\246\201\346\270\205\351\231\244\345\257\271\350\261\241.md" @@ -7,7 +7,7 @@ 大多数程序设计语言都提供了“作用域”(Scope)的概念。对于在作用域里定义的名字,作用域同时决定了它的“可见性”以及“存在时间”。在C,C++和Java里,作用域是由花括号的位置决定的。参考下面这个例子: -``` +``` java { int x = 12; /* only x available */ @@ -26,7 +26,7 @@ 注意尽管在C和C++里是合法的,但在Java里不能象下面这样书写代码: -``` +``` java { int x = 12; { @@ -41,13 +41,13 @@ Java对象不具备与主类型一样的存在时间。用new关键字创建一个Java对象的时候,它会超出作用域的范围之外。所以假若使用下面这段代码: -``` +``` java { String s = new String("a string"); } /* 作用域的终点 */ ``` -那么句柄s会在作用域的终点处消失。然而,s指向的String对象依然占据着内存空间。在上面这段代码里,我们没有办法访问对象,因为指向它的唯一一个句柄已超出了作用域的边界。在后面的章节里,大家还会继续学习如何在程序运行期间传递和复制对象句柄。 +那么指针s会在作用域的终点处消失。然而,s指向的String对象依然占据着内存空间。在上面这段代码里,我们没有办法访问对象,因为指向它的唯一一个指针已超出了作用域的边界。在后面的章节里,大家还会继续学习如何在程序运行期间传递和复制对象指针。 这样造成的结果便是:对于用new创建的对象,只要我们愿意,它们就会一直保留下去。这个编程问题在C和C++里特别突出。看来在C++里遇到的麻烦最大:由于不能从语言获得任何帮助,所以在需要对象的时候,根本无法确定它们是否可用。而且更麻烦的是,在C++里,一旦工作完成,必须保证将对象清除。 diff --git "a/2.4 \346\226\260\345\273\272\346\225\260\346\215\256\347\261\273\345\236\213\357\274\232\347\261\273.md" "b/2.4 \346\226\260\345\273\272\346\225\260\346\215\256\347\261\273\345\236\213\357\274\232\347\261\273.md" index b0594a6..6f23953 100644 --- "a/2.4 \346\226\260\345\273\272\346\225\260\346\215\256\347\261\273\345\236\213\357\274\232\347\261\273.md" +++ "b/2.4 \346\226\260\345\273\272\346\225\260\346\215\256\347\261\273\345\236\213\357\274\232\347\261\273.md" @@ -5,13 +5,13 @@ 如果说一切东西都是对象,那么用什么决定一个“类”(Class)的外观与行为呢?换句话说,是什么建立起了一个对象的“类型”(Type)呢?大家可能猜想有一个名为“type”的关键字。但从历史看来,大多数面向对象的语言都用关键字“class”表达这样一个意思:“我准备告诉你对象一种新类型的外观”。class关键字太常用了,以至于本书许多地方并没有用粗体字或双引号加以强调。在这个关键字的后面,应该跟随新数据类型的名称。例如: -``` +``` java class ATypeName {/*类主体置于这里} ``` 这样就引入了一种新类型,接下来便可用new创建这种类型的一个新对象: -``` +``` java ATypeName a = new ATypeName(); ``` @@ -19,11 +19,11 @@ ATypeName a = new ATypeName(); 2.4.1 字段和方法 -定义一个类时(我们在Java里的全部工作就是定义类、制作那些类的对象以及将消息发给那些对象),可在自己的类里设置两种类型的元素:数据成员(有时也叫“字段”)以及成员函数(通常叫“方法”)。其中,数据成员是一种对象(通过它的句柄与其通信),可以为任何类型。它也可以是主类型(并不是句柄)之一。如果是指向对象的一个句柄,则必须初始化那个句柄,用一种名为“构建器”(第4章会对此详述)的特殊函数将其与一个实际对象连接起来(就象早先看到的那样,使用new关键字)。但若是一种主类型,则可在类定义位置直接初始化(正如后面会看到的那样,句柄亦可在定义位置初始化)。 +定义一个类时(我们在Java里的全部工作就是定义类、制作那些类的对象以及将消息发给那些对象),可在自己的类里设置两种类型的元素:数据成员(有时也叫“字段”)以及成员函数(通常叫“方法”)。其中,数据成员是一种对象(通过它的指针与其通信),可以为任何类型。它也可以是主类型(并不是指针)之一。如果是指向对象的一个指针,则必须初始化那个指针,用一种名为“构造器”(第4章会对此详述)的特殊函数将其与一个实际对象连接起来(就象早先看到的那样,使用new关键字)。但若是一种主类型,则可在类定义位置直接初始化(正如后面会看到的那样,指针亦可在定义位置初始化)。 每个对象都为自己的数据成员保有存储空间;数据成员不会在对象之间共享。下面是定义了一些数据成员的类示例: -``` +``` java class DataOnly { int i; float f; @@ -33,13 +33,13 @@ class DataOnly { 这个类并没有做任何实质性的事情,但我们可创建一个对象: -``` +``` java DataOnly d = new DataOnly(); ``` -可将值赋给数据成员,但首先必须知道如何引用一个对象的成员。为达到引用对象成员的目的,首先要写上对象句柄的名字,再跟随一个点号(句点),再跟随对象内部成员的名字。即“对象句柄.成员”。例如: +可将值赋给数据成员,但首先必须知道如何引用一个对象的成员。为达到引用对象成员的目的,首先要写上对象指针的名字,再跟随一个点号(句点),再跟随对象内部成员的名字。即“对象指针.成员”。例如: -``` +``` java d.i = 47; d.f = 1.1f; d.b = false; @@ -47,7 +47,7 @@ d.b = false; 一个对象也可能包含了另一个对象,而另一个对象里则包含了我们想修改的数据。对于这个问题,只需保持“连接句点”即可。例如: -``` +``` java myPlane.leftTank.capacity = 100; ``` @@ -59,9 +59,9 @@ myPlane.leftTank.capacity = 100; 主类型 默认值 -``` -Boolean false -Char '\u0000'(null) +``` java +boolean false +char '\u0000'(null) byte (byte)0 short (short)0 int 0 @@ -74,7 +74,7 @@ double 0.0d 然而,这种保证却并不适用于“局部”变量——那些变量并非一个类的字段。所以,假若在一个函数定义中写入下述代码: -``` +``` java int x; ``` diff --git "a/2.5 \346\226\271\346\263\225\343\200\201\350\207\252\345\217\230\351\207\217\345\222\214\350\277\224\345\233\236\345\200\274.md" "b/2.5 \346\226\271\346\263\225\343\200\201\350\207\252\345\217\230\351\207\217\345\222\214\350\277\224\345\233\236\345\200\274.md" index 473556e..bf798ac 100644 --- "a/2.5 \346\226\271\346\263\225\343\200\201\350\207\252\345\217\230\351\207\217\345\222\214\350\277\224\345\233\236\345\200\274.md" +++ "b/2.5 \346\226\271\346\263\225\343\200\201\350\207\252\345\217\230\351\207\217\345\222\214\350\277\224\345\233\236\345\200\274.md" @@ -7,7 +7,7 @@ Java的“方法”决定了一个对象能够接收的消息。通过本节的 方法的基本组成部分包括名字、自变量、返回类型以及主体。下面便是它最基本的形式: -``` +``` java 返回类型 方法名( /* 自变量列表*/ ) {/* 方法主体 */} ``` @@ -15,7 +15,7 @@ Java的“方法”决定了一个对象能够接收的消息。通过本节的 Java的方法只能作为类的一部分创建。只能针对某个对象调用一个方法(注释③),而且那个对象必须能够执行那个方法调用。若试图为一个对象调用错误的方法,就会在编译期得到一条出错消息。为一个对象调用方法时,需要先列出对象的名字,在后面跟上一个句点,再跟上方法名以及它的参数列表。亦即“对象名.方法名(自变量1,自变量2,自变量3...)。举个例子来说,假设我们有一个方法名叫f(),它没有自变量,返回的是类型为int的一个值。那么,假设有一个名为a的对象,可为其调用方法f(),则代码如下: -``` +``` java int x = a.f(); ``` @@ -27,13 +27,13 @@ int x = a.f(); 2.5.1 自变量列表 -自变量列表规定了我们传送给方法的是什么信息。正如大家或许已猜到的那样,这些信息——如同Java内其他任何东西——采用的都是对象的形式。因此,我们必须在自变量列表里指定要传递的对象类型,以及每个对象的名字。正如在Java其他地方处理对象时一样,我们实际传递的是“句柄”(注释④)。然而,句柄的类型必须正确。倘若希望自变量是一个“字串”,那么传递的必须是一个字串。 +自变量列表规定了我们传送给方法的是什么信息。正如大家或许已猜到的那样,这些信息——如同Java内其他任何东西——采用的都是对象的形式。因此,我们必须在自变量列表里指定要传递的对象类型,以及每个对象的名字。正如在Java其他地方处理对象时一样,我们实际传递的是“指针”(注释④)。然而,指针的类型必须正确。倘若希望自变量是一个“字串”,那么传递的必须是一个字串。 -④:对于前面提及的“特殊”数据类型boolean,char,byte,short,int,long,,float以及double来说是一个例外。但在传递对象时,通常都是指传递指向对象的句柄。 +④:对于前面提及的“特殊”数据类型boolean,char,byte,short,int,long,,float以及double来说是一个例外。但在传递对象时,通常都是指传递指向对象的指针。 下面让我们考虑将一个字串作为自变量使用的方法。下面列出的是定义代码,必须将它置于一个类定义里,否则无法编译: -``` +``` java int storage(String s) { return s.length() * 2; } @@ -44,7 +44,7 @@ return s.length() * 2; 通过上面的例子,也可以了解return关键字的运用。它主要做两件事情。首先,它意味着“离开方法,我已完工了”。其次,假设方法生成了一个值,则那个值紧接在return语句的后面。在这种情况下,返回值是通过计算表达式“s.length()*2”而产生的。 可按自己的愿望返回任意类型,但倘若不想返回任何东西,就可指示方法返回void(空)。下面列出一些例子。 -``` +``` java boolean flag() { return true; } float naturalLogBase() { return 2.718; } void nothing() { return; } diff --git "a/2.6 \346\236\204\345\273\272Java\347\250\213\345\272\217.md" "b/2.6 \346\236\204\345\273\272Java\347\250\213\345\272\217.md" index 2bbb2cb..4edb390 100644 --- "a/2.6 \346\236\204\345\273\272Java\347\250\213\345\272\217.md" +++ "b/2.6 \346\236\204\345\273\272Java\347\250\213\345\272\217.md" @@ -23,13 +23,13 @@ Java的这种特殊机制意味着所有文件都自动存在于自己的命名 大多数时候,我们直接采用来自标准Java库的组件(部件)即可,它们是与编译器配套提供的。使用这些组件时,没有必要关心冗长的保留域名;举个例子来说,只需象下面这样写一行代码即可: -``` +``` java import java.util.Vector; ``` 它的作用是告诉编译器我们想使用Java的Vector类。然而,util包含了数量众多的类,我们有时希望使用其中的几个,同时不想全部明确地声明它们。为达到这个目的,可使用“*”通配符。如下所示: -``` +``` java import java.util.*; ``` @@ -44,7 +44,7 @@ import java.util.*; 为了将数据成员或方法设为static,只需在定义前置和这个关键字即可。例如,下述代码能生成一个static数据成员,并对其初始化: -``` +``` java class StaticTest { Static int i = 47; } @@ -52,7 +52,7 @@ Static int i = 47; 现在,尽管我们制作了两个StaticTest对象,但它们仍然只占据StaticTest.i的一个存储空间。这两个对象都共享同样的i。请考察下述代码: -``` +``` java StaticTest st1 = new StaticTest(); StaticTest st2 = new StaticTest(); ``` @@ -61,7 +61,7 @@ StaticTest st2 = new StaticTest(); 有两个办法可引用一个static变量。正如上面展示的那样,可通过一个对象命名它,如st2.i。亦可直接用它的类名引用,而这在非静态成员里是行不通的(最好用这个办法引用static变量,因为它强调了那个变量的“静态”本质)。 -``` +``` java StaticTest.i++; ``` @@ -69,7 +69,7 @@ StaticTest.i++; 类似的逻辑也适用于静态方法。既可象对其他任何方法那样通过一个对象引用静态方法,亦可用特殊的语法格式“类名.方法()”加以引用。静态方法的定义是类似的: -``` +``` java class StaticFun { static void incr() { StaticTest.i++; } } @@ -77,14 +77,14 @@ static void incr() { StaticTest.i++; } 从中可看出,StaticFun的方法incr()使静态数据i增值。通过对象,可用典型的方法调用incr(): -``` +``` java StaticFun sf = new StaticFun(); sf.incr(); ``` 或者,由于incr()是一种静态方法,所以可通过它的类直接调用: -``` +``` java StaticFun.incr(); ``` diff --git "a/2.7 \346\210\221\344\273\254\347\232\204\347\254\254\344\270\200\344\270\252Java\347\250\213\345\272\217.md" "b/2.7 \346\210\221\344\273\254\347\232\204\347\254\254\344\270\200\344\270\252Java\347\250\213\345\272\217.md" index 4051888..fdfd693 100644 --- "a/2.7 \346\210\221\344\273\254\347\232\204\347\254\254\344\270\200\344\270\252Java\347\250\213\345\272\217.md" +++ "b/2.7 \346\210\221\344\273\254\347\232\204\347\254\254\344\270\200\344\270\252Java\347\250\213\345\272\217.md" @@ -3,7 +3,7 @@ 最后,让我们正式编一个程序(注释⑤)。它能打印出与当前运行的系统有关的资料,并利用了来自Java标准库的System对象的多种方法。注意这里引入了一种额外的注释样式:“//”。它表示到本行结束前的所有内容都是注释: -``` +``` java // Property.java import java.util.*; @@ -24,7 +24,7 @@ public class Property { ⑤:在某些编程环境里,程序会在屏幕上一切而过,甚至没机会看到结果。可将下面这段代码置于main()的末尾,用它暂停输出: -``` +``` java try { Thread.currentThread().sleep(5 * 1000); } catch(InterruptedException e) {} @@ -40,34 +40,34 @@ Thread.currentThread().sleep(5 * 1000); 类名与文件是一样的。若象现在这样创建一个独立的程序,文件中的一个类必须与文件同名(如果没这样做,编译器会及时作出反应)。类里必须包含一个名为main()的方法,形式如下: -``` +``` java public static void main(String[] args) { ``` 其中,关键字“public”意味着方法可由外部世界调用(第5章会详细解释)。main()的自变量是包含了String对象的一个数组。args不会在本程序中用到,但需要在这个地方列出,因为它们保存了在命令行调用的自变量。 程序的第一行非常有趣: -``` +``` java System.out.println(new Date()); ``` 请观察它的自变量:创建Date对象唯一的目的就是将它的值发送给println()。一旦这个语句执行完毕,Date就不再需要。随之而来的“垃圾收集器”会发现这一情况,并在任何可能的时候将其回收。事实上,我们没太大的必要关心“清除”的细节。 -第二行调用了System.getProperties()。若用Web浏览器查看联机用户文档,就可知道getProperties()是System类的一个static方法。由于它是“静态”的,所以不必创建任何对象便可调用该方法。无论是否存在该类的一个对象,static方法随时都可使用。调用getProperties()时,它会将系统属性作为Properties类的一个对象生成(注意Properties是“属性”的意思)。随后的的句柄保存在一个名为p的Properties句柄里。在第三行,大家可看到Properties对象有一个名为list()的方法,它将自己的全部内容都发给一个我们作为自变量传递的PrintStream对象。 +第二行调用了System.getProperties()。若用Web浏览器查看联机用户文档,就可知道getProperties()是System类的一个static方法。由于它是“静态”的,所以不必创建任何对象便可调用该方法。无论是否存在该类的一个对象,static方法随时都可使用。调用getProperties()时,它会将系统属性作为Properties类的一个对象生成(注意Properties是“属性”的意思)。随后的的指针保存在一个名为p的Properties指针里。在第三行,大家可看到Properties对象有一个名为list()的方法,它将自己的全部内容都发给一个我们作为自变量传递的PrintStream对象。 -`main()` 的第四和第六行是典型的打印语句。注意为了打印多个String值,用加号(+)分隔它们即可。然而,也要在这里注意一些奇怪的事情。在String对象中使用时,加号并不代表真正的“相加”。处理字串时,我们通常不必考虑“+”的任何特殊含义。但是,Java的String类要受一种名为“运算符过载”的机制的制约。也就是说,只有在随同String对象使用时,加号才会产生与其他任何地方不同的表现。对于字串,它的意思是“连接这两个字串”。 +`main()` 的第四和第六行是典型的打印语句。注意为了打印多个String值,用加号(+)分隔它们即可。然而,也要在这里注意一些奇怪的事情。在String对象中使用时,加号并不代表真正的“相加”。处理字串时,我们通常不必考虑“+”的任何特殊含义。但是,Java的String类要受一种名为“运算符重载”的机制的制约。也就是说,只有在随同String对象使用时,加号才会产生与其他任何地方不同的表现。对于字串,它的意思是“连接这两个字串”。 但事情到此并未结束。请观察下述语句: -``` +``` java System.out.println("Total Memory = " + rt.totalMemory() + " Free Memory = " + rt.freeMemory()); ``` -其中,totalMemory()和freeMemory()返回的是数值,并非String对象。如果将一个数值“加”到一个字串身上,会发生什么情况呢?同我们一样,编译器也会意识到这个问题,并魔术般地调用一个方法,将那个数值(int,float等等)转换成字串。经这样处理后,它们当然能利用加号“加”到一起。这种“自动类型转换”亦划入“运算符过载”处理的范畴。 +其中,totalMemory()和freeMemory()返回的是数值,并非String对象。如果将一个数值“加”到一个字串身上,会发生什么情况呢?同我们一样,编译器也会意识到这个问题,并魔术般地调用一个方法,将那个数值(int,float等等)转换成字串。经这样处理后,它们当然能利用加号“加”到一起。这种“自动类型转换”亦划入“运算符重载”处理的范畴。 -许多Java著作都在热烈地辩论“运算符过载”(C++的一项特性)是否有用。目前就是反对它的一个好例子!然而,这最多只能算编译器(程序)的问题,而且只是对String对象而言。对于自己编写的任何源代码,都不可能使运算符“过载”。 +许多Java著作都在热烈地辩论“运算符重载”(C++的一项特性)是否有用。目前就是反对它的一个好例子!然而,这最多只能算编译器(程序)的问题,而且只是对String对象而言。对于自己编写的任何源代码,都不可能使运算符“重载”。 -通过为Runtime类调用getRuntime()方法,main()的第五行创建了一个Runtime对象。返回的则是指向一个Runtime对象的句柄。而且,我们不必关心它是一个静态对象,还是由new命令创建的一个对象。这是由于我们不必为清除工作负责,可以大模大样地使用对象。正如显示的那样,Runtime可告诉我们与内存使用有关的信息。 +通过为Runtime类调用getRuntime()方法,main()的第五行创建了一个Runtime对象。返回的则是指向一个Runtime对象的指针。而且,我们不必关心它是一个静态对象,还是由new命令创建的一个对象。这是由于我们不必为清除工作负责,可以大模大样地使用对象。正如显示的那样,Runtime可告诉我们与内存使用有关的信息。 diff --git "a/2.8 \346\263\250\351\207\212\345\222\214\345\265\214\345\205\245\346\226\207\346\241\243.md" "b/2.8 \346\263\250\351\207\212\345\222\214\345\265\214\345\205\245\346\226\207\346\241\243.md" index 17f32ad..2c64294 100644 --- "a/2.8 \346\263\250\351\207\212\345\222\214\345\265\214\345\205\245\346\226\207\346\241\243.md" +++ "b/2.8 \346\263\250\351\207\212\345\222\214\345\265\214\345\205\245\346\226\207\346\241\243.md" @@ -3,9 +3,9 @@ 2.8 注释和嵌入文档 -Java里有两种类型的注释。第一种是传统的、C语言风格的注释,是从C++继承而来的。这些注释用一个 `“/*”` 起头,随后是注释内容,并可跨越多行,最后用一个“*/”结束。注意许多程序员在连续注释内容的每一行都用一个 `“*”` 开头,所以经常能看到象下面这样的内容: +Java里有两种类型的注释。第一种是传统的、C语言风格的注释,是从C++继承而来的。这些注释用一个 `“/*”` 起头,随后是注释内容,并可跨越多行,最后用一个`“*/”`结束。注意许多程序员在连续注释内容的每一行都用一个 `“*”` 开头,所以经常能看到象下面这样的内容: -``` +``` java /* 这是 * 一段注释, * 它跨越了多个行 @@ -14,14 +14,14 @@ Java里有两种类型的注释。第一种是传统的、C语言风格的注释 但请记住,进行编译时,/*和*/之间的所有东西都会被忽略,所以上述注释与下面这段注释并没有什么不同: -``` +``` java /* 这是一段注释, 它跨越了多个行 */ ``` 第二种类型的注释也起源于C++。这种注释叫作“单行注释”,以一个 `“//”` 起头,表示这一行的所有内容都是注释。这种类型的注释更常用,因为它书写时更方便。没有必要在键盘上寻找 `“/”` ,再寻找 `“*”` (只需按同样的键两次),而且不必在注释结尾时加一个结束标记。下面便是这类注释的一个例子: -``` +``` java // 这是一条单行注释 ``` @@ -31,7 +31,7 @@ Java里有两种类型的注释。第一种是传统的、C语言风格的注释 用于提取注释的工具叫作javadoc。它采用了部分来自Java编译器的技术,查找我们置入程序的特殊注释标记。它不仅提取由这些标记指示的信息,也将毗邻注释的类名或方法名提取出来。这样一来,我们就可用最轻的工作量,生成十分专业的程序文档。 -javadoc输出的是一个HTML文件,可用自己的Web浏览器查看。该工具允许我们创建和管理单个源文件,并生动生成有用的文档。由于有了jvadoc,所以我们能够用标准的方法创建文档。而且由于它非常方便,所以我们能轻松获得所有Java库的文档。 +javadoc输出的是一个HTML文件,可用自己的Web浏览器查看。该工具允许我们创建和管理单个源文件,并生动生成有用的文档。由于有了javadoc,所以我们能够用标准的方法创建文档。而且由于它非常方便,所以我们能轻松获得所有Java库的文档。 2.8.2 具体语法 @@ -39,7 +39,7 @@ javadoc输出的是一个HTML文件,可用自己的Web浏览器查看。该工 有三种类型的注释文档,它们对应于位于注释后面的元素:类、变量或者方法。也就是说,一个类注释正好位于一个类定义之前;变量注释正好位于变量定义之前;而一个方法定义正好位于一个方法定义的前面。如下面这个简单的例子所示: -``` +``` java /** 一个类注释 */ public class docTest { /** 一个变量注释 */ @@ -57,7 +57,7 @@ public void f() {} javadoc将HTML命令传递给最终生成的HTML文档。这便使我们能够充分利用HTML的巨大威力。当然,我们的最终动机是格式化代码,不是为了哗众取宠。下面列出一个例子: -``` +``` java /** *
 * System.out.println(new Date());
@@ -67,7 +67,7 @@ javadoc将HTML命令传递给最终生成的HTML文档。这便使我们能够
 
 亦可象在其他Web文档里那样运用HTML,对普通文本进行格式化,使其更具条理、更加美观:
 
-```
+``` java
 /**
 * 您甚至可以插入一个列表:
 * 
    @@ -86,7 +86,7 @@ javadoc将HTML命令传递给最终生成的HTML文档。这便使我们能够 所有三种类型的注释文档都可包含@see标记,它允许我们引用其他类里的文档。对于这个标记,javadoc会生成相应的HTML,将其直接链接到其他文档。格式如下: -``` +``` java @see 类名 @see 完整类名 @see 完整类名#方法名 @@ -104,7 +104,7 @@ javadoc将HTML命令传递给最终生成的HTML文档。这便使我们能够 格式如下: -``` +``` java @version 版本信息 ``` @@ -114,7 +114,7 @@ javadoc将HTML命令传递给最终生成的HTML文档。这便使我们能够 格式如下: -``` +``` java @author 作者信息 ``` @@ -128,7 +128,7 @@ javadoc将HTML命令传递给最终生成的HTML文档。这便使我们能够 2.8.7 方法文档标记 -除嵌入HTML和@see引用之外,方法还允许使用针对参数、返回值以及违例的文档标记。 +除嵌入HTML和@see引用之外,方法还允许使用针对参数、返回值以及异常的文档标记。 **1. @param** 格式如下: @@ -139,7 +139,7 @@ javadoc将HTML命令传递给最终生成的HTML文档。这便使我们能够 格式如下: -``` +``` java @return 说明 ``` @@ -147,13 +147,13 @@ javadoc将HTML命令传递给最终生成的HTML文档。这便使我们能够 **3. @exception** -有关“违例”(Exception)的详细情况,我们会在第9章讲述。简言之,它们是一些特殊的对象,若某个方法失败,就可将它们“扔出”对象。调用一个方法时,尽管只有一个违例对象出现,但一些特殊的方法也许能产生任意数量的、不同类型的违例。所有这些违例都需要说明。所以,违例标记的格式如下: +有关“异常”(Exception)的详细情况,我们会在第9章讲述。简言之,它们是一些特殊的对象,若某个方法失败,就可将它们“扔出”对象。调用一个方法时,尽管只有一个异常对象出现,但一些特殊的方法也许能产生任意数量的、不同类型的异常。所有这些异常都需要说明。所以,异常标记的格式如下: -``` +``` java @exception 完整类名 说明 ``` -其中,“完整类名”明确指定了一个违例类的名字,它是在其他某个地方定义好的。而“说明”(同样可以延续到下面的行)告诉我们为什么这种特殊类型的违例会在方法调用中出现。 +其中,“完整类名”明确指定了一个异常类的名字,它是在其他某个地方定义好的。而“说明”(同样可以延续到下面的行)告诉我们为什么这种特殊类型的异常会在方法调用中出现。 **4. @deprecated** @@ -167,7 +167,7 @@ javadoc将HTML命令传递给最终生成的HTML文档。这便使我们能够 第一行: -``` +``` java //: Property.java ``` diff --git "a/2.9 \347\274\226\347\240\201\346\240\267\345\274\217.md" "b/2.9 \347\274\226\347\240\201\346\240\267\345\274\217.md" index acadab6..eb4f9c8 100644 --- "a/2.9 \347\274\226\347\240\201\346\240\267\345\274\217.md" +++ "b/2.9 \347\274\226\347\240\201\346\240\267\345\274\217.md" @@ -3,13 +3,13 @@ 一个非正式的Java编程标准是大写一个类名的首字母。若类名由几个单词构成,那么把它们紧靠到一起(也就是说,不要用下划线来分隔名字)。此外,每个嵌入单词的首字母都采用大写形式。例如: -``` +``` java class AllTheColorsOfTheRainbow { // ...} ``` -对于其他几乎所有内容:方法、字段(成员变量)以及对象句柄名称,可接受的样式与类样式差不多,只是标识符的第一个字母采用小写。例如: +对于其他几乎所有内容:方法、字段(成员变量)以及对象指针名称,可接受的样式与类样式差不多,只是标识符的第一个字母采用小写。例如: -``` +``` java class AllTheColorsOfTheRainbow { int anIntegerRepresentingColors; void changeTheHueOfTheColor(int newHue) { diff --git "a/3.1 \344\275\277\347\224\250Java\350\277\220\347\256\227\347\254\246.md" "b/3.1 \344\275\277\347\224\250Java\350\277\220\347\256\227\347\254\246.md" index 5588178..50ed672 100644 --- "a/3.1 \344\275\277\347\224\250Java\350\277\220\347\256\227\347\254\246.md" +++ "b/3.1 \344\275\277\347\224\250Java\350\277\220\347\256\227\347\254\246.md" @@ -12,13 +12,13 @@ 运算符的优先级决定了存在多个运算符时一个表达式各部分的计算顺序。Java对计算顺序作出了特别的规定。其中,最简单的规则就是乘法和除法在加法和减法之前完成。程序员经常都会忘记其他优先级规则,所以应该用括号明确规定计算顺序。例如: -``` +``` java A = X + Y - 2/2 + Z; ``` 为上述表达式加上括号后,就有了一个不同的含义。 -``` +``` java A = X + (Y - 2)/(2 + Z); ``` @@ -26,23 +26,23 @@ A = X + (Y - 2)/(2 + Z); 赋值是用等号运算符(=)进行的。它的意思是“取得右边的值,把它复制到左边”。右边的值可以是任何常数、变量或者表达式,只要能产生一个值就行。但左边的值必须是一个明确的、已命名的变量。也就是说,它必须有一个物理性的空间来保存右边的值。举个例子来说,可将一个常数赋给一个变量(A=4;),但不可将任何东西赋给一个常数(比如不能4=A)。 -对主数据类型的赋值是非常直接的。由于主类型容纳了实际的值,而且并非指向一个对象的句柄,所以在为其赋值的时候,可将来自一个地方的内容复制到另一个地方。例如,假设为主类型使用“A=B”,那么B处的内容就复制到A。若接着又修改了A,那么B根本不会受这种修改的影响。作为一名程序员,这应成为自己的常识。 +对主数据类型的赋值是非常直接的。由于主类型容纳了实际的值,而且并非指向一个对象的指针,所以在为其赋值的时候,可将来自一个地方的内容复制到另一个地方。例如,假设为主类型使用“A=B”,那么B处的内容就复制到A。若接着又修改了A,那么B根本不会受这种修改的影响。作为一名程序员,这应成为自己的常识。 -但在为对象“赋值”的时候,情况却发生了变化。对一个对象进行操作时,我们真正操作的是它的句柄。所以倘若“从一个对象到另一个对象”赋值,实际就是将句柄从一个地方复制到另一个地方。这意味着假若为对象使用“C=D”,那么C和D最终都会指向最初只有D才指向的那个对象。下面这个例子将向大家阐示这一点。 +但在为对象“赋值”的时候,情况却发生了变化。对一个对象进行操作时,我们真正操作的是它的指针。所以倘若“从一个对象到另一个对象”赋值,实际就是将指针从一个地方复制到另一个地方。这意味着假若为对象使用“C=D”,那么C和D最终都会指向最初只有D才指向的那个对象。下面这个例子将向大家阐示这一点。 这里有一些题外话。在后面,大家在代码示例里看到的第一个语句将是“package 03”使用的“package”语句,它代表本书第3章。本书每一章的第一个代码清单都会包含象这样的一个“package”(封装、打包、包裹)语句,它的作用是为那一章剩余的代码建立章节编号。在第17章,大家会看到第3章的所有代码清单(除那些有不同封装名称的以外)都会自动置入一个名为c03的子目录里;第4章的代码置入c04;以此类推。所有这些都是通过第17章展示的CodePackage.java程序实现的;“封装”的基本概念会在第5章进行详尽的解释。就目前来说,大家只需记住象“package 03”这样的形式只是用于为某一章的代码清单建立相应的子目录。 为运行程序,必须保证在classpath里包含了我们安装本书源码文件的根目录(那个目录里包含了c02,c03c,c04等等子目录)。 对于Java后续的版本(1.1.4和更高版本),如果您的main()用package语句封装到一个文件里,那么必须在程序名前面指定完整的包裹名称,否则不能运行程序。在这种情况下,命令行是: -``` +``` java java c03.Assignment ``` 运行位于一个“包裹”里的程序时,随时都要注意这方面的问题。 下面是例子: -``` +``` java //: Assignment.java // Assignment with objects is a bit tricky package c03; @@ -69,18 +69,18 @@ public class Assignment { } ///:~ ``` -Number类非常简单,它的两个实例(n1和n2)是在main()里创建的。每个Number中的i值都赋予了一个不同的值。随后,将n2赋给n1,而且n1发生改变。在许多程序设计语言中,我们都希望n1和n2任何时候都相互独立。但由于我们已赋予了一个句柄,所以下面才是真实的输出: +Number类非常简单,它的两个实例(n1和n2)是在main()里创建的。每个Number中的i值都赋予了一个不同的值。随后,将n2赋给n1,而且n1发生改变。在许多程序设计语言中,我们都希望n1和n2任何时候都相互独立。但由于我们已赋予了一个指针,所以下面才是真实的输出: -``` +``` java 1: n1.i: 9, n2.i: 47 2: n1.i: 47, n2.i: 47 3: n1.i: 27, n2.i: 27 ``` -看来改变n1的同时也改变了n2!这是由于无论n1还是n2都包含了相同的句柄,它指向相同的对象(最初的句柄位于n1内部,指向容纳了值9的一个对象。在赋值过程中,那个句柄实际已经丢失;它的对象会由“垃圾收集器”自动清除)。 +看来改变n1的同时也改变了n2!这是由于无论n1还是n2都包含了相同的指针,它指向相同的对象(最初的指针位于n1内部,指向容纳了值9的一个对象。在赋值过程中,那个指针实际已经丢失;它的对象会由“垃圾收集器”自动清除)。 这种特殊的现象通常也叫作“别名”,是Java操作对象的一种基本方式。但假若不愿意在这种情况下出现别名,又该怎么操作呢?可放弃赋值,并写入下述代码: -``` +``` java n1.i = n2.i; ``` @@ -90,7 +90,7 @@ n1.i = n2.i; 将一个对象传递到方法内部时,也会产生别名现象。 -``` +``` java //: PassObject.java // Passing objects to methods can be a bit tricky @@ -112,15 +112,15 @@ public class PassObject { } ///:~ ``` -在许多程序设计语言中,f()方法表面上似乎要在方法的作用域内制作自己的自变量Letter y的一个副本。但同样地,实际传递的是一个句柄。所以下面这个程序行: +在许多程序设计语言中,f()方法表面上似乎要在方法的作用域内制作自己的自变量Letter y的一个副本。但同样地,实际传递的是一个指针。所以下面这个程序行: -``` +``` java y.c = 'z'; -```` +``` javaa` 实际改变的是f()之外的对象。输出结果如下: -``` +``` java 1: x.c: a 2: x.c: z ``` @@ -135,7 +135,7 @@ Java也用一种简写形式进行运算,并同时进行赋值操作。这是 下面这个例子展示了算术运算符的各种用法: -``` +``` java //: MathOps.java // Demonstrates the mathematical operators import java.util.*; @@ -198,19 +198,19 @@ public class MathOps { 一元减号(-)和一元加号(+)与二元加号和减号都是相同的运算符。根据表达式的书写形式,编译器会自动判断使用哪一种。例如下述语句: -``` +``` java x = -a; ``` 它的含义是显然的。编译器能正确识别下述语句: -``` +``` java x = a * -b; ``` 但读者会被搞糊涂,所以最好更明确地写成: -``` +``` java x = a * (-b); ``` @@ -224,7 +224,7 @@ x = a * (-b); 对每种类型的运算符,都有两个版本可供选用;通常将其称为“前缀版”和“后缀版”。“前递增”表示++运算符位于变量或表达式的前面;而“后递增”表示++运算符位于变量或表达式的后面。类似地,“前递减”意味着--运算符位于变量或表达式的前面;而“后递减”意味着--运算符位于变量或表达式的后面。对于前递增和前递减(如++A或--A),会先执行运算,再生成值。而对于后递增和后递减(如A++或A--),会先生成值,再执行运算。下面是一个例子: -``` +``` java //: AutoInc.java // Demonstrates the ++ and -- operators @@ -247,7 +247,7 @@ public class AutoInc { 该程序的输出如下: -``` +``` java i : 1 ++i : 2 i++ : 2 @@ -268,7 +268,7 @@ i : 1 关系运算符==和!=也适用于所有对象,但它们的含义通常会使初涉Java领域的人找不到北。下面是一个例子: -``` +``` java //: Equivalence.java public class Equivalence { @@ -281,11 +281,11 @@ public class Equivalence { } ///:~ ``` -其中,表达式System.out.println(n1 == n2)可打印出内部的布尔比较结果。一般人都会认为输出结果肯定先是true,再是false,因为两个Integer对象都是相同的。但尽管对象的内容相同,句柄却是不同的,而==和!=比较的正好就是对象句柄。所以输出结果实际上先是false,再是true。这自然会使第一次接触的人感到惊奇。 +其中,表达式System.out.println(n1 == n2)可打印出内部的布尔比较结果。一般人都会认为输出结果肯定先是true,再是false,因为两个Integer对象都是相同的。但尽管对象的内容相同,指针却是不同的,而==和!=比较的正好就是对象指针。所以输出结果实际上先是false,再是true。这自然会使第一次接触的人感到惊奇。 若想对比两个对象的实际内容是否相同,又该如何操作呢?此时,必须使用所有对象都适用的特殊方法equals()。但这个方法不适用于“主类型”,那些类型直接使用==和!=即可。下面举例说明如何使用: -``` +``` java //: EqualsMethod.java public class EqualsMethod { @@ -299,7 +299,7 @@ public class EqualsMethod { 正如我们预计的那样,此时得到的结果是true。但事情并未到此结束!假设您创建了自己的类,就象下面这样: -``` +``` java //: EqualsMethod2.java class Value { @@ -316,15 +316,15 @@ public class EqualsMethod2 { } ///:~ ``` -此时的结果又变回了false!这是由于equals()的默认行为是比较句柄。所以除非在自己的新类中改变了equals(),否则不可能表现出我们希望的行为。不幸的是,要到第7章才会学习如何改变行为。但要注意equals()的这种行为方式同时或许能够避免一些“灾难”性的事件。 +此时的结果又变回了false!这是由于equals()的默认行为是比较指针。所以除非在自己的新类中改变了equals(),否则不可能表现出我们希望的行为。不幸的是,要到第7章才会学习如何改变行为。但要注意equals()的这种行为方式同时或许能够避免一些“灾难”性的事件。 -大多数Java类库都实现了equals(),所以它实际比较的是对象的内容,而非它们的句柄。 +大多数Java类库都实现了equals(),所以它实际比较的是对象的内容,而非它们的指针。 3.1.6 逻辑运算符 逻辑运算符AND(&&)、OR(||)以及NOT(!)能生成一个布尔值(true或false)——以自变量的逻辑关系为基础。下面这个例子向大家展示了如何使用关系和逻辑运算符。 -``` +``` java //: Bool.java // Relational and logical operators import java.util.*; @@ -363,7 +363,7 @@ public class Bool { 只可将AND,OR或NOT应用于布尔值。与在C及C++中不同,不可将一个非布尔值当作布尔值在逻辑表达式中使用。若这样做,就会发现尝试失败,并用一个“//!”标出。然而,后续的表达式利用关系比较生成布尔值,然后对结果进行逻辑运算。 输出列表看起来象下面这个样子: -``` +``` java i = 85 j = 4 i > j is true @@ -384,7 +384,7 @@ i != j is true 操作逻辑运算符时,我们会遇到一种名为“短路”的情况。这意味着只有明确得出整个表达式真或假的结论,才会对表达式进行逻辑求值。因此,一个逻辑表达式的所有部分都有可能不进行求值: -``` +``` java //: ShortCircuit.java // Demonstrates short-circuiting behavior // with logical operators. @@ -416,13 +416,13 @@ public class ShortCircuit { 每次测试都会比较自变量,并返回真或假。它不会显示与准备调用什么有关的资料。测试在下面这个表达式中进行: -``` +``` java if(test1(0)) && test2(2) && test3(2)) ``` 很自然地,你也许认为所有这三个测试都会得以执行。但希望输出结果不至于使你大吃一惊: -``` +``` java if(test1(0) && test2(2) && test3(2)) ``` @@ -448,7 +448,7 @@ if(test1(0) && test2(2) && test3(2)) 若对char,byte或者short进行移位处理,那么在移位进行之前,它们会自动转换成一个int。只有右侧的5个低位才会用到。这样可防止我们在一个int数里移动不切实际的位数。若对一个long值进行处理,最后得到的结果也是long。此时只会用到右侧的6个低位,防止移动超过long值里现成的位数。但在进行“无符号”右移位时,也可能遇到一个问题。若对byte或short值进行右移位运算,得到的可能不是正确的结果(Java 1.0和Java 1.1特别突出)。它们会自动转换成int类型,并进行右移位。但“零扩展”不会发生,所以在那些情况下会得到-1的结果。可用下面这个例子检测自己的实现方案: -``` +``` java //: URShift.java // Test of unsigned right shift @@ -474,7 +474,7 @@ public class URShift { 下面这个例子向大家阐示了如何应用涉及“按位”操作的所有运算符,以及它们的效果: -``` +``` java //: BitManipulation.java // Using the bitwise operators import java.util.*; @@ -555,7 +555,7 @@ public class BitManipulation { 除展示所有按位运算符针对int和long的效果之外,本例也展示了int和long的最小值、最大值、+1和-1值,使大家能体会它们的情况。注意高位代表正负号:0为正,1为负。下面列出int部分的输出: -``` +``` java -1, int: -1, binary: 11111111111111111111111111111111 +1, int: 1, binary: @@ -596,7 +596,7 @@ i >>> 5, int: 1846303, binary: 这种运算符比较罕见,因为它有三个运算对象。但它确实属于运算符的一种,因为它最终也会生成一个值。这与本章后一节要讲述的普通if-else语句是不同的。表达式采取下述形式: -``` +``` java 布尔表达式 ? 值0:值1 ``` @@ -606,7 +606,7 @@ i >>> 5, int: 1846303, binary: 可将条件运算符用于自己的“副作用”,或用于它生成的值。但通常都应将其用于值,因为那样做可将运算符与if-else明确区别开。下面便是一个例子: -``` +``` java static int ternary(int i) { return i < 10 ? i * 100 : i * 10; } @@ -614,7 +614,7 @@ return i < 10 ? i * 100 : i * 10; 可以看出,假设用普通的if-else结构写上述代码,代码量会比上面多出许多。如下所示: -``` +``` java static int alternative(int i) { if (i < 10) return i * 100; @@ -630,11 +630,11 @@ return i * 10; 3.1.11 字串运算符+ -这个运算符在Java里有一项特殊用途:连接不同的字串。这一点已在前面的例子中展示过了。尽管与+的传统意义不符,但用+来做这件事情仍然是非常自然的。在C++里,这一功能看起来非常不错,所以引入了一项“运算符过载”机制,以便C++程序员为几乎所有运算符增加特殊的含义。但非常不幸,与C++的另外一些限制结合,运算符过载成为一种非常复杂的特性,程序员在设计自己的类时必须对此有周到的考虑。与C++相比,尽管运算符过载在Java里更易实现,但迄今为止仍然认为这一特性过于复杂。所以Java程序员不能象C++程序员那样设计自己的过载运算符。 +这个运算符在Java里有一项特殊用途:连接不同的字串。这一点已在前面的例子中展示过了。尽管与+的传统意义不符,但用+来做这件事情仍然是非常自然的。在C++里,这一功能看起来非常不错,所以引入了一项“运算符重载”机制,以便C++程序员为几乎所有运算符增加特殊的含义。但非常不幸,与C++的另外一些限制结合,运算符重载成为一种非常复杂的特性,程序员在设计自己的类时必须对此有周到的考虑。与C++相比,尽管运算符重载在Java里更易实现,但迄今为止仍然认为这一特性过于复杂。所以Java程序员不能象C++程序员那样设计自己的重载运算符。 我们注意到运用“String +”时一些有趣的现象。若表达式以一个String起头,那么后续所有运算对象都必须是字串。如下所示: -``` +``` java int x = 0, y = 1, z = 2; String sString = "x, y, z "; System.out.println(sString + x + y + z); @@ -643,7 +643,7 @@ System.out.println(sString + x + y + z); 在这里,Java编译程序会将x,y和z转换成它们的字串形式,而不是先把它们加到一起。然而,如果使用下述语句: -``` +``` java System.out.println(x + sString); ``` @@ -654,7 +654,7 @@ System.out.println(x + sString); 使用运算符的一个缺点是括号的运用经常容易搞错。即使对一个表达式如何计算有丝毫不确定的因素,都容易混淆括号的用法。这个问题在Java里仍然存在。 在C和C++中,一个特别常见的错误如下: -``` +``` java while(x = y) { //... } @@ -670,7 +670,7 @@ while(x = y) { 为进行一次造型,要将括号中希望的数据类型(包括所有修改符)置于其他任何值的左侧。下面是一个例子: -``` +``` java void casts() { int i = 200; long l = (long)i; @@ -688,7 +688,7 @@ Java允许我们将任何主类型“造型”为其他任何一种主类型, 最开始的时候,若在一个程序里插入“字面值”(Literal),编译器通常能准确知道要生成什么样的类型。但在有些时候,对于类型却是暧昧不清的。若发生这种情况,必须对编译器加以适当的“指导”。方法是用与字面值关联的字符形式加入一些额外的信息。下面这段代码向大家展示了这些字符。 -``` +``` java //: Literals.java class Literals { @@ -726,13 +726,13 @@ class Literals { 注意如果编译器能够正确地识别类型,就不必使用尾随字符。对于下述语句: -``` +``` java long n3 = 200; ``` 它并不存在含混不清的地方,所以200后面的一个L大可省去。然而,对于下述语句: -``` +``` java float f4 = 1e-47f; //10的幂数 ``` @@ -766,7 +766,7 @@ Java不需要sizeof()运算符来满足这方面的需要,因为所有数据 3.1.16 运算符总结 下面这个例子向大家展示了如何随同特定的运算符使用主数据类型。从根本上说,它是同一个例子反反复复地执行,只是使用了不同的主数据类型。文件编译时不会报错,因为那些会导致错误的行已用//!变成了注释内容。 -``` +``` java //: AllOps.java // Tests all the operators on all the // primitive data types to show which @@ -1181,7 +1181,7 @@ class AllOps { 注意布尔值(boolean)的能力非常有限。我们只能为其赋予true和false值。而且可测试它为真还是为假,但不可为它们再添加布尔值,或进行其他其他任何类型运算。 在char,byte和short中,我们可看到算术运算符的“转型”效果。对这些类型的任何一个进行算术运算,都会获得一个int结果。必须将其明确“造型”回原来的类型(缩小转换会造成信息的丢失),以便将值赋回那个类型。但对于int值,却不必进行造型处理,因为所有数据都已经属于int类型。然而,不要放松警惕,认为一切事情都是安全的。如果对两个足够大的int值执行乘法运算,结果值就会溢出。下面这个例子向大家展示了这一点: -``` +``` java //: Overflow.java // Surprise! Java lets you overflow. @@ -1200,7 +1200,7 @@ public class Overflow { 输出结果如下: -``` +``` java big = 2147483647 bigger = -4 ``` diff --git "a/3.2 \346\211\247\350\241\214\346\216\247\345\210\266.md" "b/3.2 \346\211\247\350\241\214\346\216\247\345\210\266.md" index 7ba6164..1c6be0f 100644 --- "a/3.2 \346\211\247\350\241\214\346\216\247\345\210\266.md" +++ "b/3.2 \346\211\247\350\241\214\346\216\247\345\210\266.md" @@ -13,14 +13,14 @@ Java使用了C的全部控制语句,所以假期您以前用C或C++编程, if-else语句或许是控制程序流程最基本的形式。其中的else是可选的,所以可按下述两种形式来使用if: -``` +``` java if(布尔表达式) 语句 ``` 或者 -``` +``` java if(布尔表达式) 语句 else @@ -31,7 +31,7 @@ else 作为if-else的一个例子,下面这个test()方法可告诉我们猜测的一个数字位于目标数字之上、之下还是相等: -``` +``` java static int test(int testval) { int result = 0; if(testval > target) @@ -50,7 +50,7 @@ static int test(int testval) { return关键字有两方面的用途:指定一个方法返回什么值(假设它没有void返回值),并立即返回那个值。可据此改写上面的test()方法,使其利用这些特点: -``` +``` java static int test2(int testval) { if(testval > target) return -1; @@ -66,7 +66,7 @@ static int test2(int testval) { while,do-while和for控制着循环,有时将其划分为“反复语句”。除非用于控制反复的布尔表达式得到“假”的结果,否则语句会重复执行下去。while循环的格式如下: -``` +``` java while(布尔表达式) 语句 ``` @@ -74,7 +74,7 @@ while(布尔表达式) 在循环刚开始时,会计算一次“布尔表达式”的值。而对于后来每一次额外的循环,都会在开始前重新计算一次。 下面这个简单的例子可产生随机数,直到符合特定的条件为止: -``` +``` java //: WhileTest.java // Demonstrates the while loop @@ -95,7 +95,7 @@ public class WhileTest { do-while的格式如下: -``` +``` java do 语句 while(布尔表达式) @@ -107,7 +107,7 @@ while和do-while唯一的区别就是do-while肯定会至少执行一次;也 for循环在第一次反复之前要进行初始化。随后,它会进行条件测试,而且在每一次反复的时候,进行某种形式的“步进”(Stepping)。for循环的形式如下: -``` +``` java for(初始表达式; 布尔表达式; 步进) 语句 ``` @@ -115,7 +115,7 @@ for(初始表达式; 布尔表达式; 步进) 无论初始表达式,布尔表达式,还是步进,都可以置空。每次反复前,都要测试一下布尔表达式。若获得的结果是false,就会继续执行紧跟在for语句后面的那行代码。在每次循环的末尾,会计算一次步进。 for循环通常用于执行“计数”任务: -``` +``` java //: ListCharacters.java // Demonstrates "for" loop by listing // all the ASCII characters. @@ -137,7 +137,7 @@ public class ListCharacters { 可在for语句里定义多个变量,但它们必须具有同样的类型: -``` +``` java for(int i = 0, j = 1; i < 10 && j != 11; i++, j++) @@ -151,7 +151,7 @@ for(int i = 0, j = 1; 早在第1章,我们已提到了逗号运算符——注意不是逗号分隔符;后者用于分隔函数的不同自变量。Java里唯一用到逗号运算符的地方就是for循环的控制表达式。在控制表达式的初始化和步进控制部分,我们可使用一系列由逗号分隔的语句。而且那些语句均会独立执行。前面的例子已运用了这种能力,下面则是另一个例子: -``` +``` java //: CommaOperator.java public class CommaOperator { @@ -166,7 +166,7 @@ public class CommaOperator { 输出如下: -``` +``` java i= 1 j= 11 i= 2 j= 4 i= 3 j= 6 @@ -180,7 +180,7 @@ i= 4 j= 8 在任何循环语句的主体部分,亦可用break和continue控制循环的流程。其中,break用于强行退出循环,不执行循环中剩余的语句。而continue则停止执行当前的反复,然后退回循环起始和,开始新的反复。 下面这个程序向大家展示了break和continue在for和while循环中的例子: -``` +``` java //: BreakAndContinue.java // Demonstrates break and continue keywords @@ -207,7 +207,7 @@ public class BreakAndContinue { 在这个for循环中,i的值永远不会到达100。因为一旦i到达74,break语句就会中断循环。通常,只有在不知道中断条件何时满足时,才需象这样使用break。只要i不能被9整除,continue语句会使程序流程返回循环的最开头执行(所以使i值递增)。如果能够整除,则将值显示出来。 第二部分向大家揭示了一个“无限循环”的情况。然而,循环内部有一个break语句,可中止循环。除此以外,大家还会看到continue移回循环顶部,同时不完成剩余的内容(所以只有在i值能被9整除时才打印出值)。输出结果如下: -``` +``` java 0 9 18 @@ -237,13 +237,13 @@ goto关键字很早就在程序设计语言中出现。事实上,goto是汇编 “标签”是后面跟一个冒号的标识符,就象下面这样: -``` +``` java label1: ``` 对Java来说,唯一用到标签的地方是在循环语句之前。进一步说,它实际需要紧靠在循环语句的前方——在标签和循环之间置入任何语句都是不明智的。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另一个循环或者一个开关。这是由于break和continue关键字通常只中断当前循环,但若随同标签使用,它们就会中断到存在标签的地方。如下所示: -``` +``` java label1: 外部循环{ 内部循环{ @@ -263,7 +263,7 @@ break label1; //4 下面是for循环的一个例子: -``` +``` java //: LabeledFor.java // Java’s "labeled for loop" @@ -318,7 +318,7 @@ public class LabeledFor { 下面是输出结果: -``` +``` java i = 0 continue inner i = 1 @@ -345,7 +345,7 @@ break outer 下面这个例子向大家展示了带标签的break以及continue语句在while循环中的用法: -``` +``` java //: LabeledWhile.java // Java's "labeled while" loop @@ -395,7 +395,7 @@ public class LabeledWhile { 这个方法的输出结果是一目了然的: -``` +``` java Outer while loop i = 1 continue @@ -420,7 +420,7 @@ break outer “开关”(Switch)有时也被划分为一种“选择语句”。根据一个整数表达式的值,switch语句可从一系列代码选出一段执行。它的格式如下: -``` +``` java switch(整数选择因子) { case 整数值1 : 语句; break; case 整数值2 : 语句; break; @@ -439,7 +439,7 @@ default:语句; switch语句是实现多路选择的一种易行方式(比如从一系列执行路径中挑选一个)。但它要求使用一个选择因子,并且必须是int或char那样的整数值。例如,假若将一个字串或者浮点数作为选择因子使用,那么它们在switch语句里是不会工作的。对于非整数类型,则必须使用一系列if语句。 下面这个例子可随机生成字母,并判断它们是元音还是辅音字母: -``` +``` java //: VowelsAndConsonants.java // Demonstrates the switch statement @@ -478,7 +478,7 @@ public class VowelsAndConsonants { 1. 具体的计算 应特别留意下面这个语句: -``` +``` java char c = (char)(Math.random() * 26 + 'a'); ``` @@ -486,7 +486,7 @@ Math.random()会产生一个double值,所以26会转换成double类型,以 我们的第一个问题是,造型会对char作什么样的处理呢?换言之,假设一个值是29.7,我们把它造型成一个char,那么结果值到底是30还是29呢?答案可从下面这个例子中得到: -``` +``` java //: CastingNumbers.java // What happens when you cast a float or double // to an integral value? @@ -514,7 +514,7 @@ public class CastingNumbers { 输出结果如下: -``` +``` java above: 0.7 below: 0.4 (int)above: 0 @@ -527,7 +527,7 @@ below: 0.4 第二个问题与Math.random()有关。它会产生0和1之间的值,但是否包括值'1'呢?用正统的数学语言表达,它到底是(0,1),[0,1],(0,1],还是[0,1)呢(方括号表示“包括”,圆括号表示“不包括”)?同样地,一个示范程序向我们揭示了答案: -``` +``` java //: RandomBounds.java // Does Math.random() produce 0.0 and 1.0? @@ -558,13 +558,13 @@ public class RandomBounds { 为运行这个程序,只需在命令行键入下述命令即可: -``` +``` java java RandomBounds lower ``` 或 -``` +``` java java RandomBounds upper ``` diff --git "a/4.1 \347\224\250\346\236\204\345\273\272\345\231\250\350\207\252\345\212\250\345\210\235\345\247\213\345\214\226.md" "b/4.1 \347\224\250\346\236\204\351\200\240\345\231\250\350\207\252\345\212\250\345\210\235\345\247\213\345\214\226.md" similarity index 67% rename from "4.1 \347\224\250\346\236\204\345\273\272\345\231\250\350\207\252\345\212\250\345\210\235\345\247\213\345\214\226.md" rename to "4.1 \347\224\250\346\236\204\351\200\240\345\231\250\350\207\252\345\212\250\345\210\235\345\247\213\345\214\226.md" index b2b04be..99540ab 100644 --- "a/4.1 \347\224\250\346\236\204\345\273\272\345\231\250\350\207\252\345\212\250\345\210\235\345\247\213\345\214\226.md" +++ "b/4.1 \347\224\250\346\236\204\351\200\240\345\231\250\350\207\252\345\212\250\345\210\235\345\247\213\345\214\226.md" @@ -1,13 +1,13 @@ -# 4.1 用构建器自动初始化 +# 4.1 用构造器自动初始化 -对于方法的创建,可将其想象成为自己写的每个类都调用一次initialize()。这个名字提醒我们在使用对象之前,应首先进行这样的调用。但不幸的是,这也意味着用户必须记住调用方法。在Java中,由于提供了名为“构建器”的一种特殊方法,所以类的设计者可担保每个对象都会得到正确的初始化。若某个类有一个构建器,那么在创建对象时,Java会自动调用那个构建器——甚至在用户毫不知觉的情况下。所以说这是可以担保的! +对于方法的创建,可将其想象成为自己写的每个类都调用一次initialize()。这个名字提醒我们在使用对象之前,应首先进行这样的调用。但不幸的是,这也意味着用户必须记住调用方法。在Java中,由于提供了名为“构造器”的一种特殊方法,所以类的设计者可担保每个对象都会得到正确的初始化。若某个类有一个构造器,那么在创建对象时,Java会自动调用那个构造器——甚至在用户毫不知觉的情况下。所以说这是可以担保的! -接着的一个问题是如何命名这个方法。存在两方面的问题。第一个是我们使用的任何名字都可能与打算为某个类成员使用的名字冲突。第二是由于编译器的责任是调用构建器,所以它必须知道要调用是哪个方法。C++采取的方案看来是最简单的,且更有逻辑性,所以也在Java里得到了应用:构建器的名字与类名相同。这样一来,可保证象这样的一个方法会在初始化期间自动调用。 +接着的一个问题是如何命名这个方法。存在两方面的问题。第一个是我们使用的任何名字都可能与打算为某个类成员使用的名字冲突。第二是由于编译器的责任是调用构造器,所以它必须知道要调用是哪个方法。C++采取的方案看来是最简单的,且更有逻辑性,所以也在Java里得到了应用:构造器的名字与类名相同。这样一来,可保证象这样的一个方法会在初始化期间自动调用。 -下面是带有构建器的一个简单的类(若执行这个程序有问题,请参考第3章的“赋值”小节)。 +下面是带有构造器的一个简单的类(若执行这个程序有问题,请参考第3章的“赋值”小节)。 -``` +``` java //: SimpleConstructor.java // Demonstration of a simple constructor package c04; @@ -28,15 +28,15 @@ public class SimpleConstructor { 现在,一旦创建一个对象: -``` +``` java new Rock(); ``` -就会分配相应的存储空间,并调用构建器。这样可保证在我们经手之前,对象得到正确的初始化。 -请注意所有方法首字母小写的编码规则并不适用于构建器。这是由于构建器的名字必须与类名完全相同! -和其他任何方法一样,构建器也能使用自变量,以便我们指定对象的具体创建方式。可非常方便地改动上述例子,以便构建器使用自己的自变量。如下所示: +就会分配相应的存储空间,并调用构造器。这样可保证在我们经手之前,对象得到正确的初始化。 +请注意所有方法首字母小写的编码规则并不适用于构造器。这是由于构造器的名字必须与类名完全相同! +和其他任何方法一样,构造器也能使用自变量,以便我们指定对象的具体创建方式。可非常方便地改动上述例子,以便构造器使用自己的自变量。如下所示: -``` +``` java class Rock { Rock(int i) { System.out.println( @@ -53,13 +53,13 @@ public class SimpleConstructor { ``` -利用构建器的自变量,我们可为一个对象的初始化设定相应的参数。举个例子来说,假设类Tree有一个构建器,它用一个整数自变量标记树的高度,那么就可以象下面这样创建一个Tree对象: +利用构造器的自变量,我们可为一个对象的初始化设定相应的参数。举个例子来说,假设类Tree有一个构造器,它用一个整数自变量标记树的高度,那么就可以象下面这样创建一个Tree对象: -``` +``` java tree t = new Tree(12); // 12英尺高的树 ``` -若Tree(int)是我们唯一的构建器,那么编译器不会允许我们以其他任何方式创建一个Tree对象。 +若Tree(int)是我们唯一的构造器,那么编译器不会允许我们以其他任何方式创建一个Tree对象。 -构建器有助于消除大量涉及类的问题,并使代码更易阅读。例如在前述的代码段中,我们并未看到对initialize()方法的明确调用——那些方法在概念上独立于定义内容。在Java中,定义和初始化属于统一的概念——两者缺一不可。 -构建器属于一种较特殊的方法类型,因为它没有返回值。这与void返回值存在着明显的区别。对于void返回值,尽管方法本身不会自动返回什么,但仍然可以让它返回另一些东西。构建器则不同,它不仅什么也不会自动返回,而且根本不能有任何选择。若存在一个返回值,而且假设我们可以自行选择返回内容,那么编译器多少要知道如何对那个返回值作什么样的处理。 +构造器有助于消除大量涉及类的问题,并使代码更易阅读。例如在前述的代码段中,我们并未看到对initialize()方法的明确调用——那些方法在概念上独立于定义内容。在Java中,定义和初始化属于统一的概念——两者缺一不可。 +构造器属于一种较特殊的方法类型,因为它没有返回值。这与void返回值存在着明显的区别。对于void返回值,尽管方法本身不会自动返回什么,但仍然可以让它返回另一些东西。构造器则不同,它不仅什么也不会自动返回,而且根本不能有任何选择。若存在一个返回值,而且假设我们可以自行选择返回内容,那么编译器多少要知道如何对那个返回值作什么样的处理。 diff --git "a/4.2 \346\226\271\346\263\225\350\277\207\350\275\275.md" "b/4.2 \346\226\271\346\263\225\351\207\215\350\275\275.md" similarity index 82% rename from "4.2 \346\226\271\346\263\225\350\277\207\350\275\275.md" rename to "4.2 \346\226\271\346\263\225\351\207\215\350\275\275.md" index ad01df2..f8bdc0d 100644 --- "a/4.2 \346\226\271\346\263\225\350\277\207\350\275\275.md" +++ "b/4.2 \346\226\271\346\263\225\351\207\215\350\275\275.md" @@ -1,19 +1,19 @@ -# 4.2 方法过载 +# 4.2 方法重载 在任何程序设计语言中,一项重要的特性就是名字的运用。我们创建一个对象时,会分配到一个保存区域的名字。方法名代表的是一种具体的行动。通过用名字描述自己的系统,可使自己的程序更易人们理解和修改。它非常象写散文——目的是与读者沟通。 我们用名字引用或描述所有对象与方法。若名字选得好,可使自己及其他人更易理解自己的代码。 -将人类语言中存在细致差别的概念“映射”到一种程序设计语言中时,会出现一些特殊的问题。在日常生活中,我们用相同的词表达多种不同的含义——即词的“过载”。我们说“洗衬衫”、“洗车”以及“洗狗”。但若强制象下面这样说,就显得很愚蠢:“衬衫洗 衬衫”、“车洗 车”以及“狗洗 狗”。这是由于听众根本不需要对执行的行动作任何明确的区分。人类的大多数语言都具有很强的“冗余”性,所以即使漏掉了几个词,仍然可以推断出含义。我们不需要独一无二的标识符——可从具体的语境中推论出含义。 +将人类语言中存在细致差别的概念“映射”到一种程序设计语言中时,会出现一些特殊的问题。在日常生活中,我们用相同的词表达多种不同的含义——即词的“重载”。我们说“洗衬衫”、“洗车”以及“洗狗”。但若强制象下面这样说,就显得很愚蠢:“衬衫洗 衬衫”、“车洗 车”以及“狗洗 狗”。这是由于听众根本不需要对执行的行动作任何明确的区分。人类的大多数语言都具有很强的“冗余”性,所以即使漏掉了几个词,仍然可以推断出含义。我们不需要独一无二的标识符——可从具体的语境中推论出含义。 大多数程序设计语言(特别是C)要求我们为每个函数都设定一个独一无二的标识符。所以绝对不能用一个名为print()的函数来显示整数,再用另一个print()显示浮点数——每个函数都要求具备唯一的名字。 -在Java里,另一项因素强迫方法名出现过载情况:构建器。由于构建器的名字由类名决定,所以只能有一个构建器名称。但假若我们想用多种方式创建一个对象呢?例如,假设我们想创建一个类,令其用标准方式进行初始化,另外从文件里读取信息来初始化。此时,我们需要两个构建器,一个没有自变量(默认构建器),另一个将字串作为自变量——用于初始化对象的那个文件的名字。由于都是构建器,所以它们必须有相同的名字,亦即类名。所以为了让相同的方法名伴随不同的自变量类型使用,“方法过载”是非常关键的一项措施。同时,尽管方法过载是构建器必需的,但它亦可应用于其他任何方法,且用法非常方便。 +在Java里,另一项因素强迫方法名出现重载情况:构造器。由于构造器的名字由类名决定,所以只能有一个构造器名称。但假若我们想用多种方式创建一个对象呢?例如,假设我们想创建一个类,令其用标准方式进行初始化,另外从文件里读取信息来初始化。此时,我们需要两个构造器,一个没有自变量(默认构造器),另一个将字串作为自变量——用于初始化对象的那个文件的名字。由于都是构造器,所以它们必须有相同的名字,亦即类名。所以为了让相同的方法名伴随不同的自变量类型使用,“方法重载”是非常关键的一项措施。同时,尽管方法重载是构造器必需的,但它亦可应用于其他任何方法,且用法非常方便。 -在下面这个例子里,我们向大家同时展示了过载构建器和过载的原始方法: +在下面这个例子里,我们向大家同时展示了重载构造器和重载的原始方法: -``` +``` java //: Overloading.java // Demonstration of both constructor // and ordinary method overloading. @@ -56,19 +56,19 @@ public class Overloading { } ///:~ ``` -Tree既可创建成一颗种子,不含任何自变量;亦可创建成生长在苗圃中的植物。为支持这种创建,共使用了两个构建器,一个没有自变量(我们把没有自变量的构建器称作“默认构建器”,注释①),另一个采用现成的高度。 +Tree既可创建成一颗种子,不含任何自变量;亦可创建成生长在苗圃中的植物。为支持这种创建,共使用了两个构造器,一个没有自变量(我们把没有自变量的构造器称作“默认构造器”,注释①),另一个采用现成的高度。 -①:在Sun公司出版的一些Java资料中,用简陋但很说明问题的词语称呼这类构建器——“无参数构建器”(no-arg constructors)。但“默认构建器”这个称呼已使用了许多年,所以我选择了它。 +①:在Sun公司出版的一些Java资料中,用简陋但很说明问题的词语称呼这类构造器——“无参数构造器”(no-arg constructors)。但“默认构造器”这个称呼已使用了许多年,所以我选择了它。 -我们也有可能希望通过多种途径调用info()方法。例如,假设我们有一条额外的消息想显示出来,就使用String自变量;而假设没有其他话可说,就不使用。由于为显然相同的概念赋予了两个独立的名字,所以看起来可能有些古怪。幸运的是,方法过载允许我们为两者使用相同的名字。 +我们也有可能希望通过多种途径调用info()方法。例如,假设我们有一条额外的消息想显示出来,就使用String自变量;而假设没有其他话可说,就不使用。由于为显然相同的概念赋予了两个独立的名字,所以看起来可能有些古怪。幸运的是,方法重载允许我们为两者使用相同的名字。 -4.2.1 区分过载方法 +4.2.1 区分重载方法 -若方法有同样的名字,Java怎样知道我们指的哪一个方法呢?这里有一个简单的规则:每个过载的方法都必须采取独一无二的自变量类型列表。 +若方法有同样的名字,Java怎样知道我们指的哪一个方法呢?这里有一个简单的规则:每个重载的方法都必须采取独一无二的自变量类型列表。 若稍微思考几秒钟,就会想到这样一个问题:除根据自变量的类型,程序员如何区分两个同名方法的差异呢? 即使自变量的顺序也足够我们区分两个方法(尽管我们通常不愿意采用这种方法,因为它会产生难以维护的代码): -``` +``` java //: OverloadingOrder.java // Overloading based on the order of // the arguments. @@ -93,11 +93,11 @@ public class OverloadingOrder { 两个print()方法有完全一致的自变量,但顺序不同,可据此区分它们。 -4.2.2 主类型的过载 +4.2.2 主类型的重载 -主(数据)类型能从一个“较小”的类型自动转变成一个“较大”的类型。涉及过载问题时,这会稍微造成一些混乱。下面这个例子揭示了将主类型传递给过载的方法时发生的情况: +主(数据)类型能从一个“较小”的类型自动转变成一个“较大”的类型。涉及重载问题时,这会稍微造成一些混乱。下面这个例子揭示了将主类型传递给重载的方法时发生的情况: -``` +``` java //: PrimitiveOverloading.java // Promotion of primitives and overloading @@ -196,10 +196,10 @@ public class PrimitiveOverloading { } ///:~ ``` -若观察这个程序的输出,就会发现常数值5被当作一个int值处理。所以假若可以使用一个过载的方法,就能获取它使用的int值。在其他所有情况下,若我们的数据类型“小于”方法中使用的自变量,就会对那种数据类型进行“转型”处理。char获得的效果稍有些不同,这是由于假期它没有发现一个准确的char匹配,就会转型为int。 -若我们的自变量“大于”过载方法期望的自变量,这时又会出现什么情况呢?对前述程序的一个修改揭示出了答案: +若观察这个程序的输出,就会发现常数值5被当作一个int值处理。所以假若可以使用一个重载的方法,就能获取它使用的int值。在其他所有情况下,若我们的数据类型“小于”方法中使用的自变量,就会对那种数据类型进行“转型”处理。char获得的效果稍有些不同,这是由于假期它没有发现一个准确的char匹配,就会转型为int。 +若我们的自变量“大于”重载方法期望的自变量,这时又会出现什么情况呢?对前述程序的一个修改揭示出了答案: -``` +``` java //: Demotion.java // Demotion of primitives and overloading @@ -260,28 +260,28 @@ public class Demotion { 大家可注意到这是一种“缩小转换”。也就是说,在造型或转型过程中可能丢失一些信息。这正是编译器强迫我们明确定义的原因——我们需明确表达想要转型的愿望。 -4.2.3 返回值过载 +4.2.3 返回值重载 我们很易对下面这些问题感到迷惑:为什么只有类名和方法自变量列出?为什么不根据返回值对方法加以区分?比如对下面这两个方法来说,虽然它们有同样的名字和自变量,但其实是很容易区分的: -``` +``` java void f() {} int f() {} ``` 若编译器可根据上下文(语境)明确判断出含义,比如在int x=f()中,那么这样做完全没有问题。然而,我们也可能调用一个方法,同时忽略返回值;我们通常把这称为“为它的副作用去调用一个方法”,因为我们关心的不是返回值,而是方法调用的其他效果。所以假如我们象下面这样调用方法: -``` +``` java f(); ``` -Java怎样判断f()的具体调用方式呢?而且别人如何识别并理解代码呢?由于存在这一类的问题,所以不能根据返回值类型来区分过载的方法。 +Java怎样判断f()的具体调用方式呢?而且别人如何识别并理解代码呢?由于存在这一类的问题,所以不能根据返回值类型来区分重载的方法。 -4.2.4 默认构建器 +4.2.4 默认构造器 -正如早先指出的那样,默认构建器是没有自变量的。它们的作用是创建一个“空对象”。若创建一个没有构建器的类,则编译程序会帮我们自动创建一个默认构建器。例如: +正如早先指出的那样,默认构造器是没有自变量的。它们的作用是创建一个“空对象”。若创建一个没有构造器的类,则编译程序会帮我们自动创建一个默认构造器。例如: -``` +``` java //: DefaultConstructor.java class Bird { @@ -297,13 +297,13 @@ public class DefaultConstructor { 对于下面这一行: -``` +``` java new Bird(); ``` -它的作用是新建一个对象,并调用默认构建器——即使尚未明确定义一个象这样的构建器。若没有它,就没有方法可以调用,无法构建我们的对象。然而,如果已经定义了一个构建器(无论是否有自变量),编译程序都不会帮我们自动合成一个: +它的作用是新建一个对象,并调用默认构造器——即使尚未明确定义一个象这样的构造器。若没有它,就没有方法可以调用,无法构建我们的对象。然而,如果已经定义了一个构造器(无论是否有自变量),编译程序都不会帮我们自动合成一个: -``` +``` java class Bush { Bush(int i) {} Bush(double d) {} @@ -312,17 +312,17 @@ Bush(double d) {} 现在,假若使用下述代码: -``` +``` java new Bush(); ``` -编译程序就会报告自己找不到一个相符的构建器。就好象我们没有设置任何构建器,编译程序会说:“你看来似乎需要一个构建器,所以让我们给你制造一个吧。”但假如我们写了一个构建器,编译程序就会说:“啊,你已写了一个构建器,所以我知道你想干什么;如果你不放置一个默认的,是由于你打算省略它。” +编译程序就会报告自己找不到一个相符的构造器。就好象我们没有设置任何构造器,编译程序会说:“你看来似乎需要一个构造器,所以让我们给你制造一个吧。”但假如我们写了一个构造器,编译程序就会说:“啊,你已写了一个构造器,所以我知道你想干什么;如果你不放置一个默认的,是由于你打算省略它。” 4.2.5 this关键字 如果有两个同类型的对象,分别叫作a和b,那么您也许不知道如何为这两个对象同时调用一个f()方法: -``` +``` java class Banana { void f(int i) { /* ... */ } } Banana a = new Banana(), b = new Banana(); a.f(1); @@ -331,27 +331,27 @@ b.f(2); 若只有一个名叫f()的方法,它怎样才能知道自己是为a还是为b调用的呢? -为了能用简便的、面向对象的语法来书写代码——亦即“将消息发给对象”,编译器为我们完成了一些幕后工作。其中的秘密就是第一个自变量传递给方法f(),而且那个自变量是准备操作的那个对象的句柄。所以前述的两个方法调用就变成了下面这样的形式: +为了能用简便的、面向对象的语法来书写代码——亦即“将消息发给对象”,编译器为我们完成了一些幕后工作。其中的秘密就是第一个自变量传递给方法f(),而且那个自变量是准备操作的那个对象的指针。所以前述的两个方法调用就变成了下面这样的形式: -``` +``` java Banana.f(a,1); Banana.f(b,2); ``` 这是内部的表达形式,我们并不能这样书写表达式,并试图让编译器接受它。但是,通过它可理解幕后到底发生了什么事情。 -假定我们在一个方法的内部,并希望获得当前对象的句柄。由于那个句柄是由编译器“秘密”传递的,所以没有标识符可用。然而,针对这一目的有个专用的关键字:this。this关键字(注意只能在方法内部使用)可为已调用了其方法的那个对象生成相应的句柄。可象对待其他任何对象句柄一样对待这个句柄。但要注意,假若准备从自己某个类的另一个方法内部调用一个类方法,就不必使用this。只需简单地调用那个方法即可。当前的this句柄会自动应用于其他方法。所以我们能使用下面这样的代码: +假定我们在一个方法的内部,并希望获得当前对象的指针。由于那个指针是由编译器“秘密”传递的,所以没有标识符可用。然而,针对这一目的有个专用的关键字:this。this关键字(注意只能在方法内部使用)可为已调用了其方法的那个对象生成相应的指针。可象对待其他任何对象指针一样对待这个指针。但要注意,假若准备从自己某个类的另一个方法内部调用一个类方法,就不必使用this。只需简单地调用那个方法即可。当前的this指针会自动应用于其他方法。所以我们能使用下面这样的代码: -``` +``` java class Apricot { void pick() { /* ... */ } void pit() { pick(); /* ... */ } } ``` -在pit()内部,我们可以说this.pick(),但事实上无此必要。编译器能帮我们自动完成。this关键字只能用于那些特殊的类——需明确使用当前对象的句柄。例如,假若您希望将句柄返回给当前对象,那么它经常在return语句中使用。 +在pit()内部,我们可以说this.pick(),但事实上无此必要。编译器能帮我们自动完成。this关键字只能用于那些特殊的类——需明确使用当前对象的指针。例如,假若您希望将指针返回给当前对象,那么它经常在return语句中使用。 -``` +``` java //: Leaf.java // Simple use of the "this" keyword @@ -371,15 +371,15 @@ public class Leaf { } ///:~ ``` -由于increment()通过this关键字返回当前对象的句柄,所以可以方便地对同一个对象执行多项操作。 +由于increment()通过this关键字返回当前对象的指针,所以可以方便地对同一个对象执行多项操作。 -1. 在构建器里调用构建器 +1. 在构造器里调用构造器 -若为一个类写了多个构建器,那么经常都需要在一个构建器里调用另一个构建器,以避免写重复的代码。可用this关键字做到这一点。 +若为一个类写了多个构造器,那么经常都需要在一个构造器里调用另一个构造器,以避免写重复的代码。可用this关键字做到这一点。 -通常,当我们说this的时候,都是指“这个对象”或者“当前对象”。而且它本身会产生当前对象的一个句柄。在一个构建器中,若为其赋予一个自变量列表,那么this关键字会具有不同的含义:它会对与那个自变量列表相符的构建器进行明确的调用。这样一来,我们就可通过一条直接的途径来调用其他构建器。如下所示: +通常,当我们说this的时候,都是指“这个对象”或者“当前对象”。而且它本身会产生当前对象的一个指针。在一个构造器中,若为其赋予一个自变量列表,那么this关键字会具有不同的含义:它会对与那个自变量列表相符的构造器进行明确的调用。这样一来,我们就可通过一条直接的途径来调用其他构造器。如下所示: -``` +``` java //: Flower.java // Calling constructors with "this" @@ -420,16 +420,16 @@ public class Flower { } ///:~ ``` -其中,构建器Flower(String s,int petals)向我们揭示出这样一个问题:尽管可用this调用一个构建器,但不可调用两个。除此以外,构建器调用必须是我们做的第一件事情,否则会收到编译程序的报错信息。 +其中,构造器Flower(String s,int petals)向我们揭示出这样一个问题:尽管可用this调用一个构造器,但不可调用两个。除此以外,构造器调用必须是我们做的第一件事情,否则会收到编译程序的报错信息。 这个例子也向大家展示了this的另一项用途。由于自变量s的名字以及成员数据s的名字是相同的,所以会出现混淆。为解决这个问题,可用this.s来引用成员数据。经常都会在Java代码里看到这种形式的应用,本书的大量地方也采用了这种做法。 -在print()中,我们发现编译器不让我们从除了一个构建器之外的其他任何方法内部调用一个构建器。 +在print()中,我们发现编译器不让我们从除了一个构造器之外的其他任何方法内部调用一个构造器。 2. static的含义 理解了this关键字后,我们可更完整地理解static(静态)方法的含义。它意味着一个特定的方法没有this。我们不可从一个static方法内部发出对非static方法的调用(注释②),尽管反过来说是可以的。而且在没有任何对象的前提下,我们可针对类本身发出对一个static方法的调用。事实上,那正是static方法最基本的意义。它就好象我们创建一个全局函数的等价物(在C语言中)。除了全局函数不允许在Java中使用以外,若将一个static方法置入一个类的内部,它就可以访问其他static方法以及static字段。 -②:有可能发出这类调用的一种情况是我们将一个对象句柄传到static方法内部。随后,通过句柄(此时实际是this),我们可调用非static方法,并访问非static字段。但一般地,如果真的想要这样做,只要制作一个普通的、非static方法即可。 +②:有可能发出这类调用的一种情况是我们将一个对象指针传到static方法内部。随后,通过指针(此时实际是this),我们可调用非static方法,并访问非static字段。但一般地,如果真的想要这样做,只要制作一个普通的、非static方法即可。 有些人抱怨static方法并不是“面向对象”的,因为它们具有全局函数的某些特点;利用static方法,我们不必向对象发送一条消息,因为不存在this。这可能是一个清楚的自变量,若您发现自己使用了大量静态方法,就应重新思考自己的策略。然而,static的概念是非常实用的,许多时候都需要用到它。所以至于它们是否真的“面向对象”,应该留给理论家去讨论。事实上,即使Smalltalk在自己的“类方法”里也有类似于static的东西。 diff --git "a/4.3 \346\270\205\351\231\244\357\274\232\346\224\266\345\260\276\345\222\214\345\236\203\345\234\276\346\224\266\351\233\206.md" "b/4.3 \346\270\205\351\231\244\357\274\232\346\224\266\345\260\276\345\222\214\345\236\203\345\234\276\346\224\266\351\233\206.md" index 867ba60..6590b11 100644 --- "a/4.3 \346\270\205\351\231\244\357\274\232\346\224\266\345\260\276\345\222\214\345\236\203\345\234\276\346\224\266\351\233\206.md" +++ "b/4.3 \346\270\205\351\231\244\357\274\232\346\224\266\345\260\276\345\222\214\345\236\203\345\234\276\346\224\266\351\233\206.md" @@ -30,13 +30,13 @@ 4.3.2 必须执行清除 -为清除一个对象,那个对象的用户必须在希望进行清除的地点调用一个清除方法。这听起来似乎很容易做到,但却与C++“破坏器”的概念稍有抵触。在C++中,所有对象都会破坏(清除)。或者换句话说,所有对象都“应该”破坏。若将C++对象创建成一个本地对象,比如在堆栈中创建(在Java中是不可能的),那么清除或破坏工作就会在“结束花括号”所代表的、创建这个对象的作用域的末尾进行。若对象是用new创建的(类似于Java),那么当程序员调用C++的delete命令时(Java没有这个命令),就会调用相应的破坏器。若程序员忘记了,那么永远不会调用破坏器,我们最终得到的将是一个内存“漏洞”,另外还包括对象的其他部分永远不会得到清除。 +为清除一个对象,那个对象的用户必须在希望进行清除的地点调用一个清除方法。这听起来似乎很容易做到,但却与C++“破坏器”的概念稍有抵触。在C++中,所有对象都会破坏(清除)。或者换句话说,所有对象都“应该”破坏。若将C++对象创建成一个本地对象,比如在栈中创建(在Java中是不可能的),那么清除或破坏工作就会在“结束花括号”所代表的、创建这个对象的作用域的末尾进行。若对象是用new创建的(类似于Java),那么当程序员调用C++的delete命令时(Java没有这个命令),就会调用相应的破坏器。若程序员忘记了,那么永远不会调用破坏器,我们最终得到的将是一个内存“漏洞”,另外还包括对象的其他部分永远不会得到清除。 相反,Java不允许我们创建本地(局部)对象——无论如何都要使用new。但在Java中,没有“delete”命令来释放对象,因为垃圾收集器会帮助我们自动释放存储空间。所以如果站在比较简化的立场,我们可以说正是由于存在垃圾收集机制,所以Java没有破坏器。然而,随着以后学习的深入,就会知道垃圾收集器的存在并不能完全消除对破坏器的需要,或者说不能消除对破坏器代表的那种机制的需要(而且绝对不能直接调用finalize(),所以应尽量避免用它)。若希望执行除释放存储空间之外的其他某种形式的清除工作,仍然必须调用Java中的一个方法。它等价于C++的破坏器,只是没后者方便。 finalize()最有用处的地方之一是观察垃圾收集的过程。下面这个例子向大家展示了垃圾收集所经历的过程,并对前面的陈述进行了总结。 -``` +``` java //: Garbage.java // Demonstration of the garbage // collector and finalization @@ -107,7 +107,7 @@ public class Garbage { 所有这些都在main()的内部进行——在下面这个循环里: -``` +``` java while(!Chair.f) { new Chair(); new String("To take up space"); @@ -124,7 +124,7 @@ new String("To take up space"); 前面的程序向我们揭示出:在Java 1.1中,收尾模块肯定会运行这一许诺已成为现实——但前提是我们明确地强制它采取这一操作。若使用一个不是“before”或“after”的自变量(如“none”),那么两个收尾工作都不会进行,而且我们会得到象下面这样的输出: -``` +``` java Created 47 Created 47 diff --git "a/4.4 \346\210\220\345\221\230\345\210\235\345\247\213\345\214\226.md" "b/4.4 \346\210\220\345\221\230\345\210\235\345\247\213\345\214\226.md" index 30649e2..5d2bf19 100644 --- "a/4.4 \346\210\220\345\221\230\345\210\235\345\247\213\345\214\226.md" +++ "b/4.4 \346\210\220\345\221\230\345\210\235\345\247\213\345\214\226.md" @@ -2,7 +2,7 @@ Java尽自己的全力保证所有变量都能在使用前得到正确的初始化。若被定义成相对于一个方法的“局部”变量,这一保证就通过编译期的出错提示表现出来。因此,如果使用下述代码: -``` +``` java void f() { int i; i++; @@ -13,7 +13,7 @@ i++; 然而,若将基本类型(主类型)设为一个类的数据成员,情况就会变得稍微有些不同。由于任何方法都可以初始化或使用那个数据,所以在正式使用数据前,若还是强迫程序员将其初始化成一个适当的值,就可能不是一种实际的做法。然而,若为其赋予一个垃圾值,同样是非常不安全的。因此,一个类的所有基本类型数据成员都会保证获得一个初始值。可用下面这段小程序看到这些值: -``` +``` java //: InitialValues.java // Shows default initial values @@ -53,7 +53,7 @@ public class InitialValues { 输入结果如下: -``` +``` java Data type Inital value boolean false char @@ -67,13 +67,13 @@ double 0.0 其中,Char值为空(NULL),没有数据打印出来。 -稍后大家就会看到:在一个类的内部定义一个对象句柄时,如果不将其初始化成新对象,那个句柄就会获得一个空值。 +稍后大家就会看到:在一个类的内部定义一个对象指针时,如果不将其初始化成新对象,那个指针就会获得一个空值。 4.4.1 规定初始化 如果想自己为变量赋予一个初始值,又会发生什么情况呢?为达到这个目的,一个最直接的做法是在类内部定义变量的同时也为其赋值(注意在C++里不能这样做,尽管C++的新手们总“想”这样做)。在下面,Measurement类内部的字段定义已发生了变化,提供了初始值: -``` +``` java class Measurement { boolean b = true; char c = 'x'; @@ -88,17 +88,17 @@ class Measurement { 亦可用相同的方法初始化非基本(主)类型的对象。若Depth是一个类,那么可象下面这样插入一个变量并进行初始化: -``` +``` java class Measurement { Depth o = new Depth(); boolean b = true; // . . . ``` -若尚未为o指定一个初始值,同时不顾一切地提前试用它,就会得到一条运行期错误提示,告诉你产生了名为“违例”(Exception)的一个错误(在第9章详述)。 +若尚未为o指定一个初始值,同时不顾一切地提前试用它,就会得到一条运行期错误提示,告诉你产生了名为“异常”(Exception)的一个错误(在第9章详述)。 甚至可通过调用一个方法来提供初始值: -``` +``` java class CInit { int i = f(); //... @@ -107,7 +107,7 @@ int i = f(); 当然,这个方法亦可使用自变量,但那些自变量不可是尚未初始化的其他类成员。因此,下面这样做是合法的: -``` +``` java class CInit { int i = f(); int j = g(i); @@ -117,7 +117,7 @@ int j = g(i); 但下面这样做是非法的: -``` +``` java class CInit { int j = g(i); int i = f(); @@ -128,26 +128,26 @@ int i = f(); 这正是编译器对“向前引用”感到不适应的一个地方,因为它与初始化的顺序有关,而不是与程序的编译方式有关。 这种初始化方法非常简单和直观。它的一个限制是类型Measurement的每个对象都会获得相同的初始化值。有时,这正是我们希望的结果,但有时却需要盼望更大的灵活性。 -4.4.2 构建器初始化 +4.4.2 构造器初始化 -可考虑用构建器执行初始化进程。这样便可在编程时获得更大的灵活程度,因为我们可以在运行期调用方法和采取行动,从而“现场”决定初始化值。但要注意这样一件事情:不可妨碍自动初始化的进行,它在构建器进入之前就会发生。因此,假如使用下述代码: +可考虑用构造器执行初始化进程。这样便可在编程时获得更大的灵活程度,因为我们可以在运行期调用方法和采取行动,从而“现场”决定初始化值。但要注意这样一件事情:不可妨碍自动初始化的进行,它在构造器进入之前就会发生。因此,假如使用下述代码: -``` +``` java class Counter { int i; Counter() { i = 7; } // . . . ``` -那么i首先会初始化成零,然后变成7。对于所有基本类型以及对象句柄,这种情况都是成立的,其中包括在定义时已进行了明确初始化的那些一些。考虑到这个原因,编译器不会试着强迫我们在构建器任何特定的场所对元素进行初始化,或者在它们使用之前——初始化早已得到了保证(注释⑤)。 +那么i首先会初始化成零,然后变成7。对于所有基本类型以及对象指针,这种情况都是成立的,其中包括在定义时已进行了明确初始化的那些一些。考虑到这个原因,编译器不会试着强迫我们在构造器任何特定的场所对元素进行初始化,或者在它们使用之前——初始化早已得到了保证(注释⑤)。 -⑤:相反,C++有自己的“构建器初始模块列表”,能在进入构建器主体之前进行初始化,而且它对于对象来说是强制进行的。参见《Thinking in C++》。 +⑤:相反,C++有自己的“构造器初始模块列表”,能在进入构造器主体之前进行初始化,而且它对于对象来说是强制进行的。参见《Thinking in C++》。 1. 初始化顺序 -在一个类里,初始化的顺序是由变量在类内的定义顺序决定的。即使变量定义大量遍布于方法定义的中间,那些变量仍会在调用任何方法之前得到初始化——甚至在构建器调用之前。例如: +在一个类里,初始化的顺序是由变量在类内的定义顺序决定的。即使变量定义大量遍布于方法定义的中间,那些变量仍会在调用任何方法之前得到初始化——甚至在构造器调用之前。例如: -``` +``` java //: OrderOfInitialization.java // Demonstrates initialization order. @@ -181,9 +181,9 @@ public class OrderOfInitialization { } ///:~ ``` -在Card中,Tag对象的定义故意到处散布,以证明它们全都会在构建器进入或者发生其他任何事情之前得到初始化。除此之外,t3在构建器内部得到了重新初始化。它的输入结果如下: +在Card中,Tag对象的定义故意到处散布,以证明它们全都会在构造器进入或者发生其他任何事情之前得到初始化。除此之外,t3在构造器内部得到了重新初始化。它的输入结果如下: -``` +``` java Tag(1) Tag(2) Tag(3) @@ -192,15 +192,15 @@ Tag(33) f() ``` -因此,t3句柄会被初始化两次,一次在构建器调用前,一次在调用期间(第一个对象会被丢弃,所以它后来可被当作垃圾收掉)。从表面看,这样做似乎效率低下,但它能保证正确的初始化——若定义了一个过载的构建器,它没有初始化t3;同时在t3的定义里并没有规定“默认”的初始化方式,那么会产生什么后果呢? +因此,t3指针会被初始化两次,一次在构造器调用前,一次在调用期间(第一个对象会被丢弃,所以它后来可被当作垃圾收掉)。从表面看,这样做似乎效率低下,但它能保证正确的初始化——若定义了一个重载的构造器,它没有初始化t3;同时在t3的定义里并没有规定“默认”的初始化方式,那么会产生什么后果呢? 2. 静态数据的初始化 -若数据是静态的(static),那么同样的事情就会发生;如果它属于一个基本类型(主类型),而且未对其初始化,就会自动获得自己的标准基本类型初始值;如果它是指向一个对象的句柄,那么除非新建一个对象,并将句柄同它连接起来,否则就会得到一个空值(NULL)。 +若数据是静态的(static),那么同样的事情就会发生;如果它属于一个基本类型(主类型),而且未对其初始化,就会自动获得自己的标准基本类型初始值;如果它是指向一个对象的指针,那么除非新建一个对象,并将指针同它连接起来,否则就会得到一个空值(NULL)。 如果想在定义的同时进行初始化,采取的方法与非静态值表面看起来是相同的。但由于static值只有一个存储区域,所以无论创建多少个对象,都必然会遇到何时对那个存储区域进行初始化的问题。下面这个例子可将这个问题说更清楚一些: -``` +``` java //: StaticInitialization.java // Specifying initial values in a // class definition. @@ -257,7 +257,7 @@ public class StaticInitialization { Bowl允许我们检查一个类的创建过程,而Table和Cupboard能创建散布于类定义中的Bowl的static成员。注意在static定义之前,Cupboard先创建了一个非static的Bowl b3。它的输出结果如下: -``` +``` java Bowl(1) Bowl(2) Table() @@ -294,13 +294,13 @@ static初始化只有在必要的时候才会进行。如果不创建一个Table (5) 进行字段定义时发生的所有初始化都会执行。 -(6) 执行构建器。正如第6章将要讲到的那样,这实际可能要求进行相当多的操作,特别是在涉及继承的时候。 +(6) 执行构造器。正如第6章将要讲到的那样,这实际可能要求进行相当多的操作,特别是在涉及继承的时候。 3. 明确进行的静态初始化 Java允许我们将其他static初始化工作划分到类内一个特殊的“static构建从句”(有时也叫作“静态块”)里。它看起来象下面这个样子: -``` +``` java class Spoon { static int i; static { @@ -311,7 +311,7 @@ class Spoon { 尽管看起来象个方法,但它实际只是一个static关键字,后面跟随一个方法主体。与其他static初始化一样,这段代码仅执行一次——首次生成那个类的一个对象时,或者首次访问属于那个类的一个static成员时(即便从未生成过那个类的对象)。例如: -``` +``` java //: ExplicitStatic.java // Explicit static initialization // with the "static" clause. @@ -353,7 +353,7 @@ public class ExplicitStatic { 针对每个对象的非静态变量的初始化,Java 1.1提供了一种类似的语法格式。下面是一个例子: -``` +``` java //: Mugs.java // Java 1.1 "Instance Initialization" @@ -386,7 +386,7 @@ public class Mugs { 大家可看到实例初始化从句: -``` +``` java { c1 = new Mug(1); c2 = new Mug(2); diff --git "a/4.5 \346\225\260\347\273\204\345\210\235\345\247\213\345\214\226.md" "b/4.5 \346\225\260\347\273\204\345\210\235\345\247\213\345\214\226.md" index bdd63bb..fb4e708 100644 --- "a/4.5 \346\225\260\347\273\204\345\210\235\345\247\213\345\214\226.md" +++ "b/4.5 \346\225\260\347\273\204\345\210\235\345\247\213\345\214\226.md" @@ -5,40 +5,40 @@ 数组代表一系列对象或者基本数据类型,所有相同的类型都封装到一起——采用一个统一的标识符名称。数组的定义和使用是通过方括号索引运算符进行的([])。为定义一个数组,只需在类型名后简单地跟随一对空方括号即可: -``` +``` java int[] al; ``` 也可以将方括号置于标识符后面,获得完全一致的结果: -``` +``` java int al[]; ``` 这种格式与C和C++程序员习惯的格式是一致的。然而,最“通顺”的也许还是前一种语法,因为它指出类型是“一个int数组”。本书将沿用那种格式。 -编译器不允许我们告诉它一个数组有多大。这样便使我们回到了“句柄”的问题上。此时,我们拥有的一切就是指向数组的一个句柄,而且尚未给数组分配任何空间。为了给数组创建相应的存储空间,必须编写一个初始化表达式。对于数组,初始化工作可在代码的任何地方出现,但也可以使用一种特殊的初始化表达式,它必须在数组创建的地方出现。这种特殊的初始化是一系列由花括号封闭起来的值。存储空间的分配(等价于使用new)将由编译器在这种情况下进行。例如: +编译器不允许我们告诉它一个数组有多大。这样便使我们回到了“指针”的问题上。此时,我们拥有的一切就是指向数组的一个指针,而且尚未给数组分配任何空间。为了给数组创建相应的存储空间,必须编写一个初始化表达式。对于数组,初始化工作可在代码的任何地方出现,但也可以使用一种特殊的初始化表达式,它必须在数组创建的地方出现。这种特殊的初始化是一系列由花括号封闭起来的值。存储空间的分配(等价于使用new)将由编译器在这种情况下进行。例如: -``` +``` java int[] a1 = { 1, 2, 3, 4, 5 }; ``` -那么为什么还要定义一个没有数组的数组句柄呢? +那么为什么还要定义一个没有数组的数组指针呢? -``` +``` java int[] a2; ``` 事实上在Java中,可将一个数组分配给另一个,所以能使用下述语句: -``` +``` java a2 = a1; ``` -我们真正准备做的是复制一个句柄,就象下面演示的那样: +我们真正准备做的是复制一个指针,就象下面演示的那样: -``` +``` java //: Arrays.java // Arrays of primitives. @@ -60,11 +60,11 @@ public class Arrays { 大家看到a1获得了一个初始值,而a2没有;a2将在以后赋值——这种情况下是赋给另一个数组。 -这里也出现了一些新东西:所有数组都有一个本质成员(无论它们是对象数组还是基本类型数组),可对其进行查询——但不是改变,从而获知数组内包含了多少个元素。这个成员就是length。与C和C++类似,由于Java数组从元素0开始计数,所以能索引的最大元素编号是“length-1”。如超出边界,C和C++会“默默”地接受,并允许我们胡乱使用自己的内存,这正是许多程序错误的根源。然而,Java可保留我们这受这一问题的损害,方法是一旦超过边界,就生成一个运行期错误(即一个“违例”,这是第9章的主题)。当然,由于需要检查每个数组的访问,所以会消耗一定的时间和多余的代码量,而且没有办法把它关闭。这意味着数组访问可能成为程序效率低下的重要原因——如果它们在关键的场合进行。但考虑到因特网访问的安全,以及程序员的编程效率,Java设计人员还是应该把它看作是值得的。 +这里也出现了一些新东西:所有数组都有一个本质成员(无论它们是对象数组还是基本类型数组),可对其进行查询——但不是改变,从而获知数组内包含了多少个元素。这个成员就是length。与C和C++类似,由于Java数组从元素0开始计数,所以能索引的最大元素编号是“length-1”。如超出边界,C和C++会“默默”地接受,并允许我们胡乱使用自己的内存,这正是许多程序错误的根源。然而,Java可保留我们这受这一问题的损害,方法是一旦超过边界,就生成一个运行期错误(即一个“异常”,这是第9章的主题)。当然,由于需要检查每个数组的访问,所以会消耗一定的时间和多余的代码量,而且没有办法把它关闭。这意味着数组访问可能成为程序效率低下的重要原因——如果它们在关键的场合进行。但考虑到因特网访问的安全,以及程序员的编程效率,Java设计人员还是应该把它看作是值得的。 程序编写期间,如果不知道在自己的数组里需要多少元素,那么又该怎么办呢?此时,只需简单地用new在数组里创建元素。在这里,即使准备创建的是一个基本数据类型的数组,new也能正常地工作(new不会创建非数组的基本类型): -``` +``` java //: ArrayNew.java // Creating arrays with new. import java.util.*; @@ -91,13 +91,13 @@ public class ArrayNew { 当然,数组可能已在相同的语句中定义和初始化了,如下所示: -``` +``` java int[] a = new int[pRand(20)]; ``` -若操作的是一个非基本类型对象的数组,那么无论如何都要使用new。在这里,我们会再一次遇到句柄问题,因为我们创建的是一个句柄数组。请大家观察封装器类型Integer,它是一个类,而非基本数据类型: +若操作的是一个非基本类型对象的数组,那么无论如何都要使用new。在这里,我们会再一次遇到指针问题,因为我们创建的是一个指针数组。请大家观察封装器类型Integer,它是一个类,而非基本数据类型: -``` +``` java //: ArrayClassObj.java // Creating an array of non-primitive objects. import java.util.*; @@ -123,23 +123,23 @@ public class ArrayClassObj { 在这儿,甚至在new调用后才开始创建数组: -``` +``` java Integer[] a = new Integer[pRand(20)]; ``` -它只是一个句柄数组,而且除非通过创建一个新的Integer对象,从而初始化了对象句柄,否则初始化进程不会结束: +它只是一个指针数组,而且除非通过创建一个新的Integer对象,从而初始化了对象指针,否则初始化进程不会结束: -``` +``` java a[i] = new Integer(pRand(500)); ``` -但若忘记创建对象,就会在运行期试图读取空数组位置时获得一个“违例”错误。 +但若忘记创建对象,就会在运行期试图读取空数组位置时获得一个“异常”错误。 -下面让我们看看打印语句中String对象的构成情况。大家可看到指向Integer对象的句柄会自动转换,从而产生一个String,它代表着位于对象内部的值。 +下面让我们看看打印语句中String对象的构成情况。大家可看到指向Integer对象的指针会自动转换,从而产生一个String,它代表着位于对象内部的值。 亦可用花括号封闭列表来初始化对象数组。可采用两种形式,第一种是Java 1.0允许的唯一形式。第二种(等价)形式自Java 1.1才开始提供支持: -``` +``` java //: ArrayInit.java // Array initialization @@ -165,7 +165,7 @@ public class ArrayInit { 数组初始化的第二种形式(Java 1.1开始支持)提供了一种更简便的语法,可创建和调用方法,获得与C的“变量参数列表”(C通常把它简称为“变参表”)一致的效果。这些效果包括未知的参数(自变量)数量以及未知的类型(如果这样选择的话)。由于所有类最终都是从通用的根类Object中继承的,所以能创建一个方法,令其获取一个Object数组,并象下面这样调用它: -``` +``` java //: VarArgs.java // Using the Java 1.1 array syntax to create // variable argument lists @@ -193,7 +193,7 @@ public class VarArgs { 在Java里可以方便地创建多维数组: -``` +``` java //: MultiDimArray.java // Creating multidimensional arrays. import java.util.*; @@ -266,7 +266,7 @@ public class MultiDimArray { 用于打印的代码里使用了length,所以它不必依赖固定的数组大小。 第一个例子展示了基本数据类型的一个多维数组。我们可用花括号定出数组内每个矢量的边界: -``` +``` java int[][] a1 = { { 1, 2, 3, }, { 4, 5, 6, }, @@ -278,7 +278,7 @@ int[][] a1 = { int[][][] a2 = new int[2][2][4]; 但第三个例子却向大家揭示出构成矩阵的每个矢量都可以有任意的长度: -``` +``` java int[][][] a3 = new int[pRand(7)][][]; for(int i = 0; i < a3.length; i++) { a3[i] = new int[pRand(5)][]; @@ -291,7 +291,7 @@ int[][][] a2 = new int[2][2][4]; 根据输出结果,大家可以看到:假若没有明确指定初始化值,数组值就会自动初始化成零。 可用类似的表式处理非基本类型对象的数组。这从第四个例子可以看出,它向我们演示了用花括号收集多个new表达式的能力: -``` +``` java Integer[][] a4 = { { new Integer(1), new Integer(2)}, { new Integer(3), new Integer(4)}, @@ -301,7 +301,7 @@ int[][][] a2 = new int[2][2][4]; 第五个例子展示了如何逐渐构建非基本类型的对象数组: -``` +``` java Integer[][] a5; a5 = new Integer[3][]; for(int i = 0; i < a5.length; i++) { diff --git "a/4.6 \346\200\273\347\273\223.md" "b/4.6 \346\200\273\347\273\223.md" index bcaceac..ca35dfe 100644 --- "a/4.6 \346\200\273\347\273\223.md" +++ "b/4.6 \346\200\273\347\273\223.md" @@ -1,7 +1,7 @@ # 4.6 总结 -作为初始化的一种具体操作形式,构建器应使大家明确感受到在语言中进行初始化的重要性。与C++的程序设计一样,判断一个程序效率如何,关键是看是否由于变量的初始化不正确而造成了严重的编程错误(臭虫)。这些形式的错误很难发现,而且类似的问题也适用于不正确的清除或收尾工作。由于构建器使我们能保证正确的初始化和清除(若没有正确的构建器调用,编译器不允许对象创建),所以能获得完全的控制权和安全性。 +作为初始化的一种具体操作形式,构造器应使大家明确感受到在语言中进行初始化的重要性。与C++的程序设计一样,判断一个程序效率如何,关键是看是否由于变量的初始化不正确而造成了严重的编程错误(臭虫)。这些形式的错误很难发现,而且类似的问题也适用于不正确的清除或收尾工作。由于构造器使我们能保证正确的初始化和清除(若没有正确的构造器调用,编译器不允许对象创建),所以能获得完全的控制权和安全性。 -在C++中,与“构建”相反的“破坏”(Destruction)工作也是相当重要的,因为用new创建的对象必须明确地清除。在Java中,垃圾收集器会自动为所有对象释放内存,所以Java中等价的清除方法并不是经常都需要用到的。如果不需要类似于构建器的行为,Java的垃圾收集器可以极大简化编程工作,而且在内存的管理过程中增加更大的安全性。有些垃圾收集器甚至能清除其他资源,比如图形和文件句柄等。然而,垃圾收集器确实也增加了运行期的开销。但这种开销到底造成了多大的影响却是很难看出的,因为到目前为止,Java解释器的总体运行速度仍然是比较慢的。随着这一情况的改观,我们应该能判断出垃圾收集器的开销是否使Java不适合做一些特定的工作(其中一个问题是垃圾收集器不可预测的性质)。 +在C++中,与“构建”相反的“破坏”(Destruction)工作也是相当重要的,因为用new创建的对象必须明确地清除。在Java中,垃圾收集器会自动为所有对象释放内存,所以Java中等价的清除方法并不是经常都需要用到的。如果不需要类似于构造器的行为,Java的垃圾收集器可以极大简化编程工作,而且在内存的管理过程中增加更大的安全性。有些垃圾收集器甚至能清除其他资源,比如图形和文件指针等。然而,垃圾收集器确实也增加了运行期的开销。但这种开销到底造成了多大的影响却是很难看出的,因为到目前为止,Java解释器的总体运行速度仍然是比较慢的。随着这一情况的改观,我们应该能判断出垃圾收集器的开销是否使Java不适合做一些特定的工作(其中一个问题是垃圾收集器不可预测的性质)。 -由于所有对象都肯定能获得正确的构建,所以同这儿讲述的情况相比,构建器实际做的事情还要多得多。特别地,当我们通过“创作”或“继承”生成新类的时候,对构建的保证仍然有效,而且需要一些附加的语法来提供对它的支持。大家将在以后的章节里详细了解创作、继承以及它们对构建器造成的影响。 +由于所有对象都肯定能获得正确的构建,所以同这儿讲述的情况相比,构造器实际做的事情还要多得多。特别地,当我们通过“创作”或“继承”生成新类的时候,对构建的保证仍然有效,而且需要一些附加的语法来提供对它的支持。大家将在以后的章节里详细了解创作、继承以及它们对构造器造成的影响。 diff --git "a/4.7 \347\273\203\344\271\240.md" "b/4.7 \347\273\203\344\271\240.md" index 3e0f9b1..4127fb1 100644 --- "a/4.7 \347\273\203\344\271\240.md" +++ "b/4.7 \347\273\203\344\271\240.md" @@ -1,13 +1,13 @@ # 4.7 练习 -(1) 用默认构建器创建一个类(没有自变量),用它打印一条消息。创建属于这个类的一个对象。 +(1) 用默认构造器创建一个类(没有自变量),用它打印一条消息。创建属于这个类的一个对象。 -(2) 在练习1的基础上增加一个过载的构建器,令其采用一个String自变量,并随同自己的消息打印出来。 +(2) 在练习1的基础上增加一个重载的构造器,令其采用一个String自变量,并随同自己的消息打印出来。 -(3) 以练习2创建的类为基础上,创建属于它的对象句柄的一个数组,但不要实际创建对象并分配到数组里。运行程 -序时,注意是否打印出来自构建器调用的初始化消息。 +(3) 以练习2创建的类为基础上,创建属于它的对象指针的一个数组,但不要实际创建对象并分配到数组里。运行程 +序时,注意是否打印出来自构造器调用的初始化消息。 -(4) 创建同句柄数组联系起来的对象,最终完成练习3。 +(4) 创建同指针数组联系起来的对象,最终完成练习3。 (5) 用自变量“before”,“after”和“none”运行程序,试验Garbage.java。重复这个操作,观察是否从输出中看出了一些固定的模式。改变代码,使System.runFinalization()在System.gc()之前调用,再观察结果。 \ No newline at end of file diff --git "a/5.1 \345\214\205\357\274\232\345\272\223\345\215\225\345\205\203.md" "b/5.1 \345\214\205\357\274\232\345\272\223\345\215\225\345\205\203.md" index 66d4aea..f0e40cf 100644 --- "a/5.1 \345\214\205\357\274\232\345\272\223\345\215\225\345\205\203.md" +++ "b/5.1 \345\214\205\357\274\232\345\272\223\345\215\225\345\205\203.md" @@ -3,7 +3,7 @@ 我们用import关键字导入一个完整的库时,就会获得“包”(Package)。例如: -``` +``` java import java.util.*; ``` @@ -11,7 +11,7 @@ import java.util.*; 若想导入单独一个类,可在import语句里指定那个类的名字: -``` +``` java import java.util.Vector; ``` @@ -32,7 +32,7 @@ import java.util.Vector; 若在一个文件的开头使用下述代码: -``` +``` java package mypackage; ``` @@ -40,7 +40,7 @@ package mypackage; 例如,假定文件名是MyClass.java。它意味着在那个文件有一个、而且只能有一个public类。而且那个类的名字必须是MyClass(包括大小写形式): -``` +``` java package mypackage; public class MyClass { // . . . @@ -48,13 +48,13 @@ public class MyClass { 现在,如果有人想使用MyClass,或者想使用mypackage内的其他任何public类,他们必须用import关键字激活mypackage内的名字,使它们能够使用。另一个办法则是指定完整的名称: -``` +``` java mypackage.MyClass m = new mypackage.MyClass(); ``` import关键字则可将其变得简洁得多: -``` +``` java import mypackage.*; // . . . MyClass m = new MyClass(); @@ -75,13 +75,13 @@ Java解释器的工作程序如下:首先,它找到环境变量CLASSPATH( 为进一步理解这个问题,下面以我自己的域名为例,它是bruceeckel.com。将其反转过来后,com.bruceeckel就为我的类创建了独一无二的全局名称(com,edu,org,net等扩展名以前在Java包中都是大写的,但自Java 1.2以来,这种情况已发生了变化。现在整个包名都是小写的)。由于决定创建一个名为util的库,我可以进一步地分割它,所以最后得到的包名如下: -``` +``` java package com.bruceeckel.util; ``` 现在,可将这个包名作为下述两个文件的“命名空间”使用: -``` +``` java //: Vector.java // Creating a package package com.bruceeckel.util; @@ -96,7 +96,7 @@ public class Vector { 创建自己的包时,要求package语句必须是文件中的第一个“非注释”代码。第二个文件表面看起来是类似的: -``` +``` java //: List.java // Creating a package package com.bruceeckel.util; @@ -111,25 +111,25 @@ public class List { 这两个文件都置于我自己系统的一个子目录中: -``` +``` java C:\DOC\JavaT\com\bruceeckel\util ``` 若通过它往回走,就会发现包名com.bruceeckel.util,但路径的第一部分又是什么呢?这是由CLASSPATH环境变量决定的。在我的机器上,它是: -``` +``` java CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT ``` 可以看出,CLASSPATH里能包含大量备用的搜索路径。然而,使用JAR文件时要注意一个问题:必须将JAR文件的名字置于类路径里,而不仅仅是它所在的路径。所以对一个名为grape.jar的JAR文件来说,我们的类路径需要包括: -``` +``` java CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar ``` 正确设置好类路径后,可将下面这个文件置于任何目录里(若在执行该程序时遇到麻烦,请参见第3章的3.1.2小节“赋值”): -``` +``` java //: LibTest.java // Uses the library package c05; @@ -154,7 +154,7 @@ public class LibTest { 若通过 `*` 导入了两个库,而且它们包括相同的名字,这时会出现什么情况呢?例如,假定一个程序使用了下述导入语句: -``` +``` java import com.bruceeckel.util.*; import java.util.*; ``` @@ -163,13 +163,13 @@ import java.util.*; 如现在试着生成一个Vector,就肯定会发生冲突。如下所示: -``` +``` java Vector v = new Vector(); ``` 它引用的到底是哪个Vector类呢?编译器对这个问题没有答案,读者也不可能知道。所以编译器会报告一个错误,强迫我们进行明确的说明。例如,假设我想使用标准的Java Vector,那么必须象下面这样编程: -``` +``` java java.util.Vector v = new java.util.Vector(); ``` @@ -179,7 +179,7 @@ java.util.Vector v = new java.util.Vector(); 掌握前述的知识后,接下来就可以开始创建自己的工具库,以便减少或者完全消除重复的代码。例如,可为System.out.println()创建一个别名,减少重复键入的代码量。它可以是名为tools的一个包(package)的一部分: -``` +``` java //: P.java // The P.rint & P.rintln shorthand package com.bruceeckel.tools; @@ -248,7 +248,7 @@ public class P { 所有不同的数据类型现在都可以在一个新行输出(P.rintln()),或者不在一个新行输出(P.rint())。 大家可能会猜想这个文件所在的目录必须从某个CLASSPATH位置开始,然后继续com/bruceeckel/tools。编译完毕后,利用一个import语句,即可在自己系统的任何地方使用P.class文件。如下所示: -``` +``` java ToolTest.java ``` @@ -270,7 +270,7 @@ Java已取消的一种特性是C的“条件编译”,它允许我们改变参 可用下面这个类进行程序调试: -``` +``` java //: Assert.java // Assertion tool for debugging package com.bruceeckel.tools.debug; @@ -296,16 +296,16 @@ public class Assert { } ///:~ ``` -这个类只是简单地封装了布尔测试。如果失败,就显示出出错消息。在第9章,大家还会学习一个更高级的错误控制工具,名为“违例控制”。但在目前这种情况下,perr()方法已经可以很好地工作。 +这个类只是简单地封装了布尔测试。如果失败,就显示出出错消息。在第9章,大家还会学习一个更高级的错误控制工具,名为“异常控制”。但在目前这种情况下,perr()方法已经可以很好地工作。 如果想使用这个类,可在自己的程序中加入下面这一行: -``` +``` java import com.bruceeckel.tools.debug.*; ``` 如欲清除断定机制,以便自己能发行最终的代码,我们创建了第二个Assert类,但却是在一个不同的包里: -``` +``` java //: Assert.java // Turning off the assertion output // so you can ship the program. @@ -323,13 +323,13 @@ public class Assert { 现在,假如将前一个import语句变成下面这个样子: -``` +``` java import com.bruceeckel.tools.*; ``` 程序便不再显示出断言。下面是个例子: -``` +``` java //: TestAssert.java // Demonstrating the assertion tool package c05; diff --git "a/5.2 Java\350\256\277\351\227\256\346\214\207\347\244\272\347\254\246.md" "b/5.2 Java\350\256\277\351\227\256\346\214\207\347\244\272\347\254\246.md" index d6202e3..fb3006c 100644 --- "a/5.2 Java\350\256\277\351\227\256\346\214\207\347\244\272\347\254\246.md" +++ "b/5.2 Java\350\256\277\351\227\256\346\214\207\347\244\272\347\254\246.md" @@ -25,7 +25,7 @@ 使用public关键字时,它意味着紧随在public后面的成员声明适用于所有人,特别是适用于使用库的客户程序员。假定我们定义了一个名为dessert的包,其中包含下述单元(若执行该程序时遇到困难,请参考第3章3.1.2小节“赋值”): -``` +``` java //: Cookie.java // Creates a library package c05.dessert; @@ -41,7 +41,7 @@ public class Cookie { 请记住,Cookie.java必须驻留在名为dessert的一个子目录内,而这个子目录又必须位于由CLASSPATH指定的C05目录下面(C05代表本书的第5章)。不要错误地以为Java无论如何都会将当前目录作为搜索的起点看待。如果不将一个“.”作为CLASSPATH的一部分使用,Java就不会考虑当前目录。 现在,假若创建使用了Cookie的一个程序,如下所示: -``` +``` java //: Dinner.java // Uses the library import c05.dessert.*; @@ -57,13 +57,13 @@ public class Dinner { } ///:~ ``` -就可以创建一个Cookie对象,因为它的构建器是public的,而且类也是public的(公共类的概念稍后还会进行更详细的讲述)。然而,foo()成员不可在Dinner.java内访问,因为foo()只有在dessert包内才是“友好”的。 +就可以创建一个Cookie对象,因为它的构造器是public的,而且类也是public的(公共类的概念稍后还会进行更详细的讲述)。然而,foo()成员不可在Dinner.java内访问,因为foo()只有在dessert包内才是“友好”的。 1. 默认包 大家可能会惊讶地发现下面这些代码得以顺利编译——尽管它看起来似乎已违背了规则: -``` +``` java //: Cake.java // Accesses a class in a separate // compilation unit. @@ -78,7 +78,7 @@ class Cake { 在位于相同目录的第二个文件里: -``` +``` java //: Pie.java // The other class @@ -94,7 +94,7 @@ class Pie { private关键字意味着除非那个特定的类,而且从那个类的方法里,否则没有人能访问那个成员。同一个包内的其他成员不能访问private成员,这使其显得似乎将类与我们自己都隔离起来。另一方面,也不能由几个合作的人创建一个包。所以private允许我们自由地改变那个成员,同时毋需关心它是否会影响同一个包内的另一个类。默认的“友好”包访问通常已经是一种适当的隐藏方法;请记住,对于包的用户来说,是不能访问一个“友好”成员的。这种效果往往能令人满意,因为默认访问是我们通常采用的方法。对于希望变成public(公共)的成员,我们通常明确地指出,令其可由客户程序员自由调用。而且作为一个结果,最开始的时候通常会认为自己不必频繁使用private关键字,因为完全可以在不用它的前提下发布自己的代码(这与C++是个鲜明的对比)。然而,随着学习的深入,大家就会发现private仍然有非常重要的用途,特别是在涉及多线程处理的时候(详情见第14章)。 下面是应用了private的一个例子: -``` +``` java //: IceCream.java // Demonstrates "private" keyword @@ -113,11 +113,11 @@ public class IceCream { } ///:~ ``` -这个例子向我们证明了使用private的方便:有时可能想控制对象的创建方式,并防止有人直接访问一个特定的构建器(或者所有构建器)。在上面的例子中,我们不可通过它的构建器创建一个Sundae对象;相反,必须调用makeASundae()方法来实现(注释③)。 +这个例子向我们证明了使用private的方便:有时可能想控制对象的创建方式,并防止有人直接访问一个特定的构造器(或者所有构造器)。在上面的例子中,我们不可通过它的构造器创建一个Sundae对象;相反,必须调用makeASundae()方法来实现(注释③)。 -③:此时还会产生另一个影响:由于默认构建器是唯一获得定义的,而且它的属性是private,所以可防止对这个类的继承(这是第6章要重点讲述的主题)。 +③:此时还会产生另一个影响:由于默认构造器是唯一获得定义的,而且它的属性是private,所以可防止对这个类的继承(这是第6章要重点讲述的主题)。 -若确定一个类只有一个“助手”方法,那么对于任何方法来说,都可以把它们设为private,从而保证自己不会误在包内其他地方使用它,防止自己更改或删除方法。将一个方法的属性设为private后,可保证自己一直保持这一选项(然而,若一个句柄被设为private,并不表明其他对象不能拥有指向同一个对象的public句柄。有关“别名”的问题将在第12章详述)。 +若确定一个类只有一个“助手”方法,那么对于任何方法来说,都可以把它们设为private,从而保证自己不会误在包内其他地方使用它,防止自己更改或删除方法。将一个方法的属性设为private后,可保证自己一直保持这一选项(然而,若一个指针被设为private,并不表明其他对象不能拥有指向同一个对象的public指针。有关“别名”的问题将在第12章详述)。 5.2.4 protected:“友好的一种” @@ -125,7 +125,7 @@ protected(受到保护的)访问指示符要求大家提前有所认识。 protected关键字为我们引入了一种名为“继承”的概念,它以现有的类为基础,并在其中加入新的成员,同时不会对现有的类产生影响——我们将这种现有的类称为“基础类”或者“基本类”(Base Class)。亦可改变那个类现有成员的行为。对于从一个现有类的继承,我们说自己的新类“扩展”(extends)了那个现有的类。如下所示: -``` +``` java class Foo extends Bar { ``` @@ -133,7 +133,7 @@ class Foo extends Bar { 若新建一个包,并从另一个包内的某个类里继承,则唯一能够访问的成员就是原来那个包的public成员。当然,如果在相同的包里进行继承,那么继承获得的包能够访问所有“友好”的成员。有些时候,基础类的创建者喜欢提供一个特殊的成员,并允许访问衍生类。这正是protected的工作。若往回引用5.2.2小节“public:接口访问”的那个Cookie.java文件,则下面这个类就不能访问“友好”的成员: -``` +``` java //: ChocolateChip.java // Can't access friendly member // in another class @@ -153,7 +153,7 @@ public class ChocolateChip extends Cookie { 对于继承,值得注意的一件有趣的事情是倘若方法foo()存在于类Cookie中,那么它也会存在于从Cookie继承的所有类中。但由于foo()在外部的包里是“友好”的,所以我们不能使用它。当然,亦可将其变成public。但这样一来,由于所有人都能自由访问它,所以可能并非我们所希望的局面。若象下面这样修改类Cookie: -``` +``` java public class Cookie { public Cookie() { System.out.println("Cookie constructor"); diff --git "a/5.3 \346\216\245\345\217\243\344\270\216\345\256\236\347\216\260.md" "b/5.3 \346\216\245\345\217\243\344\270\216\345\256\236\347\216\260.md" index e3a436b..1f887ce 100644 --- "a/5.3 \346\216\245\345\217\243\344\270\216\345\256\236\347\216\260.md" +++ "b/5.3 \346\216\245\345\217\243\344\270\216\345\256\236\347\216\260.md" @@ -13,7 +13,7 @@ 为清楚起见,可考虑用特殊的样式创建一个类:将public成员置于最开头,后面跟随protected、友好以及private成员。这样做的好处是类的使用者可从上向下依次阅读,并首先看到对自己来说最重要的内容(即public成员,因为它们可从文件的外部访问),并在遇到非公共成员后停止阅读,后者已经属于内部实施细节的一部分了。然而,利用由javadoc提供支持的注释文档(已在第2章介绍),代码的可读性问题已在很大程度上得到了解决。 -``` +``` java public class X { public void pub1( ) { /* . . . */ } public void pub2( ) { /* . . . */ } diff --git "a/5.4 \347\261\273\350\256\277\351\227\256.md" "b/5.4 \347\261\273\350\256\277\351\227\256.md" index dc8ae40..7052ad3 100644 --- "a/5.4 \347\261\273\350\256\277\351\227\256.md" +++ "b/5.4 \347\261\273\350\256\277\351\227\256.md" @@ -5,19 +5,19 @@ 为控制一个类的访问,指示符必须在关键字class之前出现。所以我们能够使用: -``` +``` java public class Widget { ``` 也就是说,假若我们的库名是mylib,那么所有客户程序员都能访问Widget——通过下述语句: -``` +``` java import mylib.Widget; ``` 或者 -``` +``` java import mylib.*; ``` @@ -31,9 +31,9 @@ import mylib.*; 如果已经获得了mylib内部的一个类,准备用它完成由Widget或者mylib内部的其他某些public类执行的任务,此时又会出现什么情况呢?我们不希望花费力气为客户程序员编制文档,并感觉以后某个时候也许会进行大手笔的修改,并将自己的类一起删掉,换成另一个不同的类。为获得这种灵活处理的能力,需要保证没有客户程序员能够依赖自己隐藏于mylib内部的特定实施细节。为达到这个目的,只需将public关键字从类中剔除即可,这样便把类变成了“友好的”(类仅能在包内使用)。 -注意不可将类设成private(那样会使除类之外的其他东西都不能访问它),也不能设成protected(注释④)。因此,我们现在对于类的访问只有两个选择:“友好的”或者public。若不愿其他任何人访问那个类,可将所有构建器设为private。这样一来,在类的一个static成员内部,除自己之外的其他所有人都无法创建属于那个类的一个对象(注释⑤)。如下例所示: +注意不可将类设成private(那样会使除类之外的其他东西都不能访问它),也不能设成protected(注释④)。因此,我们现在对于类的访问只有两个选择:“友好的”或者public。若不愿其他任何人访问那个类,可将所有构造器设为private。这样一来,在类的一个static成员内部,除自己之外的其他所有人都无法创建属于那个类的一个对象(注释⑤)。如下例所示: -``` +``` java //: Lunch.java // Demonstrates class access specifiers. // Make a class effectively private @@ -77,16 +77,16 @@ public class Lunch { 迄今为止,我们创建过的大多数方法都是要么返回void,要么返回一个基本数据类型。所以对下述定义来说: -``` +``` java public static Soup access() { return psl; } ``` -它最开始多少会使人有些迷惑。位于方法名(access)前的单词指出方法到底返回什么。在这之前,我们看到的都是void,它意味着“什么也不返回”(void在英语里是“虚无”的意思。但亦可返回指向一个对象的句柄,此时出现的就是这个情况。该方法返回一个句柄,它指向类Soup的一个对象。 +它最开始多少会使人有些迷惑。位于方法名(access)前的单词指出方法到底返回什么。在这之前,我们看到的都是void,它意味着“什么也不返回”(void在英语里是“虚无”的意思。但亦可返回指向一个对象的指针,此时出现的就是这个情况。该方法返回一个指针,它指向类Soup的一个对象。 -Soup类向我们展示出如何通过将所有构建器都设为private,从而防止直接创建一个类。请记住,假若不明确地至少创建一个构建器,就会自动创建默认构建器(没有自变量)。若自己编写默认构建器,它就不会自动创建。把它变成private后,就没人能为那个类创建一个对象。但别人怎样使用这个类呢?上面的例子为我们揭示出了两个选择。第一个选择,我们可创建一个static方法,再通过它创建一个新的Soup,然后返回指向它的一个句柄。如果想在返回之前对Soup进行一些额外的操作,或者想了解准备创建多少个Soup对象(可能是为了限制它们的个数),这种方案无疑是特别有用的。 +Soup类向我们展示出如何通过将所有构造器都设为private,从而防止直接创建一个类。请记住,假若不明确地至少创建一个构造器,就会自动创建默认构造器(没有自变量)。若自己编写默认构造器,它就不会自动创建。把它变成private后,就没人能为那个类创建一个对象。但别人怎样使用这个类呢?上面的例子为我们揭示出了两个选择。第一个选择,我们可创建一个static方法,再通过它创建一个新的Soup,然后返回指向它的一个指针。如果想在返回之前对Soup进行一些额外的操作,或者想了解准备创建多少个Soup对象(可能是为了限制它们的个数),这种方案无疑是特别有用的。 -第二个选择是采用“设计方案”(Design Pattern)技术,本书后面会对此进行详细介绍。通常方案叫作“独子”,因为它仅允许创建一个对象。类Soup的对象被创建成Soup的一个static private成员,所以有一个而且只能有一个。除非通过public方法access(),否则根本无法访问它。 +第二个选择是采用“设计方案”(Design Pattern)技术,本书后面会对此进行详细介绍。通常方案叫作“单例”,因为它仅允许创建一个对象。类Soup的对象被创建成Soup的一个static private成员,所以有一个而且只能有一个。除非通过public方法access(),否则根本无法访问它。 正如早先指出的那样,如果不针对类的访问设置一个访问指示符,那么它会自动默认为“友好的”。这意味着那个类的对象可由包内的其他类创建,但不能由包外创建。请记住,对于相同目录内的所有文件,如果没有明确地进行package声明,那么它们都默认为那个目录的默认包的一部分。然而,假若那个类一个static成员的属性是public,那么客户程序员仍然能够访问那个static成员——即使它们不能创建属于那个类的一个对象。 diff --git "a/6.1 \345\220\210\346\210\220\347\232\204\350\257\255\346\263\225.md" "b/6.1 \345\220\210\346\210\220\347\232\204\350\257\255\346\263\225.md" index beccec2..bab5cf4 100644 --- "a/6.1 \345\220\210\346\210\220\347\232\204\350\257\255\346\263\225.md" +++ "b/6.1 \345\220\210\346\210\220\347\232\204\350\257\255\346\263\225.md" @@ -1,9 +1,9 @@ # 6.1 合成的语法 -就以前的学习情况来看,事实上已进行了多次“合成”操作。为进行合成,我们只需在新类里简单地置入对象句柄即可。举个例子来说,假定需要在一个对象里容纳几个String对象、两种基本数据类型以及属于另一个类的一个对象。对于非基本类型的对象来说,只需将句柄置于新类即可;而对于基本数据类型来说,则需在自己的类中定义它们。如下所示(若执行该程序时有麻烦,请参见第3章3.1.2小节“赋值”): +就以前的学习情况来看,事实上已进行了多次“合成”操作。为进行合成,我们只需在新类里简单地置入对象指针即可。举个例子来说,假定需要在一个对象里容纳几个String对象、两种基本数据类型以及属于另一个类的一个对象。对于非基本类型的对象来说,只需将指针置于新类即可;而对于基本数据类型来说,则需在自己的类中定义它们。如下所示(若执行该程序时有麻烦,请参见第3章3.1.2小节“赋值”): -``` +``` java //: SprinklerSystem.java // Composition for code reuse package c06; @@ -40,15 +40,15 @@ public class SprinklerSystem { WaterSource内定义的一个方法是比较特别的:toString()。大家不久就会知道,每种非基本类型的对象都有一个toString()方法。若编译器本来希望一个String,但却获得某个这样的对象,就会调用这个方法。所以在下面这个表达式中: -``` +``` java System.out.println("source = " + source) ; ``` 编译器会发现我们试图向一个WaterSource添加一个String对象("source =")。这对它来说是不可接受的,因为我们只能将一个字串“添加”到另一个字串,所以它会说:“我要调用toString(),把source转换成字串!”经这样处理后,它就能编译两个字串,并将结果字串传递给一个System.out.println()。每次随同自己创建的一个类允许这种行为的时候,都只需要写一个toString()方法。 -如果不深究,可能会草率地认为编译器会为上述代码中的每个句柄都自动构造对象(由于Java的安全和谨慎的形象)。例如,可能以为它会为WaterSource调用默认构建器,以便初始化source。打印语句的输出事实上是: +如果不深究,可能会草率地认为编译器会为上述代码中的每个指针都自动构造对象(由于Java的安全和谨慎的形象)。例如,可能以为它会为WaterSource调用默认构造器,以便初始化source。打印语句的输出事实上是: -``` +``` java valve1 = null valve2 = null valve3 = null @@ -58,19 +58,19 @@ f = 0.0 source = null ``` -在类内作为字段使用的基本数据会初始化成零,就象第2章指出的那样。但对象句柄会初始化成null。而且假若试图为它们中的任何一个调用方法,就会产生一次“违例”。这种结果实际是相当好的(而且很有用),我们可在不丢弃一次违例的前提下,仍然把它们打印出来。 +在类内作为字段使用的基本数据会初始化成零,就象第2章指出的那样。但对象指针会初始化成null。而且假若试图为它们中的任何一个调用方法,就会产生一次“异常”。这种结果实际是相当好的(而且很有用),我们可在不丢弃一次异常的前提下,仍然把它们打印出来。 -编译器并不只是为每个句柄创建一个默认对象,因为那样会在许多情况下招致不必要的开销。如希望句柄得到初始化,可在下面这些地方进行: +编译器并不只是为每个指针创建一个默认对象,因为那样会在许多情况下招致不必要的开销。如希望指针得到初始化,可在下面这些地方进行: -(1) 在对象定义的时候。这意味着它们在构建器调用之前肯定能得到初始化。 +(1) 在对象定义的时候。这意味着它们在构造器调用之前肯定能得到初始化。 -(2) 在那个类的构建器中。 +(2) 在那个类的构造器中。 (3) 紧靠在要求实际使用那个对象之前。这样做可减少不必要的开销——假如对象并不需要创建的话。 下面向大家展示了所有这三种方法: -``` +``` java //: Bath.java // Constructor initialization with composition @@ -118,10 +118,10 @@ public class Bath { } ///:~ ``` -请注意在Bath构建器中,在所有初始化开始之前执行了一个语句。如果不在定义时进行初始化,仍然不能保证能在将一条消息发给一个对象句柄之前会执行任何初始化——除非出现不可避免的运行期违例。 +请注意在Bath构造器中,在所有初始化开始之前执行了一个语句。如果不在定义时进行初始化,仍然不能保证能在将一条消息发给一个对象指针之前会执行任何初始化——除非出现不可避免的运行期异常。 下面是该程序的输出: -``` +``` java Inside Bath() Soap() s1 = Happy diff --git "a/6.10 \346\200\273\347\273\223.md" "b/6.10 \346\200\273\347\273\223.md" index 1fc1466..1ea314b 100644 --- "a/6.10 \346\200\273\347\273\223.md" +++ "b/6.10 \346\200\273\347\273\223.md" @@ -1,7 +1,7 @@ # 6.10 总结 -无论继承还是合成,我们都可以在现有类型的基础上创建一个新类型。但在典型情况下,我们通过合成来实现现有类型的“再生”或“重复使用”,将其作为新类型基础实施过程的一部分使用。但如果想实现接口的“再生”,就应使用继承。由于衍生或派生出来的类拥有基础类的接口,所以能够将其“上溯造型”为基础类。对于下一章要讲述的多形性问题,这一点是至关重要的。 +无论继承还是合成,我们都可以在现有类型的基础上创建一个新类型。但在典型情况下,我们通过合成来实现现有类型的“再生”或“重复使用”,将其作为新类型基础实施过程的一部分使用。但如果想实现接口的“再生”,就应使用继承。由于衍生或派生出来的类拥有基础类的接口,所以能够将其“上溯造型”为基础类。对于下一章要讲述的多态性问题,这一点是至关重要的。 尽管继承在面向对象的程序设计中得到了特别的强调,但在实际启动一个设计时,最好还是先考虑采用合成技术。只有在特别必要的时候,才应考虑采用继承技术(下一章还会讲到这个问题)。合成显得更加灵活。但是,通过对自己的成员类型应用一些继承技巧,可在运行期准确改变那些成员对象的类型,由此可改变它们的行为。 diff --git "a/6.11 \347\273\203\344\271\240.md" "b/6.11 \347\273\203\344\271\240.md" index 5fedcfe..ce52633 100644 --- "a/6.11 \347\273\203\344\271\240.md" +++ "b/6.11 \347\273\203\344\271\240.md" @@ -1,10 +1,10 @@ # 6.11 练习 -(1) 用默认构建器(空自变量列表)创建两个类:A和B,令它们自己声明自己。从A继承一个名为C的新类,并在C内创建一个成员B。不要为C创建一个构建器。创建类C的一个对象,并观察结果。 +(1) 用默认构造器(空自变量列表)创建两个类:A和B,令它们自己声明自己。从A继承一个名为C的新类,并在C内创建一个成员B。不要为C创建一个构造器。创建类C的一个对象,并观察结果。 -(2) 修改练习1,使A和B都有含有自变量的构建器,则不是采用默认构建器。为C写一个构建器,并在C的构建器中执行所有初始化工作。 +(2) 修改练习1,使A和B都有含有自变量的构造器,则不是采用默认构造器。为C写一个构造器,并在C的构造器中执行所有初始化工作。 -(3) 使用文件Cartoon.java,将Cartoon类的构建器代码变成注释内容标注出去。解释会发生什么事情。 +(3) 使用文件Cartoon.java,将Cartoon类的构造器代码变成注释内容标注出去。解释会发生什么事情。 -(4) 使用文件Chess.java,将Chess类的构建器代码作为注释标注出去。同样解释会发生什么。 \ No newline at end of file +(4) 使用文件Chess.java,将Chess类的构造器代码作为注释标注出去。同样解释会发生什么。 \ No newline at end of file diff --git "a/6.2 \347\273\247\346\211\277\347\232\204\350\257\255\346\263\225.md" "b/6.2 \347\273\247\346\211\277\347\232\204\350\257\255\346\263\225.md" index ee74b8f..b3cb26b 100644 --- "a/6.2 \347\273\247\346\211\277\347\232\204\350\257\255\346\263\225.md" +++ "b/6.2 \347\273\247\346\211\277\347\232\204\350\257\255\346\263\225.md" @@ -5,7 +5,7 @@ 用于合成的语法是非常简单且直观的。但为了进行继承,必须采用一种全然不同的形式。需要继承的时候,我们会说:“这个新类和那个旧类差不多。”为了在代码里表面这一观念,需要给出类名。但在类主体的起始花括号之前,需要放置一个关键字extends,在后面跟随“基础类”的名字。若采取这种做法,就可自动获得基础类的所有数据成员以及方法。下面是一个例子: -``` +``` java //: Detergent.java // Inheritance syntax & properties @@ -45,7 +45,7 @@ public class Detergent extends Cleanser { } ///:~ ``` -这个例子向大家展示了大量特性。首先,在Cleanser append()方法里,字串同一个s连接起来。这是用“+=”运算符实现的。同“+”一样,“+=”被Java用于对字串进行“过载”处理。 +这个例子向大家展示了大量特性。首先,在Cleanser append()方法里,字串同一个s连接起来。这是用“+=”运算符实现的。同“+”一样,“+=”被Java用于对字串进行“重载”处理。 其次,无论Cleanser还是Detergent都包含了一个main()方法。我们可为自己的每个类都创建一个main()。通常建议大家象这样进行编写代码,使自己的测试代码能够封装到类内。即便在程序中含有数量众多的类,但对于在命令行请求的public类,只有main()才会得到调用。所以在这种情况下,当我们使用“java Detergent”的时候,调用的是Degergent.main()——即使Cleanser并非一个public类。采用这种将main()置入每个类的做法,可方便地为每个类都进行单元测试。而且在完成测试以后,毋需将main()删去;可把它保留下来,用于以后的测试。 @@ -65,9 +65,9 @@ public class Detergent extends Cleanser { 由于这儿涉及到两个类——基础类及衍生类,而不再是以前的一个,所以在想象衍生类的结果对象时,可能会产生一些迷惑。从外部看,似乎新类拥有与基础类相同的接口,而且可包含一些额外的方法和字段。但继承并非仅仅简单地复制基础类的接口了事。创建衍生类的一个对象时,它在其中包含了基础类的一个“子对象”。这个子对象就象我们根据基础类本身创建了它的一个对象。从外部看,基础类的子对象已封装到衍生类的对象里了。 -当然,基础类子对象应该正确地初始化,而且只有一种方法能保证这一点:在构建器中执行初始化,通过调用基础类构建器,后者有足够的能力和权限来执行对基础类的初始化。在衍生类的构建器中,Java会自动插入对基础类构建器的调用。下面这个例子向大家展示了对这种三级继承的应用: +当然,基础类子对象应该正确地初始化,而且只有一种方法能保证这一点:在构造器中执行初始化,通过调用基础类构造器,后者有足够的能力和权限来执行对基础类的初始化。在衍生类的构造器中,Java会自动插入对基础类构造器的调用。下面这个例子向大家展示了对这种三级继承的应用: -``` +``` java //: Cartoon.java // Constructor calls during inheritance @@ -95,20 +95,20 @@ public class Cartoon extends Drawing { 该程序的输出显示了自动调用: -``` +``` java Art constructor Drawing constructor Cartoon constructor ``` 可以看出,构建是在基础类的“外部”进行的,所以基础类会在衍生类访问它之前得到正确的初始化。 -即使没有为Cartoon()创建一个构建器,编译器也会为我们自动合成一个默认构建器,并发出对基础类构建器的调用。 +即使没有为Cartoon()创建一个构造器,编译器也会为我们自动合成一个默认构造器,并发出对基础类构造器的调用。 -1. 含有自变量的构建器 +1. 含有自变量的构造器 -上述例子有自己默认的构建器;也就是说,它们不含任何自变量。编译器可以很容易地调用它们,因为不存在具体传递什么自变量的问题。如果类没有默认的自变量,或者想调用含有一个自变量的某个基础类构建器,必须明确地编写对基础类的调用代码。这是用super关键字以及适当的自变量列表实现的,如下所示: +上述例子有自己默认的构造器;也就是说,它们不含任何自变量。编译器可以很容易地调用它们,因为不存在具体传递什么自变量的问题。如果类没有默认的自变量,或者想调用含有一个自变量的某个基础类构造器,必须明确地编写对基础类的调用代码。这是用super关键字以及适当的自变量列表实现的,如下所示: -``` +``` java //: Chess.java // Inheritance, constructors and arguments @@ -136,8 +136,8 @@ public class Chess extends BoardGame { } ///:~ ``` -如果不调用BoardGames()内的基础类构建器,编译器就会报告自己找不到Games()形式的一个构建器。除此以外,在衍生类构建器中,对基础类构建器的调用是必须做的第一件事情(如操作失当,编译器会向我们指出)。 +如果不调用BoardGames()内的基础类构造器,编译器就会报告自己找不到Games()形式的一个构造器。除此以外,在衍生类构造器中,对基础类构造器的调用是必须做的第一件事情(如操作失当,编译器会向我们指出)。 -2. 捕获基本构建器的违例 +2. 捕获基本构造器的异常 -正如刚才指出的那样,编译器会强迫我们在衍生类构建器的主体中首先设置对基础类构建器的调用。这意味着在它之前不能出现任何东西。正如大家在第9章会看到的那样,这同时也会防止衍生类构建器捕获来自一个基础类的任何违例事件。显然,这有时会为我们造成不便。 +正如刚才指出的那样,编译器会强迫我们在衍生类构造器的主体中首先设置对基础类构造器的调用。这意味着在它之前不能出现任何东西。正如大家在第9章会看到的那样,这同时也会防止衍生类构造器捕获来自一个基础类的任何异常事件。显然,这有时会为我们造成不便。 diff --git "a/6.3 \345\220\210\346\210\220\344\270\216\347\273\247\346\211\277\347\232\204\347\273\223\345\220\210.md" "b/6.3 \345\220\210\346\210\220\344\270\216\347\273\247\346\211\277\347\232\204\347\273\223\345\220\210.md" index a3e27e1..7eea1f0 100644 --- "a/6.3 \345\220\210\346\210\220\344\270\216\347\273\247\346\211\277\347\232\204\347\273\223\345\220\210.md" +++ "b/6.3 \345\220\210\346\210\220\344\270\216\347\273\247\346\211\277\347\232\204\347\273\223\345\220\210.md" @@ -1,9 +1,9 @@ # 6.3 合成与继承的结合 -许多时候都要求将合成与继承两种技术结合起来使用。下面这个例子展示了如何同时采用继承与合成技术,从而创建一个更复杂的类,同时进行必要的构建器初始化工作: +许多时候都要求将合成与继承两种技术结合起来使用。下面这个例子展示了如何同时采用继承与合成技术,从而创建一个更复杂的类,同时进行必要的构造器初始化工作: -``` +``` java //: PlaceSetting.java // Combining composition & inheritance @@ -75,17 +75,17 @@ public class PlaceSetting extends Custom { } ///:~ ``` -尽管编译器会强迫我们对基础类进行初始化,并要求我们在构建器最开头做这一工作,但它并不会监视我们是否正确初始化了成员对象。所以对此必须特别加以留意。 +尽管编译器会强迫我们对基础类进行初始化,并要求我们在构造器最开头做这一工作,但它并不会监视我们是否正确初始化了成员对象。所以对此必须特别加以留意。 6.3.1 确保正确的清除 Java不具备象C++的“破坏器”那样的概念。在C++中,一旦破坏(清除)一个对象,就会自动调用破坏器方法。之所以将其省略,大概是由于在Java中只需简单地忘记对象,不需强行破坏它们。垃圾收集器会在必要的时候自动回收内存。 -垃圾收集器大多数时候都能很好地工作,但在某些情况下,我们的类可能在自己的存在时期采取一些行动,而这些行动要求必须进行明确的清除工作。正如第4章已经指出的那样,我们并不知道垃圾收集器什么时候才会显身,或者说不知它何时会调用。所以一旦希望为一个类清除什么东西,必须写一个特别的方法,明确、专门地来做这件事情。同时,还要让客户程序员知道他们必须调用这个方法。而在所有这一切的后面,就如第9章(违例控制)要详细解释的那样,必须将这样的清除代码置于一个finally从句中,从而防范任何可能出现的违例事件。 +垃圾收集器大多数时候都能很好地工作,但在某些情况下,我们的类可能在自己的存在时期采取一些行动,而这些行动要求必须进行明确的清除工作。正如第4章已经指出的那样,我们并不知道垃圾收集器什么时候才会显身,或者说不知它何时会调用。所以一旦希望为一个类清除什么东西,必须写一个特别的方法,明确、专门地来做这件事情。同时,还要让客户程序员知道他们必须调用这个方法。而在所有这一切的后面,就如第9章(异常控制)要详细解释的那样,必须将这样的清除代码置于一个finally从句中,从而防范任何可能出现的异常事件。 下面介绍的是一个计算机辅助设计系统的例子,它能在屏幕上描绘图形: -``` +``` java //: CADSystem.java // Ensuring proper cleanup import java.util.*; @@ -168,9 +168,9 @@ public class CADSystem extends Shape { } ///:~ ``` -这个系统中的所有东西都属于某种Shape(几何形状)。Shape本身是一种Object(对象),因为它是从根类明确继承的。每个类都重新定义了Shape的cleanup()方法,同时还要用super调用那个方法的基础类版本。尽管对象存在期间调用的所有方法都可负责做一些要求清除的工作,但对于特定的Shape类——Circle(圆)、Triangle(三角形)以及Line(直线),它们都拥有自己的构建器,能完成“作图”(draw)任务。每个类都有它们自己的cleanup()方法,用于将非内存的东西恢复回对象存在之前的景象。 +这个系统中的所有东西都属于某种Shape(几何形状)。Shape本身是一种Object(对象),因为它是从根类明确继承的。每个类都重新定义了Shape的cleanup()方法,同时还要用super调用那个方法的基础类版本。尽管对象存在期间调用的所有方法都可负责做一些要求清除的工作,但对于特定的Shape类——Circle(圆)、Triangle(三角形)以及Line(直线),它们都拥有自己的构造器,能完成“作图”(draw)任务。每个类都有它们自己的cleanup()方法,用于将非内存的东西恢复回对象存在之前的景象。 -在main()中,可看到两个新关键字:try和finally。我们要到第9章才会向大家正式引荐它们。其中,try关键字指出后面跟随的块(由花括号定界)是一个“警戒区”。也就是说,它会受到特别的待遇。其中一种待遇就是:该警戒区后面跟随的finally从句的代码肯定会得以执行——不管try块到底存不存在(通过违例控制技术,try块可有多种不寻常的应用)。在这里,finally从句的意思是“总是为x调用cleanup(),无论会发生什么事情”。这些关键字将在第9章进行全面、完整的解释。 +在main()中,可看到两个新关键字:try和finally。我们要到第9章才会向大家正式引荐它们。其中,try关键字指出后面跟随的块(由花括号定界)是一个“警戒区”。也就是说,它会受到特别的待遇。其中一种待遇就是:该警戒区后面跟随的finally从句的代码肯定会得以执行——不管try块到底存不存在(通过异常控制技术,try块可有多种不寻常的应用)。在这里,finally从句的意思是“总是为x调用cleanup(),无论会发生什么事情”。这些关键字将在第9章进行全面、完整的解释。 在自己的清除方法中,必须注意对基础类以及成员对象清除方法的调用顺序——假若一个子对象要以另一个为基础。通常,应采取与C++编译器对它的“破坏器”采取的同样的形式:首先完成与类有关的所有特殊工作(可能要求基础类元素仍然可见),然后调用基础类清除方法,就象这儿演示的那样。 @@ -182,9 +182,9 @@ public class CADSystem extends Shape { 6.3.2 名字的隐藏 -只有C++程序员可能才会惊讶于名字的隐藏,因为它的工作原理与在C++里是完全不同的。如果Java基础类有一个方法名被“过载”使用多次,在衍生类里对那个方法名的重新定义就不会隐藏任何基础类的版本。所以无论方法在这一级还是在一个基础类中定义,过载都会生效: +只有C++程序员可能才会惊讶于名字的隐藏,因为它的工作原理与在C++里是完全不同的。如果Java基础类有一个方法名被“重载”使用多次,在衍生类里对那个方法名的重新定义就不会隐藏任何基础类的版本。所以无论方法在这一级还是在一个基础类中定义,重载都会生效: -``` +``` java //: Hide.java // Overloading a base-class method name // in a derived class does not hide the diff --git "a/6.4 \345\210\260\345\272\225\351\200\211\346\213\251\345\220\210\346\210\220\350\277\230\346\230\257\347\273\247\346\211\277.md" "b/6.4 \345\210\260\345\272\225\351\200\211\346\213\251\345\220\210\346\210\220\350\277\230\346\230\257\347\273\247\346\211\277.md" index 05c04c6..d337ab3 100644 --- "a/6.4 \345\210\260\345\272\225\351\200\211\346\213\251\345\220\210\346\210\220\350\277\230\346\230\257\347\273\247\346\211\277.md" +++ "b/6.4 \345\210\260\345\272\225\351\200\211\346\213\251\345\220\210\346\210\220\350\277\230\346\230\257\347\273\247\346\211\277.md" @@ -6,7 +6,7 @@ 有些时候,我们想让类用户直接访问新类的合成。也就是说,需要将成员对象的属性变为public。成员对象会将自身隐藏起来,所以这是一种安全的做法。而且在用户知道我们准备合成一系列组件时,接口就更容易理解。car(汽车)对象便是一个很好的例子: -``` +``` java //: Car.java // Composition with public objects diff --git a/6.5 protected.md b/6.5 protected.md index 4d6c092..9ac1788 100644 --- a/6.5 protected.md +++ b/6.5 protected.md @@ -4,7 +4,7 @@ 我们采取的最好的做法是保持成员的private状态——无论如何都应保留对基 础的实施细节进行修改的权利。在这一前提下,可通过protected方法允许类的继承者进行受到控制的访问: -``` +``` java //: Orc.java // The protected keyword import java.util.*; diff --git "a/6.6 \347\264\257\347\247\257\345\274\200\345\217\221.md" "b/6.6 \347\264\257\347\247\257\345\274\200\345\217\221.md" index 4d2cfaa..4d10e09 100644 --- "a/6.6 \347\264\257\347\247\257\345\274\200\345\217\221.md" +++ "b/6.6 \347\264\257\347\247\257\345\274\200\345\217\221.md" @@ -4,7 +4,7 @@ 继承的一个好处是它支持“累积开发”,允许我们引入新的代码,同时不会为现有代码造成错误。这样可将新错误隔离到新代码里。通过从一个现成的、功能性的类继承,同时增添成员新的数据成员及方法(并重新定义现有方法),我们可保持现有代码原封不动(另外有人也许仍在使用它),不会为其引入自己的编程错误。一旦出现错误,就知道它肯定是由于自己的新代码造成的。这样一来,与修改现有代码的主体相比,改正错误所需的时间和精力就可以少很多。 类的隔离效果非常好,这是许多程序员事先没有预料到的。甚至不需要方法的源代码来实现代码的再生。最多只需要导入一个包(这对于继承和合并都是成立的)。 -``` +``` java 大家要记住这样一个重点:程序开发是一个不断递增或者累积的过程,就象人们学习知识一样。当然可根据要求进行尽可能多的分析,但在一个项目的设计之初,谁都不可能提前获知所有的答案。如果能将自己的项目看作一个有机的、能不断进步的生物,从而不断地发展和改进它,就有望获得更大的成功以及更直接的反馈。 尽管继承是一种非常有用的技术,但在某些情况下,特别是在项目稳定下来以后,仍然需要从新的角度考察自己的类结构,将其收缩成一个更灵活的结构。请记住,继承是对一种特殊关系的表达,意味着“这个新类属于那个旧类的一种类型”。我们的程序不应纠缠于一些细树末节,而应着眼于创建和操作各种类型的对象,用它们表达出来自“问题空间”的一个模型。 diff --git "a/6.7 \344\270\212\346\272\257\351\200\240\345\236\213.md" "b/6.7 \344\270\212\346\272\257\351\200\240\345\236\213.md" index 05afe21..6bbbeda 100644 --- "a/6.7 \344\270\212\346\272\257\351\200\240\345\236\213.md" +++ "b/6.7 \344\270\212\346\272\257\351\200\240\345\236\213.md" @@ -5,7 +5,7 @@ 这种表达并不仅仅是对继承的一种形象化解释,继承是直接由语言提供支持的。作为一个例子,大家可考虑一个名为Instrument的基础类,它用于表示乐器;另一个衍生类叫作Wind。由于继承意味着基础类的所有方法亦可在衍生出来的类中使用,所以我们发给基础类的任何消息亦可发给衍生类。若Instrument类有一个play()方法,则Wind设备也会有这个方法。这意味着我们能肯定地认为一个Wind对象也是Instrument的一种类型。下面这个例子揭示出编译器如何提供对这一概念的支持: -``` +``` java //: Wind.java // Inheritance & upcasting import java.util.*; @@ -28,7 +28,7 @@ class Wind extends Instrument { } ///:~ ``` -这个例子中最有趣的无疑是tune()方法,它能接受一个Instrument句柄。但在Wind.main()中,tune()方法是通过为其赋予一个Wind句柄来调用的。由于Java对类型检查特别严格,所以大家可能会感到很奇怪,为什么接收一种类型的方法也能接收另一种类型呢?但是,我们一定要认识到一个Wind对象也是一个Instrument对象。而且对于不在Wind中的一个Instrument(乐器),没有方法可以由tune()调用。在tune()中,代码适用于Instrument以及从Instrument衍生出来的任何东西。在这里,我们将从一个Wind句柄转换成一个Instrument句柄的行为叫作“上溯造型”。 +这个例子中最有趣的无疑是tune()方法,它能接受一个Instrument指针。但在Wind.main()中,tune()方法是通过为其赋予一个Wind指针来调用的。由于Java对类型检查特别严格,所以大家可能会感到很奇怪,为什么接收一种类型的方法也能接收另一种类型呢?但是,我们一定要认识到一个Wind对象也是一个Instrument对象。而且对于不在Wind中的一个Instrument(乐器),没有方法可以由tune()调用。在tune()中,代码适用于Instrument以及从Instrument衍生出来的任何东西。在这里,我们将从一个Wind指针转换成一个Instrument指针的行为叫作“上溯造型”。 6.7.1 何谓“上溯造型”? @@ -40,4 +40,4 @@ class Wind extends Instrument { 1. 再论合成与继承 -在面向对象的程序设计中,创建和使用代码最可能采取的一种做法是:将数据和方法统一封装到一个类里,并且使用那个类的对象。有些时候,需通过“合成”技术用现成的类来构造新类。而继承是最少见的一种做法。因此,尽管继承在学习OOP的过程中得到了大量的强调,但并不意味着应该尽可能地到处使用它。相反,使用它时要特别慎重。只有在清楚知道继承在所有方法中最有效的前提下,才可考虑它。为判断自己到底应该选用合成还是继承,一个最简单的办法就是考虑是否需要从新类上溯造型回基础类。若必须上溯,就需要继承。但如果不需要上溯造型,就应提醒自己防止继承的滥用。在下一章里(多形性),会向大家介绍必须进行上溯造型的一种场合。但只要记住经常问自己“我真的需要上溯造型吗”,对于合成还是继承的选择就不应该是个太大的问题。 +在面向对象的程序设计中,创建和使用代码最可能采取的一种做法是:将数据和方法统一封装到一个类里,并且使用那个类的对象。有些时候,需通过“合成”技术用现成的类来构造新类。而继承是最少见的一种做法。因此,尽管继承在学习OOP的过程中得到了大量的强调,但并不意味着应该尽可能地到处使用它。相反,使用它时要特别慎重。只有在清楚知道继承在所有方法中最有效的前提下,才可考虑它。为判断自己到底应该选用合成还是继承,一个最简单的办法就是考虑是否需要从新类上溯造型回基础类。若必须上溯,就需要继承。但如果不需要上溯造型,就应提醒自己防止继承的滥用。在下一章里(多态性),会向大家介绍必须进行上溯造型的一种场合。但只要记住经常问自己“我真的需要上溯造型吗”,对于合成还是继承的选择就不应该是个太大的问题。 diff --git "a/6.8 final\345\205\263\351\224\256\345\255\227.md" "b/6.8 final\345\205\263\351\224\256\345\255\227.md" index 6ed5020..0f8e417 100644 --- "a/6.8 final\345\205\263\351\224\256\345\255\227.md" +++ "b/6.8 final\345\205\263\351\224\256\345\255\227.md" @@ -17,11 +17,11 @@ 无论static还是final字段,都只能存储一个数据,而且不得改变。 -若随同对象句柄使用final,而不是基本数据类型,它的含义就稍微让人有点儿迷糊了。对于基本数据类型,final会将值变成一个常数;但对于对象句柄,final会将句柄变成一个常数。进行声明时,必须将句柄初始化到一个具体的对象。而且永远不能将句柄变成指向另一个对象。然而,对象本身是可以修改的。Java对此未提供任何手段,可将一个对象直接变成一个常数(但是,我们可自己编写一个类,使其中的对象具有“常数”效果)。这一限制也适用于数组,它也属于对象。 +若随同对象指针使用final,而不是基本数据类型,它的含义就稍微让人有点儿迷糊了。对于基本数据类型,final会将值变成一个常数;但对于对象指针,final会将指针变成一个常数。进行声明时,必须将指针初始化到一个具体的对象。而且永远不能将指针变成指向另一个对象。然而,对象本身是可以修改的。Java对此未提供任何手段,可将一个对象直接变成一个常数(但是,我们可自己编写一个类,使其中的对象具有“常数”效果)。这一限制也适用于数组,它也属于对象。 下面是演示final字段用法的一个例子: -``` +``` java //: FinalData.java // The effect of final on fields @@ -76,7 +76,7 @@ public class FinalData { 不能由于某样东西的属性是final,就认定它的值能在编译时期知道。i4和i5向大家证明了这一点。它们在运行期间使用随机生成的数字。例子的这一部分也向大家揭示出将final值设为static和非static之间的差异。只有当值在运行期间初始化的前提下,这种差异才会揭示出来。因为编译期间的值被编译器认为是相同的。这种差异可从输出结果中看出: -``` +``` java fd1: i4 = 15, i5 = 9 Creating new FinalData fd1: i4 = 15, i5 = 9 @@ -85,13 +85,13 @@ fd2: i4 = 10, i5 = 9 注意对于fd1和fd2来说,i4的值是唯一的,但i5的值不会由于创建了另一个FinalData对象而发生改变。那是因为它的属性是static,而且在载入时初始化,而非每创建一个对象时初始化。 -从v1到v4的变量向我们揭示出final句柄的含义。正如大家在main()中看到的那样,并不能认为由于v2属于final,所以就不能再改变它的值。然而,我们确实不能再将v2绑定到一个新对象,因为它的属性是final。这便是final对于一个句柄的确切含义。我们会发现同样的含义亦适用于数组,后者只不过是另一种类型的句柄而已。将句柄变成final看起来似乎不如将基本数据类型变成final那么有用。 +从v1到v4的变量向我们揭示出final指针的含义。正如大家在main()中看到的那样,并不能认为由于v2属于final,所以就不能再改变它的值。然而,我们确实不能再将v2绑定到一个新对象,因为它的属性是final。这便是final对于一个指针的确切含义。我们会发现同样的含义亦适用于数组,后者只不过是另一种类型的指针而已。将指针变成final看起来似乎不如将基本数据类型变成final那么有用。 2. 空白final Java 1.1允许我们创建“空白final”,它们属于一些特殊的字段。尽管被声明成final,但却未得到一个初始值。无论在哪种情况下,空白final都必须在实际使用前得到正确的初始化。而且编译器会主动保证这一规定得以贯彻。然而,对于final关键字的各种应用,空白final具有最大的灵活性。举个例子来说,位于类内部的一个final字段现在对每个对象都可以有所不同,同时依然保持其“不变”的本质。下面列出一个例子: -``` +``` java //: BlankFinal.java // "Blank" final data members @@ -117,13 +117,13 @@ class BlankFinal { } ///:~ ``` -现在强行要求我们对final进行赋值处理——要么在定义字段时使用一个表达 式,要么在每个构建器中。这样就可以确保final字段在使用前获得正确的初始化。 +现在强行要求我们对final进行赋值处理——要么在定义字段时使用一个表达 式,要么在每个构造器中。这样就可以确保final字段在使用前获得正确的初始化。 3. final自变量 -Java 1.1允许我们将自变量设成final属性,方法是在自变量列表中对它们进行适当的声明。这意味着在一个方法的内部,我们不能改变自变量句柄指向的东西。如下所示: +Java 1.1允许我们将自变量设成final属性,方法是在自变量列表中对它们进行适当的声明。这意味着在一个方法的内部,我们不能改变自变量指针指向的东西。如下所示: -``` +``` java //: FinalArguments.java // Using "final" with method arguments @@ -151,14 +151,14 @@ public class FinalArguments { } ///:~ ``` -注意此时仍然能为final自变量分配一个null(空)句柄,同时编译器不会捕获它。这与我们对非final自变量采取的操作是一样的。 +注意此时仍然能为final自变量分配一个null(空)指针,同时编译器不会捕获它。这与我们对非final自变量采取的操作是一样的。 方法f()和g()向我们展示出基本类型的自变量为final时会发生什么情况:我们只能读取自变量,不可改变它。 6.8.2 final方法 之所以要使用final方法,可能是出于对两方面理由的考虑。第一个是为方法“上锁”,防止任何继承类改变它的本来含义。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可以采取这种做法。 -采用final方法的第二个理由是程序执行的效率。将一个方法设成final后,编译器就可以把对那个方法的所有调用都置入“嵌入”调用里。只要编译器发现一个final方法调用,就会(根据它自己的判断)忽略为执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除堆栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。这样做可避免方法调用时的系统开销。当然,若方法体积太大,那么程序也会变得雍肿,可能受到到不到嵌入代码所带来的任何性能提升。因为任何提升都被花在方法内部的时间抵消了。Java编译器能自动侦测这些情况,并颇为“明智”地决定是否嵌入一个final方法。然而,最好还是不要完全相信编译器能正确地作出所有判断。通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。 +采用final方法的第二个理由是程序执行的效率。将一个方法设成final后,编译器就可以把对那个方法的所有调用都置入“嵌入”调用里。只要编译器发现一个final方法调用,就会(根据它自己的判断)忽略为执行方法调用机制而采取的常规代码插入方法(将自变量压入栈;跳至方法代码并执行它;跳回来;清除栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。这样做可避免方法调用时的系统开销。当然,若方法体积太大,那么程序也会变得雍肿,可能受到到不到嵌入代码所带来的任何性能提升。因为任何提升都被花在方法内部的时间抵消了。Java编译器能自动侦测这些情况,并颇为“明智”地决定是否嵌入一个final方法。然而,最好还是不要完全相信编译器能正确地作出所有判断。通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。 类内所有private方法都自动成为final。由于我们不能访问一个private方法,所以它绝对不会被其他方法覆盖(若强行这样做,编译器会给出错误提示)。可为一个private方法添加final指示符,但却不能为那个方法提供任何额外的含义。 @@ -168,7 +168,7 @@ public class FinalArguments { 除此以外,我们或许还考虑到执行效率的问题,并想确保涉及这个类各对象的所有行动都要尽可能地有效。如下所示: -``` +``` java //: Jurassic.java // Making an entire class final @@ -204,6 +204,6 @@ public class Jurassic { 但要慎重作出自己的假定。通常,我们很难预测一个类以后会以什么样的形式再生或重复利用。常规用途的类尤其如此。若将一个方法定义成final,就可能杜绝了在其他程序员的项目中对自己的类进行继承的途径,因为我们根本没有想到它会象那样使用。 -标准Java库是阐述这一观点的最好例子。其中特别常用的一个类是Vector。如果我们考虑代码的执行效率,就会发现只有不把任何方法设为final,才能使其发挥更大的作用。我们很容易就会想到自己应继承和覆盖如此有用的一个类,但它的设计者却否定了我们的想法。但我们至少可以用两个理由来反驳他们。首先,Stack(堆栈)是从Vector继承来的,亦即Stack“是”一个Vector,这种说法是不确切的。其次,对于Vector许多重要的方法,如addElement()以及elementAt()等,它们都变成了synchronized(同步的)。正如在第14章要讲到的那样,这会造成显著的性能开销,可能会把final提供的性能改善抵销得一干二净。因此,程序员不得不猜测到底应该在哪里进行优化。在标准库里居然采用了如此笨拙的设计,真不敢想象会在程序员里引发什么样的情绪。 +标准Java库是阐述这一观点的最好例子。其中特别常用的一个类是Vector。如果我们考虑代码的执行效率,就会发现只有不把任何方法设为final,才能使其发挥更大的作用。我们很容易就会想到自己应继承和覆盖如此有用的一个类,但它的设计者却否定了我们的想法。但我们至少可以用两个理由来反驳他们。首先,Stack(栈)是从Vector继承来的,亦即Stack“是”一个Vector,这种说法是不确切的。其次,对于Vector许多重要的方法,如addElement()以及elementAt()等,它们都变成了synchronized(同步的)。正如在第14章要讲到的那样,这会造成显著的性能开销,可能会把final提供的性能改善抵销得一干二净。因此,程序员不得不猜测到底应该在哪里进行优化。在标准库里居然采用了如此笨拙的设计,真不敢想象会在程序员里引发什么样的情绪。 另一个值得注意的是Hashtable(散列表),它是另一个重要的标准类。该类没有采用任何final方法。正如我们在本书其他地方提到的那样,显然一些类的设计人员与其他设计人员有着全然不同的素质(注意比较Hashtable极短的方法名与Vecor的方法名)。对类库的用户来说,这显然是不应该如此轻易就能看出的。一个产品的设计变得不一致后,会加大用户的工作量。这也从另一个侧面强调了代码设计与检查时需要很强的责任心。 diff --git "a/6.9 \345\210\235\345\247\213\345\214\226\345\222\214\347\261\273\350\243\205\350\275\275.md" "b/6.9 \345\210\235\345\247\213\345\214\226\345\222\214\347\261\273\350\243\205\350\275\275.md" index ad446be..3db04ba 100644 --- "a/6.9 \345\210\235\345\247\213\345\214\226\345\222\214\347\261\273\350\243\205\350\275\275.md" +++ "b/6.9 \345\210\235\345\247\213\345\214\226\345\222\214\347\261\273\350\243\205\350\275\275.md" @@ -11,7 +11,7 @@ Java则没有这样的问题,因为它采用了不同的装载方法。由于J 我们有必要对整个初始化过程有所认识,其中包括继承,对这个过程中发生的事情有一个整体性的概念。请观察下述代码: -``` +``` java //: Beetle.java // The full process of initialization. @@ -51,7 +51,7 @@ public class Beetle extends Insect { 该程序的输出如下: -``` +``` java static Insect.x initialized static Beetle.x initialized Beetle constructor @@ -65,4 +65,4 @@ j = 39 若基础类含有另一个基础类,则另一个基础类随即也会载入,以此类推。接下来,会在根基础类(此时是Insect)执行static初始化,再在下一个衍生类执行,以此类推。保证这个顺序是非常关键的,因为衍生类的初始化可能要依赖于对基础类成员的正确初始化。 -此时,必要的类已全部装载完毕,所以能够创建对象。首先,这个对象中的所有基本数据类型都会设成它们的默认值,而将对象句柄设为null。随后会调用基础类构建器。在这种情况下,调用是自动进行的。但也完全可以用super来自行指定构建器调用(就象在Beetle()构建器中的第一个操作一样)。基础类的构建采用与衍生类构建器完全相同的处理过程。基础顺构建器完成以后,实例变量会按本来的顺序得以初始化。最后,执行构建器剩余的主体部分。 +此时,必要的类已全部装载完毕,所以能够创建对象。首先,这个对象中的所有基本数据类型都会设成它们的默认值,而将对象指针设为null。随后会调用基础类构造器。在这种情况下,调用是自动进行的。但也完全可以用super来自行指定构造器调用(就象在Beetle()构造器中的第一个操作一样)。基础类的构建采用与衍生类构造器完全相同的处理过程。基础顺构造器完成以后,实例变量会按本来的顺序得以初始化。最后,执行构造器剩余的主体部分。 diff --git "a/7.1 \344\270\212\346\272\257\351\200\240\345\236\213.md" "b/7.1 \344\270\212\346\272\257\351\200\240\345\236\213.md" index 0bd655c..9e82d8e 100644 --- "a/7.1 \344\270\212\346\272\257\351\200\240\345\236\213.md" +++ "b/7.1 \344\270\212\346\272\257\351\200\240\345\236\213.md" @@ -1,11 +1,11 @@ # 7.1 上溯造型 -在第6章,大家已知道可将一个对象作为它自己的类型使用,或者作为它的基础类型的一个对象使用。取得一个对象句柄,并将其作为基础类型句柄使用的行为就叫作“上溯造型”——因为继承树的画法是基础类位于最上方。 +在第6章,大家已知道可将一个对象作为它自己的类型使用,或者作为它的基础类型的一个对象使用。取得一个对象指针,并将其作为基础类型指针使用的行为就叫作“上溯造型”——因为继承树的画法是基础类位于最上方。 但这样做也会遇到一个问题,如下例所示(若执行这个程序遇到麻烦,请参考第3章的3.1.2小节“赋值”): -``` +``` java //: Music.java // Inheritance & upcasting package c07; @@ -46,13 +46,13 @@ public class Music { } ///:~ ``` -其中,方法Music.tune()接收一个Instrument句柄,同时也接收从Instrument衍生出来的所有东西。当一个Wind句柄传递给tune()的时候,就会出现这种情况。此时没有造型的必要。这样做是可以接受的;Instrument里的接口必须存在于Wind中,因为Wind是从Instrument里继承得到的。从Wind向Instrument的上溯造型可能“缩小”那个接口,但不可能把它变得比Instrument的完整接口还要小。 +其中,方法Music.tune()接收一个Instrument指针,同时也接收从Instrument衍生出来的所有东西。当一个Wind指针传递给tune()的时候,就会出现这种情况。此时没有造型的必要。这样做是可以接受的;Instrument里的接口必须存在于Wind中,因为Wind是从Instrument里继承得到的。从Wind向Instrument的上溯造型可能“缩小”那个接口,但不可能把它变得比Instrument的完整接口还要小。 7.1.1 为什么要上溯造型 -这个程序看起来也许显得有些奇怪。为什么所有人都应该有意忘记一个对象的类型呢?进行上溯造型时,就可能产生这方面的疑惑。而且如果让tune()简单地取得一个Wind句柄,将其作为自己的自变量使用,似乎会更加简单、直观得多。但要注意:假如那样做,就需为系统内Instrument的每种类型写一个全新的tune()。假设按照前面的推论,加入Stringed(弦乐)和Brass(铜管)这两种Instrument(乐器): +这个程序看起来也许显得有些奇怪。为什么所有人都应该有意忘记一个对象的类型呢?进行上溯造型时,就可能产生这方面的疑惑。而且如果让tune()简单地取得一个Wind指针,将其作为自己的自变量使用,似乎会更加简单、直观得多。但要注意:假如那样做,就需为系统内Instrument的每种类型写一个全新的tune()。假设按照前面的推论,加入Stringed(弦乐)和Brass(铜管)这两种Instrument(乐器): -``` +``` java //: Music2.java // Overloading instead of upcasting @@ -110,8 +110,8 @@ public class Music2 { } ///:~ ``` -这样做当然行得通,但却存在一个极大的弊端:必须为每种新增的Instrument2类编写与类紧密相关的方法。这意味着第一次就要求多得多的编程量。以后,假如想添加一个象tune()那样的新方法或者为Instrument添加一个新类型,仍然需要进行大量编码工作。此外,即使忘记对自己的某个方法进行过载设置,编译器也不会提示任何错误。这样一来,类型的整个操作过程就显得极难管理,有失控的危险。 +这样做当然行得通,但却存在一个极大的弊端:必须为每种新增的Instrument2类编写与类紧密相关的方法。这意味着第一次就要求多得多的编程量。以后,假如想添加一个象tune()那样的新方法或者为Instrument添加一个新类型,仍然需要进行大量编码工作。此外,即使忘记对自己的某个方法进行重载设置,编译器也不会提示任何错误。这样一来,类型的整个操作过程就显得极难管理,有失控的危险。 但假如只写一个方法,将基础类作为自变量或参数使用,而不是使用那些特定的衍生类,岂不是会简单得多?也就是说,如果我们能不顾衍生类,只让自己的代码与基础类打交道,那么省下的工作量将是难以估计的。 -这正是“多形性”大显身手的地方。然而,大多数程序员(特别是有程序化编程背景的)对于多形性的工作原理仍然显得有些生疏。 +这正是“多态性”大显身手的地方。然而,大多数程序员(特别是有程序化编程背景的)对于多态性的工作原理仍然显得有些生疏。 diff --git "a/7.2 \346\267\261\345\205\245\347\220\206\350\247\243.md" "b/7.2 \346\267\261\345\205\245\347\220\206\350\247\243.md" index 12e831b..33af85d 100644 --- "a/7.2 \346\267\261\345\205\245\347\220\206\350\247\243.md" +++ "b/7.2 \346\267\261\345\205\245\347\220\206\350\247\243.md" @@ -3,20 +3,20 @@ 对于Music.java的困难性,可通过运行程序加以体会。输出是Wind.play()。这当然是我们希望的输出,但它看起来似乎并不愿按我们的希望行事。请观察一下tune()方法: -``` +``` java public static void tune(Instrument i) { // ... i.play(Note.middleC); } ``` -它接收Instrument句柄。所以在这种情况下,编译器怎样才能知道Instrument句柄指向的是一个Wind,而不是一个Brass或Stringed呢?编译器无从得知。为了深入了理解这个问题,我们有必要探讨一下“绑定”这个主题。 +它接收Instrument指针。所以在这种情况下,编译器怎样才能知道Instrument指针指向的是一个Wind,而不是一个Brass或Stringed呢?编译器无从得知。为了深入了理解这个问题,我们有必要探讨一下“绑定”这个主题。 7.2.1 方法调用的绑定 将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。若在程序运行以前执行绑定(由编译器和链接程序,如果有的话),就叫作“早期绑定”。大家以前或许从未听说过这个术语,因为它在任何程序化语言里都是不可能的。C编译器只有一种方法调用,那就是“早期绑定”。 -上述程序最令人迷惑不解的地方全与早期绑定有关,因为在只有一个Instrument句柄的前提下,编译器不知道具体该调用哪个方法。 +上述程序最令人迷惑不解的地方全与早期绑定有关,因为在只有一个Instrument指针的前提下,编译器不知道具体该调用哪个方法。 解决的方法就是“后期绑定”,它意味着绑定在运行期间进行,以对象的类型为基础。后期绑定也叫作“动态绑定”或“运行期绑定”。若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。 @@ -26,7 +26,7 @@ Java中绑定的所有方法都采用后期绑定技术,除非一个方法已 7.2.2 产生正确的行为 -知道Java里绑定的所有方法都通过后期绑定具有多形性以后,就可以相应地编写自己的代码,令其与基础类沟通。此时,所有的衍生类都保证能用相同的代码正常地工作。或者换用另一种方法,我们可以“将一条消息发给一个对象,让对象自行判断要做什么事情。” +知道Java里绑定的所有方法都通过后期绑定具有多态性以后,就可以相应地编写自己的代码,令其与基础类沟通。此时,所有的衍生类都保证能用相同的代码正常地工作。或者换用另一种方法,我们可以“将一条消息发给一个对象,让对象自行判断要做什么事情。” 在面向对象的程序设计中,有一个经典的“形状”例子。由于它很容易用可视化的形式表现出来,所以经常都用它说明问题。但很不幸的是,它可能误导初学者认为OOP只是为图形化编程设计的,这种认识当然是错误的。 @@ -36,21 +36,21 @@ Java中绑定的所有方法都采用后期绑定技术,除非一个方法已 上溯造型可用下面这个语句简单地表现出来: -``` +``` java Shape s = new Circle(); ``` -在这里,我们创建了Circle对象,并将结果句柄立即赋给一个Shape。这表面看起来似乎属于错误操作(将一种类型分配给另一个),但实际是完全可行的——因为按照继承关系,Circle属于Shape的一种。因此编译器认可上述语句,不会向我们提示一条出错消息。 +在这里,我们创建了Circle对象,并将结果指针立即赋给一个Shape。这表面看起来似乎属于错误操作(将一种类型分配给另一个),但实际是完全可行的——因为按照继承关系,Circle属于Shape的一种。因此编译器认可上述语句,不会向我们提示一条出错消息。 当我们调用其中一个基础类方法时(已在衍生类里覆盖): -``` +``` java s.draw(); ``` -同样地,大家也许认为会调用Shape的draw(),因为这毕竟是一个Shape句柄。那么编译器怎样才能知道该做其他任何事情呢?但此时实际调用的是Circle.draw(),因为后期绑定已经介入(多形性)。 +同样地,大家也许认为会调用Shape的draw(),因为这毕竟是一个Shape指针。那么编译器怎样才能知道该做其他任何事情呢?但此时实际调用的是Circle.draw(),因为后期绑定已经介入(多态性)。 下面这个例子从一个稍微不同的角度说明了问题: -``` +``` java //: Shapes.java // Polymorphism in Java @@ -109,11 +109,11 @@ public class Shapes { 针对从Shape衍生出来的所有东西,Shape建立了一个通用接口——也就是说,所有(几何)形状都可以描绘和删除。衍生类覆盖了这些定义,为每种特殊类型的几何形状都提供了独一无二的行为。 -在主类Shapes里,包含了一个static方法,名为randShape()。它的作用是在每次调用它时为某个随机选择的Shape对象生成一个句柄。请注意上溯造型是在每个return语句里发生的。这个语句取得指向一个Circle,Square或者Triangle的句柄,并将其作为返回类型Shape发给方法。所以无论什么时候调用这个方法,就绝对没机会了解它的具体类型到底是什么,因为肯定会获得一个单纯的Shape句柄。 +在主类Shapes里,包含了一个static方法,名为randShape()。它的作用是在每次调用它时为某个随机选择的Shape对象生成一个指针。请注意上溯造型是在每个return语句里发生的。这个语句取得指向一个Circle,Square或者Triangle的指针,并将其作为返回类型Shape发给方法。所以无论什么时候调用这个方法,就绝对没机会了解它的具体类型到底是什么,因为肯定会获得一个单纯的Shape指针。 -main()包含了Shape句柄的一个数组,其中的数据通过对randShape()的调用填入。在这个时候,我们知道自己拥有Shape,但不知除此之外任何具体的情况(编译器同样不知)。然而,当我们在这个数组里步进,并为每个元素调用draw()的时候,与各类型有关的正确行为会魔术般地发生,就象下面这个输出示例展示的那样: +main()包含了Shape指针的一个数组,其中的数据通过对randShape()的调用填入。在这个时候,我们知道自己拥有Shape,但不知除此之外任何具体的情况(编译器同样不知)。然而,当我们在这个数组里步进,并为每个元素调用draw()的时候,与各类型有关的正确行为会魔术般地发生,就象下面这个输出示例展示的那样: -``` +``` java Circle.draw() Triangle.draw() Circle.draw() @@ -129,14 +129,14 @@ Square.draw() 7.2.3 扩展性 -现在,让我们仍然返回乐器(Instrument)示例。由于存在多形性,所以可根据自己的需要向系统里加入任意多的新类型,同时毋需更改true()方法。在一个设计良好的OOP程序中,我们的大多数或者所有方法都会遵从tune()的模型,而且只与基础类接口通信。我们说这样的程序具有“扩展性”,因为可以从通用的基础类继承新的数据类型,从而新添一些功能。如果是为了适应新类的要求,那么对基础类接口进行操纵的方法根本不需要改变, +现在,让我们仍然返回乐器(Instrument)示例。由于存在多态性,所以可根据自己的需要向系统里加入任意多的新类型,同时毋需更改tune()方法。在一个设计良好的OOP程序中,我们的大多数或者所有方法都会遵从tune()的模型,而且只与基础类接口通信。我们说这样的程序具有“扩展性”,因为可以从通用的基础类继承新的数据类型,从而新添一些功能。如果是为了适应新类的要求,那么对基础类接口进行操纵的方法根本不需要改变, 对于乐器例子,假设我们在基础类里加入更多的方法,以及一系列新类,那么会出现什么情况呢?下面是示意图: ![](7-2.gif) 所有这些新类都能与老类——tune()默契地工作,毋需对tune()作任何调整。即使tune()位于一个独立的文件里,而将新方法添加到Instrument的接口,tune()也能正确地工作,不需要重新编译。下面这个程序是对上述示意图的具体实现: -``` +``` java //: Music3.java // An extensible program import java.util.*; @@ -216,8 +216,8 @@ public class Music3 { } ///:~ ``` -新方法是what()和adjust()。前者返回一个String句柄,同时返回对那个类的说明;后者使我们能对每种乐器进行调整。 +新方法是what()和adjust()。前者返回一个String指针,同时返回对那个类的说明;后者使我们能对每种乐器进行调整。 在main()中,当我们将某样东西置入Instrument3数组时,就会自动上溯造型到Instrument3。 -可以看到,在围绕tune()方法的其他所有代码都发生变化的同时,tune()方法却丝毫不受它们的影响,依然故我地正常工作。这正是利用多形性希望达到的目标。我们对代码进行修改后,不会对程序中不应受到影响的部分造成影响。此外,我们认为多形性是一种至关重要的技术,它允许程序员“将发生改变的东西同没有发生改变的东西区分开”。 +可以看到,在围绕tune()方法的其他所有代码都发生变化的同时,tune()方法却丝毫不受它们的影响,依然故我地正常工作。这正是利用多态性希望达到的目标。我们对代码进行修改后,不会对程序中不应受到影响的部分造成影响。此外,我们认为多态性是一种至关重要的技术,它允许程序员“将发生改变的东西同没有发生改变的东西区分开”。 diff --git "a/7.3 \350\246\206\347\233\226\344\270\216\350\277\207\350\275\275.md" "b/7.3 \350\246\206\347\233\226\344\270\216\351\207\215\350\275\275.md" similarity index 79% rename from "7.3 \350\246\206\347\233\226\344\270\216\350\277\207\350\275\275.md" rename to "7.3 \350\246\206\347\233\226\344\270\216\351\207\215\350\275\275.md" index aecc588..9b7c267 100644 --- "a/7.3 \350\246\206\347\233\226\344\270\216\350\277\207\350\275\275.md" +++ "b/7.3 \350\246\206\347\233\226\344\270\216\351\207\215\350\275\275.md" @@ -1,9 +1,9 @@ -# 7.3 覆盖与过载 +# 7.3 覆盖与重载 -现在让我们用不同的眼光来看看本章的头一个例子。在下面这个程序中,方法play()的接口会在被覆盖的过程中发生变化。这意味着我们实际并没有“覆盖”方法,而是使其“过载”。编译器允许我们对方法进行过载处理,使其不报告出错。但这种行为可能并不是我们所希望的。下面是这个例子: +现在让我们用不同的眼光来看看本章的头一个例子。在下面这个程序中,方法play()的接口会在被覆盖的过程中发生变化。这意味着我们实际并没有“覆盖”方法,而是使其“重载”。编译器允许我们对方法进行重载处理,使其不报告出错。但这种行为可能并不是我们所希望的。下面是这个例子: -``` +``` java //: WindError.java // Accidentally changing the interface @@ -20,6 +20,7 @@ class InstrumentX { class WindX extends InstrumentX { // OOPS! Changes the method interface: + @Overload public void play(NoteX n) { System.out.println("WindX.play(NoteX n)"); } @@ -37,12 +38,12 @@ public class WindError { } ///:~ ``` -这里还向大家引入了另一个易于混淆的概念。在InstrumentX中,play()方法采用了一个int(整数)数值,它的标识符是NoteX。也就是说,即使NoteX是一个类名,也可以把它作为一个标识符使用,编译器不会报告出错。但在WindX中,play()采用一个NoteX句柄,它有一个标识符n。即便我们使用“play(NoteX NoteX)”,编译器也不会报告错误。这样一来,看起来就象是程序员有意覆盖play()的功能,但对方法的类型定义却稍微有些不确切。然而,编译器此时假定的是程序员有意进行“过载”,而非“覆盖”。请仔细体会这两个术语的区别。“过载”是指同一样东西在不同的地方具有多种含义;而“覆盖”是指它随时随地都只有一种含义,只是原先的含义完全被后来的含义取代了。请注意如果遵守标准的Java命名规范,自变量标识符就应该是noteX,这样可把它与类名区分开。 +这里还向大家引入了另一个易于混淆的概念。在InstrumentX中,play()方法采用了一个int(整数)数值,它的标识符是NoteX。也就是说,即使NoteX是一个类名,也可以把它作为一个标识符使用,编译器不会报告出错。但在WindX中,play()采用一个NoteX指针,它有一个标识符n。即便我们使用“play(NoteX NoteX)”,编译器也不会报告错误。这样一来,看起来就象是程序员有意覆盖play()的功能,但对方法的类型定义却稍微有些不确切。然而,编译器此时假定的是程序员有意进行“重载”,而非“覆盖”。请仔细体会这两个术语的区别。“重载”是指同一样东西在不同的地方具有多种含义;而“覆盖”是指它随时随地都只有一种含义,只是原先的含义完全被后来的含义取代了。请注意如果遵守标准的Java命名规范,自变量标识符就应该是noteX,这样可把它与类名区分开。 -在tune中,“InstrumentX i”会发出play()消息,同时将某个NoteX成员作为自变量使用(MIDDLE_C)。由于NoteX包含了int定义,过载的play()方法的int版本会得到调用。同时由于它尚未被“覆盖”,所以会使用基础类版本。 +在tune中,“InstrumentX i”会发出play()消息,同时将某个NoteX成员作为自变量使用(MIDDLE_C)。由于NoteX包含了int定义,重载的play()方法的int版本会得到调用。同时由于它尚未被“覆盖”,所以会使用基础类版本。 输出是: -``` +``` java InstrumentX.play() -``` +``` java diff --git "a/7.4 \346\212\275\350\261\241\347\261\273\345\222\214\346\226\271\346\263\225.md" "b/7.4 \346\212\275\350\261\241\347\261\273\345\222\214\346\226\271\346\263\225.md" index bc28d93..19e612c 100644 --- "a/7.4 \346\212\275\350\261\241\347\261\273\345\222\214\346\226\271\346\263\225.md" +++ "b/7.4 \346\212\275\350\261\241\347\261\273\345\222\214\346\226\271\346\263\225.md" @@ -3,13 +3,13 @@ 在我们所有乐器(Instrument)例子中,基础类Instrument内的方法都肯定是“伪”方法。若去调用这些方法,就会出现错误。那是由于Instrument的意图是为从它衍生出去的所有类都创建一个通用接口。 -之所以要建立这个通用接口,唯一的原因就是它能为不同的子类型作出不同的表示。它为我们建立了一种基本形式,使我们能定义在所有衍生类里“通用”的一些东西。为阐述这个观念,另一个方法是把Instrument称为“抽象基础类”(简称“抽象类”)。若想通过该通用接口处理一系列类,就需要创建一个抽象类。对所有与基础类声明的签名相符的衍生类方法,都可以通过动态绑定机制进行调用(然而,正如上一节指出的那样,如果方法名与基础类相同,但自变量或参数不同,就会出现过载现象,那或许并非我们所愿意的)。 +之所以要建立这个通用接口,唯一的原因就是它能为不同的子类型作出不同的表示。它为我们建立了一种基本形式,使我们能定义在所有衍生类里“通用”的一些东西。为阐述这个观念,另一个方法是把Instrument称为“抽象基础类”(简称“抽象类”)。若想通过该通用接口处理一系列类,就需要创建一个抽象类。对所有与基础类声明的签名相符的衍生类方法,都可以通过动态绑定机制进行调用(然而,正如上一节指出的那样,如果方法名与基础类相同,但自变量或参数不同,就会出现重载现象,那或许并非我们所愿意的)。 如果有一个象Instrument那样的抽象类,那个类的对象几乎肯定没有什么意义。换言之,Instrument的作用仅仅是表达接口,而不是表达一些具体的实施细节。所以创建一个Instrument对象是没有意义的,而且我们通常都应禁止用户那样做。为达到这个目的,可令Instrument内的所有方法都显示出错消息。但这样做会延迟信息到运行期,并要求在用户那一面进行彻底、可靠的测试。无论如何,最好的方法都是在编译期间捕捉到问题。 针对这个问题,Java专门提供了一种机制,名为“抽象方法”。它属于一种不完整的方法,只含有一个声明,没有方法主体。下面是抽象方法声明时采用的语法: -``` +``` java abstract void X(); ``` @@ -27,7 +27,7 @@ Instrument类可很轻松地转换成一个抽象类。只有其中一部分方 下面是我们修改过的“管弦”乐器例子,其中采用了抽象类以及方法: -``` +``` java //: Music4.java // Abstract classes and methods import java.util.*; diff --git "a/7.5 \346\216\245\345\217\243.md" "b/7.5 \346\216\245\345\217\243.md" index 97ee705..39654dd 100644 --- "a/7.5 \346\216\245\345\217\243.md" +++ "b/7.5 \346\216\245\345\217\243.md" @@ -17,7 +17,7 @@ 在Instrument例子的修改版本中,大家可明确地看出这一点。注意接口中的每个方法都严格地是一个声明,它是编译器唯一允许的。除此以外,Instrument5中没有一个方法被声明为public,但它们都会自动获得public属性。如下所示: -``` +``` java //: Music5.java // Interfaces import java.util.*; @@ -106,7 +106,7 @@ public class Music5 { 在一个衍生类中,我们并不一定要拥有一个抽象或具体(没有抽象方法)的基础类。如果确实想从一个非接口继承,那么只能从一个继承。剩余的所有基本元素都必须是“接口”。我们将所有接口名置于implements关键字的后面,并用逗号分隔它们。可根据需要使用多个接口,而且每个接口都会成为一个独立的类型,可对其进行上溯造型。下面这个例子展示了一个“具体”类同几个接口合并的情况,它最终生成了一个新类: -``` +``` java //: Adventure.java // Multiple interfaces import java.util.*; @@ -160,7 +160,7 @@ public class Adventure { 利用继承技术,可方便地为一个接口添加新的方法声明,也可以将几个接口合并成一个新接口。在这两种情况下,最终得到的都是一个新接口,如下例所示: -``` +``` java //: HorrorShow.java // Extending an interface with inheritance @@ -207,7 +207,7 @@ Vampire的语法仅在继承接口时才可使用。通常,我们只能对单 由于置入一个接口的所有字段都自动具有static和final属性,所以接口是对常数值进行分组的一个好工具,它具有与C或C++的enum非常相似的效果。如下例所示: -``` +``` java //: Months.java // Using interfaces to create groups of constants package c07; @@ -228,7 +228,7 @@ public interface Months { 现在,通过导入 `c07.*` 或 `c07.Months` ,我们可以从包的外部使用常数——就象对其他任何包进行的操作那样。此外,也可以用类似Months.JANUARY的表达式对值进行引用。当然,我们获得的只是一个int,所以不象C++的enum那样拥有额外的类型安全性。但与将数字强行编码(硬编码)到自己的程序中相比,这种(常用的)技术无疑已经是一个巨大的进步。我们通常把“硬编码”数字的行为称为“魔术数字”,它产生的代码是非常难以维护的。 如确实不想放弃额外的类型安全性,可构建象下面这样的一个类(注释①): -``` +``` java //: Month2.java // A more robust enumeration system package c07; @@ -267,14 +267,14 @@ public final class Month2 { ①:是Rich Hoffarth的一封E-mail触发了我这样编写程序的灵感。 -这个类叫作Month2,因为标准Java库里已经有一个Month。它是一个final类,并含有一个private构建器,所以没有人能从它继承,或制作它的一个实例。唯一的实例就是那些final static对象,它们是在类本身内部创建的,包括:JAN,FEB,MAR等等。这些对象也在month数组中使用,后者让我们能够按数字挑选月份,而不是按名字(注意数组中提供了一个多余的JAN,使偏移量增加了1,也使December确实成为12月)。在main()中,我们可注意到类型的安全性:m是一个Month2对象,所以只能将其分配给Month2。在前面的Months.java例子中,只提供了int值,所以本来想用来代表一个月份的int变量可能实际获得一个整数值,那样做可能不十分安全。 +这个类叫作Month2,因为标准Java库里已经有一个Month。它是一个final类,并含有一个private构造器,所以没有人能从它继承,或制作它的一个实例。唯一的实例就是那些final static对象,它们是在类本身内部创建的,包括:JAN,FEB,MAR等等。这些对象也在month数组中使用,后者让我们能够按数字挑选月份,而不是按名字(注意数组中提供了一个多余的JAN,使偏移量增加了1,也使December确实成为12月)。在main()中,我们可注意到类型的安全性:m是一个Month2对象,所以只能将其分配给Month2。在前面的Months.java例子中,只提供了int值,所以本来想用来代表一个月份的int变量可能实际获得一个整数值,那样做可能不十分安全。 这儿介绍的方法也允许我们交换使用==或者equals(),就象main()尾部展示的那样。 7.5.4 初始化接口中的字段 接口中定义的字段会自动具有static和final属性。它们不能是“空白final”,但可初始化成非常数表达式。例如: -``` +``` java //: RandVals.java // Initializing interface fields with // non-constant initializers @@ -290,7 +290,7 @@ public interface RandVals { 由于字段是static的,所以它们会在首次装载类之后、以及首次访问任何字段之前获得初始化。下面是一个简单的测试: -``` +``` java //: TestRandVals.java public class TestRandVals { diff --git "a/7.6 \345\206\205\351\203\250\347\261\273.md" "b/7.6 \345\206\205\351\203\250\347\261\273.md" index 37466b3..0d1a451 100644 --- "a/7.6 \345\206\205\351\203\250\347\261\273.md" +++ "b/7.6 \345\206\205\351\203\250\347\261\273.md" @@ -7,7 +7,7 @@ 创建内部类的过程是平淡无奇的:将类定义置入一个用于封装它的类内部(若执行这个程序遇到麻烦,请参见第3章的3.1.2小节“赋值”): -``` +``` java //: Parcel1.java // Creating inner classes package c07.parcel1; @@ -39,9 +39,9 @@ public class Parcel1 { 若在ship()内部使用,内部类的使用看起来和其他任何类都没什么分别。在这里,唯一明显的区别就是它的名字嵌套在Parcel1里面。但大家不久就会知道,这其实并非唯一的区别。 -更典型的一种情况是,一个外部类拥有一个特殊的方法,它会返回指向一个内部类的句柄。就象下面这样: +更典型的一种情况是,一个外部类拥有一个特殊的方法,它会返回指向一个内部类的指针。就象下面这样: -``` +``` java //: Parcel2.java // Returning a handle to an inner class package c07.parcel2; @@ -85,9 +85,9 @@ public class Parcel2 { 迄今为止,内部类看起来仍然没什么特别的地方。毕竟,用它实现隐藏显得有些大题小做。Java已经有一个非常优秀的隐藏机制——只允许类成为“友好的”(只在一个包内可见),而不是把它创建成一个内部类。 -然而,当我们准备上溯造型到一个基础类(特别是到一个接口)的时候,内部类就开始发挥其关键作用(从用于实现的对象生成一个接口句柄具有与上溯造型至一个基础类相同的效果)。这是由于内部类随后可完全进入不可见或不可用状态——对任何人都将如此。所以我们可以非常方便地隐藏实施细节。我们得到的全部回报就是一个基础类或者接口的句柄,而且甚至有可能不知道准确的类型。就象下面这样: +然而,当我们准备上溯造型到一个基础类(特别是到一个接口)的时候,内部类就开始发挥其关键作用(从用于实现的对象生成一个接口指针具有与上溯造型至一个基础类相同的效果)。这是由于内部类随后可完全进入不可见或不可用状态——对任何人都将如此。所以我们可以非常方便地隐藏实施细节。我们得到的全部回报就是一个基础类或者接口的指针,而且甚至有可能不知道准确的类型。就象下面这样: -``` +``` java //: Parcel3.java // Returning a handle to an inner class package c07.parcel3; @@ -144,7 +144,7 @@ class Test { 至此,我们已基本理解了内部类的典型用途。对那些涉及内部类的代码,通常表达的都是“单纯”的内部类,非常简单,且极易理解。然而,内部类的设计非常全面,不可避免地会遇到它们的其他大量用法——假若我们在一个方法甚至一个任意的作用域内创建内部类。有两方面的原因促使我们这样做: -(1) 正如前面展示的那样,我们准备实现某种形式的接口,使自己能创建和返回一个句柄。 +(1) 正如前面展示的那样,我们准备实现某种形式的接口,使自己能创建和返回一个指针。 (2) 要解决一个复杂的问题,并希望创建一个类,用来辅助自己的程序方案。同时不愿意把它公开。 @@ -156,15 +156,15 @@ class Test { (3) 一个匿名类,用于实现一个接口 -(4) 一个匿名类,用于扩展拥有非默认构建器的一个类 +(4) 一个匿名类,用于扩展拥有非默认构造器的一个类 (5) 一个匿名类,用于执行字段初始化 -(6) 一个匿名类,通过实例初始化进行构建(匿名内部类不可拥有构建器) +(6) 一个匿名类,通过实例初始化进行构建(匿名内部类不可拥有构造器) 所有这些都在innerscopes包内发生。首先,来自前述代码的通用接口会在它们自己的文件里获得定义,使它们能在所有的例子里使用: -``` +``` java //: Destination.java package c07.innerscopes; @@ -175,7 +175,7 @@ interface Destination { 由于我们已认为Contents可能是一个抽象类,所以可采取下面这种更自然的形式,就象一个接口那样: -``` +``` java //: Contents.java package c07.innerscopes; @@ -186,7 +186,7 @@ interface Contents { 尽管是含有具体实施细节的一个普通类,但Wrapping也作为它所有衍生类的一个通用“接口”使用: -``` +``` java //: Wrapping.java package c07.innerscopes; @@ -197,10 +197,10 @@ public class Wrapping { } ///:~ ``` -在上面的代码中,我们注意到Wrapping有一个要求使用自变量的构建器,这就使情况变得更加有趣了。 +在上面的代码中,我们注意到Wrapping有一个要求使用自变量的构造器,这就使情况变得更加有趣了。 第一个例子展示了如何在一个方法的作用域(而不是另一个类的作用域)中创建一个完整的类: -``` +``` java //: Parcel4.java // Nesting a class within a method package c07.innerscopes; @@ -224,10 +224,10 @@ public class Parcel4 { } ///:~ ``` -PDestination类属于dest()的一部分,而不是Parcel4的一部分(同时注意可为相同目录内每个类内部的一个内部类使用类标识符PDestination,这样做不会发生命名的冲突)。因此,PDestination不可从dest()的外部访问。请注意在返回语句中发生的上溯造型——除了指向基础类Destination的一个句柄之外,没有任何东西超出dest()的边界之外。当然,不能由于类PDestination的名字置于dest()内部,就认为在dest()返回之后PDestination不是一个有效的对象。 +PDestination类属于dest()的一部分,而不是Parcel4的一部分(同时注意可为相同目录内每个类内部的一个内部类使用类标识符PDestination,这样做不会发生命名的冲突)。因此,PDestination不可从dest()的外部访问。请注意在返回语句中发生的上溯造型——除了指向基础类Destination的一个指针之外,没有任何东西超出dest()的边界之外。当然,不能由于类PDestination的名字置于dest()内部,就认为在dest()返回之后PDestination不是一个有效的对象。 下面这个例子展示了如何在任意作用域内嵌套一个内部类: -``` +``` java //: Parcel5.java // Nesting a class within a scope package c07.innerscopes; @@ -259,7 +259,7 @@ public class Parcel5 { TrackingSlip类嵌套于一个if语句的作用域内。这并不意味着类是有条件创建的——它会随同其他所有东西得到编译。然而,在定义它的那个作用域之外,它是不可使用的。除这些以外,它看起来和一个普通类并没有什么区别。 下面这个例子看起来有些奇怪: -``` +``` java //: Parcel6.java // A method that returns an anonymous inner class package c07.innerscopes; @@ -281,22 +281,22 @@ public class Parcel6 { cont()方法同时合并了返回值的创建代码,以及用于表示那个返回值的类。除此以外,这个类是匿名的——它没有名字。而且看起来似乎更让人摸不着头脑的是,我们准备创建一个Contents对象: -``` +``` java return new Contents() ``` 但在这之后,在遇到分号之前,我们又说:“等一等,让我先在一个类定义里再耍一下花招”: -``` +``` java return new Contents() { private int i = 11; public int value() { return i; } }; ``` -这种奇怪的语法要表达的意思是:“创建从Contents衍生出来的匿名类的一个对象”。由new表达式返回的句柄会自动上溯造型成一个Contents句柄。匿名内部类的语法其实要表达的是: +这种奇怪的语法要表达的意思是:“创建从Contents衍生出来的匿名类的一个对象”。由new表达式返回的指针会自动上溯造型成一个Contents指针。匿名内部类的语法其实要表达的是: -``` +``` java class MyContents extends Contents { private int i = 11; public int value() { return i; } @@ -304,9 +304,9 @@ public int value() { return i; } return new MyContents(); ``` -在匿名内部类中,Contents是用一个默认构建器创建的。下面这段代码展示了基础类需要含有自变量的一个构建器时做的事情: +在匿名内部类中,Contents是用一个默认构造器创建的。下面这段代码展示了基础类需要含有自变量的一个构造器时做的事情: -``` +``` java //: Parcel7.java // An anonymous inner class that calls the // base-class constructor @@ -328,11 +328,11 @@ public class Parcel7 { } ///:~ ``` -也就是说,我们将适当的自变量简单地传递给基础类构建器,在这儿表现为在“new Wrapping(x)”中传递x。匿名类不能拥有一个构建器,这和在调用super()时的常规做法不同。 +也就是说,我们将适当的自变量简单地传递给基础类构造器,在这儿表现为在“new Wrapping(x)”中传递x。匿名类不能拥有一个构造器,这和在调用super()时的常规做法不同。 在前述的两个例子中,分号并不标志着类主体的结束(和C++不同)。相反,它标志着用于包含匿名类的那个表达式的结束。因此,它完全等价于在其他任何地方使用分号。 -若想对匿名内部类的一个对象进行某种形式的初始化,此时会出现什么情况呢?由于它是匿名的,没有名字赋给构建器,所以我们不能拥有一个构建器。然而,我们可在定义自己的字段时进行初始化: +若想对匿名内部类的一个对象进行某种形式的初始化,此时会出现什么情况呢?由于它是匿名的,没有名字赋给构造器,所以我们不能拥有一个构造器。然而,我们可在定义自己的字段时进行初始化: -``` +``` java //: Parcel8.java // An anonymous inner class that performs // initialization. A briefer version @@ -356,9 +356,9 @@ public class Parcel8 { ``` 若试图定义一个匿名内部类,并想使用在匿名内部类外部定义的一个对象,则编译器要求外部对象为final属性。这正是我们将dest()的自变量设为final的原因。如果忘记这样做,就会得到一条编译期出错提示。 -只要自己只是想分配一个字段,上述方法就肯定可行。但假如需要采取一些类似于构建器的行动,又应怎样操作呢?通过Java 1.1的实例初始化,我们可以有效地为一个匿名内部类创建一个构建器: +只要自己只是想分配一个字段,上述方法就肯定可行。但假如需要采取一些类似于构造器的行动,又应怎样操作呢?通过Java 1.1的实例初始化,我们可以有效地为一个匿名内部类创建一个构造器: -``` +``` java //: Parcel9.java // Using "instance initialization" to perform // construction on an anonymous inner class @@ -386,13 +386,13 @@ public class Parcel9 { } ///:~ ``` -在实例初始化模块中,我们可看到代码不能作为类初始化模块(即if语句)的一部分执行。所以实际上,一个实例初始化模块就是一个匿名内部类的构建器。当然,它的功能是有限的;我们不能对实例初始化模块进行过载处理,所以只能拥有这些构建器的其中一个。 +在实例初始化模块中,我们可看到代码不能作为类初始化模块(即if语句)的一部分执行。所以实际上,一个实例初始化模块就是一个匿名内部类的构造器。当然,它的功能是有限的;我们不能对实例初始化模块进行重载处理,所以只能拥有这些构造器的其中一个。 7.6.3 链接到外部类 迄今为止,我们见到的内部类好象仅仅是一种名字隐藏以及代码组织方案。尽管这些功能非常有用,但似乎并不特别引人注目。然而,我们还忽略了另一个重要的事实。创建自己的内部类时,那个类的对象同时拥有指向封装对象(这些对象封装或生成了内部类)的一个链接。所以它们能访问那个封装对象的成员——毋需取得任何资格。除此以外,内部类拥有对封装类所有元素的访问权限(注释②)。下面这个例子阐示了这个问题: -``` +``` java //: Sequence.java // Holds a sequence of Objects @@ -448,13 +448,13 @@ public class Sequence { 在这里,SSelector是一个私有类,它提供了Selector功能。在main()中,大家可看到Sequence的创建过程,在它后面是一系列字串对象的添加。随后,通过对getSelector()的一个调用生成一个Selector。并用它在Sequence中移动,同时选择每一个项目。 -从表面看,SSelector似乎只是另一个内部类。但不要被表面现象迷惑。请注意观察end(),current()以及next(),它们每个方法都引用了o。o是个不属于SSelector一部分的句柄,而是位于封装类里的一个private字段。然而,内部类可以从封装类访问方法与字段,就象已经拥有了它们一样。这一特征对我们来说是非常方便的,就象在上面的例子中看到的那样。 +从表面看,SSelector似乎只是另一个内部类。但不要被表面现象迷惑。请注意观察end(),current()以及next(),它们每个方法都引用了o。o是个不属于SSelector一部分的指针,而是位于封装类里的一个private字段。然而,内部类可以从封装类访问方法与字段,就象已经拥有了它们一样。这一特征对我们来说是非常方便的,就象在上面的例子中看到的那样。 -因此,我们现在知道一个内部类可以访问封装类的成员。这是如何实现的呢?内部类必须拥有对封装类的特定对象的一个引用,而封装类的作用就是创建这个内部类。随后,当我们引用封装类的一个成员时,就利用那个(隐藏)的引用来选择那个成员。幸运的是,编译器会帮助我们照管所有这些细节。但我们现在也可以理解内部类的一个对象只能与封装类的一个对象联合创建。在这个创建过程中,要求对封装类对象的句柄进行初始化。若不能访问那个句柄,编译器就会报错。进行所有这些操作的时候,大多数时候都不要求程序员的任何介入。 +因此,我们现在知道一个内部类可以访问封装类的成员。这是如何实现的呢?内部类必须拥有对封装类的特定对象的一个引用,而封装类的作用就是创建这个内部类。随后,当我们引用封装类的一个成员时,就利用那个(隐藏)的引用来选择那个成员。幸运的是,编译器会帮助我们照管所有这些细节。但我们现在也可以理解内部类的一个对象只能与封装类的一个对象联合创建。在这个创建过程中,要求对封装类对象的指针进行初始化。若不能访问那个指针,编译器就会报错。进行所有这些操作的时候,大多数时候都不要求程序员的任何介入。 7.6.4 static内部类 -为正确理解static在应用于内部类时的含义,必须记住内部类的对象默认持有创建它的那个封装类的一个对象的句柄。然而,假如我们说一个内部类是static的,这种说法却是不成立的。static内部类意味着: +为正确理解static在应用于内部类时的含义,必须记住内部类的对象默认持有创建它的那个封装类的一个对象的指针。然而,假如我们说一个内部类是static的,这种说法却是不成立的。static内部类意味着: (1) 为创建一个static内部类的对象,我们不需要一个外部类对象。 @@ -464,7 +464,7 @@ public class Sequence { 倘若为了创建内部类的对象而不需要创建外部类的一个对象,那么可将所有东西都设为static。为了能正常工作,同时也必须将内部类设为static。如下所示: -``` +``` java //: Parcel10.java // Static inner classes package c07.parcel10; @@ -504,11 +504,11 @@ public class Parcel10 { } ///:~ ``` -在main()中,我们不需要Parcel10的对象;相反,我们用常规的语法来选择一个static成员,以便调用将句柄返回Contents和Destination的方法。 +在main()中,我们不需要Parcel10的对象;相反,我们用常规的语法来选择一个static成员,以便调用将指针返回Contents和Destination的方法。 通常,我们不在一个接口里设置任何代码,但static内部类可以成为接口的一部分。由于类是“静态”的,所以它不会违反接口的规则——static内部类只位于接口的命名空间内部: -``` +``` java //: IInterface.java // Static inner classes inside interfaces @@ -523,7 +523,7 @@ interface IInterface { 在本书早些时候,我建议大家在每个类里都设置一个main(),将其作为那个类的测试床使用。这样做的一个缺点就是额外代码的数量太多。若不愿如此,可考虑用一个static内部类容纳自己的测试代码。如下所示: -``` +``` java //: TestBed.java // Putting test code in a static inner class @@ -543,11 +543,11 @@ class TestBed { 7.6.5 引用外部类对象 -若想生成外部类对象的句柄,就要用一个点号以及一个this来命名外部类。举个例子来说,在Sequence.SSelector类中,它的所有方法都能产生外部类Sequence的存储句柄,方法是采用Sequence.this的形式。结果获得的句柄会自动具备正确的类型(这会在编译期间检查并核实,所以不会出现运行期的开销)。 +若想生成外部类对象的指针,就要用一个点号以及一个this来命名外部类。举个例子来说,在Sequence.SSelector类中,它的所有方法都能产生外部类Sequence的存储指针,方法是采用Sequence.this的形式。结果获得的指针会自动具备正确的类型(这会在编译期间检查并核实,所以不会出现运行期的开销)。 -有些时候,我们想告诉其他某些对象创建它某个内部类的一个对象。为达到这个目的,必须在new表达式中提供指向其他外部类对象的一个句柄,就象下面这样: +有些时候,我们想告诉其他某些对象创建它某个内部类的一个对象。为达到这个目的,必须在new表达式中提供指向其他外部类对象的一个指针,就象下面这样: -``` +``` java //: Parcel11.java // Creating inner classes package c07.parcel11; @@ -577,17 +577,17 @@ public class Parcel11 { 为直接创建内部类的一个对象,不能象大家或许猜想的那样——采用相同的形式,并引用外部类名Parcel11。此时,必须利用外部类的一个对象生成内部类的一个对象: -``` +``` java Parcel11.Contents c = p.new Contents(); ``` -因此,除非已拥有外部类的一个对象,否则不可能创建内部类的一个对象。这是由于内部类的对象已同创建它的外部类的对象“默默”地连接到一起。然而,如果生成一个static内部类,就不需要指向外部类对象的一个句柄。 +因此,除非已拥有外部类的一个对象,否则不可能创建内部类的一个对象。这是由于内部类的对象已同创建它的外部类的对象“默默”地连接到一起。然而,如果生成一个static内部类,就不需要指向外部类对象的一个指针。 7.6.6 从内部类继承 -由于内部类构建器必须同封装类对象的一个句柄联系到一起,所以从一个内部类继承的时候,情况会稍微变得有些复杂。这儿的问题是封装类的“秘密”句柄必须获得初始化,而且在衍生类中不再有一个默认的对象可以连接。解决这个问题的办法是采用一种特殊的语法,明确建立这种关联: +由于内部类构造器必须同封装类对象的一个指针联系到一起,所以从一个内部类继承的时候,情况会稍微变得有些复杂。这儿的问题是封装类的“秘密”指针必须获得初始化,而且在衍生类中不再有一个默认的对象可以连接。解决这个问题的办法是采用一种特殊的语法,明确建立这种关联: -``` +``` java //: InheritInner.java // Inheriting an inner class @@ -608,19 +608,19 @@ public class InheritInner } ///:~ ``` -从中可以看到,InheritInner只对内部类进行了扩展,没有扩展外部类。但在需要创建一个构建器的时候,默认对象已经没有意义,我们不能只是传递封装对象的一个句柄。此外,必须在构建器中采用下述语法: +从中可以看到,InheritInner只对内部类进行了扩展,没有扩展外部类。但在需要创建一个构造器的时候,默认对象已经没有意义,我们不能只是传递封装对象的一个指针。此外,必须在构造器中采用下述语法: -``` +``` java enclosingClassHandle.super(); ``` -它提供了必要的句柄,以便程序正确编译。 +它提供了必要的指针,以便程序正确编译。 7.6.7 内部类可以覆盖吗? 若创建一个内部类,然后从封装类继承,并重新定义内部类,那么会出现什么情况呢?也就是说,我们有可能覆盖一个内部类吗?这看起来似乎是一个非常有用的概念,但“覆盖”一个内部类——好象它是外部类的另一个方法——这一概念实际不能做任何事情: -``` +``` java //: BigEgg.java // An inner class cannot be overriden // like a method @@ -650,16 +650,16 @@ public class BigEgg extends Egg { } ///:~ ``` -默认构建器是由编译器自动合成的,而且会调用基础类的默认构建器。大家或许会认为由于准备创建一个BigEgg,所以会使用Yolk的“被覆盖”版本。但实际情况并非如此。输出如下: +默认构造器是由编译器自动合成的,而且会调用基础类的默认构造器。大家或许会认为由于准备创建一个BigEgg,所以会使用Yolk的“被覆盖”版本。但实际情况并非如此。输出如下: -``` +``` java New Egg() Egg.Yolk() ``` 这个例子简单地揭示出当我们从外部类继承的时候,没有任何额外的内部类继续下去。然而,仍然有可能“明确”地从内部类继承: -``` +``` java //: BigEgg2.java // Proper inheritance of an inner class @@ -697,9 +697,9 @@ public class BigEgg2 extends Egg2 { } ///:~ ``` -现在,BigEgg2.Yolk明确地扩展了Egg2.Yolk,而且覆盖了它的方法。方法insertYolk()允许BigEgg2将它自己的某个Yolk对象上溯造型至Egg2的y句柄。所以当g()调用y.f()的时候,就会使用f()被覆盖版本。输出结果如下: +现在,BigEgg2.Yolk明确地扩展了Egg2.Yolk,而且覆盖了它的方法。方法insertYolk()允许BigEgg2将它自己的某个Yolk对象上溯造型至Egg2的y指针。所以当g()调用y.f()的时候,就会使用f()被覆盖版本。输出结果如下: -``` +``` java Egg2.Yolk() New Egg2() Egg2.Yolk() @@ -707,14 +707,14 @@ BigEgg2.Yolk() BigEgg2.Yolk.f() ``` -对Egg2.Yolk()的第二个调用是BigEgg2.Yolk构建器的基础类构建器调用。调用 +对Egg2.Yolk()的第二个调用是BigEgg2.Yolk构造器的基础类构造器调用。调用 g()的时候,可发现使用的是f()的被覆盖版本。 7.6.8 内部类标识符 由于每个类都会生成一个.class文件,用于容纳与如何创建这个类型的对象有关的所有信息(这种信息产生了一个名为Class对象的元类),所以大家或许会猜到内部类也必须生成相应的.class文件,用来容纳与它们的Class对象有关的信息。这些文件或类的名字遵守一种严格的形式:先是封装类的名字,再跟随一个$,再跟随内部类的名字。例如,由InheritInner.java创建的.class文件包括: -``` +``` java InheritInner.class WithInner$Inner.class WithInner.class @@ -734,7 +734,7 @@ WithInner.class 为理解内部类如何简化控制框架的创建与使用,可认为一个控制框架的工作就是在事件“就绪”以后执行它们。尽管“就绪”的意思很多,但在目前这种情况下,我们却是以计算机时钟为基础。随后,请认识到针对控制框架需要控制的东西,框架内并未包含任何特定的信息。首先,它是一个特殊的接口,描述了所有控制事件。它可以是一个抽象类,而非一个实际的接口。由于默认行为是根据时间控制的,所以部分实施细节可能包括: -``` +``` java //: Event.java // The common methods for any control event package c07.controller; @@ -752,13 +752,13 @@ abstract public class Event { } ///:~ ``` -希望Event(事件)运行的时候,构建器即简单地捕获时间。同时ready()告诉我们何时该运行它。当然,ready()也可以在一个衍生类中被覆盖,将事件建立在除时间以外的其他东西上。 +希望Event(事件)运行的时候,构造器即简单地捕获时间。同时ready()告诉我们何时该运行它。当然,ready()也可以在一个衍生类中被覆盖,将事件建立在除时间以外的其他东西上。 action()是事件就绪后需要调用的方法,而description()提供了与事件有关的文字信息。 下面这个文件包含了实际的控制框架,用于管理和触发事件。第一个类实际只是一个“助手”类,它的职责是容纳Event对象。可用任何适当的集合替换它。而且通过第8章的学习,大家会知道另一些集合可简化我们的工作,不需要我们编写这些额外的代码: -``` +``` java //: Controller.java // Along with Event, the generic // framework for all control systems: @@ -812,7 +812,7 @@ public class Controller { EventSet可容纳100个事件(若在这里使用来自第8章的一个“真实”集合,就不必担心它的最大尺寸,因为它会根据情况自动改变大小)。index(索引)在这里用于跟踪下一个可用的空间,而next(下一个)帮助我们寻找列表中的下一个事件,了解自己是否已经循环到头。在对getNext()的调用中,这一点是至关重要的,因为一旦运行,Event对象就会从列表中删去(使用removeCurrent())。所以getNext()会在列表中向前移动时遇到“空洞”。 -注意removeCurrent()并不只是指示一些标志,指出对象不再使用。相反,它将句柄设为null。这一点是非常重要的,因为假如垃圾收集器发现一个句柄仍在使用,就不会清除对象。若认为自己的句柄可能象现在这样被挂起,那么最好将其设为null,使垃圾收集器能够正常地清除它们。 +注意removeCurrent()并不只是指示一些标志,指出对象不再使用。相反,它将指针设为null。这一点是非常重要的,因为假如垃圾收集器发现一个指针仍在使用,就不会清除对象。若认为自己的指针可能象现在这样被挂起,那么最好将其设为null,使垃圾收集器能够正常地清除它们。 Controller是进行实际工作的地方。它用一个EventSet容纳自己的Event对象,而且addEvent()允许我们向这个列表加入新事件。但最重要的方法是run()。该方法会在EventSet中遍历,搜索一个准备运行的Event对象——ready()。对于它发现ready()的每一个对象,都会调用action()方法,打印出description(),然后将事件从列表中删去。 @@ -830,7 +830,7 @@ Controller是进行实际工作的地方。它用一个EventSet容纳自己的Ev 作为应用程序框架的一种典型行为,GreenhouseControls类是从Controller继承的: -``` +``` java //: GreenhouseControls.java // This produces a specific application of the // control system, all in a single class. Inner diff --git "a/7.7 \346\236\204\345\273\272\345\231\250\345\222\214\345\244\232\345\275\242\346\200\247.md" "b/7.7 \346\236\204\351\200\240\345\231\250\345\222\214\345\244\232\346\200\201\346\200\247.md" similarity index 79% rename from "7.7 \346\236\204\345\273\272\345\231\250\345\222\214\345\244\232\345\275\242\346\200\247.md" rename to "7.7 \346\236\204\351\200\240\345\231\250\345\222\214\345\244\232\346\200\201\346\200\247.md" index 433b4c7..1045328 100644 --- "a/7.7 \346\236\204\345\273\272\345\231\250\345\222\214\345\244\232\345\275\242\346\200\247.md" +++ "b/7.7 \346\236\204\351\200\240\345\231\250\345\222\214\345\244\232\346\200\201\346\200\247.md" @@ -1,16 +1,16 @@ -# 7.7 构建器和多形性 +# 7.7 构造器和多态性 -同往常一样,构建器与其他种类的方法是有区别的。在涉及到多形性的问题后,这种方法依然成立。尽管构建器并不具有多形性(即便可以使用一种“虚拟构建器”——将在第11章介绍),但仍然非常有必要理解构建器如何在复杂的分级结构中以及随同多形性使用。这一理解将有助于大家避免陷入一些令人不快的纠纷。 +同往常一样,构造器与其他种类的方法是有区别的。在涉及到多态性的问题后,这种方法依然成立。尽管构造器并不具有多态性(即便可以使用一种“虚拟构造器”——将在第11章介绍),但仍然非常有必要理解构造器如何在复杂的分级结构中以及随同多态性使用。这一理解将有助于大家避免陷入一些令人不快的纠纷。 -7.7.1 构建器的调用顺序 +7.7.1 构造器的调用顺序 -构建器调用的顺序已在第4章进行了简要说明,但那是在继承和多形性问题引入之前说的话。 +构造器调用的顺序已在第4章进行了简要说明,但那是在继承和多态性问题引入之前说的话。 -用于基础类的构建器肯定在一个衍生类的构建器中调用,而且逐渐向上链接,使每个基础类使用的构建器都能得到调用。之所以要这样做,是由于构建器负有一项特殊任务:检查对象是否得到了正确的构建。一个衍生类只能访问它自己的成员,不能访问基础类的成员(这些成员通常都具有private属性)。只有基础类的构建器在初始化自己的元素时才知道正确的方法以及拥有适当的权限。所以,必须令所有构建器都得到调用,否则整个对象的构建就可能不正确。那正是编译器为什么要强迫对衍生类的每个部分进行构建器调用的原因。在衍生类的构建器主体中,若我们没有明确指定对一个基础类构建器的调用,它就会“默默”地调用默认构建器。如果不存在默认构建器,编译器就会报告一个错误(若某个类没有构建器,编译器会自动组织一个默认构建器)。 +用于基础类的构造器肯定在一个衍生类的构造器中调用,而且逐渐向上链接,使每个基础类使用的构造器都能得到调用。之所以要这样做,是由于构造器负有一项特殊任务:检查对象是否得到了正确的构建。一个衍生类只能访问它自己的成员,不能访问基础类的成员(这些成员通常都具有private属性)。只有基础类的构造器在初始化自己的元素时才知道正确的方法以及拥有适当的权限。所以,必须令所有构造器都得到调用,否则整个对象的构建就可能不正确。那正是编译器为什么要强迫对衍生类的每个部分进行构造器调用的原因。在衍生类的构造器主体中,若我们没有明确指定对一个基础类构造器的调用,它就会“默默”地调用默认构造器。如果不存在默认构造器,编译器就会报告一个错误(若某个类没有构造器,编译器会自动组织一个默认构造器)。 -下面让我们看看一个例子,它展示了按构建顺序进行合成、继承以及多形性的效果: +下面让我们看看一个例子,它展示了按构建顺序进行合成、继承以及多态性的效果: -``` +``` java //: Sandwich.java // Order of constructor calls @@ -53,9 +53,9 @@ class Sandwich extends PortableLunch { } ///:~ ``` -这个例子在其他类的外部创建了一个复杂的类,而且每个类都有一个构建器对自己进行了宣布。其中最重要的类是Sandwich,它反映出了三个级别的继承(若将从Object的默认继承算在内,就是四级)以及三个成员对象。在main()里创建了一个Sandwich对象后,输出结果如下: +这个例子在其他类的外部创建了一个复杂的类,而且每个类都有一个构造器对自己进行了宣布。其中最重要的类是Sandwich,它反映出了三个级别的继承(若将从Object的默认继承算在内,就是四级)以及三个成员对象。在main()里创建了一个Sandwich对象后,输出结果如下: -``` +``` java Meal() Lunch() PortableLunch() @@ -65,21 +65,21 @@ Lettuce() Sandwich() ``` -这意味着对于一个复杂的对象,构建器的调用遵照下面的顺序: +这意味着对于一个复杂的对象,构造器的调用遵照下面的顺序: -(1) 调用基础类构建器。这个步骤会不断重复下去,首先得到构建的是分级结构的根部,然后是下一个衍生类,等等。直到抵达最深一层的衍生类。 +(1) 调用基础类构造器。这个步骤会不断重复下去,首先得到构建的是分级结构的根部,然后是下一个衍生类,等等。直到抵达最深一层的衍生类。 (2) 按声明顺序调用成员初始化模块。 -(3) 调用衍生构建器的主体。 +(3) 调用衍生构造器的主体。 -构建器调用的顺序是非常重要的。进行继承时,我们知道关于基础类的一切,并且能访问基础类的任何public和protected成员。这意味着当我们在衍生类的时候,必须能假定基础类的所有成员都是有效的。采用一种标准方法,构建行动已经进行,所以对象所有部分的成员均已得到构建。但在构建器内部,必须保证使用的所有成员都已构建。为达到这个要求,唯一的办法就是首先调用基础类构建器。然后在进入衍生类构建器以后,我们在基础类能够访问的所有成员都已得到初始化。此外,所有成员对象(亦即通过合成方法置于类内的对象)在类内进行定义的时候(比如上例中的b,c和l),由于我们应尽可能地对它们进行初始化,所以也应保证构建器内部的所有成员均为有效。若坚持按这一规则行事,会有助于我们确定所有基础类成员以及当前对象的成员对象均已获得正确的初始化。但不幸的是,这种做法并不适用于所有情况,这将在下一节具体说明。 +构造器调用的顺序是非常重要的。进行继承时,我们知道关于基础类的一切,并且能访问基础类的任何public和protected成员。这意味着当我们在衍生类的时候,必须能假定基础类的所有成员都是有效的。采用一种标准方法,构建行动已经进行,所以对象所有部分的成员均已得到构建。但在构造器内部,必须保证使用的所有成员都已构建。为达到这个要求,唯一的办法就是首先调用基础类构造器。然后在进入衍生类构造器以后,我们在基础类能够访问的所有成员都已得到初始化。此外,所有成员对象(亦即通过合成方法置于类内的对象)在类内进行定义的时候(比如上例中的b,c和l),由于我们应尽可能地对它们进行初始化,所以也应保证构造器内部的所有成员均为有效。若坚持按这一规则行事,会有助于我们确定所有基础类成员以及当前对象的成员对象均已获得正确的初始化。但不幸的是,这种做法并不适用于所有情况,这将在下一节具体说明。 7.7.2 继承和finalize() 通过“合成”方法创建新类时,永远不必担心对那个类的成员对象的收尾工作。每个成员都是一个独立的对象,所以会得到正常的垃圾收集以及收尾处理——无论它是不是不自己某个类一个成员。但在进行初始化的时候,必须覆盖衍生类中的finalize()方法——如果已经设计了某个特殊的清除进程,要求它必须作为垃圾收集的一部分进行。覆盖衍生类的finalize()时,务必记住调用finalize()的基础类版本。否则,基础类的初始化根本不会发生。下面这个例子便是明证: -``` +``` java //: Frog.java // Testing finalize with inheritance @@ -180,7 +180,7 @@ DoBasefinalization类只是简单地容纳了一个标志,向分级结构中 在Frog.main()中,DoBaseFinalization标志会得到配置,而且会创建单独一个Frog对象。请记住垃圾收集(特别是收尾工作)可能不会针对任何特定的对象发生,所以为了强制采取这一行动,System.runFinalizersOnExit(true)添加了额外的开销,以保证收尾工作的正常进行。若没有基础类初始化,则输出结果是: -``` +``` java not finalizing bases Creating Characteristic is alive LivingCreature() @@ -198,7 +198,7 @@ finalizing Characteristic can live in water 从中可以看出确实没有为基础类Frog调用收尾模块。但假如在命令行加入“finalize”自变量,则会获得下述结果: -``` +``` java Creating Characteristic is alive LivingCreature() Creating Characteristic has heart @@ -218,15 +218,15 @@ finalizing Characteristic can live in water 尽管成员对象按照与它们创建时相同的顺序进行收尾,但从技术角度说,并没有指定对象收尾的顺序。但对于基础类,我们可对收尾的顺序进行控制。采用的最佳顺序正是在这里采用的顺序,它与初始化顺序正好相反。按照与C++中用于“破坏器”相同的形式,我们应该首先执行衍生类的收尾,再是基础类的收尾。这是由于衍生类的收尾可能调用基础类中相同的方法,要求基础类组件仍然处于活动状态。因此,必须提前将它们清除(破坏)。 -7.7.3 构建器内部的多形性方法的行为 +7.7.3 构造器内部的多态性方法的行为 -构建器调用的分级结构(顺序)为我们带来了一个有趣的问题,或者说让我们进入了一种进退两难的局面。若当前位于一个构建器的内部,同时调用准备构建的那个对象的一个动态绑定方法,那么会出现什么情况呢?在原始的方法内部,我们完全可以想象会发生什么——动态绑定的调用会在运行期间进行解析,因为对象不知道它到底从属于方法所在的那个类,还是从属于从它衍生出来的某些类。为保持一致性,大家也许会认为这应该在构建器内部发生。 -但实际情况并非完全如此。若调用构建器内部一个动态绑定的方法,会使用那个方法被覆盖的定义。然而,产生的效果可能并不如我们所愿,而且可能造成一些难于发现的程序错误。 +构造器调用的分级结构(顺序)为我们带来了一个有趣的问题,或者说让我们进入了一种进退两难的局面。若当前位于一个构造器的内部,同时调用准备构建的那个对象的一个动态绑定方法,那么会出现什么情况呢?在原始的方法内部,我们完全可以想象会发生什么——动态绑定的调用会在运行期间进行解析,因为对象不知道它到底从属于方法所在的那个类,还是从属于从它衍生出来的某些类。为保持一致性,大家也许会认为这应该在构造器内部发生。 +但实际情况并非完全如此。若调用构造器内部一个动态绑定的方法,会使用那个方法被覆盖的定义。然而,产生的效果可能并不如我们所愿,而且可能造成一些难于发现的程序错误。 -从概念上讲,构建器的职责是让对象实际进入存在状态。在任何构建器内部,整个对象可能只是得到部分组织——我们只知道基础类对象已得到初始化,但却不知道哪些类已经继承。然而,一个动态绑定的方法调用却会在分级结构里“向前”或者“向外”前进。它调用位于衍生类里的一个方法。如果在构建器内部做这件事情,那么对于调用的方法,它要操纵的成员可能尚未得到正确的初始化——这显然不是我们所希望的。 +从概念上讲,构造器的职责是让对象实际进入存在状态。在任何构造器内部,整个对象可能只是得到部分组织——我们只知道基础类对象已得到初始化,但却不知道哪些类已经继承。然而,一个动态绑定的方法调用却会在分级结构里“向前”或者“向外”前进。它调用位于衍生类里的一个方法。如果在构造器内部做这件事情,那么对于调用的方法,它要操纵的成员可能尚未得到正确的初始化——这显然不是我们所希望的。 通过观察下面这个例子,这个问题便会昭然若揭: -``` +``` java //: PolyConstructors.java // Constructors and polymorphism // don't produce what you might expect. @@ -261,28 +261,28 @@ public class PolyConstructors { } ///:~ ``` -在Glyph中,draw()方法是“抽象的”(abstract),所以它可以被其他方法覆盖。事实上,我们在RoundGlyph中不得不对其进行覆盖。但Glyph构建器会调用这个方法,而且调用会在RoundGlyph.draw()中止,这看起来似乎是有意的。但请看看输出结果: +在Glyph中,draw()方法是“抽象的”(abstract),所以它可以被其他方法覆盖。事实上,我们在RoundGlyph中不得不对其进行覆盖。但Glyph构造器会调用这个方法,而且调用会在RoundGlyph.draw()中止,这看起来似乎是有意的。但请看看输出结果: -``` +``` java Glyph() before draw() RoundGlyph.draw(), radius = 0 Glyph() after draw() RoundGlyph.RoundGlyph(), radius = 5 ``` -当Glyph的构建器调用draw()时,radius的值甚至不是默认的初始值1,而是0。这可能是由于一个点号或者屏幕上根本什么都没有画而造成的。这样就不得不开始查找程序中的错误,试着找出程序不能工作的原因。 +当Glyph的构造器调用draw()时,radius的值甚至不是默认的初始值1,而是0。这可能是由于一个点号或者屏幕上根本什么都没有画而造成的。这样就不得不开始查找程序中的错误,试着找出程序不能工作的原因。 前一节讲述的初始化顺序并不十分完整,而那是解决问题的关键所在。初始化的实际过程是这样的: (1) 在采取其他任何操作之前,为对象分配的存储空间初始化成二进制零。 -(2) 就象前面叙述的那样,调用基础类构建器。此时,被覆盖的draw()方法会得到调用(的确是在RoundGlyph构建器调用之前),此时会发现radius的值为0,这是由于步骤(1)造成的。 +(2) 就象前面叙述的那样,调用基础类构造器。此时,被覆盖的draw()方法会得到调用(的确是在RoundGlyph构造器调用之前),此时会发现radius的值为0,这是由于步骤(1)造成的。 (3) 按照原先声明的顺序调用成员初始化代码。 -(4) 调用衍生类构建器的主体。 +(4) 调用衍生类构造器的主体。 -采取这些操作要求有一个前提,那就是所有东西都至少要初始化成零(或者某些特殊数据类型与“零”等价的值),而不是仅仅留作垃圾。其中包括通过“合成”技术嵌入一个类内部的对象句柄。如果假若忘记初始化那个句柄,就会在运行期间出现违例事件。其他所有东西都会变成零,这在观看结果时通常是一个严重的警告信号。 +采取这些操作要求有一个前提,那就是所有东西都至少要初始化成零(或者某些特殊数据类型与“零”等价的值),而不是仅仅留作垃圾。其中包括通过“合成”技术嵌入一个类内部的对象指针。如果假若忘记初始化那个指针,就会在运行期间出现异常事件。其他所有东西都会变成零,这在观看结果时通常是一个严重的警告信号。 在另一方面,应对这个程序的结果提高警惕。从逻辑的角度说,我们似乎已进行了无懈可击的设计,所以它的错误行为令人非常不可思议。而且没有从编译器那里收到任何报错信息(C++在这种情况下会表现出更合理的行为)。象这样的错误会很轻易地被人忽略,而且要花很长的时间才能找出。 -因此,设计构建器时一个特别有效的规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构建器内唯一能够安全调用的是在基础类中具有final属性的那些方法(也适用于private方法,它们自动具有final属性)。这些方法不能被覆盖,所以不会出现上述潜在的问题。 +因此,设计构造器时一个特别有效的规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构造器内唯一能够安全调用的是在基础类中具有final属性的那些方法(也适用于private方法,它们自动具有final属性)。这些方法不能被覆盖,所以不会出现上述潜在的问题。 diff --git "a/7.8 \351\200\232\350\277\207\347\273\247\346\211\277\350\277\233\350\241\214\350\256\276\350\256\241.md" "b/7.8 \351\200\232\350\277\207\347\273\247\346\211\277\350\277\233\350\241\214\350\256\276\350\256\241.md" index 1ffa02c..0b17e7f 100644 --- "a/7.8 \351\200\232\350\277\207\347\273\247\346\211\277\350\277\233\350\241\214\350\256\276\350\256\241.md" +++ "b/7.8 \351\200\232\350\277\207\347\273\247\346\211\277\350\277\233\350\241\214\350\256\276\350\256\241.md" @@ -1,11 +1,11 @@ # 7.8 通过继承进行设计 -学习了多形性的知识后,由于多形性是如此“聪明”的一种工具,所以看起来似乎所有东西都应该继承。但假如过度使用继承技术,也会使自己的设计变得不必要地复杂起来。事实上,当我们以一个现成类为基础建立一个新类时,如首先选择继承,会使情况变得异常复杂。 +学习了多态性的知识后,由于多态性是如此“聪明”的一种工具,所以看起来似乎所有东西都应该继承。但假如过度使用继承技术,也会使自己的设计变得不必要地复杂起来。事实上,当我们以一个现成类为基础建立一个新类时,如首先选择继承,会使情况变得异常复杂。 一个更好的思路是首先选择“合成”——如果不能十分确定自己应使用哪一个。合成不会强迫我们的程序设计进入继承的分级结构中。同时,合成显得更加灵活,因为可以动态选择一种类型(以及行为),而继承要求在编译期间准确地知道一种类型。下面这个例子对此进行了阐释: -``` +``` java //: Transmogrify.java // Dynamically changing the behavior of // an object via composition. @@ -42,7 +42,7 @@ public class Transmogrify { } ///:~ ``` -在这里,一个Stage对象包含了指向一个Actor的句柄,后者被初始化成一个HappyActor对象。这意味着go()会产生特定的行为。但由于句柄在运行期间可以重新与一个不同的对象绑定或结合起来,所以SadActor对象的句柄可在a中得到替换,然后由go()产生的行为发生改变。这样一来,我们在运行期间就获得了很大的灵活性。与此相反,我们不能在运行期间换用不同的形式来进行继承;它要求在编译期间完全决定下来。 +在这里,一个Stage对象包含了指向一个Actor的指针,后者被初始化成一个HappyActor对象。这意味着go()会产生特定的行为。但由于指针在运行期间可以重新与一个不同的对象绑定或结合起来,所以SadActor对象的指针可在a中得到替换,然后由go()产生的行为发生改变。这样一来,我们在运行期间就获得了很大的灵活性。与此相反,我们不能在运行期间换用不同的形式来进行继承;它要求在编译期间完全决定下来。 一条常规的设计准则是:用继承表达行为间的差异,并用成员变量表达状态的变化。在上述例子中,两者都得到了应用:继承了两个不同的类,用于表达act()方法的差异;而Stage通过合成技术允许它自己的状态发生变化。在这种情况下,那种状态的改变同时也产生了行为的变化。 @@ -58,7 +58,7 @@ public class Transmogrify { ![](7-7.gif) -也就是说,基础类可接收我们发给衍生类的任何消息,因为两者拥有完全一致的接口。我们要做的全部事情就是从衍生上溯造型,而且永远不需要回过头来检查对象的准确类型是什么。所有细节都已通过多形性获得了完美的控制。 +也就是说,基础类可接收我们发给衍生类的任何消息,因为两者拥有完全一致的接口。我们要做的全部事情就是从衍生上溯造型,而且永远不需要回过头来检查对象的准确类型是什么。所有细节都已通过多态性获得了完美的控制。 若按这种思路考虑问题,那么一个纯粹的“属于”关系似乎是唯一明智的设计方法,其他任何设计方法都会导致混乱不清的思路,而且在定义上存在很大的困难。但这种想法又属于另一个极端。经过细致的研究,我们发现扩展接口对于一些特定问题来说是特别有效的方案。可将其称为“类似于”关系,因为扩展后的衍生类“类似于”基础类——它们有相同的基础接口——但它增加了一些特性,要求用额外的方法加以实现。如下所示: ![](7-8.gif) @@ -77,9 +77,9 @@ public class Transmogrify { 为解决这个问题,必须有一种办法能够保证下溯造型正确进行。只有这样,我们才不会冒然造型成一种错误的类型,然后发出一条对象不可能收到的消息。这样做是非常不安全的。 -在某些语言中(如C++),为了进行保证“类型安全”的下溯造型,必须采取特殊的操作。但在Java中,所有造型都会自动得到检查和核实!所以即使我们只是进行一次普通的括弧造型,进入运行期以后,仍然会毫无留情地对这个造型进行检查,保证它的确是我们希望的那种类型。如果不是,就会得到一个ClassCastException(类造型违例)。在运行期间对类型进行检查的行为叫作“运行期类型标识”(RTTI)。下面这个例子向大家演示了RTTI的行为: +在某些语言中(如C++),为了进行保证“类型安全”的下溯造型,必须采取特殊的操作。但在Java中,所有造型都会自动得到检查和核实!所以即使我们只是进行一次普通的括弧造型,进入运行期以后,仍然会毫无留情地对这个造型进行检查,保证它的确是我们希望的那种类型。如果不是,就会得到一个ClassCastException(类造型异常)。在运行期间对类型进行检查的行为叫作“运行期类型标识”(RTTI)。下面这个例子向大家演示了RTTI的行为: -``` +``` java //: RTTI.java // Downcasting & Run-Time Type // Identification (RTTI) @@ -116,6 +116,6 @@ public class RTTI { 和在示意图中一样,MoreUseful(更有用的)对Useful(有用的)的接口进行了扩展。但由于它是继承来的,所以也能上溯造型到一个Useful。我们可看到这会在对数组x(位于main()中)进行初始化的时候发生。由于数组中的两个对象都属于Useful类,所以可将f()和g()方法同时发给它们两个。而且假如试图调用u()(它只存在于MoreUseful),就会收到一条编译期出错提示。 -若想访问一个MoreUseful对象的扩展接口,可试着进行下溯造型。如果它是正确的类型,这一行动就会成功。否则,就会得到一个ClassCastException。我们不必为这个违例编写任何特殊的代码,因为它指出的是一个可能在程序中任何地方发生的一个编程错误。 +若想访问一个MoreUseful对象的扩展接口,可试着进行下溯造型。如果它是正确的类型,这一行动就会成功。否则,就会得到一个ClassCastException。我们不必为这个异常编写任何特殊的代码,因为它指出的是一个可能在程序中任何地方发生的一个编程错误。 RTTI的意义远不仅仅反映在造型处理上。例如,在试图下溯造型之前,可通过一种方法了解自己处理的是什么类型。整个第11章都在讲述Java运行期类型标识的方方面面。 diff --git "a/7.9 \346\200\273\347\273\223.md" "b/7.9 \346\200\273\347\273\223.md" index d1ae039..023b719 100644 --- "a/7.9 \346\200\273\347\273\223.md" +++ "b/7.9 \346\200\273\347\273\223.md" @@ -1,9 +1,9 @@ # 7.9 总结 -“多形性”意味着“不同的形式”。在面向对象的程序设计中,我们有相同的外观(基础类的通用接口)以及使用那个外观的不同形式:动态绑定或组织的、不同版本的方法。 +“多态性”意味着“不同的形式”。在面向对象的程序设计中,我们有相同的外观(基础类的通用接口)以及使用那个外观的不同形式:动态绑定或组织的、不同版本的方法。 -通过这一章的学习,大家已知道假如不利用数据抽象以及继承技术,就不可能理解、甚至去创建多形性的一个例子。多形性是一种不可独立应用的特性(就象一个switch语句),只可与其他元素协同使用。我们应将其作为类总体关系的一部分来看待。人们经常混淆Java其他的、非面向对象的特性,比如方法过载等,这些特性有时也具有面向对象的某些特征。但不要被愚弄:如果以后没有绑定,就不成其为多形性。 +通过这一章的学习,大家已知道假如不利用数据抽象以及继承技术,就不可能理解、甚至去创建多态性的一个例子。多态性是一种不可独立应用的特性(就象一个switch语句),只可与其他元素协同使用。我们应将其作为类总体关系的一部分来看待。人们经常混淆Java其他的、非面向对象的特性,比如方法重载等,这些特性有时也具有面向对象的某些特征。但不要被愚弄:如果以后没有绑定,就不成其为多态性。 -为使用多形性乃至面向对象的技术,特别是在自己的程序中,必须将自己的编程视野扩展到不仅包括单独一个类的成员和消息,也要包括类与类之间的一致性以及它们的关系。尽管这要求学习时付出更多的精力,但却是非常值得的,因为只有这样才可真正有效地加快自己的编程速度、更好地组织代码、更容易做出包容面广的程序以及更易对自己的代码进行维护与扩展。 +为使用多态性乃至面向对象的技术,特别是在自己的程序中,必须将自己的编程视野扩展到不仅包括单独一个类的成员和消息,也要包括类与类之间的一致性以及它们的关系。尽管这要求学习时付出更多的精力,但却是非常值得的,因为只有这样才可真正有效地加快自己的编程速度、更好地组织代码、更容易做出包容面广的程序以及更易对自己的代码进行维护与扩展。 diff --git "a/8.1 \346\225\260\347\273\204.md" "b/8.1 \346\225\260\347\273\204.md" index 2319b4a..35c2b12 100644 --- "a/8.1 \346\225\260\347\273\204.md" +++ "b/8.1 \346\225\260\347\273\204.md" @@ -2,21 +2,21 @@ 对数组的大多数必要的介绍已在第4章的最后一节进行。通过那里的学习,大家已知道自己该如何定义及初始化一个数组。对象的容纳是本章的重点,而数组只是容纳对象的一种方式。但由于还有其他大量方法可容纳数组,所以是哪些地方使数组显得如此特别呢? -有两方面的问题将数组与其他集合类型区分开来:效率和类型。对于Java来说,为保存和访问一系列对象(实际是对象的句柄)数组,最有效的方法莫过于数组。数组实际代表一个简单的线性序列,它使得元素的访问速度非常快,但我们却要为这种速度付出代价:创建一个数组对象时,它的大小是固定的,而且不可在那个数组对象的“存在时间”内发生改变。可创建特定大小的一个数组,然后假如用光了存储空间,就再创建一个新数组,将所有句柄从旧数组移到新数组。这属于“矢量”(Vector)类的行为,本章稍后还会详细讨论它。然而,由于为这种大小的灵活性要付出较大的代价,所以我们认为矢量的效率并没有数组高。 +有两方面的问题将数组与其他集合类型区分开来:效率和类型。对于Java来说,为保存和访问一系列对象(实际是对象的指针)数组,最有效的方法莫过于数组。数组实际代表一个简单的线性序列,它使得元素的访问速度非常快,但我们却要为这种速度付出代价:创建一个数组对象时,它的大小是固定的,而且不可在那个数组对象的“存在时间”内发生改变。可创建特定大小的一个数组,然后假如用光了存储空间,就再创建一个新数组,将所有指针从旧数组移到新数组。这属于“矢量”(Vector)类的行为,本章稍后还会详细讨论它。然而,由于为这种大小的灵活性要付出较大的代价,所以我们认为矢量的效率并没有数组高。 -C++的矢量类知道自己容纳的是什么类型的对象,但同Java的数组相比,它却有一个明显的缺点:C++矢量类的operator[]不能进行范围检查,所以很容易超出边界(然而,它可以查询vector有多大,而且at()方法确实能进行范围检查)。在Java中,无论使用的是数组还是集合,都会进行范围检查——若超过边界,就会获得一个RuntimeException(运行期违例)错误。正如大家在第9章会学到的那样,这类违例指出的是一个程序员错误,所以不需要在代码中检查它。在另一方面,由于C++的vector不进行范围检查,所以访问速度较快——在Java中,由于对数组和集合都要进行范围检查,所以对性能有一定的影响。 +C++的矢量类知道自己容纳的是什么类型的对象,但同Java的数组相比,它却有一个明显的缺点:C++矢量类的operator[]不能进行范围检查,所以很容易超出边界(然而,它可以查询vector有多大,而且at()方法确实能进行范围检查)。在Java中,无论使用的是数组还是集合,都会进行范围检查——若超过边界,就会获得一个RuntimeException(运行期异常)错误。正如大家在第9章会学到的那样,这类异常指出的是一个程序员错误,所以不需要在代码中检查它。在另一方面,由于C++的vector不进行范围检查,所以访问速度较快——在Java中,由于对数组和集合都要进行范围检查,所以对性能有一定的影响。 -本章还要学习另外几种常见的集合类:Vector(矢量)、Stack(堆栈)以及Hashtable(散列表)。这些类都涉及对对象的处理——好象它们没有特定的类型。换言之,它们将其当作Object类型处理(Object类型是Java中所有类的“根”类)。从某个角度看,这种处理方法是非常合理的:我们仅需构建一个集合,然后任何Java对象都可以进入那个集合(除基本数据类型外——可用Java的基本类型封装类将其作为常数置入集合,或者将其封装到自己的类内,作为可以变化的值使用)。这再一次反映了数组优于常规集合:创建一个数组时,可令其容纳一种特定的类型。这意味着可进行编译期类型检查,预防自己设置了错误的类型,或者错误指定了准备提取的类型。当然,在编译期或者运行期,Java会防止我们将不当的消息发给一个对象。所以我们不必考虑自己的哪种做法更加危险,只要编译器能及时地指出错误,同时在运行期间加快速度,目的也就达到了。此外,用户很少会对一次违例事件感到非常惊讶的。 +本章还要学习另外几种常见的集合类:Vector(矢量)、Stack(栈)以及Hashtable(散列表)。这些类都涉及对对象的处理——好象它们没有特定的类型。换言之,它们将其当作Object类型处理(Object类型是Java中所有类的“根”类)。从某个角度看,这种处理方法是非常合理的:我们仅需构建一个集合,然后任何Java对象都可以进入那个集合(除基本数据类型外——可用Java的基本类型封装类将其作为常数置入集合,或者将其封装到自己的类内,作为可以变化的值使用)。这再一次反映了数组优于常规集合:创建一个数组时,可令其容纳一种特定的类型。这意味着可进行编译期类型检查,预防自己设置了错误的类型,或者错误指定了准备提取的类型。当然,在编译期或者运行期,Java会防止我们将不当的消息发给一个对象。所以我们不必考虑自己的哪种做法更加危险,只要编译器能及时地指出错误,同时在运行期间加快速度,目的也就达到了。此外,用户很少会对一次异常事件感到非常惊讶的。 考虑到执行效率和类型检查,应尽可能地采用数组。然而,当我们试图解决一个更常规的问题时,数组的局限也可能显得非常明显。在研究过数组以后,本章剩余的部分将把重点放到Java提供的集合类身上。 8.1.1 数组和第一类对象 -无论使用的数组属于什么类型,数组标识符实际都是指向真实对象的一个句柄。那些对象本身是在内存“堆”里创建的。堆对象既可“隐式”创建(即默认产生),亦可“显式”创建(即明确指定,用一个new表达式)。堆对象的一部分(实际是我们能访问的唯一字段或方法)是只读的length(长度)成员,它告诉我们那个数组对象里最多能容纳多少元素。对于数组对象,“[]”语法是我们能采用的唯一另类访问方法。 +无论使用的数组属于什么类型,数组标识符实际都是指向真实对象的一个指针。那些对象本身是在内存“堆”里创建的。堆对象既可“隐式”创建(即默认产生),亦可“显式”创建(即明确指定,用一个new表达式)。堆对象的一部分(实际是我们能访问的唯一字段或方法)是只读的length(长度)成员,它告诉我们那个数组对象里最多能容纳多少元素。对于数组对象,“[]”语法是我们能采用的唯一另类访问方法。 -下面这个例子展示了对数组进行初始化的不同方式,以及如何将数组句柄分配给不同的数组对象。它也揭示出对象数组和基本数据类型数组在使用方法上几乎是完全一致的。唯一的差别在于对象数组容纳的是句柄,而基本数据类型数组容纳的是具体的数值(若在执行此程序时遇到困难,请参考第3章的“赋值”小节): +下面这个例子展示了对数组进行初始化的不同方式,以及如何将数组指针分配给不同的数组对象。它也揭示出对象数组和基本数据类型数组在使用方法上几乎是完全一致的。唯一的差别在于对象数组容纳的是指针,而基本数据类型数组容纳的是具体的数值(若在执行此程序时遇到困难,请参考第3章的“赋值”小节): -``` +``` java //: ArraySize.java // Initialization & re-assignment of arrays package c08; @@ -98,26 +98,26 @@ e.length = 3 e.length = 2 ``` -其中,数组a只是初始化成一个null句柄。此时,编译器会禁止我们对这个句柄作任何实际操作,除非已正确地初始化了它。数组b被初始化成指向由Weeble句柄构成的一个数组,但那个数组里实际并未放置任何Weeble对象。然而,我们仍然可以查询那个数组的大小,因为b指向的是一个合法对象。这也为我们带来了一个难题:不可知道那个数组里实际包含了多少个元素,因为length只告诉我们可将多少元素置入那个数组。换言之,我们只知道数组对象的大小或容量,不知其实际容纳了多少个元素。尽管如此,由于数组对象在创建之初会自动初始化成null,所以可检查它是否为null,判断一个特定的数组“空位”是否容纳一个对象。类似地,由基本数据类型构成的数组会自动初始化成零(针对数值类型)、null(字符类型)或者false(布尔类型)。 +其中,数组a只是初始化成一个null指针。此时,编译器会禁止我们对这个指针作任何实际操作,除非已正确地初始化了它。数组b被初始化成指向由Weeble指针构成的一个数组,但那个数组里实际并未放置任何Weeble对象。然而,我们仍然可以查询那个数组的大小,因为b指向的是一个合法对象。这也为我们带来了一个难题:不可知道那个数组里实际包含了多少个元素,因为length只告诉我们可将多少元素置入那个数组。换言之,我们只知道数组对象的大小或容量,不知其实际容纳了多少个元素。尽管如此,由于数组对象在创建之初会自动初始化成null,所以可检查它是否为null,判断一个特定的数组“空位”是否容纳一个对象。类似地,由基本数据类型构成的数组会自动初始化成零(针对数值类型)、null(字符类型)或者false(布尔类型)。 数组c显示出我们首先创建一个数组对象,再将Weeble对象赋给那个数组的所有“空位”。数组d揭示出“集合初始化”语法,从而创建数组对象(用new命令明确进行,类似于数组c),然后用Weeble对象进行初始化,全部工作在一条语句里完成。 下面这个表达式: -``` +``` java a = d; ``` -向我们展示了如何取得同一个数组对象连接的句柄,然后将其赋给另一个数组对象,就象我们针对对象句柄的其他任何类型做的那样。现在,a和d都指向内存堆内同样的数组对象。 +向我们展示了如何取得同一个数组对象连接的指针,然后将其赋给另一个数组对象,就象我们针对对象指针的其他任何类型做的那样。现在,a和d都指向内存堆内同样的数组对象。 Java 1.1加入了一种新的数组初始化语法,可将其想象成“动态集合初始化”。由d采用的Java 1.0集合初始化方法则必须在定义d的同时进行。但若采用Java 1.1的语法,却可以在任何地方创建和初始化一个数组对象。例如,假设hide()方法用于取得一个Weeble对象数组,那么调用它时传统的方法是: -``` +``` java hide(d); ``` 但在Java 1.1中,亦可动态创建想作为参数传递的数组,如下所示: -``` +``` java hide(new Weeble[] {new Weeble(), new Weeble() }); ``` @@ -127,7 +127,7 @@ hide(new Weeble[] {new Weeble(), new Weeble() }); 1. 基本数据类型集合 -集合类只能容纳对象句柄。但对一个数组,却既可令其直接容纳基本类型的数据,亦可容纳指向对象的句柄。利用象Integer、Double之类的“封装器”类,可将基本数据类型的值置入一个集合里。但正如本章后面会在WordCount.java例子中讲到的那样,用于基本数据类型的封装器类只是在某些场合下才能发挥作用。无论将基本类型的数据置入数组,还是将其封装进入位于集合的一个类内,都涉及到执行效率的问题。显然,若能创建和访问一个基本数据类型数组,那么比起访问一个封装数据的集合,前者的效率会高出许多。 +集合类只能容纳对象指针。但对一个数组,却既可令其直接容纳基本类型的数据,亦可容纳指向对象的指针。利用象Integer、Double之类的“封装器”类,可将基本数据类型的值置入一个集合里。但正如本章后面会在WordCount.java例子中讲到的那样,用于基本数据类型的封装器类只是在某些场合下才能发挥作用。无论将基本类型的数据置入数组,还是将其封装进入位于集合的一个类内,都涉及到执行效率的问题。显然,若能创建和访问一个基本数据类型数组,那么比起访问一个封装数据的集合,前者的效率会高出许多。 当然,假如准备一种基本数据类型,同时又想要集合的灵活性(在需要的时候可自动扩展,腾出更多的空间),就不宜使用数组,必须使用由封装的数据构成的一个集合。大家或许认为针对每种基本数据类型,都应有一种特殊类型的Vector。但Java并未提供这一特性。某些形式的建模机制或许会在某一天帮助Java更好地解决这个问题(注释①)。 @@ -140,7 +140,7 @@ hide(new Weeble[] {new Weeble(), new Weeble() }); Java采用的是类似的方法,但我们能“返回一个数组”。当然,此时返回的实际仍是指向数组的指针。但在Java里,我们永远不必担心那个数组的是否可用——只要需要,它就会自动存在。而且垃圾收集器会在我们完成后自动将其清除。 作为一个例子,请思考如何返回一个字串数组: -``` +``` java //: IceCream.java // Returning arrays from methods @@ -184,7 +184,7 @@ public class IceCream { } ///:~ ``` -flavorSet()方法创建了一个名为results的String数组。该数组的大小为n——具体数值取决于我们传递给方法的自变量。随后,它从数组flav里随机挑选一些“香料”(Flavor),并将它们置入results里,并最终返回results。返回数组与返回其他任何对象没什么区别——最终返回的都是一个句柄。至于数组到底是在flavorSet()里创建的,还是在其他什么地方创建的,这个问题并不重要,因为反正返回的仅是一个句柄。一旦我们的操作完成,垃圾收集器会自动关照数组的清除工作。而且只要我们需要数组,它就会乖乖地听候调遣。 +flavorSet()方法创建了一个名为results的String数组。该数组的大小为n——具体数值取决于我们传递给方法的自变量。随后,它从数组flav里随机挑选一些“香料”(Flavor),并将它们置入results里,并最终返回results。返回数组与返回其他任何对象没什么区别——最终返回的都是一个指针。至于数组到底是在flavorSet()里创建的,还是在其他什么地方创建的,这个问题并不重要,因为反正返回的仅是一个指针。一旦我们的操作完成,垃圾收集器会自动关照数组的清除工作。而且只要我们需要数组,它就会乖乖地听候调遣。 另一方面,注意当flavorSet()随机挑选香料的时候,它需要保证以前出现过的一次随机选择不会再次出现。为达到这个目的,它使用了一个无限while循环,不断地作出随机选择,直到发现未在picks数组里出现过的一个元素为止(当然,也可以进行字串比较,检查随机选择是否在results数组里出现过,但字串比较的效率比较低)。若成功,就添加这个元素,并中断循环(break),再查找下一个(i值会递增)。但假若t是一个已在picks里出现过的数组,就用标签式的continue往回跳两级,强制选择一个新t。用一个调试程序可以很清楚地看到这个过程。 diff --git "a/8.2 \351\233\206\345\220\210.md" "b/8.2 \351\233\206\345\220\210.md" index e0070c8..76fa541 100644 --- "a/8.2 \351\233\206\345\220\210.md" +++ "b/8.2 \351\233\206\345\220\210.md" @@ -1,23 +1,23 @@ # 8.2 集合 -现在总结一下我们前面学过的东西:为容纳一组对象,最适宜的选择应当是数组。而且假如容纳的是一系列基本数据类型,更是必须采用数组。在本章剩下的部分,大家将接触到一些更常规的情况。当我们编写程序时,通常并不能确切地知道最终需要多少个对象。有些时候甚至想用更复杂的方式来保存对象。为解决这个问题,Java提供了四种类型的“集合类”:Vector(矢量)、BitSet(位集)、Stack(堆栈)以及Hashtable(散列表)。与拥有集合功能的其他语言相比,尽管这儿的数量显得相当少,但仍然能用它们解决数量惊人的实际问题。 +现在总结一下我们前面学过的东西:为容纳一组对象,最适宜的选择应当是数组。而且假如容纳的是一系列基本数据类型,更是必须采用数组。在本章剩下的部分,大家将接触到一些更常规的情况。当我们编写程序时,通常并不能确切地知道最终需要多少个对象。有些时候甚至想用更复杂的方式来保存对象。为解决这个问题,Java提供了四种类型的“集合类”:Vector(矢量)、BitSet(位集)、Stack(栈)以及Hashtable(散列表)。与拥有集合功能的其他语言相比,尽管这儿的数量显得相当少,但仍然能用它们解决数量惊人的实际问题。 这些集合类具有形形色色的特征。例如,Stack实现了一个LIFO(先入先出)序列,而Hashtable是一种“关联数组”,允许我们将任何对象关联起来。除此以外,所有Java集合类都能自动改变自身的大小。所以,我们在编程时可使用数量众多的对象,同时不必担心会将集合弄得有多大。 8.2.1 缺点:类型未知 -使用Java集合的“缺点”是在将对象置入一个集合时丢失了类型信息。之所以会发生这种情况,是由于当初编写集合时,那个集合的程序员根本不知道用户到底想把什么类型置入集合。若指示某个集合只允许特定的类型,会妨碍它成为一个“常规用途”的工具,为用户带来麻烦。为解决这个问题,集合实际容纳的是类型为Object的一些对象的句柄。这种类型当然代表Java中的所有对象,因为它是所有类的根。当然,也要注意这并不包括基本数据类型,因为它们并不是从“任何东西”继承来的。这是一个很好的方案,只是不适用下述场合: +使用Java集合的“缺点”是在将对象置入一个集合时丢失了类型信息。之所以会发生这种情况,是由于当初编写集合时,那个集合的程序员根本不知道用户到底想把什么类型置入集合。若指示某个集合只允许特定的类型,会妨碍它成为一个“常规用途”的工具,为用户带来麻烦。为解决这个问题,集合实际容纳的是类型为Object的一些对象的指针。这种类型当然代表Java中的所有对象,因为它是所有类的根。当然,也要注意这并不包括基本数据类型,因为它们并不是从“任何东西”继承来的。这是一个很好的方案,只是不适用下述场合: -(1) 将一个对象句柄置入集合时,由于类型信息会被抛弃,所以任何类型的对象都可进入我们的集合——即便特别指示它只能容纳特定类型的对象。举个例子来说,虽然指示它只能容纳猫,但事实上任何人都可以把一条狗扔进来。 +(1) 将一个对象指针置入集合时,由于类型信息会被抛弃,所以任何类型的对象都可进入我们的集合——即便特别指示它只能容纳特定类型的对象。举个例子来说,虽然指示它只能容纳猫,但事实上任何人都可以把一条狗扔进来。 -(2) 由于类型信息不复存在,所以集合能肯定的唯一事情就是自己容纳的是指向一个对象的句柄。正式使用它之前,必须对其进行造型,使其具有正确的类型。 +(2) 由于类型信息不复存在,所以集合能肯定的唯一事情就是自己容纳的是指向一个对象的指针。正式使用它之前,必须对其进行造型,使其具有正确的类型。 -值得欣慰的是,Java不允许人们滥用置入集合的对象。假如将一条狗扔进一个猫的集合,那么仍会将集合内的所有东西都看作猫,所以在使用那条狗时会得到一个“违例”错误。在同样的意义上,假若试图将一条狗的句柄“造型”到一只猫,那么运行期间仍会得到一个“违例”错误。 +值得欣慰的是,Java不允许人们滥用置入集合的对象。假如将一条狗扔进一个猫的集合,那么仍会将集合内的所有东西都看作猫,所以在使用那条狗时会得到一个“异常”错误。在同样的意义上,假若试图将一条狗的指针“造型”到一只猫,那么运行期间仍会得到一个“异常”错误。 下面是个例子: -``` +``` java //: CatsAndDogs.java // Simple collection example (Vector) import java.util.*; @@ -56,11 +56,11 @@ public class CatsAndDogs { } ///:~ ``` -可以看出,Vector的使用是非常简单的:先创建一个,再用addElement()置入对象,以后用elementAt()取得那些对象(注意Vector有一个size()方法,可使我们知道已添加了多少个元素,以便防止误超边界,造成违例错误)。 +可以看出,Vector的使用是非常简单的:先创建一个,再用addElement()置入对象,以后用elementAt()取得那些对象(注意Vector有一个size()方法,可使我们知道已添加了多少个元素,以便防止误超边界,造成异常错误)。 -Cat和Dog类都非常浅显——除了都是“对象”之外,它们并无特别之处(倘若不明确指出从什么类继承,就默认为从Object继承。所以我们不仅能用Vector方法将Cat对象置入这个集合,也能添加Dog对象,同时不会在编译期和运行期得到任何出错提示。用Vector方法elementAt()获取原本认为是Cat的对象时,实际获得的是指向一个Object的句柄,必须将那个对象造型为Cat。随后,需要将整个表达式用括号封闭起来,在为Cat调用print()方法之前进行强制造型;否则就会出现一个语法错误。在运行期间,如果试图将Dog对象造型为Cat,就会得到一个违例。 +Cat和Dog类都非常浅显——除了都是“对象”之外,它们并无特别之处(倘若不明确指出从什么类继承,就默认为从Object继承。所以我们不仅能用Vector方法将Cat对象置入这个集合,也能添加Dog对象,同时不会在编译期和运行期得到任何出错提示。用Vector方法elementAt()获取原本认为是Cat的对象时,实际获得的是指向一个Object的指针,必须将那个对象造型为Cat。随后,需要将整个表达式用括号封闭起来,在为Cat调用print()方法之前进行强制造型;否则就会出现一个语法错误。在运行期间,如果试图将Dog对象造型为Cat,就会得到一个异常。 -这些处理的意义都非常深远。尽管显得有些麻烦,但却获得了安全上的保证。我们从此再难偶然造成一些隐藏得深的错误。若程序的一个部分(或几个部分)将对象插入一个集合,但我们只是通过一次违例在程序的某个部分发现一个错误的对象置入了集合,就必须找出插入错误的位置。当然,可通过检查代码达到这个目的,但这或许是最笨的调试工具。另一方面,我们可从一些标准化的集合类开始自己的编程。尽管它们在功能上存在一些不足,且显得有些笨拙,但却能保证没有隐藏的错误。 +这些处理的意义都非常深远。尽管显得有些麻烦,但却获得了安全上的保证。我们从此再难偶然造成一些隐藏得深的错误。若程序的一个部分(或几个部分)将对象插入一个集合,但我们只是通过一次异常在程序的某个部分发现一个错误的对象置入了集合,就必须找出插入错误的位置。当然,可通过检查代码达到这个目的,但这或许是最笨的调试工具。另一方面,我们可从一些标准化的集合类开始自己的编程。尽管它们在功能上存在一些不足,且显得有些笨拙,但却能保证没有隐藏的错误。 1. 错误有时并不显露出来 @@ -68,7 +68,7 @@ Cat和Dog类都非常浅显——除了都是“对象”之外,它们并无 因此,为了让自己类的对象能显示出来,要做的全部事情就是覆盖toString()方法,如下例所示: -``` +``` java //: WorksAnyway.java // In special cases, things just seem // to work correctly. @@ -115,15 +115,15 @@ public class WorksAnyway { 可在Mouse里看到对toString()的重定义代码。在main()的第二个for循环中,可发现下述语句: -``` +``` java System.out.println("Free mouse: " + mice.elementAt(i)); ``` 在“+”后,编译器预期看到的是一个String对象。elementAt()生成了一个Object,所以为获得希望的String,编译器会默认调用toString()。但不幸的是,只有针对String才能得到象这样的结果;其他任何类型都不会进行这样的转换。 -隐藏造型的第二种方法已在Mousetrap里得到了应用。caughtYa()方法接收的不是一个Mouse,而是一个Object。随后再将其造型为一个Mouse。当然,这样做是非常冒失的,因为通过接收一个Object,任何东西都可以传递给方法。然而,假若造型不正确——如果我们传递了错误的类型——就会在运行期间得到一个违例错误。这当然没有在编译期进行检查好,但仍然能防止问题的发生。注意在使用这个方法时毋需进行造型: +隐藏造型的第二种方法已在Mousetrap里得到了应用。caughtYa()方法接收的不是一个Mouse,而是一个Object。随后再将其造型为一个Mouse。当然,这样做是非常冒失的,因为通过接收一个Object,任何东西都可以传递给方法。然而,假若造型不正确——如果我们传递了错误的类型——就会在运行期间得到一个异常错误。这当然没有在编译期进行检查好,但仍然能防止问题的发生。注意在使用这个方法时毋需进行造型: -``` +``` java MouseTrap.caughtYa(mice.elementAt(i)); ``` @@ -131,7 +131,7 @@ MouseTrap.caughtYa(mice.elementAt(i)); 大家或许不想放弃刚才那个问题。一个更“健壮”的方案是用Vector创建一个新类,使其只接收我们指定的类型,也只生成我们希望的类型。如下所示: -``` +``` java //: GopherVector.java // A type-conscious Vector import java.util.*; @@ -176,7 +176,7 @@ class GopherVector { 这前一个例子类似,只是新的GopherVector类有一个类型为Vector的private成员(从Vector继承有些麻烦,理由稍后便知),而且方法也和Vector类似。然而,它不会接收和产生普通Object,只对Gopher对象感兴趣。 由于GopherVector只接收一个Gopher(地鼠),所以假如我们使用: -``` +``` java gophers.addElement(new Pigeon()); ``` diff --git "a/8.3 \346\236\232\344\270\276\345\231\250\357\274\210\345\217\215\345\244\215\345\231\250\357\274\211.md" "b/8.3 \350\277\255\344\273\243\345\231\250.md" similarity index 90% rename from "8.3 \346\236\232\344\270\276\345\231\250\357\274\210\345\217\215\345\244\215\345\231\250\357\274\211.md" rename to "8.3 \350\277\255\344\273\243\345\231\250.md" index 271207c..3210bd5 100644 --- "a/8.3 \346\236\232\344\270\276\345\231\250\357\274\210\345\217\215\345\244\215\345\231\250\357\274\211.md" +++ "b/8.3 \350\277\255\344\273\243\345\231\250.md" @@ -1,12 +1,12 @@ -# 8.3 枚举器(反复器) +# 8.3 迭代器 在任何集合类中,必须通过某种方法在其中置入对象,再用另一种方法从中取得对象。毕竟,容纳各种各样的对象正是集合的首要任务。在Vector中,addElement()便是我们插入对象采用的方法,而elementAt()是提取对象的唯一方法。Vector非常灵活,我们可在任何时候选择任何东西,并可使用不同的索引选择多个元素。 若从更高的角度看这个问题,就会发现它的一个缺陷:需要事先知道集合的准确类型,否则无法使用。乍看来,这一点似乎没什么关系。但假若最开始决定使用Vector,后来在程序中又决定(考虑执行效率的原因)改变成一个List(属于Java1.2集合库的一部分),这时又该如何做呢? -可利用“反复器”(Iterator)的概念达到这个目的。它可以是一个对象,作用是遍历一系列对象,并选择那个序列中的每个对象,同时不让客户程序员知道或关注那个序列的基础结构。此外,我们通常认为反复器是一种“轻量级”对象;也就是说,创建它只需付出极少的代价。但也正是由于这个原因,我们常发现反复器存在一些似乎很奇怪的限制。例如,有些反复器只能朝一个方向移动。 -Java的Enumeration(枚举,注释②)便是具有这些限制的一个反复器的例子。除下面这些外,不可再用它做其他任何事情: +可利用“迭代器”(Iterator)的概念达到这个目的。它可以是一个对象,作用是遍历一系列对象,并选择那个序列中的每个对象,同时不让客户程序员知道或关注那个序列的基础结构。此外,我们通常认为迭代器是一种“轻量级”对象;也就是说,创建它只需付出极少的代价。但也正是由于这个原因,我们常发现迭代器存在一些似乎很奇怪的限制。例如,有些迭代器只能朝一个方向移动。 +Java的Enumeration(枚举,注释②)便是具有这些限制的一个迭代器的例子。除下面这些外,不可再用它做其他任何事情: (1) 用一个名为elements()的方法要求集合为我们提供一个Enumeration。我们首次调用它的nextElement()时,这个Enumeration会返回序列中的第一个元素。 @@ -14,11 +14,11 @@ Java的Enumeration(枚举,注释②)便是具有这些限制的一个反 (3) 用hasMoreElements()检查序列中是否还有更多的对象。 -②:“反复器”这个词在C++和OOP的其他地方是经常出现的,所以很难确定为什么Java的开发者采用了这样一个奇怪的名字。Java 1.2的集合库修正了这个问题以及其他许多问题。 +②:“迭代器”这个词在C++和OOP的其他地方是经常出现的,所以很难确定为什么Java的开发者采用了这样一个奇怪的名字。Java 1.2的集合库修正了这个问题以及其他许多问题。 -只可用Enumeration做这些事情,不能再有更多。它属于反复器一种简单的实现方式,但功能依然十分强大。为体会它的运作过程,让我们复习一下本章早些时候提到的CatsAndDogs.java程序。在原始版本中,elementAt()方法用于选择每一个元素,但在下述修订版中,可看到使用了一个“枚举”: +只可用Enumeration做这些事情,不能再有更多。它属于迭代器一种简单的实现方式,但功能依然十分强大。为体会它的运作过程,让我们复习一下本章早些时候提到的CatsAndDogs.java程序。在原始版本中,elementAt()方法用于选择每一个元素,但在下述修订版中,可看到使用了一个“枚举”: -``` +``` java //: CatsAndDogs2.java // Simple collection with Enumeration import java.util.*; @@ -60,14 +60,14 @@ public class CatsAndDogs2 { 我们看到唯一的改变就是最后几行。不再是: -``` +``` java for(int i = 0; i < cats.size(); i++) ((Cat)cats.elementAt(i)).print(); ``` 而是用一个Enumeration遍历整个序列: -``` +``` java while(e.hasMoreElements()) ((Cat2)e.nextElement()).print(); ``` @@ -75,7 +75,7 @@ while(e.hasMoreElements()) 使用Enumeration,我们不必关心集合中的元素数量。所有工作均由hasMoreElements()和nextElement()自动照管了。 下面再看看另一个例子,让我们创建一个常规用途的打印方法: -``` +``` java //: HamsterMaze.java // Using an Enumeration import java.util.*; @@ -110,7 +110,7 @@ public class HamsterMaze { 仔细研究一下打印方法: -``` +``` java static void printAll(Enumeration e) { while(e.hasMoreElements()) System.out.println( @@ -122,14 +122,14 @@ static void printAll(Enumeration e) { 这个看似特殊的例子甚至可以更为通用,因为它使用了常规的toString()方法(之所以称为常规,是由于它属于Object类的一部分)。下面是调用打印的另一个方法(尽管在效率上可能会差一些): -``` +``` java System.out.println("" + e.nextElement()); ``` 它采用了封装到Java内部的“自动转换成字串”技术。一旦编译器碰到一个字串,后面跟随一个“+”,就会希望后面又跟随一个字串,并自动调用toString()。在Java 1.1中,第一个字串是不必要的;所有对象都会转换成字串。亦可对此执行一次造型,获得与调用toString()同样的效果: -``` +``` java System.out.println((String)e.nextElement()) ``` -但我们想做的事情通常并不仅仅是调用Object方法,所以会再度面临类型造型的问题。对于自己感兴趣的类型,必须假定自己已获得了一个Enumeration,然后将结果对象造型成为那种类型(若操作错误,会得到运行期违例)。 +但我们想做的事情通常并不仅仅是调用Object方法,所以会再度面临类型造型的问题。对于自己感兴趣的类型,必须假定自己已获得了一个Enumeration,然后将结果对象造型成为那种类型(若操作错误,会得到运行期异常)。 diff --git "a/8.4 \351\233\206\345\220\210\347\232\204\347\261\273\345\236\213.md" "b/8.4 \351\233\206\345\220\210\347\232\204\347\261\273\345\236\213.md" index 01fb427..04aa61a 100644 --- "a/8.4 \351\233\206\345\220\210\347\232\204\347\261\273\345\236\213.md" +++ "b/8.4 \351\233\206\345\220\210\347\232\204\347\261\273\345\236\213.md" @@ -10,7 +10,7 @@ Vector的用法很简单,这已在前面的例子中得到了证明。尽管 Java标准集合里包含了toString()方法,所以它们能生成自己的String表达方式,包括它们容纳的对象。例如在Vector中,toString()会在Vector的各个元素中步进和遍历,并为每个元素调用toString()。假定我们现在想打印出自己类的地址。看起来似乎简单地引用this即可(特别是C++程序员有这样做的倾向): -``` +``` java //: CrashJava.java // One way to crash Java import java.util.*; @@ -28,15 +28,15 @@ public class CrashJava { } ///:~ ``` -若只是简单地创建一个CrashJava对象,并将其打印出来,就会得到无穷无尽的一系列违例错误。然而,假如将CrashJava对象置入一个Vector,并象这里演示的那样打印Vector,就不会出现什么错误提示,甚至连一个违例都不会出现。此时Java只是简单地崩溃(但至少它没有崩溃我的操作系统)。这已在Java 1.1中测试通过。 +若只是简单地创建一个CrashJava对象,并将其打印出来,就会得到无穷无尽的一系列异常错误。然而,假如将CrashJava对象置入一个Vector,并象这里演示的那样打印Vector,就不会出现什么错误提示,甚至连一个异常都不会出现。此时Java只是简单地崩溃(但至少它没有崩溃我的操作系统)。这已在Java 1.1中测试通过。 此时发生的是字串的自动类型转换。当我们使用下述语句时: -``` +``` java "CrashJava address: " + this ``` -编译器就在一个字串后面发现了一个“+”以及好象并非字串的其他东西,所以它会试图将this转换成一个字串。转换时调用的是toString(),后者会产生一个递归调用。若在一个Vector内出现这种事情,看起来堆栈就会溢出,同时违例控制机制根本没有机会作出响应。 +编译器就在一个字串后面发现了一个“+”以及好象并非字串的其他东西,所以它会试图将this转换成一个字串。转换时调用的是toString(),后者会产生一个递归调用。若在一个Vector内出现这种事情,看起来栈就会溢出,同时异常控制机制根本没有机会作出响应。 若确实想在这种情况下打印出对象的地址,解决方案就是调用Object的toString方法。此时就不必加入this,只需使用super.toString()。当然,采取这种做法也有一个前提:我们必须从Object直接继承,或者没有一个父类覆盖了toString方法。 @@ -48,7 +48,7 @@ BitSet实际是由“二进制位”构成的一个Vector。如果希望高效 在一个普通的Vector中,随我们加入越来越多的元素,集合也会自我膨胀。在某种程度上,BitSet也不例外。也就是说,它有时会自行扩展,有时则不然。而且Java的1.0版本似乎在这方面做得最糟,它的BitSet表现十分差强人意(Java1.1已改正了这个问题)。下面这个例子展示了BitSet是如何运作的,同时演示了1.0版本的错误: -``` +``` java //: Bits.java // Demonstration of BitSet import java.util.*; @@ -111,17 +111,17 @@ public class Bits { } ///:~ ``` -随机数字生成器用于创建一个随机的byte、short和int。每一个都会转换成BitSet内相应的位模型。此时一切都很正常,因为BitSet是64位的,所以它们都不会造成最终尺寸的增大。但在Java 1.0中,一旦BitSet大于64位,就会出现一些令人迷惑不解的行为。假如我们设置一个只比BitSet当前分配存储空间大出1的一个位,它能够正常地扩展。但一旦试图在更高的位置设置位,同时不先接触边界,就会得到一个恼人的违例。这正是由于BitSet在Java 1.0里不能正确扩展造成的。本例创建了一个512位的BitSet。构建器分配的存储空间是位数的两倍。所以假如设置位1024或更高的位,同时没有先设置位1023,就会在Java 1.0里得到一个违例。但幸运的是,这个问题已在Java 1.1得到了改正。所以如果是为Java 1.0写代码,请尽量避免使用BitSet。 +随机数字生成器用于创建一个随机的byte、short和int。每一个都会转换成BitSet内相应的位模型。此时一切都很正常,因为BitSet是64位的,所以它们都不会造成最终尺寸的增大。但在Java 1.0中,一旦BitSet大于64位,就会出现一些令人迷惑不解的行为。假如我们设置一个只比BitSet当前分配存储空间大出1的一个位,它能够正常地扩展。但一旦试图在更高的位置设置位,同时不先接触边界,就会得到一个恼人的异常。这正是由于BitSet在Java 1.0里不能正确扩展造成的。本例创建了一个512位的BitSet。构造器分配的存储空间是位数的两倍。所以假如设置位1024或更高的位,同时没有先设置位1023,就会在Java 1.0里得到一个异常。但幸运的是,这个问题已在Java 1.1得到了改正。所以如果是为Java 1.0写代码,请尽量避免使用BitSet。 8.4.3 Stack -Stack有时也可以称为“后入先出”(LIFO)集合。换言之,我们在堆栈里最后“压入”的东西将是以后第一个“弹出”的。和其他所有Java集合一样,我们压入和弹出的都是“对象”,所以必须对自己弹出的东西进行“造型”。 +Stack有时也可以称为“后入先出”(LIFO)集合。换言之,我们在栈里最后“压入”的东西将是以后第一个“弹出”的。和其他所有Java集合一样,我们压入和弹出的都是“对象”,所以必须对自己弹出的东西进行“造型”。 一种很少见的做法是拒绝使用Vector作为一个Stack的基本构成元素,而是从Vector里“继承”一个Stack。这样一来,它就拥有了一个Vector的所有特征及行为,另外加上一些额外的Stack行为。很难判断出设计者到底是明确想这样做,还是属于一种固有的设计。 -下面是一个简单的堆栈示例,它能读入数组的每一行,同时将其作为字串压入堆栈。 +下面是一个简单的栈示例,它能读入数组的每一行,同时将其作为字串压入栈。 -``` +``` java //: Stacks.java // Demonstration of Stack Class import java.util.*; @@ -147,17 +147,17 @@ public class Stacks { } ///:~ ``` -months数组的每一行都通过push()继承进入堆栈,稍后用pop()从堆栈的顶部将其取出。要声明的一点是,Vector操作亦可针对Stack对象进行。这可能是由继承的特质决定的——Stack“属于”一种Vector。因此,能对Vector进行的操作亦可针对Stack进行,例如elementAt()方法。 +months数组的每一行都通过push()继承进入栈,稍后用pop()从栈的顶部将其取出。要声明的一点是,Vector操作亦可针对Stack对象进行。这可能是由继承的特质决定的——Stack“属于”一种Vector。因此,能对Vector进行的操作亦可针对Stack进行,例如elementAt()方法。 8.4.4 Hashtable -Vector允许我们用一个数字从一系列对象中作出选择,所以它实际是将数字同对象关联起来了。但假如我们想根据其他标准选择一系列对象呢?堆栈就是这样的一个例子:它的选择标准是“最后压入堆栈的东西”。这种“从一系列对象中选择”的概念亦可叫作一个“映射”、“字典”或者“关联数组”。从概念上讲,它看起来象一个Vector,但却不是通过数字来查找对象,而是用另一个对象来查找它们!这通常都属于一个程序中的重要进程。 +Vector允许我们用一个数字从一系列对象中作出选择,所以它实际是将数字同对象关联起来了。但假如我们想根据其他标准选择一系列对象呢?栈就是这样的一个例子:它的选择标准是“最后压入栈的东西”。这种“从一系列对象中选择”的概念亦可叫作一个“映射”、“字典”或者“关联数组”。从概念上讲,它看起来象一个Vector,但却不是通过数字来查找对象,而是用另一个对象来查找它们!这通常都属于一个程序中的重要进程。 在Java中,这个概念具体反映到抽象类Dictionary身上。该类的接口是非常直观的size()告诉我们其中包含了多少元素;isEmpty()判断是否包含了元素(是则为true);put(Object key, Object value)添加一个值(我们希望的东西),并将其同一个键关联起来(想用于搜索它的东西);get(Object key)获得与某个键对应的值;而remove(Object Key)用于从列表中删除“键-值”对。还可以使用枚举技术:keys()产生对键的一个枚举(Enumeration);而elements()产生对所有值的一个枚举。这便是一个Dictionary(字典)的全部。 Dictionary的实现过程并不麻烦。下面列出一种简单的方法,它使用了两个Vector,一个用于容纳键,另一个用来容纳值: -``` +``` java //: AssocArray.java // Simple version of a Dictionary import java.util.*; @@ -209,7 +209,7 @@ public class AssocArray extends Dictionary { } ///:~ ``` -在对AssocArray的定义中,我们注意到的第一个问题是它“扩展”了字典。这意味着AssocArray属于Dictionary的一种类型,所以可对其发出与Dictionary一样的请求。如果想生成自己的Dictionary,而且就在这里进行,那么要做的全部事情只是填充位于Dictionary内的所有方法(而且必须覆盖所有方法,因为它们——除构建器外——都是抽象的)。 +在对AssocArray的定义中,我们注意到的第一个问题是它“扩展”了字典。这意味着AssocArray属于Dictionary的一种类型,所以可对其发出与Dictionary一样的请求。如果想生成自己的Dictionary,而且就在这里进行,那么要做的全部事情只是填充位于Dictionary内的所有方法(而且必须覆盖所有方法,因为它们——除构造器外——都是抽象的)。 Vector key和value通过一个标准索引编号链接起来。也就是说,如果用“roof”的一个键以及“blue”的一个值调用put()——假定我们准备将一个房子的各部分与它们的油漆颜色关联起来,而且AssocArray里已有100个元素,那么“roof”就会有101个键元素,而“blue”有101个值元素。而且要注意一下get(),假如我们作为键传递“roof”,它就会产生与keys.index.Of()的索引编号,然后用那个索引编号生成相关的值矢量内的值。 @@ -224,7 +224,7 @@ main()中进行的测试是非常简单的;它只是将小写字符转换成 作为应用散列表的一个例子,可考虑用一个程序来检验Java的Math.random()方法的随机性到底如何。在理想情况下,它应该产生一系列完美的随机分布数字。但为了验证这一点,我们需要生成数量众多的随机数字,然后计算落在不同范围内的数字多少。散列表可以极大简化这一工作,因为它能将对象同对象关联起来(此时是将Math.random()生成的值同那些值出现的次数关联起来)。如下所示: -``` +``` java //: Statistics.java // Simple demonstration of Hashtable import java.util.*; @@ -253,26 +253,26 @@ class Statistics { } ///:~ ``` -在main()中,每次产生一个随机数字,它都会封装到一个Integer对象里,使句柄能够随同散列表一起使用(不可对一个集合使用基本数据类型,只能使用对象句柄)。containKey()方法检查这个键是否已经在集合里(也就是说,那个数字以前发现过吗?)若已在集合里,则get()方法获得那个键关联的值,此时是一个Counter(计数器)对象。计数器内的值i随后会增加1,表明这个特定的随机数字又出现了一次。 +在main()中,每次产生一个随机数字,它都会封装到一个Integer对象里,使指针能够随同散列表一起使用(不可对一个集合使用基本数据类型,只能使用对象指针)。containKey()方法检查这个键是否已经在集合里(也就是说,那个数字以前发现过吗?)若已在集合里,则get()方法获得那个键关联的值,此时是一个Counter(计数器)对象。计数器内的值i随后会增加1,表明这个特定的随机数字又出现了一次。 假如键以前尚未发现过,那么方法put()仍然会在散列表内置入一个新的“键-值”对。在创建之初,Counter会自己的变量i自动初始化为1,它标志着该随机数字的第一次出现。 为显示散列表,只需把它简单地打印出来即可。Hashtable toString()方法能遍历所有键-值对,并为每一对都调用toString()。Integer toString()是事先定义好的,可看到计数器使用的toString。一次运行的结果(添加了一些换行)如下: -``` +``` java {19=526, 18=533, 17=460, 16=513, 15=521, 14=495, 13=512, 12=483, 11=488, 10=487, 9=514, 8=523, 7=497, 6=487, 5=480, 4=489, 3=509, 2=503, 1=475, 0=505} ``` -大家或许会对Counter类是否必要感到疑惑,它看起来似乎根本没有封装类Integer的功能。为什么不用int或Integer呢?事实上,由于所有集合能容纳的仅有对象句柄,所以根本不可以使用整数。学过集合后,封装类的概念对大家来说就可能更容易理解了,因为不可以将任何基本数据类型置入集合里。然而,我们对Java封装器能做的唯一事情就是将其初始化成一个特定的值,然后读取那个值。也就是说,一旦封装器对象已经创建,就没有办法改变一个值。这使得Integer封装器对解决我们的问题毫无意义,所以不得不创建一个新类,用它来满足自己的要求。 +大家或许会对Counter类是否必要感到疑惑,它看起来似乎根本没有封装类Integer的功能。为什么不用int或Integer呢?事实上,由于所有集合能容纳的仅有对象指针,所以根本不可以使用整数。学过集合后,封装类的概念对大家来说就可能更容易理解了,因为不可以将任何基本数据类型置入集合里。然而,我们对Java封装器能做的唯一事情就是将其初始化成一个特定的值,然后读取那个值。也就是说,一旦封装器对象已经创建,就没有办法改变一个值。这使得Integer封装器对解决我们的问题毫无意义,所以不得不创建一个新类,用它来满足自己的要求。 1. 创建“关键”类 在前面的例子里,我们用一个标准库的类(Integer)作为Hashtable的一个键使用。作为一个键,它能很好地工作,因为它已经具备正确运行的所有条件。但在使用散列表的时候,一旦我们创建自己的类作为键使用,就会遇到一个很常见的问题。例如,假设一套天气预报系统将Groundhog(土拔鼠)对象匹配成Prediction(预报)。这看起来非常直观:我们创建两个类,然后将Groundhog作为键使用,而将Prediction作为值使用。如下所示: -``` +``` java //: SpringDetector.java // Looks plausible, but doesn't work right. import java.util.*; @@ -314,7 +314,7 @@ public class SpringDetector { 因此,为了在散列表中将自己的类作为键使用,必须同时覆盖hashCode()和equals(),就象下面展示的那样: -``` +``` java //: SpringDetector2.java // If you create a class that's used as a key in // a Hashtable, you must override hashCode() @@ -355,7 +355,7 @@ equals()方法要进行两种检查:检查对象是否为null;若不为null 在本书的第一个例子中,我们使用了一个名为Properties(属性)的Hashtable类型。在那个例子中,下述程序行: -``` +``` java Properties p = System.getProperties(); p.list(System.out); ``` @@ -370,7 +370,7 @@ Properties类亦可在我们的程序中使用(第17章的ClassScanner.java便 我们现在可以开始演示Enumeration(枚举)的真正威力:将穿越一个序列的操作与那个序列的基础结构分隔开。在下面的例子里,PrintData类用一个Enumeration在一个序列中移动,并为每个对象都调用toString()方法。此时创建了两个不同类型的集合:一个Vector和一个Hashtable。并且在它们里面分别填充Mouse和Hamster对象(本章早些时候已定义了这些类;注意必须先编译HamsterMaze.java和WorksAnyway.java,否则下面的程序不能编译)。由于Enumeration隐藏了基层集合的结构,所以PrintData不知道或者不关心Enumeration来自于什么类型的集合: -``` +``` java //: Enumerators2.java // Revisiting Enumerations import java.util.*; diff --git "a/8.5 \346\216\222\345\272\217.md" "b/8.5 \346\216\222\345\272\217.md" index f56e1c4..10fcfea 100644 --- "a/8.5 \346\216\222\345\272\217.md" +++ "b/8.5 \346\216\222\345\272\217.md" @@ -8,7 +8,7 @@ Java 1.0和1.1库都缺少的一样东西是算术运算,甚至没有最简单 下面这个“接口”(Interface)展示了如何比较两个对象,它将那些“要发生变化的东西”封装在内: -``` +``` java //: Compare.java // Interface for sorting callback: package c08; @@ -23,7 +23,7 @@ interface Compare { 可创建Vector的一个子类,通过Compare实现“快速排序”。对于这种算法,包括它的速度以及原理等等,在此不具体说明。欲知详情,可参考Binstock和Rex编著的《Practical Algorithms for Programmers》,由Addison-Wesley于1995年出版。 -``` +``` java //: SortVector.java // A generic sorting vector package c08; @@ -70,7 +70,7 @@ public class SortVector extends Vector { 为使用SortVector,必须创建一个类,令其为我们准备排序的对象实现Compare。此时内部类并不显得特别重要,但对于代码的组织却是有益的。下面是针对String对象的一个例子: -``` +``` java //: StringSortTest.java // Testing the generic sorting Vector package c08; @@ -117,7 +117,7 @@ public class StringSortTest { 但在另一方面,请考虑采用“合成”方法:将一个对象置入一个新类的内部。此时,不是改写上述代码来达到这个目的,而是在新类里简单地使用一个SortVector。在这种情况下,用于实现Compare接口的内部类就可以“匿名”地创建。如下所示: -``` +``` java //: StrSortVector.java // Automatically sorted Vector that // accepts and produces only Strings @@ -181,6 +181,6 @@ public class StrSortVector { 这样便可快速再生来自SortVector的代码,从而获得希望的功能。然而,并不是来自SortVector和Vector的所有public方法都能在StrSortVector中出现。若按这种形式再生代码,可在新类里为包含类内的每一个方法都生成一个定义。当然,也可以在刚开始时只添加少数几个,以后根据需要再添加更多的。新类的设计最终会稳定下来。 -这种方法的好处在于它仍然只接纳String对象,也只产生String对象。而且相应的检查是在编译期间进行的,而非在运行期。当然,只有addElement()和elementAt()才具备这一特性;elements()仍然会产生一个Enumeration(枚举),它在编译期的类型是未定的。当然,对Enumeration以及在StrSortVector中的类型检查会照旧进行;如果真的有什么错误,运行期间会简单地产生一个违例。事实上,我们在编译或运行期间能保证一切都正确无误吗?(也就是说,“代码测试时也许不能保证”,以及“该程序的用户有可能做一些未经我们测试的事情”)。尽管存在其他选择和争论,使用继承都要容易得多,只是在造型时让人深感不便。同样地,一旦为Java加入参数化类型,就有望解决这个问题。 +这种方法的好处在于它仍然只接纳String对象,也只产生String对象。而且相应的检查是在编译期间进行的,而非在运行期。当然,只有addElement()和elementAt()才具备这一特性;elements()仍然会产生一个Enumeration(枚举),它在编译期的类型是未定的。当然,对Enumeration以及在StrSortVector中的类型检查会照旧进行;如果真的有什么错误,运行期间会简单地产生一个异常。事实上,我们在编译或运行期间能保证一切都正确无误吗?(也就是说,“代码测试时也许不能保证”,以及“该程序的用户有可能做一些未经我们测试的事情”)。尽管存在其他选择和争论,使用继承都要容易得多,只是在造型时让人深感不便。同样地,一旦为Java加入参数化类型,就有望解决这个问题。 大家在这个类中可以看到有一个名为“sorted”的标志。每次调用addElement()时,都可对Vector进行排序,而且将其连续保持在一个排好序的状态。但在开始读取之前,人们总是向一个Vector添加大量元素。所以与其在每个addElement()后排序,不如一直等到有人想读取Vector,再对其进行排序。后者的效率要高得多。这种除非绝对必要,否则就不采取行动的方法叫作“懒惰求值”(还有一种类似的技术叫作“懒惰初始化”——除非真的需要一个字段值,否则不进行初始化)。 diff --git "a/8.6 \351\200\232\347\224\250\351\233\206\345\220\210\345\272\223.md" "b/8.6 \351\200\232\347\224\250\351\233\206\345\220\210\345\272\223.md" index bcbe42e..70b403b 100644 --- "a/8.6 \351\200\232\347\224\250\351\233\206\345\220\210\345\272\223.md" +++ "b/8.6 \351\200\232\347\224\250\351\233\206\345\220\210\345\272\223.md" @@ -1,6 +1,6 @@ # 8.6 通用集合库 -通过本章的学习,大家已知道标准Java库提供了一些特别有用的集合,但距完整意义的集合尚远。除此之外,象排序这样的算法根本没有提供支持。C++出色的一个地方就是它的库,特别是“标准模板库”(STL)提供了一套相当完整的集合,以及许多象排序和检索这样的算法,可以非常方便地对那些集合进行操作。有感这一现状,并以这个模型为基础,ObjectSpace公司设计了Java版本的“通用集合库”(从前叫作“Java通用库”,即JGL;但JGL这个缩写形式侵犯了Sun公司的版权——尽管本书仍然沿用这个简称)。这个库尽可能遵照STL的设计(照顾到两种语言间的差异)。JGL实现了许多功能,可满足对一个集合库的大多数常规需求,它与C++的模板机制非常相似。JGL包括相互链接起来的列表、设置、队列、映射、堆栈、序列以及反复器,它们的功能比Enumeration(枚举)强多了。同时提供了一套完整的算法,如检索和排序等。在某些方面,ObjectSpace的设计也显得比Sun的库设计方案“智能”一些。举个例子来说,JGL集合中的方法不会进入final状态,所以很容易继承和改写那些方法。 +通过本章的学习,大家已知道标准Java库提供了一些特别有用的集合,但距完整意义的集合尚远。除此之外,象排序这样的算法根本没有提供支持。C++出色的一个地方就是它的库,特别是“标准模板库”(STL)提供了一套相当完整的集合,以及许多象排序和检索这样的算法,可以非常方便地对那些集合进行操作。有感这一现状,并以这个模型为基础,ObjectSpace公司设计了Java版本的“通用集合库”(从前叫作“Java通用库”,即JGL;但JGL这个缩写形式侵犯了Sun公司的版权——尽管本书仍然沿用这个简称)。这个库尽可能遵照STL的设计(照顾到两种语言间的差异)。JGL实现了许多功能,可满足对一个集合库的大多数常规需求,它与C++的模板机制非常相似。JGL包括相互链接起来的列表、设置、队列、映射、堆栈、序列以及迭代器,它们的功能比Enumeration(枚举)强多了。同时提供了一套完整的算法,如检索和排序等。在某些方面,ObjectSpace的设计也显得比Sun的库设计方案“智能”一些。举个例子来说,JGL集合中的方法不会进入final状态,所以很容易继承和改写那些方法。 JGL已包括到一些厂商发行的Java套件中,而且ObjectSpace公司自己也允许所有用户免费使用JGL,包括商业性的使用。详细情况和软件下载可访问 http://www.ObjectSpace.com 。与JGL配套提供的联机文档做得非常好,可作为自己的一个绝佳起点使用。 diff --git "a/8.7 \346\226\260\351\233\206\345\220\210.md" "b/8.7 \346\226\260\351\233\206\345\220\210.md" index c0da8ec..57abce3 100644 --- "a/8.7 \346\226\260\351\233\206\345\220\210.md" +++ "b/8.7 \346\226\260\351\233\206\345\220\210.md" @@ -2,7 +2,7 @@ 对我来说,集合类属于最强大的一种工具,特别适合在原创编程中使用。大家可能已感觉到我对Java 1.1提供的集合多少有点儿失望。因此,看到Java 1.2对集合重新引起了正确的注意后,确实令人非常愉快。这个版本的集合也得到了完全的重新设计(由Sun公司的Joshua Bloch)。我认为新设计的集合是Java 1.2中两项最主要的特性之一(另一项是Swing库,将在第13章叙述),因为它们极大方便了我们的编程,也使Java变成一种更成熟的编程系统。 -有些设计使得元素间的结合变得更紧密,也更容易让人理解。例如,许多名字都变得更短、更明确了,而且更易使用;类型同样如此。有些名字进行了修改,更接近于通俗:我感觉特别好的一个是用“反复器”(Inerator)代替了“枚举”(Enumeration)。 +有些设计使得元素间的结合变得更紧密,也更容易让人理解。例如,许多名字都变得更短、更明确了,而且更易使用;类型同样如此。有些名字进行了修改,更接近于通俗:我感觉特别好的一个是用“迭代器”(Inerator)代替了“枚举”(Enumeration)。 此次重新设计也加强了集合库的功能。现在新增的行为包括链接列表、队列以及撤消组队(即“双终点队列”)。 @@ -22,18 +22,18 @@ Collection和Map可通过多种形式实现,具体由编程要求决定。下 ⑥:写作本章时,Java 1.2尚处于β测试阶段,所以这张示意图没有包括以后会加入的TreeSet。 -虚线框代表“接口”,点线框代表“抽象”类,而实线框代表普通(实际)类。点线箭头表示一个特定的类准备实现一个接口(在抽象类的情况下,则是“部分”实现一个接口)。双线箭头表示一个类可生成箭头指向的那个类的对象。例如,任何集合都可以生成一个反复器(Iterator),而一个列表可以生成一个ListIterator(以及原始的反复器,因为列表是从集合继承的)。 +虚线框代表“接口”,点线框代表“抽象”类,而实线框代表普通(实际)类。点线箭头表示一个特定的类准备实现一个接口(在抽象类的情况下,则是“部分”实现一个接口)。双线箭头表示一个类可生成箭头指向的那个类的对象。例如,任何集合都可以生成一个迭代器(Iterator),而一个列表可以生成一个ListIterator(以及原始的迭代器,因为列表是从集合继承的)。 致力于容纳对象的接口是Collection,List,Set和Map。在传统情况下,我们需要写大量代码才能同这些接口打交道。而且为了指定自己想使用的准确类型,必须在创建之初进行设置。所以可能创建下面这样的一个List: -``` +``` java List x = new LinkedList(); ``` 当然,也可以决定将x作为一个LinkedList使用(而不是一个普通的List),并用x负载准确的类型信息。使用接口的好处就是一旦决定改变自己的实施细节,要做的全部事情就是在创建的时候改变它,就象下面这样: -``` +``` java List x = new ArrayList(); ``` @@ -43,7 +43,7 @@ List x = new ArrayList(); 因此,在观看这张示意图时,真正需要关心的只有位于最顶部的“接口”以及普通(实际)类——均用实线方框包围。通常需要生成实际类的一个对象,将其上溯造型为对应的接口。以后即可在代码的任何地方使用那个接口。下面是一个简单的例子,它用String对象填充一个集合,然后打印出集合内的每一个元素: -``` +``` java //: SimpleCollection.java // A simple example using the new Collections package c08.newcollections; @@ -63,7 +63,7 @@ public class SimpleCollection { 新集合库的所有代码示例都置于子目录newcollections下,这样便可提醒自己这些工作只对于Java 1.2有效。这样一来,我们必须用下述代码来调用程序: -``` +``` java java c08.newcollections.SimpleCollection ``` @@ -74,21 +74,21 @@ java c08.newcollections.SimpleCollection main()的第一行创建了一个ArrayList对象,然后将其上溯造型成为一个集合。由于这个例子只使用了Collection方法,所以从Collection继承的一个类的任何对象都可以正常工作。但ArrayList是一个典型的Collection,它代替了Vector的位置。 显然,add()方法的作用是将一个新元素置入集合里。然而,用户文档谨慎地指出add()“保证这个集合包含了指定的元素”。这一点是为Set作铺垫的,后者只有在元素不存在的前提下才会真的加入那个元素。对于ArrayList以及其他任何形式的List,add()肯定意味着“直接加入”。 -利用iterator()方法,所有集合都能生成一个“反复器”(Iterator)。反复器其实就象一个“枚举”(Enumeration),是后者的一个替代物,只是: +利用iterator()方法,所有集合都能生成一个“迭代器”(Iterator)。迭代器其实就象一个“枚举”(Enumeration),是后者的一个替代物,只是: -(1) 它采用了一个历史上默认、而且早在OOP中得到广泛采纳的名字(反复器)。 +(1) 它采用了一个历史上默认、而且早在OOP中得到广泛采纳的名字(迭代器)。 (2) 采用了比Enumeration更短的名字:hasNext()代替了hasMoreElement(),而next()代替了nextElement()。 (3) 添加了一个名为remove()的新方法,可删除由Iterator生成的上一个元素。所以每次调用next()的时候,只需调用remove()一次。 -在SimpleCollection.java中,大家可看到创建了一个反复器,并用它在集合里遍历,打印出每个元素。 +在SimpleCollection.java中,大家可看到创建了一个迭代器,并用它在集合里遍历,打印出每个元素。 8.7.1 使用Collections 下面这张表格总结了用一个集合能做的所有事情(亦可对Set和List做同样的事情,尽管List还提供了一些额外的功能)。Map不是从Collection继承的,所以要单独对待。 -``` +``` java Boolean add(Object) *Ensures that the Collection contains the argument. Returns false if it doesn’t add the argument. @@ -151,19 +151,19 @@ void clear() *删除集合内的所有元素 boolean contains(Object) 若集合包含自变量,就返回“真” boolean containsAll(Collection) 若集合包含了自变量内的所有元素,就返回“真” boolean isEmpty() 若集合内没有元素,就返回“真” -Iterator iterator() 返回一个反复器,以用它遍历集合的各元素 +Iterator iterator() 返回一个迭代器,以用它遍历集合的各元素 boolean remove(Object) *如自变量在集合里,就删除那个元素的一个实例。如果已进行了删除,就返回“真” boolean removeAll(Collection) *删除自变量里的所有元素。如果已进行了任何删除,就返回“真” boolean retainAll(Collection) *只保留包含在一个自变量里的元素(一个理论的“交集”)。如果已进行了任何改变,就返回“真” int size() 返回集合内的元素数量 Object[] toArray() 返回包含了集合内所有元素的一个数组 -*这是一个“可选的”方法,有的集合可能并未实现它。若确实如此,该方法就会遇到一个UnsupportedOperatiionException,即一个“操作不支持”违例,详见第9章。 +*这是一个“可选的”方法,有的集合可能并未实现它。若确实如此,该方法就会遇到一个UnsupportedOperatiionException,即一个“操作不支持”异常,详见第9章。 ``` 下面这个例子向大家演示了所有方法。同样地,它们只对从集合继承的东西有效,一个ArrayList作为一种“不常用的分母”使用: -``` +``` java //: Collection1.java // Things you can do with all Collections package c08.newcollections; @@ -265,7 +265,7 @@ public class Collection1 { newCollection()的两个版本都创建了ArrayList,用于包含不同的数据集,并将它们作为集合对象返回。所以很明显,除了Collection接口之外,不会再用到其他什么。 -print()方法也会在本节经常用到。由于它用一个反复器(Iterator)在一个集合内遍历,而任何集合都可以产生这样的一个反复器,所以它适用于List和Set,也适用于由一个Map生成的Collection。 +print()方法也会在本节经常用到。由于它用一个迭代器(Iterator)在一个集合内遍历,而任何集合都可以产生这样的一个迭代器,所以它适用于List和Set,也适用于由一个Map生成的Collection。 main()用简单的手段显示出了集合内的所有方法。 @@ -273,7 +273,7 @@ main()用简单的手段显示出了集合内的所有方法。 8.7.2 使用Lists -``` +``` java List (interface) Order is the most important feature of a List; it promises to maintain elements in a particular sequence. List adds a number of methods to Collection that allow insertion and removal of elements in the middle of a List. (This is recommended only for a LinkedList.) A List will produce a ListIterator, and using this you can traverse the List in both directions, as well as insert and remove elements in the middle of the list (again, recommended only for a LinkedList). @@ -288,15 +288,15 @@ Provides optimal sequential access, with inexpensive insertions and deletions fr ``` -List(接口) 顺序是List最重要的特性;它可保证元素按照规定的顺序排列。List为Collection添加了大量方法,以便我们在List中部插入和删除元素(只推荐对LinkedList这样做)。List也会生成一个ListIterator(列表反复器),利用它可在一个列表里朝两个方向遍历,同时插入和删除位于列表中部的元素(同样地,只建议对LinkedList这样做) +List(接口) 顺序是List最重要的特性;它可保证元素按照规定的顺序排列。List为Collection添加了大量方法,以便我们在List中部插入和删除元素(只推荐对LinkedList这样做)。List也会生成一个ListIterator(列表迭代器),利用它可在一个列表里朝两个方向遍历,同时插入和删除位于列表中部的元素(同样地,只建议对LinkedList这样做) ArrayList* 由一个数组后推得到的List。作为一个常规用途的对象容器使用,用于替换原先的Vector。允许我们快速访问元素,但在从列表中部插入和删除元素时,速度却嫌稍慢。一般只应该用ListIterator对一个ArrayList进行向前和向后遍历,不要用它删除和插入元素;与LinkedList相比,它的效率要低许多 LinkedList 提供优化的顺序访问性能,同时可以高效率地在列表中部进行插入和删除操作。但在进行随机访问时,速度却相当慢,此时应换用ArrayList。也提供了addFirst(),addLast(),getFirst(),getLast(),removeFirst()以及removeLast()(未在任何接口或基础类中定义),以便将其作为一个规格、队列以及一个双向队列使用 -下面这个例子中的方法每个都覆盖了一组不同的行为:每个列表都能做的事情(basicTest()),通过一个反复器遍历(iterMotion())、用一个反复器改变某些东西(iterManipulation())、体验列表处理的效果(testVisual())以及只有LinkedList才能做的事情等: +下面这个例子中的方法每个都覆盖了一组不同的行为:每个列表都能做的事情(basicTest()),通过一个迭代器遍历(iterMotion())、用一个迭代器改变某些东西(iterManipulation())、体验列表处理的效果(testVisual())以及只有LinkedList才能做的事情等: -``` +``` java //: List1.java // Things you can do with Lists package c08.newcollections; @@ -447,9 +447,9 @@ public class List1 { 8.7.3 使用Sets -Set拥有与Collection完全相同的接口,所以和两种不同的List不同,它没有什么额外的功能。相反,Set完全就是一个Collection,只是具有不同的行为(这是实例和多形性最理想的应用:用于表达不同的行为)。在这里,一个Set只允许每个对象存在一个实例(正如大家以后会看到的那样,一个对象的“值”的构成是相当复杂的)。 +Set拥有与Collection完全相同的接口,所以和两种不同的List不同,它没有什么额外的功能。相反,Set完全就是一个Collection,只是具有不同的行为(这是实例和多态性最理想的应用:用于表达不同的行为)。在这里,一个Set只允许每个对象存在一个实例(正如大家以后会看到的那样,一个对象的“值”的构成是相当复杂的)。 -``` +``` java Set (interface) Each element that you add to the Set must be unique; otherwise the Set doesn’t add the duplicate element. Objects added to a Set must define equals( ) to establish object uniqueness. Set has exactly the same interface as Collection. The Set interface does not guarantee it will maintain its elements in any particular order. @@ -473,7 +473,7 @@ TreeSet 由一个“红黑树”后推得到的顺序Set(注释⑦)。这样 下面这个例子并没有列出用一个Set能够做的全部事情,因为接口与Collection是相同的,前例已经练习过了。相反,我们要例示的重点在于使一个Set独一无二的行为: -``` +``` java //: Set1.java // Things you can do with Sets package c08.newcollections; @@ -507,7 +507,7 @@ public class Set1 { 运行这个程序时,会注意到由HashSet维持的顺序与ArraySet是不同的。这是由于它们采用了不同的方法来保存元素,以便它们以后的定位。ArraySet保持着它们的顺序状态,而HashSet使用一个散列函数,这是特别为快速检索设计的)。创建自己的类型时,一定要注意Set需要通过一种方式来维持一种存储顺序,就象本章早些时候展示的“groundhog”(土拔鼠)例子那样。下面是一个例子: -``` +``` java //: Set2.java // Putting your own type in a Set package c08.newcollections; @@ -557,7 +557,7 @@ public class Set2 { 8.7.4 使用Maps -``` +``` java Map (interface) Maintains key-value associations (pairs), so you can look up a value using a key. @@ -575,7 +575,7 @@ Implementation based on a red-black tree. When you view the keys or the pairs, t Map(接口) 维持“键-值”对应关系(对),以便通过一个键查找相应的值 -`HashMap*` 基于一个散列表实现(用它代替Hashtable)。针对“键-值”对的插入和检索,这种形式具有最稳定的性能。可通过构建器对这一性能进行调整,以便设置散列表的“能力”和“装载因子” +`HashMap*` 基于一个散列表实现(用它代替Hashtable)。针对“键-值”对的插入和检索,这种形式具有最稳定的性能。可通过构造器对这一性能进行调整,以便设置散列表的“能力”和“装载因子” ArrayMap 由一个ArrayList后推得到的Map。对反复的顺序提供了精确的控制。面向非常小的Map设计,特别是那些需要经常创建和删除的。对于非常小的Map,创建和反复所付出的代价要比HashMap低得多。但在Map变大以后,性能也会相应地大幅度降低 @@ -584,7 +584,7 @@ Comparator,稍后即会讲到)。TreeMap最大的好处就是我们得到的 下例包含了两套测试数据以及一个fill()方法,利用该方法可以用任何两维数组(由Object构成)填充任何Map。这些工具也会在其他Map例子中用到。 -``` +``` java //: Map1.java // Things you can do with Maps package c08.newcollections; @@ -673,7 +673,7 @@ public class Map1 { printKeys(),printValues()以及print()方法并不只是有用的工具,它们也清楚地揭示了一个Map的Collection“景象”的产生过程。keySet()方法会产生一个Set,它由Map中的键后推得来。在这儿,它只被当作一个Collection对待。values()也得到了类似的对待,它的作用是产生一个List,其中包含了Map中的所有值(注意键必须是独一无二的,而值可以有重复)。由于这些Collection是由Map后推得到的,所以一个Collection中的任何改变都会在相应的Map中反映出来。 -print()方法的作用是收集由entries产生的Iterator(反复器),并用它同时打印出每个“键-值”对的键和值。程序剩余的部分提供了每种Map操作的简单示例,并对每种类型的Map进行了测试。 +print()方法的作用是收集由entries产生的Iterator(迭代器),并用它同时打印出每个“键-值”对的键和值。程序剩余的部分提供了每种Map操作的简单示例,并对每种类型的Map进行了测试。 当创建自己的类,将其作为Map中的一个键使用时,必须注意到和以前的Set相同的问题。 @@ -683,7 +683,7 @@ print()方法的作用是收集由entries产生的Iterator(反复器),并 为理解这个问题,必须认识到每种不同的实施方案都有自己的特点、优点和缺点。比如在那张示意图中,可以看到Hashtable,Vector和Stack的“特点”是它们都属于“传统”类,所以不会干扰原有的代码。但在另一方面,应尽量避免为新的(Java 1.2)代码使用它们。 -其他集合间的差异通常都可归纳为它们具体是由什么“后推”的。换言之,取决于物理意义上用于实施目标接口的数据结构是什么。例如,ArrayList,LinkedList以及Vector(大致等价于ArrayList)都实现了List接口,所以无论选用哪一个,我们的程序都会得到类似的结果。然而,ArrayList(以及Vector)是由一个数组后推得到的;而LinkedList是根据常规的双重链接列表方式实现的,因为每个单独的对象都包含了数据以及指向列表内前后元素的句柄。正是由于这个原因,假如想在一个列表中部进行大量插入和删除操作,那么LinkedList无疑是最恰当的选择(LinkedList还有一些额外的功能,建立于AbstractSequentialList中)。若非如此,就情愿选择ArrayList,它的速度可能要快一些。 +其他集合间的差异通常都可归纳为它们具体是由什么“后推”的。换言之,取决于物理意义上用于实施目标接口的数据结构是什么。例如,ArrayList,LinkedList以及Vector(大致等价于ArrayList)都实现了List接口,所以无论选用哪一个,我们的程序都会得到类似的结果。然而,ArrayList(以及Vector)是由一个数组后推得到的;而LinkedList是根据常规的双重链接列表方式实现的,因为每个单独的对象都包含了数据以及指向列表内前后元素的指针。正是由于这个原因,假如想在一个列表中部进行大量插入和删除操作,那么LinkedList无疑是最恰当的选择(LinkedList还有一些额外的功能,建立于AbstractSequentialList中)。若非如此,就情愿选择ArrayList,它的速度可能要快一些。 作为另一个例子,Set既可作为一个ArraySet实现,亦可作为HashSet实现。ArraySet是由一个ArrayList后推得到的,设计成只支持少量元素,特别适合要求创建和删除大量Set对象的场合使用。然而,一旦需要在自己的Set中容纳大量元素,ArraySet的性能就会大打折扣。写一个需要Set的程序时,应默认选择HashSet。而且只有在某些特殊情况下(对性能的提升有迫切的需求),才应切换到ArraySet。 @@ -691,7 +691,7 @@ print()方法的作用是收集由entries产生的Iterator(反复器),并 为体会各种List实施方案间的差异,最简便的方法就是进行一次性能测验。下述代码的作用是建立一个内部基础类,将其作为一个测试床使用。然后为每次测验都创建一个匿名内部类。每个这样的内部类都由一个test()方法调用。利用这种方法,可以方便添加和删除测试项目。 -``` +``` java //: ListPerformance.java // Demonstrates performance differences in Lists package c08.newcollections; @@ -765,13 +765,13 @@ public class ListPerformance { } ///:~ ``` -内部类Tester是一个抽象类,用于为特定的测试提供一个基础类。它包含了一个要在测试开始时打印的字串、一个用于计算测试次数或元素数量的size参数、用于初始化字段的一个构建器以及一个抽象方法test()。test()做的是最实际的测试工作。各种类型的测试都集中到一个地方:tests数组。我们用继承于Tester的不同匿名内部类来初始化该数组。为添加或删除一个测试项目,只需在数组里简单地添加或移去一个内部类定义即可,其他所有工作都是自动进行的。 +内部类Tester是一个抽象类,用于为特定的测试提供一个基础类。它包含了一个要在测试开始时打印的字串、一个用于计算测试次数或元素数量的size参数、用于初始化字段的一个构造器以及一个抽象方法test()。test()做的是最实际的测试工作。各种类型的测试都集中到一个地方:tests数组。我们用继承于Tester的不同匿名内部类来初始化该数组。为添加或删除一个测试项目,只需在数组里简单地添加或移去一个内部类定义即可,其他所有工作都是自动进行的。 首先用元素填充传递给test()的List,然后对tests数组中的测试计时。由于测试用机器的不同,结果当然也会有所区别。这个程序的宗旨是揭示出不同集合类型的相对性能比较。下面是某一次运行得到的结果: 类型 获取 反复 插入 删除 -``` +``` java ArrayList 110 270 1920 4780 LinkedList 1870 7580 170 110 ``` @@ -782,7 +782,7 @@ LinkedList 1870 7580 170 110 可在ArraySet以及HashSet间作出选择,具体取决于Set的大小(如果需要从一个Set中获得一个顺序列表,请用TreeSet;注释⑧)。下面这个测试程序将有助于大家作出这方面的抉择: -``` +``` java //: SetPerformance.java package c08.newcollections; import java.util.*; @@ -928,7 +928,7 @@ HashSet 选择不同的Map实施方案时,注意Map的大小对于性能的影响是最大的,下面这个测试程序清楚地阐示了这一点: -``` +``` java //: MapPerformance.java // Demonstrates performance differences in Maps package c08.newcollections; @@ -1110,7 +1110,7 @@ HashMap 此外,在上面那张表里,有另一个性能问题没有反映出来。下述程序用于测试不同类型Map的创建速度: -``` +``` java //: MapCreation.java // Demonstrates time differences in Map creation package c08.newcollections; @@ -1142,13 +1142,13 @@ public class MapCreation { ``` -在写这个程序期间,TreeMap的创建速度比其他两种类型明显快得多(但你应亲自尝试一下,因为据说新版本可能会改善ArrayMap的性能)。考虑到这方面的原因,同时由于前述TreeMap出色的put()性能,所以如果需要创建大量Map,而且只有在以后才需要涉及大量检索操作,那么最佳的策略就是:创建和填充TreeMap;以后检索量增大的时候,再将重要的TreeMap转换成HashMap——使用HashMap(Map)构建器。同样地,只有在事实证明确实存在性能瓶颈后,才应关心这些方面的问题——先用起来,再根据需要加快速度。 +在写这个程序期间,TreeMap的创建速度比其他两种类型明显快得多(但你应亲自尝试一下,因为据说新版本可能会改善ArrayMap的性能)。考虑到这方面的原因,同时由于前述TreeMap出色的put()性能,所以如果需要创建大量Map,而且只有在以后才需要涉及大量检索操作,那么最佳的策略就是:创建和填充TreeMap;以后检索量增大的时候,再将重要的TreeMap转换成HashMap——使用HashMap(Map)构造器。同样地,只有在事实证明确实存在性能瓶颈后,才应关心这些方面的问题——先用起来,再根据需要加快速度。 8.7.6 未支持的操作 利用static(静态)数组Arrays.toList(),也许能将一个数组转换成List,如下所示: -``` +``` java //: Unsupported.java // Sometimes methods defined in the Collection // interfaces don't work! @@ -1198,15 +1198,15 @@ public class Unsupported { ``` -从中可以看出,实际只实现了Collection和List接口的一部分。剩余的方法导致了不受欢迎的一种情况,名为UnsupportedOperationException。在下一章里,我们会讲述违例的详细情况,但在这里有必要进行一下简单说明。这里的关键在于“集合接口”,以及新集合库内的另一些接口,它们都包含了“可选的”方法。在实现那些接口的集合类中,或者提供、或者没有提供对那些方法的支持。若调用一个未获支持的方法,就会导致一个UnsupportedOperationException(操作未支持违例),这表明出现了一个编程错误。 +从中可以看出,实际只实现了Collection和List接口的一部分。剩余的方法导致了不受欢迎的一种情况,名为UnsupportedOperationException。在下一章里,我们会讲述异常的详细情况,但在这里有必要进行一下简单说明。这里的关键在于“集合接口”,以及新集合库内的另一些接口,它们都包含了“可选的”方法。在实现那些接口的集合类中,或者提供、或者没有提供对那些方法的支持。若调用一个未获支持的方法,就会导致一个UnsupportedOperationException(操作未支持异常),这表明出现了一个编程错误。 -大家或许会觉得奇怪,不是说“接口”和基础类最大的“卖点”就是它们许诺这些方法能产生一些有意义的行为吗?上述违例破坏了那个许诺——它调用的一部分方法不仅不能产生有意义的行为,而且还会中止程序的运行。在这些情况下,类型的所谓安全保证似乎显得一钱不值!但是,情况并没有想象的那么坏。通过Collection,List,Set或者Map,编译器仍然限制我们只能调用那个接口中的方法,所以它和Smalltalk还是存在一些区别的(在Smalltalk中,可为任何对象调用任何方法,而且只有在运行程序时才知道这些调用是否可行)。除此以外,以Collection作为自变量的大多数方法只能从那个集合中读取数据——Collection的所有“read”方法都不是可选的。 +大家或许会觉得奇怪,不是说“接口”和基础类最大的“卖点”就是它们许诺这些方法能产生一些有意义的行为吗?上述异常破坏了那个许诺——它调用的一部分方法不仅不能产生有意义的行为,而且还会中止程序的运行。在这些情况下,类型的所谓安全保证似乎显得一钱不值!但是,情况并没有想象的那么坏。通过Collection,List,Set或者Map,编译器仍然限制我们只能调用那个接口中的方法,所以它和Smalltalk还是存在一些区别的(在Smalltalk中,可为任何对象调用任何方法,而且只有在运行程序时才知道这些调用是否可行)。除此以外,以Collection作为自变量的大多数方法只能从那个集合中读取数据——Collection的所有“read”方法都不是可选的。 这样一来,系统就可避免在设计期间出现接口的冲突。而在集合库的其他设计方案中,最终经常都会得到数量过多的接口,用它们描述基本方案的每一种变化形式,所以学习和掌握显得非常困难。有些时候,甚至难于捕捉接口中的所有特殊情况,因为人们可能设计出任何新接口。但Java的“不支持的操作”方法却达到了新集合库的一个重要设计目标:易于学习和使用。但是,为了使这一方法真正有效,却需满足下述条件: (1) UnsupportedOperationException必须属于一种“非常”事件。也就是说,对于大多数类来说,所有操作都应是可行的。只有在一些特殊情况下,一、两个操作才可能未获支持。新集合库满足了这一条件,因为绝大多数时候用到的类——ArrayList,LinkedList,HashList和HashMap,以及其他集合方案——都提供了对所有操作的支持。但是,如果想新建一个集合,同时不想为集合接口中的所有方法都提供有意义的定义,同时令其仍与现有库配合,这种设计方法也确实提供了一个“后门”可以利用。 -(2) 若一个操作未获支持,那么UnsupportedOperationException(未支持的操作违例)极有可能在实现期间出现,则不是在产品已交付给客户以后才会出现。它毕竟指出的是一个编程错误——不正确地使用了一个类。这一点不能十分确定,通过也可以看出这种方案的“试验”特征——只有经过多次试验,才能找出最理想的工作方式。 +(2) 若一个操作未获支持,那么UnsupportedOperationException(未支持的操作异常)极有可能在实现期间出现,则不是在产品已交付给客户以后才会出现。它毕竟指出的是一个编程错误——不正确地使用了一个类。这一点不能十分确定,通过也可以看出这种方案的“试验”特征——只有经过多次试验,才能找出最理想的工作方式。 在上面的例子中,Arrays.toList()产生了一个List(列表),该列表是由一个固定长度的数组后推出来的。因此唯一能够支持的就是那些不改变数组长度的操作。在另一方面,若请求一个新接口表达不同种类的行为(可能叫作“FixedSizeList”——固定长度列表),就有遭遇更大的复杂程度的危险。这样一来,以后试图使用库的时候,很快就会发现自己不知从何处下手。 @@ -1218,9 +1218,9 @@ Java 1.2添加了自己的一套实用工具,可用来对数组或列表进行 1. 数组 -Arrays类为所有基本数据类型的数组提供了一个过载的sort()和binarySearch(),它们亦可用于String和Object。下面这个例子显示出如何排序和搜索一个字节数组(其他所有基本数据类型都是类似的)以及一个String数组: +Arrays类为所有基本数据类型的数组提供了一个重载的sort()和binarySearch(),它们亦可用于String和Object。下面这个例子显示出如何排序和搜索一个字节数组(其他所有基本数据类型都是类似的)以及一个String数组: -``` +``` java //: Array1.java // Testing the sorting & searching in Arrays package c08.newcollections; @@ -1294,7 +1294,7 @@ public class Array1 { 针对Object数组(以及String,它当然属于Object的一种),可使用一个sort(),并令其接纳另一个参数:实现了Comparator接口(即“比较器”接口,新集合库的一部分)的一个对象,并用它的单个compare()方法进行比较。这个方法将两个准备比较的对象作为自己的参数使用——若第一个参数小于第二个,返回一个负整数;若相等,返回零;若第一个参数大于第二个,则返回正整数。基于这一规则,上述例子的String部分便可重新写过,令其进行真正按字母顺序的排序: -``` +``` java //: AlphaComp.java // Using Comparator to perform an alphabetic sort package c08.newcollections; @@ -1327,7 +1327,7 @@ public class AlphaComp implements Comparator { Arrays类提供了另一个sort()方法,它会采用单个自变量:一个Object数组,但没有Comparator。这个sort()方法也必须用同样的方式来比较两个Object。通过实现Comparable接口,它采用了赋予一个类的“自然比较方法”。这个接口含有单独一个方法——compareTo(),能分别根据它小于、等于或者大于自变量而返回负数、零或者正数,从而实现对象的比较。下面这个例子简单地阐示了这一点: -``` +``` java //: CompClass.java // A class that implements Comparable package c08.newcollections; @@ -1372,7 +1372,7 @@ public class CompClass implements Comparable { 而binarySearch(List,Object,Comparator)则用于查找那个列表中的一个对象(注释⑨)。下面这个例子利用了预先定义好的CompClass和AlphaComp来示范Collections中的各种排序工具: -``` +``` java //: ListSort.java // Sorting and searching Lists with 'Collections' package c08.newcollections; @@ -1420,7 +1420,7 @@ TreeMap也必须根据Comparable或者Comparator对自己的对象进行排序 Collections类中含有其他大量有用的实用工具: -``` +``` java enumeration(Collection) Produces an old-style Enumeration for the argument. @@ -1444,14 +1444,14 @@ Returns an immutable List of size n whose handles all point to o. subList(List, int min, int max) Returns a new List backed by the specified argument List that is a window into that argument with indexes starting at min and stopping just before max. -``` +``` java enumeration(Collection) 为自变量产生原始风格的Enumeration(枚举) max(Collection),min(Collection) 在自变量中用集合内对象的自然比较方法产生最大或最小元素 max(Collection,Comparator),min(Collection,Comparator) 在集合内用比较器产生最大或最小元素 -nCopies(int n, Object o) 返回长度为n的一个不可变列表,它的所有句柄均指向o +nCopies(int n, Object o) 返回长度为n的一个不可变列表,它的所有指针均指向o subList(List,int min,int max) 返回由指定参数列表后推得到的一个新列表。可将这个列表想象成一个“窗口”,它自索引为min的地方开始,正好结束于max的前面 @@ -1461,7 +1461,7 @@ subList(List,int min,int max) 返回由指定参数列表后推得到的一个 通常,创建Collection或Map的一个“只读”版本显得更有利一些。Collections类允许我们达到这个目标,方法是将原始容器传递进入一个方法,并令其传回一个只读版本。这个方法共有四种变化形式,分别用于Collection(如果不想把集合当作一种更特殊的类型对待)、List、Set以及Map。下面这个例子演示了为它们分别构建只读版本的正确方法: -``` +``` java //: ReadOnly.java // Using the Collections.unmodifiable methods package c08.newcollections; @@ -1497,15 +1497,15 @@ public class ReadOnly { } ///:~ ``` -对于每种情况,在将其正式变为只读以前,都必须用有有效的数据填充容器。一旦载入成功,最佳的做法就是用“不可修改”调用产生的句柄替换现有的句柄。这样做可有效避免将其变成不可修改后不慎改变其中的内容。在另一方面,该工具也允许我们在一个类中将能够修改的容器保持为private状态,并可从一个方法调用中返回指向那个容器的一个只读句柄。这样一来,虽然我们可在类里修改它,但其他任何人都只能读。 +对于每种情况,在将其正式变为只读以前,都必须用有有效的数据填充容器。一旦载入成功,最佳的做法就是用“不可修改”调用产生的指针替换现有的指针。这样做可有效避免将其变成不可修改后不慎改变其中的内容。在另一方面,该工具也允许我们在一个类中将能够修改的容器保持为private状态,并可从一个方法调用中返回指向那个容器的一个只读指针。这样一来,虽然我们可在类里修改它,但其他任何人都只能读。 -为特定类型调用“不可修改”的方法不会造成编译期间的检查,但一旦发生任何变化,对修改特定容器的方法的调用便会产生一个UnsupportedOperationException违例。 +为特定类型调用“不可修改”的方法不会造成编译期间的检查,但一旦发生任何变化,对修改特定容器的方法的调用便会产生一个UnsupportedOperationException异常。 2. Collection或Map的同步 synchronized关键字是“多线程”机制一个非常重要的部分。我们到第14章才会对这一机制作深入的探讨。在这儿,大家只需注意到Collections类提供了对整个容器进行自动同步的一种途径。它的语法与“不可修改”的方法是类似的: -``` +``` java //: Synchronization.java // Using the Collections.synchronized methods package c08.newcollections; @@ -1528,5 +1528,5 @@ public class Synchronization { 在这种情况下,我们通过适当的“同步”方法直接传递新容器;这样做可避免不慎暴露出未同步的版本。 -新集合也提供了能防止多个进程同时修改一个容器内容的机制。若在一个容器里反复,同时另一些进程介入,并在那个容器中插入、删除或修改一个对象,便会面临发生冲突的危险。我们可能已传递了那个对象,可能它位位于我们前面,可能容器的大小在我们调用size()后已发生了收缩——我们面临各种各样可能的危险。针对这个问题,新的集合库集成了一套解决的机制,能查出除我们的进程自己需要负责的之外的、对容器的其他任何修改。若探测到有其他方面也准备修改容器,便会立即产生一个ConcurrentModificationException(并发修改违例)。我们将这一机制称为“立即失败”——它并不用更复杂的算法在“以后”侦测问题,而是“立即”产生违例。 +新集合也提供了能防止多个进程同时修改一个容器内容的机制。若在一个容器里反复,同时另一些进程介入,并在那个容器中插入、删除或修改一个对象,便会面临发生冲突的危险。我们可能已传递了那个对象,可能它位位于我们前面,可能容器的大小在我们调用size()后已发生了收缩——我们面临各种各样可能的危险。针对这个问题,新的集合库集成了一套解决的机制,能查出除我们的进程自己需要负责的之外的、对容器的其他任何修改。若探测到有其他方面也准备修改容器,便会立即产生一个ConcurrentModificationException(并发修改异常)。我们将这一机制称为“立即失败”——它并不用更复杂的算法在“以后”侦测问题,而是“立即”产生异常。 diff --git "a/8.8 \346\200\273\347\273\223.md" "b/8.8 \346\200\273\347\273\223.md" index c70514d..93dcec0 100644 --- "a/8.8 \346\200\273\347\273\223.md" +++ "b/8.8 \346\200\273\347\273\223.md" @@ -4,11 +4,11 @@ (1) 数组包含了对象的数字化索引。它容纳的是一种已知类型的对象,所以在查找一个对象时,不必对结果进行造型处理。数组可以是多维的,而且能够容纳基本数据类型。但是,一旦把它创建好以后,大小便不能变化了。 -(2) Vector(矢量)也包含了对象的数字索引——可将数组和Vector想象成随机访问集合。当我们加入更多的元素时,Vector能够自动改变自身的大小。但Vector只能容纳对象的句柄,所以它不可包含基本数据类型;而且将一个对象句柄从集合中取出来的时候,必须对结果进行造型处理。 +(2) Vector(矢量)也包含了对象的数字索引——可将数组和Vector想象成随机访问集合。当我们加入更多的元素时,Vector能够自动改变自身的大小。但Vector只能容纳对象的指针,所以它不可包含基本数据类型;而且将一个对象指针从集合中取出来的时候,必须对结果进行造型处理。 (3) Hashtable(散列表)属于Dictionary(字典)的一种类型,是一种将对象(而不是数字)同其他对象关联到一起的方式。散列表也支持对对象的随机访问,事实上,它的整个设计方案都在突出访问的“高速度”。 -(4) Stack(堆栈)是一种“后入先出”(LIFO)的队列。 +(4) Stack(栈)是一种“后入先出”(LIFO)的队列。 若你曾经熟悉数据结构,可能会疑惑为何没看到一套更大的集合。从功能的角度出发,你真的需要一套更大的集合吗?对于Hashtable,可将任何东西置入其中,并以非常快的速度检索;对于Enumeration(枚举),可遍历一个序列,并对其中的每个元素都采取一个特定的操作。那是一种功能足够强劲的工具。 diff --git "a/8.9 \347\273\203\344\271\240.md" "b/8.9 \347\273\203\344\271\240.md" index ce9be3e..b0b6a00 100644 --- "a/8.9 \347\273\203\344\271\240.md" +++ "b/8.9 \347\273\203\344\271\240.md" @@ -1,6 +1,6 @@ # 8.9 练习 -(1) 新建一个名为Gerbil的类,在构建器中初始化一个int gerbilNumber(类似本章的Mouse例子)。为其写一个名为hop()的方法,用它打印出符合hop()条件的Gerbil的编号。建一个Vector,并为Vector添加一系列Gerbil对象。现在,用elementAt()方法在Vector中遍历,并为每个Gerbil都调用hop()。 +(1) 新建一个名为Gerbil的类,在构造器中初始化一个int gerbilNumber(类似本章的Mouse例子)。为其写一个名为hop()的方法,用它打印出符合hop()条件的Gerbil的编号。建一个Vector,并为Vector添加一系列Gerbil对象。现在,用elementAt()方法在Vector中遍历,并为每个Gerbil都调用hop()。 (2) 修改练习1,用Enumeration在调用hop()的同时遍历Vector。 diff --git "a/9.1 \345\237\272\346\234\254\350\277\235\344\276\213.md" "b/9.1 \345\237\272\346\234\254\350\277\235\344\276\213.md" deleted file mode 100644 index e6ad83b..0000000 --- "a/9.1 \345\237\272\346\234\254\350\277\235\344\276\213.md" +++ /dev/null @@ -1,33 +0,0 @@ -# 9.1 基本违例 - -“违例条件”表示在出现什么问题的时候应中止方法或作用域的继续。为了将违例条件与普通问题区分开,违例条件是非常重要的一个因素。在普通问题的情况下,我们在当地已拥有足够的信息,可在某种程度上解决碰到的问题。而在违例条件的情况下,却无法继续下去,因为当地没有提供解决问题所需的足够多的信息。此时,我们能做的唯一事情就是跳出当地环境,将那个问题委托给一个更高级的负责人。这便是出现违例时出现的情况。 - -一个简单的例子是“除法”。如可能被零除,就有必要进行检查,确保程序不会冒进,并在那种情况下执行除法。但具体通过什么知道分母是零呢?在那个特定的方法里,在我们试图解决的那个问题的环境中,我们或许知道该如何对待一个零分母。但假如它是一个没有预料到的值,就不能对其进行处理,所以必须产生一个违例,而非不顾一切地继续执行下去。 - -产生一个违例时,会发生几件事情。首先,按照与创建Java对象一样的方法创建违例对象:在内存“堆”里,使用new来创建。随后,停止当前执行路径(记住不可沿这条路径继续下去),然后从当前的环境中释放出违例对象的句柄。此时,违例控制机制会接管一切,并开始查找一个恰当的地方,用于继续程序的执行。这个恰当的地方便是“违例控制器”,它的职责是从问题中恢复,使程序要么尝试另一条执行路径,要么简单地继续。 - -作为产生违例的一个简单示例,大家可思考一个名为t的对象句柄。有些时候,程序可能传递一个尚未初始化的句柄。所以在用那个对象句柄调用一个方法之前,最好进行一番检查。可将与错误有关的信息发送到一个更大的场景中,方法是创建一个特殊的对象,用它代表我们的信息,并将其“掷”(Throw)出我们当前的场景之外。这就叫作“产生一个违例”或者“掷出一个违例”。下面是它的大概形式: - -``` -if(t == null) -throw new NullPointerException(); -``` - -这样便“掷”出了一个违例。在当前场景中,它使我们能放弃进一步解决该问题的企图。该问题会被转移到其他更恰当的地方解决。准确地说,那个地方不久就会显露出来。 - -9.1.1 违例自变量 - -和Java的其他任何对象一样,需要用new在内存堆里创建违例,并需调用一个构建器。在所有标准违例中,存在着两个构建器:第一个是默认构建器,第二个则需使用一个字串自变量,使我们能在违例里置入相关信息: - -``` -if(t == null) -throw new NullPointerException("t = null"); -``` - -稍后,字串可用各种方法提取出来,就象稍后会展示的那样。 - -在这儿,关键字throw会象变戏法一样做出一系列不可思议的事情。它首先执行new表达式,创建一个不在程序常规执行范围之内的对象。而且理所当然,会为那个对象调用构建器。随后,对象实际会从方法中返回——尽管对象的类型通常并不是方法设计为返回的类型。为深入理解违例控制,可将其想象成另一种返回机制——但是不要在这个问题上深究,否则会遇到麻烦。通过“掷”出一个违例,亦可从原来的作用域中退出。但是会先返回一个值,再退出方法或作用域。 - -但是,与普通方法返回的相似性到此便全部结束了,因为我们返回的地方与从普通方法调用中返回的地方是迥然有异的(我们结束于一个恰当的违例控制器,它距离违例“掷”出的地方可能相当遥远——在调用堆栈中要低上许多级)。 - -此外,我们可根据需要掷出任何类型的“可掷”对象。典型情况下,我们要为每种不同类型的错误“掷”出一类不同的违例。我们的思路是在违例对象以及挑选的违例对象类型中保存信息,所以在更大场景中的某个人可知道如何对待我们的违例(通常,唯一的信息是违例对象的类型,而违例对象中保存的没什么意义)。 diff --git "a/9.10 \347\273\203\344\271\240.md" "b/9.10 \347\273\203\344\271\240.md" index 74f6319..5839d8b 100644 --- "a/9.10 \347\273\203\344\271\240.md" +++ "b/9.10 \347\273\203\344\271\240.md" @@ -1,9 +1,9 @@ # 9.10 练习 -(1) 用main()创建一个类,令其掷出try块内的Exception类的一个对象。为Exception的构建器赋予一个字串参数。在catch从句内捕获违例,并打印出字串参数。添加一个finally从句,并打印一条消息,证明自己真正到达那里。 +(1) 用main()创建一个类,令其掷出try块内的Exception类的一个对象。为Exception的构造器赋予一个字串参数。在catch从句内捕获异常,并打印出字串参数。添加一个finally从句,并打印一条消息,证明自己真正到达那里。 -(2) 用extends关键字创建自己的违例类。为这个类写一个构建器,令其采用String参数,并随同String句柄把它保存到对象内。写一个方法,令其打印出保存下来的String。创建一个try-catch从句,练习实际操作新违例。 +(2) 用extends关键字创建自己的异常类。为这个类写一个构造器,令其采用String参数,并随同String指针把它保存到对象内。写一个方法,令其打印出保存下来的String。创建一个try-catch从句,练习实际操作新异常。 -(3) 写一个类,并令一个方法掷出在练习2中创建的类型的一个违例。试着在没有违例规范的前提下编译它,观察编译器会报告什么。接着添加适当的违例规范。在一个try-catch从句中尝试自己的类以及它的违例。 +(3) 写一个类,并令一个方法掷出在练习2中创建的类型的一个异常。试着在没有异常规范的前提下编译它,观察编译器会报告什么。接着添加适当的异常规范。在一个try-catch从句中尝试自己的类以及它的异常。 -(4) 在第5章,找到调用了Assert.java的两个程序,并修改它们,令其掷出自己的违例类型,而不是打印到System.err。该违例应是扩展了RuntimeException的一个内部类。 \ No newline at end of file +(4) 在第5章,找到调用了Assert.java的两个程序,并修改它们,令其掷出自己的异常类型,而不是打印到System.err。该异常应是扩展了RuntimeException的一个内部类。 \ No newline at end of file diff --git "a/9.2 \350\277\235\344\276\213\347\232\204\346\215\225\350\216\267.md" "b/9.2 \350\277\235\344\276\213\347\232\204\346\215\225\350\216\267.md" deleted file mode 100644 index 8a14c59..0000000 --- "a/9.2 \350\277\235\344\276\213\347\232\204\346\215\225\350\216\267.md" +++ /dev/null @@ -1,284 +0,0 @@ -# 9.2 违例的捕获 - -若某个方法产生一个违例,必须保证该违例能被捕获,并获得正确对待。对于Java的违例控制机制,它的一个好处就是允许我们在一个地方将精力集中在要解决的问题上,然后在另一个地方对待来自那个代码内部的错误。 - -为理解违例是如何捕获的,首先必须掌握“警戒区”的概念。它代表一个特殊的代码区域,有可能产生违例,并在后面跟随用于控制那些违例的代码。 - -9.2.1 try块 - -若位于一个方法内部,并“掷”出一个违例(或在这个方法内部调用的另一个方法产生了违例),那个方法就会在违例产生过程中退出。若不想一个throw离开方法,可在那个方法内部设置一个特殊的代码块,用它捕获违例。这就叫作“try块”,因为要在这个地方“尝试”各种方法调用。try块属于一种普通的作用域,用一个try关键字开头: - -``` -try { -// 可能产生违例的代码 -} -``` - -若用一种不支持违例控制的编程语言全面检查错误,必须用设置和错误检测代码将每个方法都包围起来——即便多次调用相同的方法。而在使用了违例控制技术后,可将所有东西都置入一个try块内,在同一地点捕获所有违例。这样便可极大简化我们的代码,并使其更易辨读,因为代码本身要达到的目标再也不会与繁复的错误检查混淆。 - -9.2.2 违例控制器 - -当然,生成的违例必须在某个地方中止。这个“地方”便是违例控制器或者违例控制模块。而且针对想捕获的每种违例类型,都必须有一个相应的违例控制器。违例控制器紧接在try块后面,且用catch(捕获)关键字标记。如下所示: - -``` -try { - // Code that might generate exceptions -} catch(Type1 id1) { - // Handle exceptions of Type1 -} catch(Type2 id2) { - // Handle exceptions of Type2 -} catch(Type3 id3) { - // Handle exceptions of Type3 -} - -// etc... -``` - -每个catch从句——即违例控制器——都类似一个小型方法,它需要采用一个(而且只有一个)特定类型的自变量。可在控制器内部使用标识符(id1,id2等等),就象一个普通的方法自变量那样。我们有时也根本不使用标识符,因为违例类型已提供了足够的信息,可有效处理违例。但即使不用,标识符也必须就位。 - -控制器必须“紧接”在try块后面。若“掷”出一个违例,违例控制机制就会搜寻自变量与违例类型相符的第一个控制器。随后,它会进入那个catch从句,并认为违例已得到控制(一旦catch从句结束,对控制器的搜索也会停止)。只有相符的catch从句才会得到执行;它与switch语句不同,后者在每个case后都需要一个break命令,防止误执行其他语句。 -在try块内部,请注意大量不同的方法调用可能生成相同的违例,但只需要一个控制器。 - -1. 中断与恢复 - -在违例控制理论中,共存在两种基本方法。在“中断”方法中(Java和C++提供了对这种方法的支持),我们假定错误非常关键,没有办法返回违例发生的地方。无论谁只要“掷”出一个违例,就表明没有办法补救错误,而且也不希望再回来。 - -另一种方法叫作“恢复”。它意味着违例控制器有责任来纠正当前的状况,然后取得出错的方法,假定下一次会成功执行。若使用恢复,意味着在违例得到控制以后仍然想继续执行。在这种情况下,我们的违例更象一个方法调用——我们用它在Java中设置各种各样特殊的环境,产生类似于“恢复”的行为(换言之,此时不是“掷”出一个违例,而是调用一个用于解决问题的方法)。另外,也可以将自己的try块置入一个while循环里,用它不断进入try块,直到结果满意时为止。 - -从历史的角度看,若程序员使用的操作系统支持可恢复的违例控制,最终都会用到类似于中断的代码,并跳过恢复进程。所以尽管“恢复”表面上十分不错,但在实际应用中却显得困难重重。其中决定性的原因可能是:我们的控制模块必须随时留意是否产生了违例,以及是否包含了由产生位置专用的代码。这便使代码很难编写和维护——大型系统尤其如此,因为违例可能在多个位置产生。 - -9.2.3 违例规范 - -在Java中,对那些要调用方法的客户程序员,我们要通知他们可能从自己的方法里“掷”出违例。这是一种有礼貌的做法,只有它才能使客户程序员准确地知道要编写什么代码来捕获所有潜在的违例。当然,若你同时提供了源码,客户程序员甚至能全盘检查代码,找出相应的throw语句。但尽管如此,通常并不随同源码提供库。为解决这个问题,Java提供了一种特殊的语法格式(并强迫我们采用),以便礼貌地告诉客户程序员该方法会“掷”出什么违例,令对方方便地加以控制。这便是我们在这里要讲述的“违例规范”,它属于方法声明的一部分,位于自变量(参数)列表的后面。 - -违例规范采用了一个额外的关键字:throws;后面跟随全部潜在的违例类型。因此,我们的方法定义看起来应象下面这个样子: - -``` -void f() throws tooBig, tooSmall, divZero { //... -``` - -若使用下述代码: - -``` -void f() [ // ... -``` - -它意味着不会从方法里“掷”出违例(除类型为RuntimeException的违例以外,它可能从任何地方掷出——稍后还会详细讲述)。 -但不能完全依赖违例规范——假若方法造成了一个违例,但没有对其进行控制,编译器会侦测到这个情况,并告诉我们必须控制违例,或者指出应该从方法里“掷”出一个违例规范。通过坚持从顶部到底部排列违例规范,Java可在编译期保证违例的正确性(注释②)。 - -②:这是在C++违例控制基础上一个显著的进步,后者除非到运行期,否则不会捕获不符合违例规范的错误。这使得C++的违例控制机制显得用处不大。 - -我们在这个地方可采取欺骗手段:要求“掷”出一个并没有发生的违例。编译器能理解我们的要求,并强迫使用这个方法的用户当作真的产生了那个违例处理。在实际应用中,可将其作为那个违例的一个“占位符”使用。这样一来,以后可以方便地产生实际的违例,毋需修改现有的代码。 - -9.2.4 捕获所有违例 - -我们可创建一个控制器,令其捕获所有类型的违例。具体的做法是捕获基础类违例类型Exception(也存在其他类型的基础违例,但Exception是适用于几乎所有编程活动的基础)。如下所示: - -``` -catch(Exception e) { -System.out.println("caught an exception"); -} -``` - -这段代码能捕获任何违例,所以在实际使用时最好将其置于控制器列表的末尾,防止跟随在后面的任何特殊违例控制器失效。 -对于程序员常用的所有违例类来说,由于Exception类是它们的基础,所以我们不会获得关于违例太多的信息,但可调用来自它的基础类Throwable的方法: - -``` -String getMessage() -``` - -获得详细的消息。 - -``` -String toString() -``` - -返回对Throwable的一段简要说明,其中包括详细的消息(如果有的话)。 - -``` -void printStackTrace() -void printStackTrace(PrintStream) -``` - -打印出Throwable和Throwable的调用堆栈路径。调用堆栈显示出将我们带到违例发生地点的方法调用的顺序。 - -第一个版本会打印出标准错误,第二个则打印出我们的选择流程。若在Windows下工作,就不能重定向标准错误。因此,我们一般愿意使用第二个版本,并将结果送给System.out;这样一来,输出就可重定向到我们希望的任何路径。 - -除此以外,我们还可从Throwable的基础类Object(所有对象的基础类型)获得另外一些方法。对于违例控制来说,其中一个可能有用的是getClass(),它的作用是返回一个对象,用它代表这个对象的类。我们可依次用getName()或toString()查询这个Class类的名字。亦可对Class对象进行一些复杂的操作,尽管那些操作在违例控制中是不必要的。本章稍后还会详细讲述Class对象。 - -下面是一个特殊的例子,它展示了Exception方法的使用(若执行该程序遇到困难,请参考第3章3.1.2小节“赋值”): - -``` -//: ExceptionMethods.java -// Demonstrating the Exception Methods -package c09; - -public class ExceptionMethods { - public static void main(String[] args) { - try { - throw new Exception("Here's my Exception"); - } catch(Exception e) { - System.out.println("Caught Exception"); - System.out.println( - "e.getMessage(): " + e.getMessage()); - System.out.println( - "e.toString(): " + e.toString()); - System.out.println("e.printStackTrace():"); - e.printStackTrace(); - } - } -} ///:~ -``` - -该程序输出如下: - -``` -Caught Exception -e.getMessage(): Here's my Exception -e.toString(): java.lang.Exception: Here's my Exception -e.printStackTrace(): -java.lang.Exception: Here's my Exception - at ExceptionMethods.main -``` - -可以看到,该方法连续提供了大量信息——每类信息都是前一类信息的一个子集。 - -9.2.5 重新“掷”出违例 - -在某些情况下,我们想重新掷出刚才产生过的违例,特别是在用Exception捕获所有可能的违例时。由于我们已拥有当前违例的句柄,所以只需简单地重新掷出那个句柄即可。下面是一个例子: - -``` -catch(Exception e) { -System.out.println("一个违例已经产生"); -throw e; -} -``` - -重新“掷”出一个违例导致违例进入更高一级环境的违例控制器中。用于同一个try块的任何更进一步的catch从句仍然会被忽略。此外,与违例对象有关的所有东西都会得到保留,所以用于捕获特定违例类型的更高一级的控制器可以从那个对象里提取出所有信息。 -若只是简单地重新掷出当前违例,我们打印出来的、与printStackTrace()内的那个违例有关的信息会与违例的起源地对应,而不是与重新掷出它的地点对应。若想安装新的堆栈跟踪信息,可调用fillInStackTrace(),它会返回一个特殊的违例对象。这个违例的创建过程如下:将当前堆栈的信息填充到原来的违例对象里。下面列出它的形式: - -``` -//: Rethrowing.java -// Demonstrating fillInStackTrace() - -public class Rethrowing { - public static void f() throws Exception { - System.out.println( - "originating the exception in f()"); - throw new Exception("thrown from f()"); - } - public static void g() throws Throwable { - try { - f(); - } catch(Exception e) { - System.out.println( - "Inside g(), e.printStackTrace()"); - e.printStackTrace(); - throw e; // 17 - // throw e.fillInStackTrace(); // 18 - } - } - public static void - main(String[] args) throws Throwable { - try { - g(); - } catch(Exception e) { - System.out.println( - "Caught in main, e.printStackTrace()"); - e.printStackTrace(); - } - } -} ///:~ -``` - -其中最重要的行号在注释内标记出来。注意第17行没有设为注释行。它的输出结果如下: - -``` -originating the exception in f() -Inside g(), e.printStackTrace() -java.lang.Exception: thrown from f() - at Rethrowing.f(Rethrowing.java:8) - at Rethrowing.g(Rethrowing.java:12) - at Rethrowing.main(Rethrowing.java:24) -Caught in main, e.printStackTrace() -java.lang.Exception: thrown from f() - at Rethrowing.f(Rethrowing.java:8) - at Rethrowing.g(Rethrowing.java:12) - at Rethrowing.main(Rethrowing.java:24) -``` - -因此,违例堆栈路径无论如何都会记住它的真正起点,无论自己被重复“掷”了好几次。 -若将第17行标注(变成注释行),而撤消对第18行的标注,就会换用fillInStackTrace(),结果如下: - -``` -originating the exception in f() -Inside g(), e.printStackTrace() -java.lang.Exception: thrown from f() - at Rethrowing.f(Rethrowing.java:8) - at Rethrowing.g(Rethrowing.java:12) - at Rethrowing.main(Rethrowing.java:24) -Caught in main, e.printStackTrace() -java.lang.Exception: thrown from f() - at Rethrowing.g(Rethrowing.java:18) - at Rethrowing.main(Rethrowing.java:24) -``` - -由于使用的是fillInStackTrace(),第18行成为违例的新起点。 - -针对g()和main(),Throwable类必须在违例规格中出现,因为fillInStackTrace()会生成一个Throwable对象的句柄。由于Throwable是Exception的一个基础类,所以有可能获得一个能够“掷”出的对象(具有Throwable属性),但却并非一个Exception(违例)。因此,在main()中用于Exception的句柄可能丢失自己的目标。为保证所有东西均井然有序,编译器强制Throwable使用一个违例规范。举个例子来说,下述程序的违例便不会在main()中被捕获到: - -``` -//: ThrowOut.java -public class ThrowOut { - public static void - main(String[] args) throws Throwable { - try { - throw new Throwable(); - } catch(Exception e) { - System.out.println("Caught in main()"); - } - } -} ///:~ -``` - -也有可能从一个已经捕获的违例重新“掷”出一个不同的违例。但假如这样做,会得到与使用fillInStackTrace()类似的效果:与违例起源地有关的信息会全部丢失,我们留下的是与新的throw有关的信息。如下所示: - -``` -//: RethrowNew.java -// Rethrow a different object from the one that -// was caught - -public class RethrowNew { - public static void f() throws Exception { - System.out.println( - "originating the exception in f()"); - throw new Exception("thrown from f()"); - } - public static void main(String[] args) { - try { - f(); - } catch(Exception e) { - System.out.println( - "Caught in main, e.printStackTrace()"); - e.printStackTrace(); - throw new NullPointerException("from main"); - } - } -} ///:~ -``` - -输出如下: - -``` -originating the exception in f() -Caught in main, e.printStackTrace() -java.lang.Exception: thrown from f() - at RethrowNew.f(RethrowNew.java:8) - at RethrowNew.main(RethrowNew.java:13) -java.lang.NullPointerException: from main - at RethrowNew.main(RethrowNew.java:18) -``` - -最后一个违例只知道自己来自main(),而非来自f()。注意Throwable在任何违例规范中都不是必需的。 - -永远不必关心如何清除前一个违例,或者与之有关的其他任何违例。它们都属于用new创建的、以内存堆为基础的对象,所以垃圾收集器会自动将其清除。 diff --git "a/9.3 \346\240\207\345\207\206Java\350\277\235\344\276\213.md" "b/9.3 \346\240\207\345\207\206Java\350\277\235\344\276\213.md" deleted file mode 100644 index 93a26fc..0000000 --- "a/9.3 \346\240\207\345\207\206Java\350\277\235\344\276\213.md" +++ /dev/null @@ -1,66 +0,0 @@ -# 9.3 标准Java违例 - - -Java包含了一个名为Throwable的类,它对可以作为违例“掷”出的所有东西进行了描述。Throwable对象有两种常规类型(亦即“从Throwable继承”)。其中,Error代表编译期和系统错误,我们一般不必特意捕获它们(除在特殊情况以外)。Exception是可以从任何标准Java库的类方法中“掷”出的基本类型。此外,它们亦可从我们自己的方法以及运行期偶发事件中“掷”出。 - -为获得违例的一个综合概念,最好的方法是阅读由http://java.sun.com提供的联机Java文档(当然,首先下载它们更好)。为了对各种违例有一个大概的印象,这个工作是相当有价值的。但大家不久就会发现,除名字外,一个违例和下一个违例之间并不存在任何特殊的地方。此外,Java提供的违例数量正在日益增多;从本质上说,把它们印到一本书里是没有意义的。大家从其他地方获得的任何新库可能也提供了它们自己的违例。我们最需要掌握的是基本概念,以及用这些违例能够做什么。 - -``` -java.lang.Exception -``` - -这是程序能捕获的基本违例。其他违例都是从它衍生出去的。这里要注意的是违例的名字代表发生的问题,而且违例名通常都是精心挑选的,可以很清楚地说明到底发生了什么事情。违例并不全是在java.lang中定义的;有些是为了提供对其他库的支持,如util,net以及io等——我们可以从它们的完整类名中看出这一点,或者观察它们从什么继承。例如,所有IO违例都是从java.io.IOException继承的。 - -9.3.1 RuntimeException的特殊情况 - -本章的第一个例子是: - -``` -if(t == null) -throw new NullPointerException(); -``` - -看起来似乎在传递进入一个方法的每个句柄中都必须检查null(因为不知道调用者是否已传递了一个有效的句柄),这无疑是相当可怕的。但幸运的是,我们根本不必这样做——它属于Java进行的标准运行期检查的一部分。若对一个空句柄发出了调用,Java会自动产生一个NullPointerException违例。所以上述代码在任何情况下都是多余的。 - -这个类别里含有一系列违例类型。它们全部由Java自动生成,毋需我们亲自动手把它们包含到自己的违例规范里。最方便的是,通过将它们置入单独一个名为RuntimeException的基础类下面,它们全部组合到一起。这是一个很好的继承例子:它建立了一系列具有某种共通性的类型,都具有某些共通的特征与行为。此外,我们没必要专门写一个违例规范,指出一个方法可能会“掷”出一个RuntimeException,因为已经假定可能出现那种情况。由于它们用于指出编程中的错误,所以几乎永远不必专门捕获一个“运行期违例”——RuntimeException——它在默认情况下会自动得到处理。若必须检查RuntimeException,我们的代码就会变得相当繁复。在我们自己的包里,可选择“掷”出一部分RuntimeException。 - -如果不捕获这些违例,又会出现什么情况呢?由于编译器并不强制违例规范捕获它们,所以假如不捕获的话,一个RuntimeException可能过滤掉我们到达main()方法的所有途径。为体会此时发生的事情,请试试下面这个例子: - -``` -//: NeverCaught.java -// Ignoring RuntimeExceptions - -public class NeverCaught { - static void f() { - throw new RuntimeException("From f()"); - } - static void g() { - f(); - } - public static void main(String[] args) { - g(); - } -} ///:~ -``` - -大家已经看到,一个RuntimeException(或者从它继承的任何东西)属于一种特殊情况,因为编译器不要求为这些类型指定违例规范。 - -输出如下: - -``` -java.lang.RuntimeException: From f() -at NeverCaught.f(NeverCaught.java:9) -at NeverCaught.g(NeverCaught.java:12) -at NeverCaught.main(NeverCaught.java:15) -``` - -所以答案就是:假若一个RuntimeException获得到达main()的所有途径,同时不被捕获,那么当程序退出时,会为那个违例调用printStackTrace()。 - -注意也许能在自己的代码中仅忽略RuntimeException,因为编译器已正确实行了其他所有控制。因为RuntimeException在此时代表一个编程错误: - -(1) 一个我们不能捕获的错误(例如,由客户程序员接收传递给自己方法的一个空句柄)。 - -(2) 作为一名程序员,一个应在自己的代码中检查的错误(如ArrayIndexOutOfBoundException,此时应注意数组的大小)。 -可以看出,最好的做法是在这种情况下违例,因为它们有助于程序的调试。 - -另外一个有趣的地方是,我们不可将Java违例划分为单一用途的工具。的确,它们设计用于控制那些讨厌的运行期错误——由代码控制范围之外的其他力量产生。但是,它也特别有助于调试某些特殊类型的编程错误,那些是编译器侦测不到的。 diff --git "a/9.4 \345\210\233\345\273\272\350\207\252\345\267\261\347\232\204\350\277\235\344\276\213.md" "b/9.4 \345\210\233\345\273\272\350\207\252\345\267\261\347\232\204\350\277\235\344\276\213.md" deleted file mode 100644 index 46a7aac..0000000 --- "a/9.4 \345\210\233\345\273\272\350\207\252\345\267\261\347\232\204\350\277\235\344\276\213.md" +++ /dev/null @@ -1,155 +0,0 @@ -# 9.4 创建自己的违例 - -并不一定非要使用Java违例。这一点必须掌握,因为经常都需要创建自己的违例,以便指出自己的库可能生成的一个特殊错误——但创建Java分级结构的时候,这个错误是无法预知的。 - -为创建自己的违例类,必须从一个现有的违例类型继承——最好在含义上与新违例近似。继承一个违例相当简单: - -``` -//: Inheriting.java -// Inheriting your own exceptions - -class MyException extends Exception { - public MyException() {} - public MyException(String msg) { - super(msg); - } -} - -public class Inheriting { - public static void f() throws MyException { - System.out.println( - "Throwing MyException from f()"); - throw new MyException(); - } - public static void g() throws MyException { - System.out.println( - "Throwing MyException from g()"); - throw new MyException("Originated in g()"); - } - public static void main(String[] args) { - try { - f(); - } catch(MyException e) { - e.printStackTrace(); - } - try { - g(); - } catch(MyException e) { - e.printStackTrace(); - } - } -} ///:~ -``` - -继承在创建新类时发生: - -``` -class MyException extends Exception { - public MyException() {} - public MyException(String msg) { - super(msg); - } -} -``` - -这里的关键是“extends Exception”,它的意思是:除包括一个Exception的全部含义以外,还有更多的含义。增加的代码数量非常少——实际只添加了两个构建器,对MyException的创建方式进行了定义。请记住,假如我们不明确调用一个基础类构建器,编译器会自动调用基础类默认构建器。在第二个构建器中,通过使用super关键字,明确调用了带有一个String参数的基础类构建器。 - -该程序输出结果如下: - -``` -Throwing MyException from f() -MyException - at Inheriting.f(Inheriting.java:16) - at Inheriting.main(Inheriting.java:24) -Throwing MyException from g() -MyException: Originated in g() - at Inheriting.g(Inheriting.java:20) - at Inheriting.main(Inheriting.java:29) -``` - -可以看到,在从f()“掷”出的MyException违例中,缺乏详细的消息。 - -创建自己的违例时,还可以采取更多的操作。我们可添加额外的构建器及成员: - -``` -//: Inheriting2.java -// Inheriting your own exceptions - -class MyException2 extends Exception { - public MyException2() {} - public MyException2(String msg) { - super(msg); - } - public MyException2(String msg, int x) { - super(msg); - i = x; - } - public int val() { return i; } - private int i; -} - -public class Inheriting2 { - public static void f() throws MyException2 { - System.out.println( - "Throwing MyException2 from f()"); - throw new MyException2(); - } - public static void g() throws MyException2 { - System.out.println( - "Throwing MyException2 from g()"); - throw new MyException2("Originated in g()"); - } - public static void h() throws MyException2 { - System.out.println( - "Throwing MyException2 from h()"); - throw new MyException2( - "Originated in h()", 47); - } - public static void main(String[] args) { - try { - f(); - } catch(MyException2 e) { - e.printStackTrace(); - } - try { - g(); - } catch(MyException2 e) { - e.printStackTrace(); - } - try { - h(); - } catch(MyException2 e) { - e.printStackTrace(); - System.out.println("e.val() = " + e.val()); - } - } -} ///:~ -``` - -此时添加了一个数据成员i;同时添加了一个特殊的方法,用它读取那个值;也添加了一个额外的构建器,用它设置那个值。输出结果如下: - -``` -Throwing MyException2 from f() -MyException2 - at Inheriting2.f(Inheriting2.java:22) - at Inheriting2.main(Inheriting2.java:34) -Throwing MyException2 from g() -MyException2: Originated in g() - at Inheriting2.g(Inheriting2.java:26) - at Inheriting2.main(Inheriting2.java:39) -Throwing MyException2 from h() -MyException2: Originated in h() - at Inheriting2.h(Inheriting2.java:30) - at Inheriting2.main(Inheriting2.java:44) -e.val() = 47 -``` - -由于违例不过是另一种形式的对象,所以可以继续这个进程,进一步增强违例类的能力。但要注意,对使用自己这个包的客户程序员来说,他们可能错过所有这些增强。因为他们可能只是简单地寻找准备生成的违例,除此以外不做任何事情——这是大多数Java库违例的标准用法。若出现这种情况,有可能创建一个新违例类型,其中几乎不包含任何代码: - -``` -//: SimpleException.java -class SimpleException extends Exception { -} ///:~ -``` - -它要依赖编译器来创建默认构建器(会自动调用基础类的默认构建器)。当然,在这种情况下,我们不会得到一个SimpleException(String)构建器,但它实际上也不会经常用到。 diff --git "a/9.5 \350\277\235\344\276\213\347\232\204\351\231\220\345\210\266.md" "b/9.5 \350\277\235\344\276\213\347\232\204\351\231\220\345\210\266.md" deleted file mode 100644 index d331f4a..0000000 --- "a/9.5 \350\277\235\344\276\213\347\232\204\351\231\220\345\210\266.md" +++ /dev/null @@ -1,95 +0,0 @@ -# 9.5 违例的限制 - - -覆盖一个方法时,只能产生已在方法的基础类版本中定义的违例。这是一个重要的限制,因为它意味着与基础类协同工作的代码也会自动应用于从基础类衍生的任何对象(当然,这属于基本的OOP概念),其中包括违例。 -下面这个例子演示了强加在违例身上的限制类型(在编译期): - -``` -//: StormyInning.java -// Overridden methods may throw only the -// exceptions specified in their base-class -// versions, or exceptions derived from the -// base-class exceptions. - -class BaseballException extends Exception {} -class Foul extends BaseballException {} -class Strike extends BaseballException {} - -abstract class Inning { - Inning() throws BaseballException {} - void event () throws BaseballException { - // Doesn't actually have to throw anything - } - abstract void atBat() throws Strike, Foul; - void walk() {} // Throws nothing -} - -class StormException extends Exception {} -class RainedOut extends StormException {} -class PopFoul extends Foul {} - -interface Storm { - void event() throws RainedOut; - void rainHard() throws RainedOut; -} - -public class StormyInning extends Inning - implements Storm { - // OK to add new exceptions for constructors, - // but you must deal with the base constructor - // exceptions: - StormyInning() throws RainedOut, - BaseballException {} - StormyInning(String s) throws Foul, - BaseballException {} - // Regular methods must conform to base class: -//! void walk() throws PopFoul {} //Compile error - // Interface CANNOT add exceptions to existing - // methods from the base class: -//! public void event() throws RainedOut {} - // If the method doesn't already exist in the - // base class, the exception is OK: - public void rainHard() throws RainedOut {} - // You can choose to not throw any exceptions, - // even if base version does: - public void event() {} - // Overridden methods can throw - // inherited exceptions: - void atBat() throws PopFoul {} - public static void main(String[] args) { - try { - StormyInning si = new StormyInning(); - si.atBat(); - } catch(PopFoul e) { - } catch(RainedOut e) { - } catch(BaseballException e) {} - // Strike not thrown in derived version. - try { - // What happens if you upcast? - Inning i = new StormyInning(); - i.atBat(); - // You must catch the exceptions from the - // base-class version of the method: - } catch(Strike e) { - } catch(Foul e) { - } catch(RainedOut e) { - } catch(BaseballException e) {} - } -} ///:~ -``` - -在Inning中,可以看到无论构建器还是event()方法都指出自己会“掷”出一个违例,但它们实际上没有那样做。这是合法的,因为它允许我们强迫用户捕获可能在覆盖过的event()版本里添加的任何违例。同样的道理也适用于abstract方法,就象在atBat()里展示的那样。 - -“interface Storm”非常有趣,因为它包含了在Incoming中定义的一个方法——event(),以及不是在其中定义的一个方法。这两个方法都会“掷”出一个新的违例类型:RainedOut。当执行到“StormyInning extends”和“implements Storm”的时候,可以看到Storm中的event()方法不能改变Inning中的event()的违例接口。同样地,这种设计是十分合理的;否则的话,当我们操作基础类时,便根本无法知道自己捕获的是否正确的东西。当然,假如interface中定义的一个方法不在基础类里,比如rainHard(),它产生违例时就没什么问题。 - -对违例的限制并不适用于构建器。在StormyInning中,我们可看到一个构建器能够“掷”出它希望的任何东西,无论基础类构建器“掷”出什么。然而,由于必须坚持按某种方式调用基础类构建器(在这里,会自动调用默认构建器),所以衍生类构建器必须在自己的违例规范中声明所有基础类构建器违例。 - -StormyInning.walk()不会编译的原因是它“掷”出了一个违例,而Inning.walk()却不会“掷”出。若允许这种情况发生,就可让自己的代码调用Inning.walk(),而且它不必控制任何违例。但在以后替换从Inning衍生的一个类的对象时,违例就会“掷”出,造成代码执行的中断。通过强迫衍生类方法遵守基础类方法的违例规范,对象的替换可保持连贯性。 - -覆盖过的event()方法向我们显示出一个方法的衍生类版本可以不产生任何违例——即便基础类版本要产生违例。同样地,这样做是必要的,因为它不会中断那些已假定基础类版本会产生违例的代码。差不多的道理亦适用于atBat(),它会“掷”出PopFoul——从Foul衍生出来的一个违例,而Foul违例是由atBat()的基础类版本产生的。这样一来,假如有人在自己的代码里操作Inning,同时调用了atBat(),就必须捕获Foul违例。由于PopFoul是从Foul衍生的,所以违例控制器(模块)也会捕获PopFoul。 - -最后一个有趣的地方在main()内部。在这个地方,假如我们明确操作一个StormyInning对象,编译器就会强迫我们只捕获特定于那个类的违例。但假如我们上溯造型到基础类型,编译器就会强迫我们捕获针对基础类的违例。通过所有这些限制,违例控制代码的“健壮”程度获得了大幅度改善(注释③)。 - -③:ANSI/ISO C++施加了类似的限制,要求衍生方法违例与基础类方法掷出的违例相同,或者从后者衍生。在这种情况下,C++实际上能够在编译期间检查违例规范。 - -我们必须认识到这一点:尽管违例规范是由编译器在继承期间强行遵守的,但违例规范并不属于方法类型的一部分,后者仅包括了方法名以及自变量类型。因此,我们不可在违例规范的基础上覆盖方法。除此以外,尽管违例规范存在于一个方法的基础类版本中,但并不表示它必须在方法的衍生类版本中存在。这与方法的“继承”颇有不同(进行继承时,基础类中的方法也必须在衍生类中存在)。换言之,用于一个特定方法的“违例规范接口”可能在继承和覆盖时变得更“窄”,但它不会变得更“宽”——这与继承时的类接口规则是正好相反的。 diff --git "a/9.6 \347\224\250finally\346\270\205\351\231\244.md" "b/9.6 \347\224\250finally\346\270\205\351\231\244.md" index 1c53d55..1a2d589 100644 --- "a/9.6 \347\224\250finally\346\270\205\351\231\244.md" +++ "b/9.6 \347\224\250finally\346\270\205\351\231\244.md" @@ -1,8 +1,8 @@ # 9.6 用finally清除 -无论一个违例是否在try块中发生,我们经常都想执行一些特定的代码。对一些特定的操作,经常都会遇到这种情况,但在恢复内存时一般都不需要(因为垃圾收集器会自动照料一切)。为达到这个目的,可在所有违例控制器的末尾使用一个finally从句(注释④)。所以完整的违例控制小节象下面这个样子: +无论一个异常是否在try块中发生,我们经常都想执行一些特定的代码。对一些特定的操作,经常都会遇到这种情况,但在恢复内存时一般都不需要(因为垃圾收集器会自动照料一切)。为达到这个目的,可在所有异常控制器的末尾使用一个finally从句(注释④)。所以完整的异常控制小节象下面这个样子: -``` +``` java try { // 要保卫的区域: // 可能“掷”出A,B,或C的危险情况 @@ -17,11 +17,11 @@ try { } ``` -④:C++违例控制未提供finally从句,因为它依赖构建器来达到这种清除效果。 +④:C++异常控制未提供finally从句,因为它依赖构造器来达到这种清除效果。 为演示finally从句,请试验下面这个程序: -``` +``` java //: FinallyWorks.java // The finally clause is always executed @@ -45,28 +45,28 @@ public class FinallyWorks { } ///:~ ``` -通过该程序,我们亦可知道如何应付Java违例(类似C++的违例)不允许我们恢复至违例产生地方的这一事实。若将自己的try块置入一个循环内,就可建立一个条件,它必须在继续程序之前满足。亦可添加一个static计数器或者另一些设备,允许循环在放弃以前尝试数种不同的方法。这样一来,我们的程序可以变得更加“健壮”。 +通过该程序,我们亦可知道如何应付Java异常(类似C++的异常)不允许我们恢复至异常产生地方的这一事实。若将自己的try块置入一个循环内,就可建立一个条件,它必须在继续程序之前满足。亦可添加一个static计数器或者另一些设备,允许循环在放弃以前尝试数种不同的方法。这样一来,我们的程序可以变得更加“健壮”。 输出如下: -``` +``` java Exception thrown in finally clause No exception in finally clause ``` -无论是否“掷”出一个违例,finally从句都会执行。 +无论是否“掷”出一个异常,finally从句都会执行。 9.6.1 用finally做什么 -在没有“垃圾收集”以及“自动调用破坏器”机制的一种语言中(注释⑤),finally显得特别重要,因为程序员可用它担保内存的正确释放——无论在try块内部发生了什么状况。但Java提供了垃圾收集机制,所以内存的释放几乎绝对不会成为问题。另外,它也没有构建器可供调用。既然如此,Java里何时才会用到finally呢? +在没有“垃圾收集”以及“自动调用破坏器”机制的一种语言中(注释⑤),finally显得特别重要,因为程序员可用它担保内存的正确释放——无论在try块内部发生了什么状况。但Java提供了垃圾收集机制,所以内存的释放几乎绝对不会成为问题。另外,它也没有构造器可供调用。既然如此,Java里何时才会用到finally呢? -⑤:“破坏器”(Destructor)是“构建器”(Constructor)的反义词。它代表一个特殊的函数,一旦某个对象失去用处,通常就会调用它。我们肯定知道在哪里以及何时调用破坏器。C++提供了自动的破坏器调用机制,但Delphi的Object Pascal版本1及2却不具备这一能力(在这种语言中,破坏器的含义与用法都发生了变化)。 +⑤:“破坏器”(Destructor)是“构造器”(Constructor)的反义词。它代表一个特殊的函数,一旦某个对象失去用处,通常就会调用它。我们肯定知道在哪里以及何时调用破坏器。C++提供了自动的破坏器调用机制,但Delphi的Object Pascal版本1及2却不具备这一能力(在这种语言中,破坏器的含义与用法都发生了变化)。 除将内存设回原始状态以外,若要设置另一些东西,finally就是必需的。例如,我们有时需要打开一个文件或者建立一个网络连接,或者在屏幕上画一些东西,甚至设置外部世界的一个开关,等等。如下例所示: -``` +``` java //: OnOffSwitch.java // Why use finally? @@ -95,9 +95,9 @@ public class OnOffSwitch { } ///:~ ``` -这里的目标是保证main()完成时开关处于关闭状态,所以将sw.off()置于try块以及每个违例控制器的末尾。但产生的一个违例有可能不是在这里捕获的,这便会错过sw.off()。然而,利用finally,我们可以将来自try块的关闭代码只置于一个地方: +这里的目标是保证main()完成时开关处于关闭状态,所以将sw.off()置于try块以及每个异常控制器的末尾。但产生的一个异常有可能不是在这里捕获的,这便会错过sw.off()。然而,利用finally,我们可以将来自try块的关闭代码只置于一个地方: -``` +``` java //: WithFinally.java // Finally Guarantees cleanup @@ -127,9 +127,9 @@ public class WithFinally { 在这儿,sw.off()已移至一个地方。无论发生什么事情,都肯定会运行它。 -即使违例不在当前的catch从句集里捕获,finally都会在违例控制机制转到更高级别搜索一个控制器之前得以执行。如下所示: +即使异常不在当前的catch从句集里捕获,finally都会在异常控制机制转到更高级别搜索一个控制器之前得以执行。如下所示: -``` +``` java //: AlwaysFinally.java // Finally is always executed @@ -161,7 +161,7 @@ public class AlwaysFinally { 该程序的输出展示了具体发生的事情: -``` +``` java Entering first try block Entering second try block finally in 2nd try block @@ -171,11 +171,11 @@ finally in 1st try block 若调用了break和continue语句,finally语句也会得以执行。请注意,与作上标签的break和continue一道,finally排除了Java对goto跳转语句的需求。 -9.6.2 缺点:丢失的违例 +9.6.2 缺点:丢失的异常 -一般情况下,Java的违例实施方案都显得十分出色。不幸的是,它依然存在一个缺点。尽管违例指出程序里存在一个危机,而且绝不应忽略,但一个违例仍有可能简单地“丢失”。在采用finally从句的一种特殊配置下,便有可能发生这种情况: +一般情况下,Java的异常实施方案都显得十分出色。不幸的是,它依然存在一个缺点。尽管异常指出程序里存在一个危机,而且绝不应忽略,但一个异常仍有可能简单地“丢失”。在采用finally从句的一种特殊配置下,便有可能发生这种情况: -``` +``` java //: LostMessage.java // How an exception can be lost @@ -212,12 +212,12 @@ public class LostMessage { 输出如下: -``` +``` java A trivial exception at LostMessage.dispose(LostMessage.java:21) at LostMessage.main(LostMessage.java:29) ``` -可以看到,这里不存在VeryImportantException(非常重要的违例)的迹象,它只是简单地被finally从句中的HoHumException代替了。 +可以看到,这里不存在VeryImportantException(非常重要的异常)的迹象,它只是简单地被finally从句中的HoHumException代替了。 -这是一项相当严重的缺陷,因为它意味着一个违例可能完全丢失。而且就象前例演示的那样,这种丢失显得非常“自然”,很难被人查出蛛丝马迹。而与此相反,C++里如果第二个违例在第一个违例得到控制前产生,就会被当作一个严重的编程错误处理。或许Java以后的版本会纠正这个问题(上述结果是用Java 1.1生成的)。 +这是一项相当严重的缺陷,因为它意味着一个异常可能完全丢失。而且就象前例演示的那样,这种丢失显得非常“自然”,很难被人查出蛛丝马迹。而与此相反,C++里如果第二个异常在第一个异常得到控制前产生,就会被当作一个严重的编程错误处理。或许Java以后的版本会纠正这个问题(上述结果是用Java 1.1生成的)。 diff --git "a/9.7 \346\236\204\345\273\272\345\231\250.md" "b/9.7 \346\236\204\351\200\240\345\231\250.md" similarity index 75% rename from "9.7 \346\236\204\345\273\272\345\231\250.md" rename to "9.7 \346\236\204\351\200\240\345\231\250.md" index 814e051..472af9a 100644 --- "a/9.7 \346\236\204\345\273\272\345\231\250.md" +++ "b/9.7 \346\236\204\351\200\240\345\231\250.md" @@ -1,13 +1,13 @@ -# 9.7 构建器 +# 9.7 构造器 -为违例编写代码时,我们经常要解决的一个问题是:“一旦产生违例,会正确地进行清除吗?”大多数时候都会非常安全,但在构建器中却是一个大问题。构建器将对象置于一个安全的起始状态,但它可能执行一些操作——如打开一个文件。除非用户完成对象的使用,并调用一个特殊的清除方法,否则那些操作不会得到正确的清除。若从一个构建器内部“掷”出一个违例,这些清除行为也可能不会正确地发生。所有这些都意味着在编写构建器时,我们必须特别加以留意。 +为异常编写代码时,我们经常要解决的一个问题是:“一旦产生异常,会正确地进行清除吗?”大多数时候都会非常安全,但在构造器中却是一个大问题。构造器将对象置于一个安全的起始状态,但它可能执行一些操作——如打开一个文件。除非用户完成对象的使用,并调用一个特殊的清除方法,否则那些操作不会得到正确的清除。若从一个构造器内部“掷”出一个异常,这些清除行为也可能不会正确地发生。所有这些都意味着在编写构造器时,我们必须特别加以留意。 -由于前面刚学了finally,所以大家可能认为它是一种合适的方案。但事情并没有这么简单,因为finally每次都会执行清除代码——即使我们在清除方法运行之前不想执行清除代码。因此,假如真的用finally进行清除,必须在构建器正常结束时设置某种形式的标志。而且只要设置了标志,就不要执行finally块内的任何东西。由于这种做法并不完美(需要将一个地方的代码同另一个地方的结合起来),所以除非特别需要,否则一般不要尝试在finally中进行这种形式的清除。 +由于前面刚学了finally,所以大家可能认为它是一种合适的方案。但事情并没有这么简单,因为finally每次都会执行清除代码——即使我们在清除方法运行之前不想执行清除代码。因此,假如真的用finally进行清除,必须在构造器正常结束时设置某种形式的标志。而且只要设置了标志,就不要执行finally块内的任何东西。由于这种做法并不完美(需要将一个地方的代码同另一个地方的结合起来),所以除非特别需要,否则一般不要尝试在finally中进行这种形式的清除。 在下面这个例子里,我们创建了一个名为InputFile的类。它的作用是打开一个文件,然后每次读取它的一行内容(转换为一个字串)。它利用了由Java标准IO库提供的FileReader以及BufferedReader类(将于第10章讨论)。这两个类都非常简单,大家现在可以毫无困难地掌握它们的基本用法: -``` +``` java //: Cleanup.java // Paying attention to exceptions // in constructors @@ -81,17 +81,17 @@ public class Cleanup { 该例使用了Java 1.1 IO类。 -用于InputFile的构建器采用了一个String(字串)参数,它代表我们想打开的那个文件的名字。在一个try块内部,它用该文件名创建了一个FileReader。对FileReader来说,除非转移并用它创建一个能够实际与之“交谈”的BufferedReader,否则便没什么用处。注意InputFile的一个好处就是它同时合并了这两种行动。 +用于InputFile的构造器采用了一个String(字串)参数,它代表我们想打开的那个文件的名字。在一个try块内部,它用该文件名创建了一个FileReader。对FileReader来说,除非转移并用它创建一个能够实际与之“交谈”的BufferedReader,否则便没什么用处。注意InputFile的一个好处就是它同时合并了这两种行动。 -若FileReader构建器不成功,就会产生一个FileNotFoundException(文件未找到违例)。必须单独捕获这个违例——这属于我们不想关闭文件的一种特殊情况,因为文件尚未成功打开。其他任何捕获从句(catch)都必须关闭文件,因为文件已在进入那些捕获从句时打开(当然,如果多个方法都能产生一个FileNotFoundException违例,就需要稍微用一些技巧。此时,我们可将不同的情况分隔到数个try块内)。close()方法会掷出一个尝试过的违例。即使它在另一个catch从句的代码块内,该违例也会得以捕获——对Java编译器来说,那个catch从句不过是另一对花括号而已。执行完本地操作后,违例会被重新“掷”出。这样做是必要的,因为这个构建器的执行已经失败,我们不希望调用方法来假设对象已正确创建以及有效。 +若FileReader构造器不成功,就会产生一个FileNotFoundException(文件未找到异常)。必须单独捕获这个异常——这属于我们不想关闭文件的一种特殊情况,因为文件尚未成功打开。其他任何捕获从句(catch)都必须关闭文件,因为文件已在进入那些捕获从句时打开(当然,如果多个方法都能产生一个FileNotFoundException异常,就需要稍微用一些技巧。此时,我们可将不同的情况分隔到数个try块内)。close()方法会掷出一个尝试过的异常。即使它在另一个catch从句的代码块内,该异常也会得以捕获——对Java编译器来说,那个catch从句不过是另一对花括号而已。执行完本地操作后,异常会被重新“掷”出。这样做是必要的,因为这个构造器的执行已经失败,我们不希望调用方法来假设对象已正确创建以及有效。 -在这个例子中,没有采用前述的标志技术,finally从句显然不是关闭文件的正确地方,因为这可能在每次构建器结束的时候关闭它。由于我们希望文件在InputFile对象处于活动状态时一直保持打开状态,所以这样做并不恰当。 +在这个例子中,没有采用前述的标志技术,finally从句显然不是关闭文件的正确地方,因为这可能在每次构造器结束的时候关闭它。由于我们希望文件在InputFile对象处于活动状态时一直保持打开状态,所以这样做并不恰当。 -getLine()方法会返回一个字串,其中包含了文件中下一行的内容。它调用了readLine(),后者可能产生一个违例,但那个违例会被捕获,使getLine()不会再产生任何违例。对违例来说,一项特别的设计问题是决定在这一级完全控制一个违例,还是进行部分控制,并传递相同(或不同)的违例,或者只是简单地传递它。在适当的时候,简单地传递可极大简化我们的编码工作。 +getLine()方法会返回一个字串,其中包含了文件中下一行的内容。它调用了readLine(),后者可能产生一个异常,但那个异常会被捕获,使getLine()不会再产生任何异常。对异常来说,一项特别的设计问题是决定在这一级完全控制一个异常,还是进行部分控制,并传递相同(或不同)的异常,或者只是简单地传递它。在适当的时候,简单地传递可极大简化我们的编码工作。 getLine()方法会变成: -``` +``` java String getLine() throws IOException { return in.readLine(); } @@ -99,10 +99,10 @@ return in.readLine(); 但是当然,调用者现在需要对可能产生的任何IOException进行控制。 -用户使用完毕InputFile对象后,必须调用cleanup()方法,以便释放由BufferedReader以及/或者FileReader占用的系统资源(如文件句柄)——注释⑥。除非InputFile对象使用完毕,而且到了需要弃之不用的时候,否则不应进行清除。大家可能想把这样的机制置入一个finalize()方法内,但正如第4章指出的那样,并非总能保证finalize()获得正确的调用(即便确定它会调用,也不知道何时开始)。这属于Java的一项缺陷——除内存清除之外的所有清除都不会自动进行,所以必须知会客户程序员,告诉他们有责任用finalize()保证清除工作的正确进行。 +用户使用完毕InputFile对象后,必须调用cleanup()方法,以便释放由BufferedReader以及/或者FileReader占用的系统资源(如文件指针)——注释⑥。除非InputFile对象使用完毕,而且到了需要弃之不用的时候,否则不应进行清除。大家可能想把这样的机制置入一个finalize()方法内,但正如第4章指出的那样,并非总能保证finalize()获得正确的调用(即便确定它会调用,也不知道何时开始)。这属于Java的一项缺陷——除内存清除之外的所有清除都不会自动进行,所以必须知会客户程序员,告诉他们有责任用finalize()保证清除工作的正确进行。 ⑥:在C++里,“破坏器”可帮我们控制这一局面。 -在Cleanup.java中,我们创建了一个InputFile,用它打开用于创建程序的相同的源文件。同时一次读取该文件的一行内容,而且添加相应的行号。所有违例都会在main()中被捕获——尽管我们可选择更大的可靠性。 +在Cleanup.java中,我们创建了一个InputFile,用它打开用于创建程序的相同的源文件。同时一次读取该文件的一行内容,而且添加相应的行号。所有异常都会在main()中被捕获——尽管我们可选择更大的可靠性。 -这个示例也向大家展示了为何在本书的这个地方引入违例的概念。违例与Java的编程具有很高的集成度,这主要是由于编译器会强制它们。只有知道了如何操作那些违例,才可更进一步地掌握编译器的知识。 +这个示例也向大家展示了为何在本书的这个地方引入异常的概念。异常与Java的编程具有很高的集成度,这主要是由于编译器会强制它们。只有知道了如何操作那些异常,才可更进一步地掌握编译器的知识。 diff --git "a/9.8 \350\277\235\344\276\213\345\214\271\351\205\215.md" "b/9.8 \350\277\235\344\276\213\345\214\271\351\205\215.md" deleted file mode 100644 index 1643849..0000000 --- "a/9.8 \350\277\235\344\276\213\345\214\271\351\205\215.md" +++ /dev/null @@ -1,71 +0,0 @@ -# 9.8 违例匹配 - -“掷”出一个违例后,违例控制系统会按当初编写的顺序搜索“最接近”的控制器。一旦找到相符的控制器,就认为违例已得到控制,不再进行更多的搜索工作。 - -在违例和它的控制器之间,并不需要非常精确的匹配。一个衍生类对象可与基础类的一个控制器相配,如下例所示: - -``` -//: Human.java -// Catching Exception Hierarchies - -class Annoyance extends Exception {} -class Sneeze extends Annoyance {} - -public class Human { - public static void main(String[] args) { - try { - throw new Sneeze(); - } catch(Sneeze s) { - System.out.println("Caught Sneeze"); - } catch(Annoyance a) { - System.out.println("Caught Annoyance"); - } - } -} ///:~ -``` - -Sneeze违例会被相符的第一个catch从句捕获。当然,这只是第一个。然而,假如我们删除第一个catch从句: - -``` - try { - throw new Sneeze(); - } catch(Annoyance a) { - System.out.println("Caught Annoyance"); - } -``` - -那么剩下的catch从句依然能够工作,因为它捕获的是Sneeze的基础类。换言之,catch(Annoyance e)能捕获一个Annoyance以及从它衍生的任何类。这一点非常重要,因为一旦我们决定为一个方法添加更多的违例,而且它们都是从相同的基础类继承的,那么客户程序员的代码就不需要更改。至少能够假定它们捕获的是基础类。 - -若将基础类捕获从句置于第一位,试图“屏蔽”衍生类违例,就象下面这样: - -``` - try { - throw new Sneeze(); - } catch(Annoyance a) { - System.out.println("Caught Annoyance"); - } catch(Sneeze s) { - System.out.println("Caught Sneeze"); - } -``` - -则编译器会产生一条出错消息,因为它发现永远不可能抵达Sneeze捕获从句。 - -9.8.1 违例准则 - -用违例做下面这些事情: - -(1) 解决问题并再次调用造成违例的方法。 - -(2) 平息事态的发展,并在不重新尝试方法的前提下继续。 - -(3) 计算另一些结果,而不是希望方法产生的结果。 - -(4) 在当前环境中尽可能解决问题,以及将相同的违例重新“掷”出一个更高级的环境。 - -(5) 在当前环境中尽可能解决问题,以及将不同的违例重新“掷”出一个更高级的环境。 - -(6) 中止程序执行。 - -(7) 简化编码。若违例方案使事情变得更加复杂,那就会令人非常烦恼,不如不用。 - -(8) 使自己的库和程序变得更加安全。这既是一种“短期投资”(便于调试),也是一种“长期投资”(改善应用程序的健壮性) diff --git "a/9.9 \346\200\273\347\273\223.md" "b/9.9 \346\200\273\347\273\223.md" index 1f7aedb..c2f3605 100644 --- "a/9.9 \346\200\273\347\273\223.md" +++ "b/9.9 \346\200\273\347\273\223.md" @@ -2,6 +2,6 @@ 通过先进的错误纠正与恢复机制,我们可以有效地增强代码的健壮程度。对我们编写的每个程序来说,错误恢复都属于一个基本的考虑目标。它在Java中显得尤为重要,因为该语言的一个目标就是创建不同的程序组件,以便其他用户(客户程序员)使用。为构建一套健壮的系统,每个组件都必须非常健壮。 -在Java里,违例控制的目的是使用尽可能精简的代码创建大型、可靠的应用程序,同时排除程序里那些不能控制的错误。 +在Java里,异常控制的目的是使用尽可能精简的代码创建大型、可靠的应用程序,同时排除程序里那些不能控制的错误。 -违例的概念很难掌握。但只有很好地运用它,才可使自己的项目立即获得显著的收益。Java强迫遵守违例所有方面的问题,所以无论库设计者还是客户程序员,都能够连续一致地使用它。 +异常的概念很难掌握。但只有很好地运用它,才可使自己的项目立即获得显著的收益。Java强迫遵守异常所有方面的问题,所以无论库设计者还是客户程序员,都能够连续一致地使用它。 diff --git a/SUMMARY.md b/SUMMARY.md index f019a51..7cccb0a 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -9,16 +9,16 @@ * [1.3 实现方案的隐藏](1.3 实现方案的隐藏.md) * [1.4 方案的重复使用](1.4 方案的重复使用.md) * [1.5 继承:重新使用接口](1.5 继承:重新使用接口.md) - * [1.6 多形对象的互换使用](1.6 多形对象的互换使用.md) + * [1.6 多态对象的互换使用](1.6 多态对象的互换使用.md) * [1.7 对象的创建和存在时间](1.7 对象的创建和存在时间.md) - * [1.8 违例控制:解决错误](1.8 违例控制:解决错误.md) + * [1.8 异常控制:解决错误](1.8 异常控制:解决错误.md) * [1.9 多线程](1.9 多线程.md) * [1.10 永久性](1.10 永久性.md) * [1.11 Java和因特网](1.11 Java和因特网.md) * [1.12 分析和设计](1.12 分析和设计.md) * [1.13 Java还是C++](1.13 Java还是C++.md) * [第2章 一切都是对象](第2章 一切都是对象.md) - * [2.1 用句柄操纵对象](2.1 用句柄操纵对象.md) + * [2.1 用指针操纵对象](2.1 用指针操纵对象.md) * [2.2 所有对象都必须创建](2.2 所有对象都必须创建.md) * [2.3 绝对不要清除对象](2.3 绝对不要清除对象.md) * [2.4 新建数据类型:类](2.4 新建数据类型:类.md) @@ -35,8 +35,8 @@ * [3.3 总结](3.3 总结.md) * [3.4 练习](3.4 练习.md) * [第4章 初始化和清除](第4章 初始化和清除.md) - * [4.1 用构建器自动初始化](4.1 用构建器自动初始化.md) - * [4.2 方法过载](4.2 方法过载.md) + * [4.1 用构造器自动初始化](4.1 用构造器自动初始化.md) + * [4.2 方法重载](4.2 方法重载.md) * [4.3 清除:收尾和垃圾收集](4.3 清除:收尾和垃圾收集.md) * [4.4 成员初始化](4.4 成员初始化.md) * [4.5 数组初始化](4.5 数组初始化.md) @@ -61,36 +61,36 @@ * [6.9 初始化和类装载](6.9 初始化和类装载.md) * [6.10 总结](6.10 总结.md) * [6.11 练习](6.11 练习.md) -* [第7章 多形性](第7章 多形性.md) +* [第7章 多态性](第7章 多态性.md) * [7.1 上溯造型](7.1 上溯造型.md) * [7.2 深入理解](7.2 深入理解.md) - * [7.3 覆盖与过载](7.3 覆盖与过载.md) + * [7.3 覆盖与重载](7.3 覆盖与重载.md) * [7.4 抽象类和方法](7.4 抽象类和方法.md) * [7.5 接口](7.5 接口.md) * [7.6 内部类](7.6 内部类.md) - * [7.7 构建器和多形性](7.7 构建器和多形性.md) + * [7.7 构造器和多态性](7.7 构造器和多态性.md) * [7.8 通过继承进行设计](7.8 通过继承进行设计.md) * [7.9 总结](7.9 总结.md) * [7.10 练习](7.10 练习.md) * [第8章 对象的容纳](第8章 对象的容纳.md) * [8.1 数组](8.1 数组.md) * [8.2 集合](8.2 集合.md) - * [8.3 枚举器(反复器)](8.3 枚举器(反复器).md) + * [8.3 枚举器(迭代器)](8.3 枚举器(迭代器).md) * [8.4 集合的类型](8.4 集合的类型.md) * [8.5 排序](8.5 排序.md) * [8.6 通用集合库](8.6 通用集合库.md) * [8.7 新集合](8.7 新集合.md) * [8.8 总结](8.8 总结.md) * [8.9 练习](8.9 练习.md) -* [第9章 违例差错控制](第9章 违例差错控制.md) - * [9.1 基本违例](9.1 基本违例.md) - * [9.2 违例的捕获](9.2 违例的捕获.md) - * [9.3 标准Java违例](9.3 标准Java违例.md) - * [9.4 创建自己的违例](9.4 创建自己的违例.md) - * [9.5 违例的限制](9.5 违例的限制.md) +* [第9章 异常差错控制](第9章 异常差错控制.md) + * [9.1 基本异常](9.1 基本异常.md) + * [9.2 异常的捕获](9.2 异常的捕获.md) + * [9.3 标准Java异常](9.3 标准Java异常.md) + * [9.4 创建自己的异常](9.4 创建自己的异常.md) + * [9.5 异常的限制](9.5 异常的限制.md) * [9.6 用finally清除](9.6 用finally清除.md) - * [9.7 构建器](9.7 构建器.md) - * [9.8 违例匹配](9.8 违例匹配.md) + * [9.7 构造器](9.7 构造器.md) + * [9.8 异常匹配](9.8 异常匹配.md) * [9.9 总结](9.9 总结.md) * [9.10 练习](9.10 练习.md) * [第10章 Java IO系统](第10章 Java IO系统.md) @@ -112,7 +112,7 @@ * [11.4 总结](11.4 总结.md) * [11.5 练习](11.5 练习.md) * [第12章 传递和返回对象](第12章 传递和返回对象.md) - * [12.1 传递句柄](12.1 传递句柄.md) + * [12.1 传递指针](12.1 传递指针.md) * [12.2 制作本地副本](12.2 制作本地副本.md) * [12.3 克隆的控制](12.3 克隆的控制.md) * [12.4 只读类](12.4 只读类.md) diff --git "a/\345\274\225\350\250\200.md" "b/\345\274\225\350\250\200.md" index 0b18169..93f1a9f 100644 --- "a/\345\274\225\350\250\200.md" +++ "b/\345\274\225\350\250\200.md" @@ -51,11 +51,11 @@ **(1) 第1章:对象入门** -这一章是对面向对象的程序设计(OOP)的一个综述,其中包括对“什么是对象”之类的基本问题的回答,并讲述了接口与实现、抽象与封装、消息与函数、继承与合成以及非常重要的多形性的概念。这一章会向大家提出一些对象创建的基本问题,比如构建器、对象存在于何处、创建好后把它们置于什么地方以及魔术般的垃圾收集器(能够清除不再需要的对象)。要介绍的另一些问题还包括通过违例实现的错误控制机制、反应灵敏的用户界面的多线程处理以及连网和因特网等等。大家也会从中了解到是什么使得Java如此特别,它为什么取得了这么大的成功,以及与面向对象的分析与设计有关的问题。 +这一章是对面向对象的程序设计(OOP)的一个综述,其中包括对“什么是对象”之类的基本问题的回答,并讲述了接口与实现、抽象与封装、消息与函数、继承与合成以及非常重要的多态性的概念。这一章会向大家提出一些对象创建的基本问题,比如构造器、对象存在于何处、创建好后把它们置于什么地方以及魔术般的垃圾收集器(能够清除不再需要的对象)。要介绍的另一些问题还包括通过异常实现的错误控制机制、反应灵敏的用户界面的多线程处理以及连网和因特网等等。大家也会从中了解到是什么使得Java如此特别,它为什么取得了这么大的成功,以及与面向对象的分析与设计有关的问题。 **(2) 第2章:一切都是对象** -本章将大家带到可以着手写自己的第一个Java程序的地方,所以必须对一些基本概念作出解释,其中包括对象“句柄”的概念;怎样创建一个对象;对基本数据类型和数组的一个介绍;作用域以及垃圾收集器清除对象的方式;如何将Java中的所有东西都归为一种新数据类型(类),以及如何创建自己的类;函数、自变量以及返回值;名字的可见度以及使用来自其他库的组件;static关键字;注释和嵌入文档等等。 +本章将大家带到可以着手写自己的第一个Java程序的地方,所以必须对一些基本概念作出解释,其中包括对象“指针”的概念;怎样创建一个对象;对基本数据类型和数组的一个介绍;作用域以及垃圾收集器清除对象的方式;如何将Java中的所有东西都归为一种新数据类型(类),以及如何创建自己的类;函数、自变量以及返回值;名字的可见度以及使用来自其他库的组件;static关键字;注释和嵌入文档等等。 **(3) 第3章:控制程序流程** @@ -63,7 +63,7 @@ **(4) 第4章:初始化和清除** -本章开始介绍构建器,它的作用是担保初始化的正确实现。对构建器的定义要涉及函数过载的概念(因为可能同时有几个构建器)。随后要讨论的是清除过程,它并非肯定如想象的那么简单。用完一个对象后,通常可以不必管它,垃圾收集器会自动介入,释放由它占据的内存。这里详细探讨了垃圾收集器以及它的一些特点。在这一章的最后,我们将更贴近地观察初始化过程:自动成员初始化、指定成员初始化、初始化的顺序、static(静态)初始化以及数组初始化等等。 +本章开始介绍构造器,它的作用是担保初始化的正确实现。对构造器的定义要涉及函数重载的概念(因为可能同时有几个构造器)。随后要讨论的是清除过程,它并非肯定如想象的那么简单。用完一个对象后,通常可以不必管它,垃圾收集器会自动介入,释放由它占据的内存。这里详细探讨了垃圾收集器以及它的一些特点。在这一章的最后,我们将更贴近地观察初始化过程:自动成员初始化、指定成员初始化、初始化的顺序、static(静态)初始化以及数组初始化等等。 **(5) 第5章:隐藏实现过程** @@ -73,17 +73,17 @@ 继承的概念是几乎所有OOP语言中都占有重要的地位。它是对现有类加以利用,并为其添加新功能的一种有效途径(同时可以修改它,这是第7章的主题)。通过继承来重复使用原有的代码时(再生),一般需要保持“基础类”不变,只是将这儿或那儿的东西串联起来,以达到预期的效果。然而,继承并不是在现有类基础上制造新类的唯一手段。通过“合成”,亦可将一个对象嵌入新类。在这一章中,大家将学习在Java中重复使用代码的这两种方法,以及具体如何运用。 -**(7) 第7章:多形性** +**(7) 第7章:多态性** -若由你自己来干,可能要花9个月的时间才能发现和理解多形性的问题,这一特性实际是OOP一个重要的基础。通过一些小的、简单的例子,读者可知道如何通过继承来创建一系列类型,并通过它们共有的基础类对那个系列中的对象进行操作。通过Java的多形性概念,同一系列中的所有对象都具有了共通性。这意味着我们编写的代码不必再依赖特定的类型信息。这使程序更易扩展,包容力也更强。由此,程序的构建和代码的维护可以变得更方便,付出的代价也会更低。此外,Java还通过“接口”提供了设置再生关系的第三种途径。这儿所谓的“接口”是对对象物理“接口”一种纯粹的抽象。一旦理解了多形性的概念,接口的含义就很容易解释了。本章也向大家介绍了Java 1.1的“内部类”。 +若由你自己来干,可能要花9个月的时间才能发现和理解多态性的问题,这一特性实际是OOP一个重要的基础。通过一些小的、简单的例子,读者可知道如何通过继承来创建一系列类型,并通过它们共有的基础类对那个系列中的对象进行操作。通过Java的多态性概念,同一系列中的所有对象都具有了共通性。这意味着我们编写的代码不必再依赖特定的类型信息。这使程序更易扩展,包容力也更强。由此,程序的构建和代码的维护可以变得更方便,付出的代价也会更低。此外,Java还通过“接口”提供了设置再生关系的第三种途径。这儿所谓的“接口”是对对象物理“接口”一种纯粹的抽象。一旦理解了多态性的概念,接口的含义就很容易解释了。本章也向大家介绍了Java 1.1的“内部类”。 **(8) 第8章:对象的容纳** 对一个非常简单的程序来说,它可能只拥有一个固定数量的对象,而且对象的“生存时间”或者“存在时间”是已知的。但是通常,我们的程序会在不定的时间创建新对象,只有在程序运行时才可了解到它们的详情。此外,除非进入运行期,否则无法知道所需对象的数量,甚至无法得知它们确切的类型。为解决这个常见的程序设计问题,我们需要拥有一种能力,可在任何时间、任何地点创建任何数量的对象。本章的宗旨便是探讨在使用对象的同时用来容纳它们的一些Java工具:从简单的数组到复杂的集合(数据结构),如Vector和Hashtable等。最后,我们还会深入讨论新型和改进过的Java 1.2集合库。 -**(9) 第9章:违例差错控制** +**(9) 第9章:异常差错控制** -Java最基本的设计宗旨之一便是组织错误的代码不会真的运行起来。编译器会尽可能捕获问题。但某些情况下,除非进入运行期,否则问题是不会被发现的。这些问题要么属于编程错误,要么则是一些自然的出错状况,它们只有在作为程序正常运行的一部分时才会成立。Java为此提供了“违例控制”机制,用于控制程序运行时产生的一切问题。这一章将解释try、catch、throw、throws以及finally等关键字在Java中的工作原理。并讲述什么时候应当“掷”出违例,以及在捕获到违例后该采取什么操作。此外,大家还会学习Java的一些标准违例,如何构建自己的违例,违例发生在构建器中怎么办,以及违例控制器如何定位等等。 +Java最基本的设计宗旨之一便是组织错误的代码不会真的运行起来。编译器会尽可能捕获问题。但某些情况下,除非进入运行期,否则问题是不会被发现的。这些问题要么属于编程错误,要么则是一些自然的出错状况,它们只有在作为程序正常运行的一部分时才会成立。Java为此提供了“异常控制”机制,用于控制程序运行时产生的一切问题。这一章将解释try、catch、throw、throws以及finally等关键字在Java中的工作原理。并讲述什么时候应当“掷”出异常,以及在捕获到异常后该采取什么操作。此外,大家还会学习Java的一些标准异常,如何构建自己的异常,异常发生在构造器中怎么办,以及异常控制器如何定位等等。 **(10) 第10章:Java IO系统** @@ -91,11 +91,11 @@ Java最基本的设计宗旨之一便是组织错误的代码不会真的运行 **(11) 第11章:运行期类型鉴定** -若只有指向基础类的一个句柄,Java的运行期类型标鉴定(RTTI)使我们能获知一个对象的准确类型是什么。一般情况下,我们需要有意忽略一个对象的准确类型,让Java的动态绑定机制(多形性)为那一类型实现正确的行为。但在某些场合下,对于只有一个基础句柄的对象,我们仍然特别有必要了解它的准确类型是什么。拥有这个资料后,通常可以更有效地执行一次特殊情况下的操作。本章将解释RTTI的用途、如何使用以及在适当的时候如何放弃它。此外,Java 1.1的“反射”特性也会在这里得到介绍。 +若只有指向基础类的一个指针,Java的运行期类型标鉴定(RTTI)使我们能获知一个对象的准确类型是什么。一般情况下,我们需要有意忽略一个对象的准确类型,让Java的动态绑定机制(多态性)为那一类型实现正确的行为。但在某些场合下,对于只有一个基础指针的对象,我们仍然特别有必要了解它的准确类型是什么。拥有这个资料后,通常可以更有效地执行一次特殊情况下的操作。本章将解释RTTI的用途、如何使用以及在适当的时候如何放弃它。此外,Java 1.1的“反射”特性也会在这里得到介绍。 **(12) 第12章:传递和返回对象** -由于我们在Java中同对象沟通的唯一途径是“句柄”,所以将对象传递到一个函数里以及从那个函数返回一个对象的概念就显得非常有趣了。本章将解释在函数中进出时,什么才是为了管理对象需要了解的。同时也会讲述String(字串)类的概念,它用一种不同的方式解决了同样的问题。 +由于我们在Java中同对象沟通的唯一途径是“指针”,所以将对象传递到一个函数里以及从那个函数返回一个对象的概念就显得非常有趣了。本章将解释在函数中进出时,什么才是为了管理对象需要了解的。同时也会讲述String(字串)类的概念,它用一种不同的方式解决了同样的问题。 **(13) 第13章:创建窗口和程序片** @@ -155,7 +155,7 @@ Java提供了一套内建的机制,可提供对多个并发子任务的支持 (2) 为方便读者在屏幕上阅读和索引,CD-ROM提供了一个独特的超链接版本。这些超链接包括: -``` +``` java ■230个章、节和小标题链接 ■3600个索引链接 @@ -175,7 +175,7 @@ CD-ROM包含了本书打印版的所有东西,另外还有来自五天快速 在每个源码文件中,都能发现下述版本声明文字: -``` +``` java 16-17页程序 ``` @@ -212,7 +212,7 @@ Bruce@EckelObjects.com #### 12. 错误 无论作者花多大精力来避免,错误总是从意想不到的地方冒出来。如果您认为自己发现了一个错误,请在源文件(可在 http://www.BruceEckel.com 处找到)里指出有可能是错误的地方,填好我们提供的表单。将您推荐的纠错方法通过电子函件发给Bruce@EckelObjects.com。经适当的核对与处理,Web站点的电子版以及本书的下一个印刷版本会作出相应的改正。具体格式如下: -``` +``` java (1) 在主题行(Subject)写上“TIJ Correction”(去掉引号),以便您的函件进入对应的目录。 (2) 在函件正文,采用下述形式: diff --git "a/\347\254\25411\347\253\240 \350\277\220\350\241\214\346\234\237\347\261\273\345\236\213\351\211\264\345\256\232.md" "b/\347\254\25411\347\253\240 \350\277\220\350\241\214\346\234\237\347\261\273\345\236\213\351\211\264\345\256\232.md" index 4716a0f..e166aa6 100644 --- "a/\347\254\25411\347\253\240 \350\277\220\350\241\214\346\234\237\347\261\273\345\236\213\351\211\264\345\256\232.md" +++ "b/\347\254\25411\347\253\240 \350\277\220\350\241\214\346\234\237\347\261\273\345\236\213\351\211\264\345\256\232.md" @@ -1,6 +1,6 @@ # 第11章 运行期类型鉴定 -运行期类型鉴定(RTTI)的概念初看非常简单——手上只有基础类型的一个句柄时,利用它判断一个对象的正确类型。 +运行期类型鉴定(RTTI)的概念初看非常简单——手上只有基础类型的一个指针时,利用它判断一个对象的正确类型。 然而,对RTTI的需要暴露出了面向对象设计许多有趣(而且经常是令人困惑的)的问题,并把程序的构造问题正式摆上了桌面。 本章将讨论如何利用Java在运行期间查找对象和类信息。这主要采取两种形式:一种是“传统”RTTI,它假定我们已在编译和运行期拥有所有类型;另一种是Java1.1特有的“反射”机制,利用它可在运行期独立查找类信息。首先讨论“传统”的RTTI,再讨论反射问题。 diff --git "a/\347\254\25412\347\253\240 \344\274\240\351\200\222\345\222\214\350\277\224\345\233\236\345\257\271\350\261\241.md" "b/\347\254\25412\347\253\240 \344\274\240\351\200\222\345\222\214\350\277\224\345\233\236\345\257\271\350\261\241.md" index 5167721..6f34bf5 100644 --- "a/\347\254\25412\347\253\240 \344\274\240\351\200\222\345\222\214\350\277\224\345\233\236\345\257\271\350\261\241.md" +++ "b/\347\254\25412\347\253\240 \344\274\240\351\200\222\345\222\214\350\277\224\345\233\236\345\257\271\350\261\241.md" @@ -1,8 +1,8 @@ # 第12章 传递和返回对象 -到目前为止,读者应对对象的“传递”有了一个较为深刻的认识,记住实际传递的只是一个句柄。 +到目前为止,读者应对对象的“传递”有了一个较为深刻的认识,记住实际传递的只是一个指针。 在许多程序设计语言中,我们可用语言的“普通”方式到处传递对象,而且大多数时候都不会遇到问题。但有些时候却不得不采取一些非常做法,使得情况突然变得稍微复杂起来(在C++中则是变得非常复杂)。Java亦不例外,我们十分有必要准确认识在对象传递和赋值时所发生的一切。这正是本章的宗旨。 -若读者是从某些特殊的程序设计环境中转移过来的,那么一般都会问到:“Java有指针吗?”有些人认为指针的操作很困难,而且十分危险,所以一厢情愿地认为它没有好处。同时由于Java有如此好的口碑,所以应该很轻易地免除自己以前编程中的麻烦,其中不可能夹带有指针这样的“危险品”。然而准确地说,Java是有指针的!事实上,Java中每个对象(除基本数据类型以外)的标识符都属于指针的一种。但它们的使用受到了严格的限制和防范,不仅编译器对它们有“戒心”,运行期系统也不例外。或者换从另一个角度说,Java有指针,但没有传统指针的麻烦。我曾一度将这种指针叫做“句柄”,但你可以把它想像成“安全指针”。和预备学校为学生提供的安全剪刀类似——除非特别有意,否则不会伤着自己,只不过有时要慢慢来,要习惯一些沉闷的工作。 +若读者是从某些特殊的程序设计环境中转移过来的,那么一般都会问到:“Java有指针吗?”有些人认为指针的操作很困难,而且十分危险,所以一厢情愿地认为它没有好处。同时由于Java有如此好的口碑,所以应该很轻易地免除自己以前编程中的麻烦,其中不可能夹带有指针这样的“危险品”。然而准确地说,Java是有指针的!事实上,Java中每个对象(除基本数据类型以外)的标识符都属于指针的一种。但它们的使用受到了严格的限制和防范,不仅编译器对它们有“戒心”,运行期系统也不例外。或者换从另一个角度说,Java有指针,但没有传统指针的麻烦。我曾一度将这种指针叫做“指针”,但你可以把它想像成“安全指针”。和预备学校为学生提供的安全剪刀类似——除非特别有意,否则不会伤着自己,只不过有时要慢慢来,要习惯一些沉闷的工作。 diff --git "a/\347\254\25413\347\253\240 \345\210\233\345\273\272\347\252\227\345\217\243\345\222\214\347\250\213\345\272\217\347\211\207.md" "b/\347\254\25413\347\253\240 \345\210\233\345\273\272\347\252\227\345\217\243\345\222\214\347\250\213\345\272\217\347\211\207.md" index 7f016c7..ed51283 100644 --- "a/\347\254\25413\347\253\240 \345\210\233\345\273\272\347\252\227\345\217\243\345\222\214\347\250\213\345\272\217\347\211\207.md" +++ "b/\347\254\25413\347\253\240 \345\210\233\345\273\272\347\252\227\345\217\243\345\222\214\347\250\213\345\272\217\347\211\207.md" @@ -14,7 +14,7 @@ Java的主要设计目的之一是建立程序片,也就是建立运行在WEB 对于本章要学习的“老式”AWT,它最严重的缺点就是它无论在面向对象设计方面,还是在GUI开发包设计方面,都有不尽如人意的表现。它使我们回到了程序设计的黑暗年代(换成其他话就是“拙劣的”、“可怕的”、“恶劣的”等等)。必须为执行每一个事件编写代码,包括在其他环境中利用“资源”即可轻松完成的一些任务。 许多象这样的问题在Java 1.1里都得到了缓解或排除,因为: (1)Java 1.1的新型AWT是一个更好的编程模型,并向更好的库设计迈出了可喜的一步。而Java Beans则是那个库的框架。 -(2)“GUI构建器”(可视编程环境)将适用于所有开发系统。在我们用图形化工具将组件置入窗体的时候,Java Beans和新的AWT使GUI构建器能帮我们自动完成代码。其它组件技术如ActiveX等也将以相同的形式支持。 +(2)“GUI构造器”(可视编程环境)将适用于所有开发系统。在我们用图形化工具将组件置入窗体的时候,Java Beans和新的AWT使GUI构造器能帮我们自动完成代码。其它组件技术如ActiveX等也将以相同的形式支持。 既然如此,为什么还要学习使用老的AWT呢?原因很简单,因为它的存在是个事实。就目前来说,这个事实对我们来说显得有些不利,它涉及到面向对象库设计的一个宗旨:一旦我们在库中公布一个组件,就再不能去掉它。如去掉它,就会损害别人已存在的代码。另外,当我们学习Java和所有使用老AWT的程序时,会发现有许多原来的代码使用的都是老式AWT。 AWT必须能与固有操作系统的GUI组件打交通,这意味着它需要执行一个程序片不可能做到的任务。一个不被信任的程序片在操作系统中不能作出任何直接调用,否则它会对用户的机器做出不恰当的事情。一个不被信任的程序片不能访问重要的功能。例如,“在屏幕上画一个窗口”的唯一方法是通过调用拥有特殊接口和安全检查的标准Java库。Sun公司的原始模型创建的信任库将仅仅供给Web浏览器中的Java系统信任关系自动授权器使用,自动授权器将控制怎样进入到库中去。 @@ -57,8 +57,8 @@ paint() 基础类Component的一部分(继承结构中上溯三级)。作为 stop() 每次程序片从Web浏览器的视线中离开时调用,使程序片能关闭代价高昂的操作;同样在调用destroy()前调用 destroy() 程序片不再需要,将它从页面中卸载时调用,以执行资源的最后清除工作 -现在来看一看paint()方法。一旦Component(目前是程序片)决定自己需要更新,就会调用这个方法——可能是由于它再次回转屏幕,首次在屏幕上显示,或者是由于其他窗口临时覆盖了你的Web浏览器。此时程序片会调用它的update()方法(在基础类Component中定义),该方法会恢复一切该恢复的东西,而调用paint()正是这个过程的一部分。没必要对paint()进行过载处理,但构建一个简单的程序片无疑是方便的方法,所以我们首先从paint()方法开始。 -update()调用paint()时,会向其传递指向Graphics对象的一个句柄,那个对象代表准备在上面描绘(作图)的表面。这是非常重要的,因为我们受到项目组件的外观的限制,因此不能画到区域外,这可是一件好事,否则我们就会画到线外去。在程序片的例子中,程序片的外观就是这界定的区域。 +现在来看一看paint()方法。一旦Component(目前是程序片)决定自己需要更新,就会调用这个方法——可能是由于它再次回转屏幕,首次在屏幕上显示,或者是由于其他窗口临时覆盖了你的Web浏览器。此时程序片会调用它的update()方法(在基础类Component中定义),该方法会恢复一切该恢复的东西,而调用paint()正是这个过程的一部分。没必要对paint()进行重载处理,但构建一个简单的程序片无疑是方便的方法,所以我们首先从paint()方法开始。 +update()调用paint()时,会向其传递指向Graphics对象的一个指针,那个对象代表准备在上面描绘(作图)的表面。这是非常重要的,因为我们受到项目组件的外观的限制,因此不能画到区域外,这可是一件好事,否则我们就会画到线外去。在程序片的例子中,程序片的外观就是这界定的区域。 图形对象同样有一系列我们可对其进行的操作。这些操作都与在画布上作图有关。所以其中的大部分都要涉及图像、几何菜状、圆弧等等的描绘(注意如果有兴趣,可在Java文档中找到更详细的说明)。有些方法允许我们画出字符,而其中最常用的就是drawString()。对于它,需指出自己想描绘的String(字串),并指定它在程序片作图区域的起点。这个位置用像素表示,所以它在不同的机器上看起来是不同的,但至少是可以移植的。 根据这些信息即可创建一个简单的程序片: //: Applet1.java @@ -139,12 +139,12 @@ public class Applet3 extends Applet { } } ///:~ -正常情况下,当我们过载一个方法时,需检查自己是否需要调用方法的基础类版本,这是十分重要的。例如,使用init()时可能需要调用super.init()。然而,Applet文档特别指出init()、start()和stop()在Applet中没有用处,所以这里不需要调用它们。 +正常情况下,当我们重载一个方法时,需检查自己是否需要调用方法的基础类版本,这是十分重要的。例如,使用init()时可能需要调用super.init()。然而,Applet文档特别指出init()、start()和stop()在Applet中没有用处,所以这里不需要调用它们。 试验这个程序片时,会发现假如最小化WEB浏览器,或者用另一个窗口将其覆盖,那么就不能再调用stop()和start()(这一行为会随着不同的实现方案变化;可考虑将Web浏览器的行为同程序片观察器的行为对照一下)。调用唯一发生的场合是在我们转移到一个不同的Web页,然后返回包含了程序片的那个页时。 13.3 制作按钮 -制作一个按钮非常简单:只需要调用Button构建器,并指定想在按钮上出现的标签就行了(如果不想要标签,亦可使用默认构建器,但那种情况极少出现)。可参照后面的程序为按钮创建一个句柄,以便以后能够引用它。 -Button是一个组件,象它自己的小窗口一样,会在更新时得以重绘。这意味着我们不必明确描绘一个按钮或者其他任意种类的控件;只需将它们纳入窗体,以后的描绘工作会由它们自行负责。所以为了将一个按钮置入窗体,需要过载init()方法,而不是过载paint(): +制作一个按钮非常简单:只需要调用Button构造器,并指定想在按钮上出现的标签就行了(如果不想要标签,亦可使用默认构造器,但那种情况极少出现)。可参照后面的程序为按钮创建一个指针,以便以后能够引用它。 +Button是一个组件,象它自己的小窗口一样,会在更新时得以重绘。这意味着我们不必明确描绘一个按钮或者其他任意种类的控件;只需将它们纳入窗体,以后的描绘工作会由它们自行负责。所以为了将一个按钮置入窗体,需要重载init()方法,而不是重载paint(): //: Button1.java // Putting buttons on an applet import java.awt.*; @@ -164,7 +164,7 @@ public class Button1 extends Applet { 13.4 捕获事件 大家可注意到假如编译和运行上面的程序片,按下按钮后不会发生任何事情。必须进入程序片内部,编写用于决定要发生什么事情的代码。对于由事件驱动的程序设计,它的基本目标就是用代码捕获发生的事件,并由代码对那些事件作出响应。事实上,GUI的大部分内容都是围绕这种事件驱动的程序设计展开的。 -经过本书前面的学习,大家应该有了面向对象程序设计的一些基础,此时可能会想到应当有一些面向对象的方法来专门控制事件。例如,也许不得不继承每个按钮,并过载一些“按钮按下”方法(尽管这显得非常麻烦有有限)。大家也可能认为存在一些主控“事件”类,其中为希望响应的每个事件都包含了一个方法。 +经过本书前面的学习,大家应该有了面向对象程序设计的一些基础,此时可能会想到应当有一些面向对象的方法来专门控制事件。例如,也许不得不继承每个按钮,并重载一些“按钮按下”方法(尽管这显得非常麻烦有有限)。大家也可能认为存在一些主控“事件”类,其中为希望响应的每个事件都包含了一个方法。 在对象以前,事件控制的典型方式是switch语句。每个事件都对应一个独一无二的整数编号;而且在主事件控制方法中,需要专门为那个值写一个switch。 Java 1.0的AWT没有采用任何面向对象的手段。此外,它也没有使用switch语句,没有打算依靠那些分配给事件的数字。相反,我们必须创建if语句的一个嵌套系列。通过if语句,我们需要尝试做的事情是侦测到作为事件“目标”的对象。换言之,那是我们关心的全部内容——假如某个按钮是一个事件的目标,那么它肯定是一次鼠标点击,并要基于那个假设继续下去。但是,事件里也可能包含了其他信息。例如,假如想调查一次鼠标点击的像素位置,以便画一条引向那个位置的线,那么Event对象里就会包含那个位置的信息(也要注意Java 1.0的组件只能产生有限种类的事件,而Java 1.1和Swing/JFC组件则可产生完整的一系列事件)。 Java 1.0版的AWT方法串联的条件语句中存在action()方法的调用。虽然整个Java 1.0版的事件模型不兼容Java 1.1版,但它在还不支持Java1.1版的机器和运行简单的程序片的系统中更广泛地使用,忠告您使用它会变得非常的舒适,包括对下面使用的action()程序方法而言。 @@ -196,7 +196,7 @@ public class Button2 extends Applet { } } ///:~ -为了解目标是什么,需要向Event对象询问它的target(目标)成员是什么,然后用equals()方法检查它是否与自己感兴趣的目标对象句柄相符。为所有感兴趣的对象写好句柄后,必须在末尾的else语句中调用super.action(evt, arg)方法。我们在第7章已经说过(有关多形性的那一章),此时调用的是我们过载过的方法,而非它的基础类版本。然而,基础类版本也针对我们不感兴趣的所有情况提供了相应的控制代码。除非明确进行,否则它们是不会得到调用的。返回值指出我们是否已经处理了它,所以假如确实与一个事件相符,就应返回true;否则就返回由基础类event()返回的东西。 +为了解目标是什么,需要向Event对象询问它的target(目标)成员是什么,然后用equals()方法检查它是否与自己感兴趣的目标对象指针相符。为所有感兴趣的对象写好指针后,必须在末尾的else语句中调用super.action(evt, arg)方法。我们在第7章已经说过(有关多态性的那一章),此时调用的是我们重载过的方法,而非它的基础类版本。然而,基础类版本也针对我们不感兴趣的所有情况提供了相应的控制代码。除非明确进行,否则它们是不会得到调用的。返回值指出我们是否已经处理了它,所以假如确实与一个事件相符,就应返回true;否则就返回由基础类event()返回的东西。 对这个例子来说,最简单的行动就是打印出到底是什么按钮被按下。一些系统允许你弹出一个小消息窗口,但Java程序片却防碍窗口的弹出。不过我们可以用调用Applet方法的getAppletContext()来访问浏览器,然后用showStatus()在浏览器窗口底部的状态栏上显示一条信息(注释③)。还可用同样的方法打印出对事件的一段完整说明文字,方法是调用getAppletConext().showStatus(evt + "")。空字串会强制编译器将evt转换成一个字符串。这些报告对于测试和调试特别有用,因为浏览器可能会覆盖我们的消息。 ③:ShowStatus()也属于Applet的一个方法,所以可直接调用它,不必调用getAppletContext()。 @@ -270,7 +270,7 @@ public class TextField1 extends Applet { 按下按钮1 是得到我们用鼠标选择的文字就是得到字段内所有的文字并转换成字符串S。它也允许字段被编辑。按下按钮2 放一条信息和字符串s到Text fields,并且阻止字段被编辑(尽管我们能够一直选择文字)。文字的可编辑性是通过setEditable()的真假值来控制的。 13.6 文本区域 -“文本区域”很像文字字段,只是它拥有更多的行以及一些引人注目的更多的功能。另外你能在给定位置对一个文本字段追加、插入或者修改文字。这看起来对文本字段有用的功能相当不错,所以设法发现它设计的特性会产生一些困惑。我们可以认为如果我们处处需要“文本区域”的功能,那么可以简单地使用一个线型文字区域在我们将另外使用文本字段的地方。在Java 1.0版中,当它们不是固定的时候我们也得到了一个文本区域的垂直和水平方向的滚动条。在Java 1.1版中,对高级构建器的修改允许我们选择哪个滚动条是当前的。下面的例子演示的仅仅是在Java1.0版的状况下滚动条一直打开。在下一章里我们将看到一个证明Java 1.1版中的文字区域的例程。 +“文本区域”很像文字字段,只是它拥有更多的行以及一些引人注目的更多的功能。另外你能在给定位置对一个文本字段追加、插入或者修改文字。这看起来对文本字段有用的功能相当不错,所以设法发现它设计的特性会产生一些困惑。我们可以认为如果我们处处需要“文本区域”的功能,那么可以简单地使用一个线型文字区域在我们将另外使用文本字段的地方。在Java 1.0版中,当它们不是固定的时候我们也得到了一个文本区域的垂直和水平方向的滚动条。在Java 1.1版中,对高级构造器的修改允许我们选择哪个滚动条是当前的。下面的例子演示的仅仅是在Java1.0版的状况下滚动条一直打开。在下一章里我们将看到一个证明Java 1.1版中的文字区域的例程。 //: TextArea1.java // Using the text area control import java.awt.*; @@ -312,11 +312,11 @@ public class TextArea1 extends Applet { } } ///:~ -程序中有几个不同的“文本区域”构建器,这其中的一个在此处显示了一个初始字符串和行号和列号。不同的按钮显示得到、追加、修改和插入文字。 +程序中有几个不同的“文本区域”构造器,这其中的一个在此处显示了一个初始字符串和行号和列号。不同的按钮显示得到、追加、修改和插入文字。 13.7 标签 标签准确地运作:安放一个标签到窗体上。这对没有标签的TextFields和Text areas 来说非常的重要,如果我们简单地想安放文字的信息在窗体上也能同样的使用。我们能像本章中第一个例程中演示的那样,使用drawString()里边的paint()在确定的位置去安置一个文字。当我们使用的标签允许我们通过布局管理加入其它的文字组件。(在这章的后面我们将进入讨论。) -使用构建器我们能创建一条包括初始化文字的标签(这是我们典型的作法),一个标签包括一行CENTER(中间)、LEFT(左)和RIGHT(右)(静态的结果取整定义在类标签里)。如果我们忘记了可以用getText()和getalignment()读取值,我们同样可以用setText()和setAlignment()来改变和调整。下面的例子将演示标签的特点: +使用构造器我们能创建一条包括初始化文字的标签(这是我们典型的作法),一个标签包括一行CENTER(中间)、LEFT(左)和RIGHT(右)(静态的结果取整定义在类标签里)。如果我们忘记了可以用getText()和getalignment()读取值,我们同样可以用setText()和setAlignment()来改变和调整。下面的例子将演示标签的特点: //: Label1.java // Using labels import java.awt.*; @@ -356,12 +356,12 @@ public class Label1 extends Applet { 首先是标签的最典型的用途:标记一个文本字段或文本区域。在例程的第二部分,当我们按下“test 1”按钮通过setText()将一串空的空格插入到的字段里。因为空的空格数不等于同样的字符数(在一个等比例间隔的字库里),当插入文字到标签里时我们会看到文字将被省略掉。在例子的第三部分保留的空的空格在我们第一次按下“test 2”会发现标签是空的(trim()删除了每个字符串结尾部分的空格)并且在开头的左列插入了一个短的标签。在工作的其余时间中我们按下按钮进行调整,因此就能看到效果。 我们可能会认为我们可以创建一个空的标签,然后用setText()安放文字在里面。然而我们不能在一个空标签内加入文字-这大概是因为空标签没有宽度-所以创建一个没有文字的空标签是没有用处的。在上面的例子里,“blank”标签里充满空的空格,所以它足够容纳后面加入的文字。 -同样的,setAlignment()在我们用构建器创建的典型的文字标签上没有作用。这个标签的宽度就是文字的宽度,所以不能对它进行任何的调整。但是,如果我们启动一个长标签,然后把它变成短的,我们就可以看到调整的效果。 +同样的,setAlignment()在我们用构造器创建的典型的文字标签上没有作用。这个标签的宽度就是文字的宽度,所以不能对它进行任何的调整。但是,如果我们启动一个长标签,然后把它变成短的,我们就可以看到调整的效果。 这些导致事件连同它们最小化的尺寸被挤压的状况被程序片使用的默认布局管理器所发现。有关布局管理器的部分包含在本章的后面。 13.8 复选框 复选框提供一个制造单一选择开关的方法;它包括一个小框和一个标签。典型的复选框有一个小的“X”(或者它设置的其它类型)或是空的,这依靠项目是否被选择来决定的。 -我们会使用构建器正常地创建一个复选框,使用它的标签来充当它的自变量。如果我们在创建复选框后想读出或改变它,我们能够获取和设置它的状态,同样也能获取和设置它的标签。注意,复选框的大写是与其它的控制相矛盾的。 +我们会使用构造器正常地创建一个复选框,使用它的标签来充当它的自变量。如果我们在创建复选框后想读出或改变它,我们能够获取和设置它的状态,同样也能获取和设置它的标签。注意,复选框的大写是与其它的控制相矛盾的。 无论何时一个复选框都可以设置和清除一个事件指令,我们可以捕捉同样的方法做一个按钮。在下面的例子里使用一个文字区域枚举所有被选中的复选框: //: CheckBox1.java // Using check boxes @@ -399,8 +399,8 @@ trace()方法将选中的复选框名和当前状态用appendText()发送到文 13.9 单选钮 单选钮在GUI程序设计中的概念来自于老式的电子管汽车收音机的机械按钮:当我们按下一个按钮时,其它的按钮就会弹起。因此它允许我们强制从众多选择中作出单一选择。 -AWT没有单独的描述单选钮的类;取而代之的是复用复选框。然而将复选框放在单选钮组中(并且修改它的外形使它看起来不同于一般的复选框)我们必须使用一个特殊的构建器象一个自变量一样的作用在checkboxGroup对象上。(我们同样能在创建复选框后调用setCheckboxGroup()方法。) -一个复选框组没有构建器的自变量;它存在的唯一理由就是聚集一些复选框到单选钮组里。一个复选框对象必须在我们试图显示单选钮组之前将它的状态设置成true,否则在运行时我们就会得到一个异常。如果我们设置超过一个的单选钮为true,只有最后的一个能被设置成真。 +AWT没有单独的描述单选钮的类;取而代之的是复用复选框。然而将复选框放在单选钮组中(并且修改它的外形使它看起来不同于一般的复选框)我们必须使用一个特殊的构造器象一个自变量一样的作用在checkboxGroup对象上。(我们同样能在创建复选框后调用setCheckboxGroup()方法。) +一个复选框组没有构造器的自变量;它存在的唯一理由就是聚集一些复选框到单选钮组里。一个复选框对象必须在我们试图显示单选钮组之前将它的状态设置成true,否则在运行时我们就会得到一个异常。如果我们设置超过一个的单选钮为true,只有最后的一个能被设置成真。 这里有个简单的使用单选钮的例子。注意我们可以像其它的组件一样捕捉单选钮的事件: //: RadioButton1.java // Using radio buttons @@ -525,8 +525,8 @@ public class List1 extends Applet { 13.11.1 handleEvent() 到目前为止,我们已使用了action(),现有另一种方法handleEvent()可对每一事件进行尝试。当一个事件发生时,它总是针对单独事件或发生在单独的事件对象上。该对象的handleEvent()方法是自动调用的,并且是被handleEvent()创建并传递到handleEvent()里。默认的handleEvent()(handleEvent()定义在组件里,基础类的所有控件都在AWT里)将像我们以前一样调用action()或其它同样的方法去指明鼠标的活动、键盘活动或者指明移动的焦点。我们将会在本章的后面部分看到。 -如果其它的方法-特别是action()-不能满足我们的需要怎么办呢?至于列表框,例如,如果我想捕捉鼠标单击,但action()只响应双击怎么办呢?这个解答是过载handleEvent(),毕竟它是从程序片中得到的,因此可以过载任何非确定的方法。当我们为程序片过载handleEvent()时,我们会得到所有的事件在它们发送出去之前,所以我们不能假设“这里有我的按钮可做的事件,所以我们可以假设按钮被按下了”从它被action()设为真值。在handleEvent()中按钮拥有焦点且某人对它进行分配都是可能的。不论它合理与否,我们可测试这些事件并遵照handleEvent()来进行操作。 -为了修改列表样本,使它会响应鼠标的单击,在action()中按钮测试将被过载,但代码会处理的列表将像下面的例子被移进handleEvent()中去: +如果其它的方法-特别是action()-不能满足我们的需要怎么办呢?至于列表框,例如,如果我想捕捉鼠标单击,但action()只响应双击怎么办呢?这个解答是重载handleEvent(),毕竟它是从程序片中得到的,因此可以重载任何非确定的方法。当我们为程序片重载handleEvent()时,我们会得到所有的事件在它们发送出去之前,所以我们不能假设“这里有我的按钮可做的事件,所以我们可以假设按钮被按下了”从它被action()设为真值。在handleEvent()中按钮拥有焦点且某人对它进行分配都是可能的。不论它合理与否,我们可测试这些事件并遵照handleEvent()来进行操作。 +为了修改列表样本,使它会响应鼠标的单击,在action()中按钮测试将被重载,但代码会处理的列表将像下面的例子被移进handleEvent()中去: //: List2.java // Using lists with handleEvent() import java.awt.*; @@ -577,7 +577,7 @@ public class List2 extends Applet { } } ///:~ -这个例子同前面的例子相同除了增加了handleEvent()外简直一模一样。在程序中做了试验来验证是否列表框的选择和非选择存在。现在请记住,handleEvent()被程序片所过载,所以它能在窗体中任何存在,并且被其它的列表当成事件来处理。因此我们同样必须通过试验来观察目标。(虽然在这个例子中,程序片中只有一个列表框所以我们能假设所有的列表框事件必须服务于列表框。这是一个不好的习惯,一旦其它的列表框加入,它就会变成程序中的一个缺陷。)如果列表框匹配一个我们感兴趣的列表框,像前面的一样的代码将按上面的策略来运行。注意handleEvent()的窗体与action()的相同:如果我们处理一个单独的事件,将返回真值,但如果我们对其它的一些事件不感兴趣,通过handleEvent()我们必须返回super.handleEvent()值。这便是程序的核心,如果我们不那样做,其它的任何一个事件处理代码也不会被调用。例如,试注解在上面的代码中返回super.handleEvent(evt)的值。我们将发现action()没有被调用,当然那不是我们想得到的。对action()和handlEvent()而言,最重要的是跟着上面例子中的格式,并且当我们自己不处理事件时一直返回基础类的方法版本信息。(在例子中我们将返回真值)。(幸运的是,这些类型的错误的仅属于Java 1.0版,在本章后面将看到的新设计的Java 1.1消除了这些类型的错误。) +这个例子同前面的例子相同除了增加了handleEvent()外简直一模一样。在程序中做了试验来验证是否列表框的选择和非选择存在。现在请记住,handleEvent()被程序片所重载,所以它能在窗体中任何存在,并且被其它的列表当成事件来处理。因此我们同样必须通过试验来观察目标。(虽然在这个例子中,程序片中只有一个列表框所以我们能假设所有的列表框事件必须服务于列表框。这是一个不好的习惯,一旦其它的列表框加入,它就会变成程序中的一个缺陷。)如果列表框匹配一个我们感兴趣的列表框,像前面的一样的代码将按上面的策略来运行。注意handleEvent()的窗体与action()的相同:如果我们处理一个单独的事件,将返回真值,但如果我们对其它的一些事件不感兴趣,通过handleEvent()我们必须返回super.handleEvent()值。这便是程序的核心,如果我们不那样做,其它的任何一个事件处理代码也不会被调用。例如,试注解在上面的代码中返回super.handleEvent(evt)的值。我们将发现action()没有被调用,当然那不是我们想得到的。对action()和handlEvent()而言,最重要的是跟着上面例子中的格式,并且当我们自己不处理事件时一直返回基础类的方法版本信息。(在例子中我们将返回真值)。(幸运的是,这些类型的错误的仅属于Java 1.0版,在本章后面将看到的新设计的Java 1.1消除了这些类型的错误。) 在windows里,如果我们按下shift键,列表框自动允许我们做多个选择。这非常的棒,因为它允许用户做单个或多个的选择而不是编程期间固定的。我们可能会认为我们变得更加的精明,并且当一个鼠标单击被evt.shiftdown()产生时如果shift键是按下的将执行我们自己的试验程序。AWT的设计妨碍了我们-我们不得不去了解哪个项目被鼠标点击时是否按下了shift键,所以我们能取消其余部分所有的选择并且只选择那一个。不管怎样,我们是不可能在Java 1.0版中做出来的。(Java 1.1将所有的鼠标、键盘、焦点事件传送到列表中,所以我们能够完成它。) 13.12 布局的控制 @@ -627,7 +627,7 @@ public class BorderLayout1 extends Applet { BorderLayout是应用程序和对话框的默认布局管理器。 13.12.3 GridLayout -GridLayout允许我们建立一个组件表。添加那些组件时,它们会按从左到右、从上到下的顺序在网格中排列。在构建器里,需要指定自己希望的行、列数,它们将按正比例展开。 +GridLayout允许我们建立一个组件表。添加那些组件时,它们会按从左到右、从上到下的顺序在网格中排列。在构造器里,需要指定自己希望的行、列数,它们将按正比例展开。 //: GridLayout1.java // Demonstrating the GridLayout import java.awt.*; @@ -702,7 +702,7 @@ public class CardLayout1 extends Applet { } ///:~ 这个例子首先会创建一种新类型的面板:BottonPanel(按钮面板)。它包括一个单独的按钮,安放在BorderLayout的中央,那意味着它将充满整个的面板。按钮上的标签将让我们知道我们在CardLayout上的那个面板上。 -在程序片里,面板卡片上将存放卡片和布局管理器CL因为CardLayout必须组成类,因为当我们需要处理卡片时我们需要访问这些句柄。 +在程序片里,面板卡片上将存放卡片和布局管理器CL因为CardLayout必须组成类,因为当我们需要处理卡片时我们需要访问这些指针。 这个程序片变成使用BorderLayout来取代它的默认FlowLayout,创建面板来容纳三个按钮(使用FlowLayout),并且这个面板安置在程序片末尾的“North”。卡片面板增加到程序片的“Center”里,有效地占据面板的其余地方。 当我们增加BottonPanels(或者任何其它我们想要的组件)到卡片面板时,add()方法的第一个自变量不是“North”,“South”等等。相反的是,它是一个描述卡片的字符串。如果我们想轻击那张卡片使用字符串,我们就可以使用,虽然这字符串不会显示在卡片的任何地方。使用的方法不是使用action();代之使用first()、next()和last()等方法。请查看我们有关其它方法的文件。 在Java中,使用的一些卡片式面板结构十分的重要,因为(我们将在后面看到)在程序片编程中使用的弹出式对话框是十分令人沮丧的。对于Java 1.0版的程序片而言,CardLayout是唯一有效的取得很多不同的“弹出式”的窗体。 @@ -712,7 +712,7 @@ public class CardLayout1 extends Applet { 在这范围内还有其它的:在JFC/Swing库里有一个新的使用Smalltalk的受人欢迎的“Spring and Struts”布局管理器并且它能显著地减少GridBagLayout的需要。 13.13 action的替代品 -正如早先指出的那样,action()并不是我们对所有事进行分类后自动为handleEvent()调用的唯一方法。有三个其它的被调用的方法集,如果我们想捕捉某些类型的事件(键盘、鼠标和焦点事件),因此我们不得不过载规定的方法。这些方法是定义在基础类组件里,所以他们几乎在所有我们可能安放在窗体中的组件中都是有用的。然而,我们也注意到这种方法在Java 1.1版中是不被支持的,同样尽管我们可能注意到继承代码利用了这种方法,我们将会使用Java 1.1版的方法来代替(本章后面有详细介绍)。 +正如早先指出的那样,action()并不是我们对所有事进行分类后自动为handleEvent()调用的唯一方法。有三个其它的被调用的方法集,如果我们想捕捉某些类型的事件(键盘、鼠标和焦点事件),因此我们不得不重载规定的方法。这些方法是定义在基础类组件里,所以他们几乎在所有我们可能安放在窗体中的组件中都是有用的。然而,我们也注意到这种方法在Java 1.1版中是不被支持的,同样尽管我们可能注意到继承代码利用了这种方法,我们将会使用Java 1.1版的方法来代替(本章后面有详细介绍)。 组件方法 何时调用 @@ -728,8 +728,8 @@ mouseDrag(Event evt, int x, int y) 鼠标在一次mouseDown事件发生后拖 mouseEnter(Event evt, int x, int y) 鼠标从前不在组件上方,但目前在 mouseExit(Event evt, int x, int y) 鼠标曾经位于组件上方,但目前不在 -当我们处理特殊情况时——一个鼠标事件,例如,它恰好是我们想得到的鼠标事件存在的座标,我们将看到每个程序接收一个事件连同一些我们所需要的信息。有趣的是,当组件的handleEvent()调用这些方法时(典型的事例),附加的自变量总是多余的因为它们包含在事件对象里。事实上,如果我们观察component.handleEvent()的源代码,我们能发现它显然将增加的自变量抽出事件对象(这可能是考虑到在一些语言中无效率的编码,但请记住Java的焦点是安全的,不必担心。)试验对我们表明这些事件事实上在被调用并且作为一个有趣的尝试是值得创建一个过载每个方法的程序片,(action()的过载在本章的其它地方)当事件发生时显示它们的相关数据。 -这个例子同样向我们展示了怎样制造自己的按钮对象,因为它是作为目标的所有事件权益来使用。我可能会首先(也是必须的)假设制造一个新的按钮,我们从按钮处继承。但它并不能运行。取而代之的是,我们从画布组件处(一个非常普通组件)继承,并在其上不使用paint()方法画出一个按钮。正如我们所看到的,自从一些代码混入到画按钮中去,按钮根本就不运行,这实在是太糟糕了。(如果您不相信我,试图在例子中为画布组件交换按钮,请记住调用称为super的基础类构建器。我们会看到按钮不会被画出,事件也不会被处理。) +当我们处理特殊情况时——一个鼠标事件,例如,它恰好是我们想得到的鼠标事件存在的座标,我们将看到每个程序接收一个事件连同一些我们所需要的信息。有趣的是,当组件的handleEvent()调用这些方法时(典型的事例),附加的自变量总是多余的因为它们包含在事件对象里。事实上,如果我们观察component.handleEvent()的源代码,我们能发现它显然将增加的自变量抽出事件对象(这可能是考虑到在一些语言中无效率的编码,但请记住Java的焦点是安全的,不必担心。)试验对我们表明这些事件事实上在被调用并且作为一个有趣的尝试是值得创建一个重载每个方法的程序片,(action()的重载在本章的其它地方)当事件发生时显示它们的相关数据。 +这个例子同样向我们展示了怎样制造自己的按钮对象,因为它是作为目标的所有事件权益来使用。我可能会首先(也是必须的)假设制造一个新的按钮,我们从按钮处继承。但它并不能运行。取而代之的是,我们从画布组件处(一个非常普通组件)继承,并在其上不使用paint()方法画出一个按钮。正如我们所看到的,自从一些代码混入到画按钮中去,按钮根本就不运行,这实在是太糟糕了。(如果您不相信我,试图在例子中为画布组件交换按钮,请记住调用称为super的基础类构造器。我们会看到按钮不会被画出,事件也不会被处理。) myButton类是明确说明的:它只和一个自动事件(AutoEvent)“父窗口”一起运行(父窗口不是一个基础类,它是按钮创建和存在的窗口。)。通过这个知识,myButton可能进入到父窗口并且处理它的文字字段,必然就能将状态信息写入到父窗口的字段里。当然这是一种非常有限的解决方法,myButton仅能在连结AutoEvent时被使用。这种代码有时称为“高度结合”。但是,制造myButton更需要很多的不是为例子(和可能为我们将写的一些程序片)担保的努力。再者,请注意下面的代码使用了Java 1.1版不支持的API。 //: AutoEvent.java // Alternatives to action() @@ -858,11 +858,11 @@ public class AutoEvent extends Applet { } } ///:~ -我们可以看到构建器使用利用自变量同名的方法,所以自变量被赋值,并且使用this来区分: +我们可以看到构造器使用利用自变量同名的方法,所以自变量被赋值,并且使用this来区分: this.label = label; paint()方法由简单的开始:它用按钮的颜色填充了一个“圆角矩形”,然后画了一个黑线围绕它。请注意size()的使用决定了组件的宽度和长度(当然,是像素)。这之后,paint()看起来非常的复杂,因为有大量的预测去计算出怎样利用“font metrics”集中按钮的标签到按钮里。我们能得到一个相当好的关于继续关注方法调用的主意,它将程序中那些相当平凡的代码挑出,当我们想集中一个标签到一些组件里时,我们正好可以对它进行剪切和粘贴。 您直到注意到AutoEvent类才能正确地理解keyDown(),keyUp()及其它方法的运行。这包含一个Hashtable(译者注:散列表)去控制字符串来描述关于事件处理的事件和TextField类型。当然,这些能被静态的创建而不是放入Hashtable但我认为您会同意它是更容易使用和改变的。特别是,如果我们需要在AutoEvent中增加或删除一个新的事件类型,我们只需要简单地在事件列队中增加或删除一个字符串——所有的工作都自动地完成了。 -我们查出在keyDown(),keyup()及其它方法中的字符串的位置回到myButton中。这些方法中的任何一个都用父句柄试图回到父窗口。父类是一个AutoEvent,它包含Hashtable h和get()方法,当拥有特定的字符串时,将对一个我们知道的TextField对象产生一个句柄(因此它被选派到那)。然后事件对象修改显示在TextField中的字符串陈述。从我们可以真正注意到举出的例子在我们的程序中运行事件时以来,可以发现这个例子运行起来颇为有趣的。 +我们查出在keyDown(),keyup()及其它方法中的字符串的位置回到myButton中。这些方法中的任何一个都用父指针试图回到父窗口。父类是一个AutoEvent,它包含Hashtable h和get()方法,当拥有特定的字符串时,将对一个我们知道的TextField对象产生一个指针(因此它被选派到那)。然后事件对象修改显示在TextField中的字符串陈述。从我们可以真正注意到举出的例子在我们的程序中运行事件时以来,可以发现这个例子运行起来颇为有趣的。 13.14 程序片的局限 出于安全缘故,程序片十分受到限制,并且有很多的事我们都不能做。您一般会问:程序片看起来能做什么,传闻它又能做什么:扩展浏览器中WEB页的功能。自从作为一个网上冲浪者,我们从未真正想了解是否一个WEB页来自友好的或者不友好的站点,我们想要一些可以安全地行动的代码。所以我们可能会注意到大量的限制: @@ -995,15 +995,15 @@ public class Menu1 extends Frame { } ///:~ 在这个程序中,我避免了为每个菜单编写典型的冗长的add()列表调用,因为那看起来像许多的无用的标志。取而代之的是,我安放菜单项到数组中,然后在一个for的循环中通过每个数组调用add()简单地跳过。这样的话,增加和减少菜单项变得没那么讨厌了。 -作为一个可选择的方法(我发现这很难令我满意,因为它需要更多的分配)CheckboxMenuItems在数组的句柄中被创建是被称为安全创建;这对数组文件和其它的文件而言是真正的安全。 +作为一个可选择的方法(我发现这很难令我满意,因为它需要更多的分配)CheckboxMenuItems在数组的指针中被创建是被称为安全创建;这对数组文件和其它的文件而言是真正的安全。 程序中创建了不是一个而是二个的菜单条来证明菜单条在程序运行时能被交换激活。我们可以看到菜单条怎样组成菜单,每个菜单怎样组成菜单项(MenuItems),chenkboxMenuItems或者其它的菜单(产生子菜单)。当菜单组合后,可以用setMenuBar()方法安装到现在的程序中。值得注意的是当按钮被压下时,它将检查当前的菜单安装使用getMenuBar(),然后安放其它的菜单条在它的位置上。 当测试是“open”(即开始)时,注意拼写和大写,如果开始时没有对象,Java发出no error(没有错误)的信号。这种字符串比较是一个明显的程序设计错误源。 校验和非校验的菜单项自动地运行,与之相关的CheckBoxMenuItems着实令人吃惊,这是因为一些原因它们不允许字符串匹配。(这似乎是自相矛盾的,尽管字符串匹配并不是一种很好的办法。)因此,我们可以匹配一个目标对象而不是它们的标签。当演示时,getState()方法用来显示状态。我们同样可以用setState()改变CheckboxMenuItem的状态。 -我们可能会认为一个菜单可以合理地置入超过一个的菜单条中。这看似合理,因为所有我们忽略的菜单条的add()方法都是一个句柄。然而,如果我们试图这样做,这个结果将会变得非常的别扭,而远非我们所希望得到的结果。(很难知道这是一个编程中的错误或者说是他们试图使它以这种方法去运行所产生的。)这个例子同样向我们展示了为什么我们需要建立一个应用程序以替代程序片。(这是因为应用程序能支持菜单,而程序片是不能直接使用菜单的。)我们从帧处继承代替从程序片处继承。另外,我们为类建一个构建器以取代init()安装事件。最后,我们创建一个main()方法并且在我们建的新型对象里,调整它的大小,然后调用show()。它与程序片只在很小的地方有不同之处,然而这时它已经是一个独立的设置窗口应用程序并且我们可以使用菜单。 +我们可能会认为一个菜单可以合理地置入超过一个的菜单条中。这看似合理,因为所有我们忽略的菜单条的add()方法都是一个指针。然而,如果我们试图这样做,这个结果将会变得非常的别扭,而远非我们所希望得到的结果。(很难知道这是一个编程中的错误或者说是他们试图使它以这种方法去运行所产生的。)这个例子同样向我们展示了为什么我们需要建立一个应用程序以替代程序片。(这是因为应用程序能支持菜单,而程序片是不能直接使用菜单的。)我们从帧处继承代替从程序片处继承。另外,我们为类建一个构造器以取代init()安装事件。最后,我们创建一个main()方法并且在我们建的新型对象里,调整它的大小,然后调用show()。它与程序片只在很小的地方有不同之处,然而这时它已经是一个独立的设置窗口应用程序并且我们可以使用菜单。 13.15.2 对话框 对话框是一个从其它窗口弹出的窗口。它的目的是处理一些特殊的争议和它们的细节而不使原来的窗口陷入混乱之中。对话框大量在设置窗口的编程环境中使用,但就像前面提到的一样,鲜于在程序片中使用。 -我们需要从对话类处继承以创建其它类型的窗口、像帧一样的对话框。和窗框不同,对话框不能拥有菜单条也不能改变光标,但除此之外它们十分的相似。一个对话框拥有布局管理器(默认的是BorderLayout布局管理器)和过载action()等等,或用handleEvent()去处理事件。我们会注意到handleEvent()的一个重要差异:当WINDOW_DESTORY事件发生时,我们并不希望关闭正在运行的应用程序! +我们需要从对话类处继承以创建其它类型的窗口、像帧一样的对话框。和窗框不同,对话框不能拥有菜单条也不能改变光标,但除此之外它们十分的相似。一个对话框拥有布局管理器(默认的是BorderLayout布局管理器)和重载action()等等,或用handleEvent()去处理事件。我们会注意到handleEvent()的一个重要差异:当WINDOW_DESTORY事件发生时,我们并不希望关闭正在运行的应用程序! 相反,我们可以使用对话窗口通过调用dispace()释放资源。在下面的例子中,对话框是由定义在那儿作为类的ToeButton的特殊按钮组成的网格构成的(利用GridLayout布局管理器)。ToeButton按钮围绕它自已画了一个帧,并且依赖它的状态:在空的中的“X”或者“O”。它从空白开始,然后依靠使用者的选择,转换成“X”或“O”。但是,当我们单击在按钮上时,它会在“X”和“O”之间来回交换。(这产生了一种类似填字游戏的感觉,当然比它更令人讨厌。)另外,这个对话框可以被设置为在主应用程序窗口中为很多的行和列变更号码。 //: ToeTest.java // Demonstration of dialog boxes @@ -1112,12 +1112,12 @@ public class ToeTest extends Frame { } } ///:~ -ToeButton类保留了一个句柄到它ToeDialog型的父类中。正如前面所述,ToeButton和ToeDialog高度的结合因为一个ToeButton只能被一个ToeDialog所使用,但它却解决了一系列的问题,事实上这实在不是一个糟糕的解决方案因为没有另外的可以记录用户选择的对话类。当然我们可以使用其它的制造ToeDialog.turn(ToeButton的静态的一部分)方法。这种方法消除了它们的紧密联系,但却阻止了我们一次拥有多个ToeDialog(无论如何,至少有一个正常地运行)。 +ToeButton类保留了一个指针到它ToeDialog型的父类中。正如前面所述,ToeButton和ToeDialog高度的结合因为一个ToeButton只能被一个ToeDialog所使用,但它却解决了一系列的问题,事实上这实在不是一个糟糕的解决方案因为没有另外的可以记录用户选择的对话类。当然我们可以使用其它的制造ToeDialog.turn(ToeButton的静态的一部分)方法。这种方法消除了它们的紧密联系,但却阻止了我们一次拥有多个ToeDialog(无论如何,至少有一个正常地运行)。 paint()是一种与图形有关的方法:它围绕按钮画出矩形并画出“X”或“O”。这完全是冗长的计算,但却十分的直观。 -一个鼠标单击被过载的mouseDown()方法所俘获,最要紧的是检查是否有事件写在按钮上。如果没有,父窗口会被询问以找出谁选择了它并用来确定按钮的状态。值得注意的是按钮随后交回到父类中并且改变它的选择。如果按钮已经显示这为“X”和“O”,那么它们会被改变状态。我们能注意到本书第三章中描述的在这些计算中方便的使用的三个一组的If-else。当一个按钮的状态改变后,按钮会被重画。 -ToeDialog的构建器十分的简单:它像我们所需要的一样增加一些按钮到GridLayout布局管理器中,然后调整每个按钮每边大小为50个像素(如果我们不调整窗口,那么它就不会显示出来)。注意handleEvent()正好为WINDOW_DESTROY调用dispose(),因此整个应用程序不会被关闭。 +一个鼠标单击被重载的mouseDown()方法所俘获,最要紧的是检查是否有事件写在按钮上。如果没有,父窗口会被询问以找出谁选择了它并用来确定按钮的状态。值得注意的是按钮随后交回到父类中并且改变它的选择。如果按钮已经显示这为“X”和“O”,那么它们会被改变状态。我们能注意到本书第三章中描述的在这些计算中方便的使用的三个一组的If-else。当一个按钮的状态改变后,按钮会被重画。 +ToeDialog的构造器十分的简单:它像我们所需要的一样增加一些按钮到GridLayout布局管理器中,然后调整每个按钮每边大小为50个像素(如果我们不调整窗口,那么它就不会显示出来)。注意handleEvent()正好为WINDOW_DESTROY调用dispose(),因此整个应用程序不会被关闭。 ToeTest设置整个应用程序以创建TextField(为输入按钮网格的行和列)和“go”按钮。我们会领会action()在这个程序中使用不太令人满意的“字符串匹配”技术来测试按钮的按下(请确定我们拼写和大写都是正确的!)。当按钮按下时,TextField中的数据将被取出,并且,因为它们在字符串结构中,所以需要利用静态的Integer.paresInt()方法来转变成中断。一旦对话类被建立,我们就必须调用show()方法来显示和激活它。 -我们会注意到ToeDialog对象赋值给一个对话句柄 d。这是一个上溯造型的例子,尽管它没有真正地产生重要的差异,因为所有的事件都是show()调用的。但是,如果我们想调用ToeDialog中已经存在的一些方法,我们需要对ToeDialog句柄赋值,就不会在一个上溯中丢失信息。 +我们会注意到ToeDialog对象赋值给一个对话指针 d。这是一个上溯造型的例子,尽管它没有真正地产生重要的差异,因为所有的事件都是show()调用的。但是,如果我们想调用ToeDialog中已经存在的一些方法,我们需要对ToeDialog指针赋值,就不会在一个上溯中丢失信息。 1. 文件对话类 在一些操作系统中拥有许多的特殊内建对话框去处理选择的事件,例如:字库,颜色,打印机以及类似的事件。几乎所有的操作系统都支持打开和保存文件,但是,Java的FileDialog包更容易使用。当然这会不再检测所有使用的程序片,因为程序片在本地磁盘上既不能读也不能写文件。(这会在新的浏览器中交换程序片的信任关系。) @@ -1197,9 +1197,9 @@ public class FileDialogTest extends Frame { } } ///:~ -对一个“打开文件”对话框,我们使用构建器设置两个自变量;首先是父窗口句柄,其次是FileDialog标题条的标题。setFile()方法提供一个初始文件名--也许本地操作系统支持通配符,因此在这个例子中所有的.java文件最开头会被显示出来。setDirectory()方法选择文件决定开始的目录(一般而言,操作系统允许用户改变目录)。 +对一个“打开文件”对话框,我们使用构造器设置两个自变量;首先是父窗口指针,其次是FileDialog标题条的标题。setFile()方法提供一个初始文件名--也许本地操作系统支持通配符,因此在这个例子中所有的.java文件最开头会被显示出来。setDirectory()方法选择文件决定开始的目录(一般而言,操作系统允许用户改变目录)。 show()命令直到对话类关闭才返回。FileDialog对象一直存在,因此我们可以从它那里读取数据。如果我们调用getFile()并且它返回空,这意味着用户退出了对话类。文件名和调用getDirectory()方法的结果都显示在TextFields里。 -按钮的保存工作使用同样的方法,除了因为FileDialog而使用不同的构建器。这个构建器设置了三个自变量并且第三的一个自变量必须为FileDialog.SAVE或FileDialog.OPEN。 +按钮的保存工作使用同样的方法,除了因为FileDialog而使用不同的构造器。这个构造器设置了三个自变量并且第三的一个自变量必须为FileDialog.SAVE或FileDialog.OPEN。 13.16 新型AWT 在Java 1.1中一个显著的改变就是完善了新AWT的创新。大多数的改变围绕在Java 1.1中使用的新事件模型:老的事件模型是糟糕的、笨拙的、非面向对象的,而新的事件模型可能是我所见过的最优秀的。难以理解一个如此糟糕的(老的AWT)和一个如此优秀的(新的事件模型)程序语言居然出自同一个集团之手。新的考虑事件的方法看来中止了,因此争议不再变成障碍,从而轻易进入我们的意识里;相反,它是一个帮助我们设计系统的工具。它同样是Java Beans的精华,我们会在本章后面部分进入讲述。 @@ -1210,7 +1210,7 @@ show()命令直到对话类关闭才返回。FileDialog对象一直存在,因 13.16.1 新的事件模型 在新的事件模型的组件可以开始一个事件。每种类型的事件被一个个别的类所描绘。当事件开始后,它受理一个或更多事件指明“接收器”。因此,事件源和处理事件的地址可以被分离。 每个事件接收器都是执行特定的接收器类型接口的类对象。因此作为一个程序开发者,我们所要做的是创建接收器对象并且在被激活事件的组件中进行注册。event-firing组件调用一个addXXXListener()方法来完成注册,以描述XXX事件类型接受。我们可以容易地了解到以addListened名的方法通知我们任何的事件类型都可以被处理,如果我们试图接收事件我们会发现编译时我们的错误。Java Beans同样使用这种addListener名的方法去判断那一个程序可以运行。 -我们所有的事件逻辑将装入到一个接收器类中。当我们创建一个接收器类时唯一的一点限制是必须执行专用的接口。我们可以创建一个全局接收器类,这种情况在内部类中有助于被很好地使用,不仅仅是因为它们提供了一个理论上的接收器类组到它们服务的UI或业务逻辑类中,但因为(正像我们将会在本章后面看到的)事实是一个内部类维持一个句柄到它的父对象,提供了一个很好的通过类和子系统边界的调用方法。 +我们所有的事件逻辑将装入到一个接收器类中。当我们创建一个接收器类时唯一的一点限制是必须执行专用的接口。我们可以创建一个全局接收器类,这种情况在内部类中有助于被很好地使用,不仅仅是因为它们提供了一个理论上的接收器类组到它们服务的UI或业务逻辑类中,但因为(正像我们将会在本章后面看到的)事实是一个内部类维持一个指针到它的父对象,提供了一个很好的通过类和子系统边界的调用方法。 一个简单的例子将使这一切变得清晰明确。同时思考本章前部Button2.java例子与这个例子的差异。 //: Button2New.java // Capturing button presses @@ -1537,7 +1537,7 @@ textValueChanged(TextEvent) 1. 用接收器适配器简化操作 在上面的表格中,我们可以注意到一些接收器接口只有唯一的一个方法。它们的执行是无轻重的,因为我们仅当需要书写特殊方法时才会执行它们。然而,接收器接口拥有多个方法,使用起来却不太友好。例如,我们必须一直运行某些事物,当我们创建一个应用程序时对帧提供一个WindowListener,以便当我们得到windowClosing()事件时可以调用System.exit(0)以退出应用程序。但因为WindowListener是一个接口,我们必须执行其它所有的方法即使它们不运行任何事件。这真令人讨厌。 -为了解决这个问题,每个拥有超过一个方法的接收器接口都可拥有适配器,它们的名我们可以在上面的表格中看到。每个适配器为每个接口方法提供默认的方法。(WindowAdapter的默认方法不是windowClosing(),而是System.exit(0)方法。)此外我们所要做的就是从适配器处继承并过载唯一的需要变更的方法。例如,典型的WindowListener我们会像下面这样的使用。 +为了解决这个问题,每个拥有超过一个方法的接收器接口都可拥有适配器,它们的名我们可以在上面的表格中看到。每个适配器为每个接口方法提供默认的方法。(WindowAdapter的默认方法不是windowClosing(),而是System.exit(0)方法。)此外我们所要做的就是从适配器处继承并重载唯一的需要变更的方法。例如,典型的WindowListener我们会像下面这样的使用。 class MyWindowListener extends WindowAdapter { public void windowClosing(WindowEvent e) { System.exit(0); @@ -1603,7 +1603,7 @@ public class Button2NewB extends Applet { } ///:~ 内部类WL和main()方法是加入程序片的唯一两个元素,程序片剩余的部分则原封未动。事实上,我们通常将WL类和main()方法做一结小的改进复制和粘贴到我们自己的程序片里(请记住创建内部类时通常需要一个外部类来处理它,形成它静态地消除这个需要)。我们可以看到在main()方法里,程序片明确地初始化和开始,因为在这个例子里浏览器不能为我们有效地运行它。当然,这不会提供全部的浏览器调用stop()和destroy()的行为,但对大多数的情况而言它都是可接受的。如果它变成一个麻烦,我们可以: -(1) 使程序片句柄为一个静态类(以代替局部可变的main()),然后: +(1) 使程序片指针为一个静态类(以代替局部可变的main()),然后: (2) 在我们调用System.exit()之前在WindowAdapter.windowClosing()中调用applet.stop()和applet.destroy()。 注意最后一行: aFrame.setVisible(true); @@ -1749,12 +1749,12 @@ T1K有着其它的一些有趣的活动。我们必须测试backspace(因为 } } -t1和t2不属于T1的一部分,并且到目前为止它们都是很容易理解的,没有任何的特殊限制。这是因为一个内部类的对象能自动地捕捉一个句柄到外部的创建它的对象那里,因此我们可以处理封装类对象的方法和内容。正像我们看到的,这十分方便(注释⑥)。 +t1和t2不属于T1的一部分,并且到目前为止它们都是很容易理解的,没有任何的特殊限制。这是因为一个内部类的对象能自动地捕捉一个指针到外部的创建它的对象那里,因此我们可以处理封装类对象的方法和内容。正像我们看到的,这十分方便(注释⑥)。 ⑥:它也解决了“回调”的问题,不必为Java加入任何令人恼火的“方法指针”特性。 2. 文本区域 -Java 1.1版中Text Area最重要的改变就滚动条。对于TextArea的构建器而言,我们可以立即控制TextArea是否会拥有滚动条:水平的,垂直的,两者都有或者都没有。这个例子更正了前面Java 1.0版TextArea1.java程序片,演示了Java 1.1版的滚动条构建器: +Java 1.1版中Text Area最重要的改变就滚动条。对于TextArea的构造器而言,我们可以立即控制TextArea是否会拥有滚动条:水平的,垂直的,两者都有或者都没有。这个例子更正了前面Java 1.0版TextArea1.java程序片,演示了Java 1.1版的滚动条构造器: //: TextAreaNew.java // Controlling scrollbars with the TextArea // component in Java 1.1 @@ -2190,10 +2190,10 @@ public class MenuNew extends Frame { } ///:~ 在我们开始初始化节(由注解“Initialization code:”后的右大括号指明)的前面部分的代码同先前(Java 1.0版)版本相同。这里我们可以注意到项目接收器和动作接收器被附加在不同的菜单组件上。 -Java 1.1支持“菜单快捷键”,因此我们可以选择一个菜单项目利用键盘替代鼠标。这十分的简单;我们只要使用过载菜单项构建器设置第二个自变量为一个MenuShortcut(菜单快捷键事件)对象即可。菜单快捷键构建器设置重要的方法,当它按下时不可思议地显示在菜单项上。上面的例子增加了Control-E到“Exit” +Java 1.1支持“菜单快捷键”,因此我们可以选择一个菜单项目利用键盘替代鼠标。这十分的简单;我们只要使用重载菜单项构造器设置第二个自变量为一个MenuShortcut(菜单快捷键事件)对象即可。菜单快捷键构造器设置重要的方法,当它按下时不可思议地显示在菜单项上。上面的例子增加了Control-E到“Exit” 菜单项中。 我们同样会注意setActionCommand()的使用。这看似一点陌生因为在各种情况下“action command”完全同菜单组件上的标签一样。为什么不正好使用标签代替可选择的字符串呢?这个难题是国际化的。如果我们重新用其它语言写这个程序,我们只需要改变菜单中的标签,并不审查代码中可能包含新错误的所有逻辑。因此使这对检查文字字符串联合菜单组件的代码而言变得简单容易,当菜单标签能改变时“动作指令”可以不作任何的改变。所有这些代码同“动作指令”一同工作,因此它不会受改变菜单标签的影响。注意在这个程序中,不是所有的菜单组件都被它们的动作指令所审查,因此这些组件都没有它们的动作指令集。 -大多数的构建器同前面的一样,将几个调用的异常增加到接收器中。大量的工作发生在接收器里。在前面例子的BL中,菜单交替发生。在ML中,“寻找ring”方法被作为动作事件(ActionEvent)的资源并对它进行造型送入菜单项,然后得到动作指令字符串,再通过它去贯穿串联组,当然条件是对它进行声明。这些大多数同前面的一样,但请注意如果“Exit”被选中,通过进入封装类对象的句柄(MenuNew.this)并创建一个WINDOW_CLOSING事件,一个新的窗口事件就被创建了。新的事件被分配到封装类对象的dispatchEvent()方法,然后结束调用windowsClosing()内部帧的窗口接收器(这个接收器作为一个内部类被创建在main()里),似乎这是“正常”产生消息的方法。通过这种机制,我们可以在任何情况下迅速处理任何的信息,因此,它非常的强大。 +大多数的构造器同前面的一样,将几个调用的异常增加到接收器中。大量的工作发生在接收器里。在前面例子的BL中,菜单交替发生。在ML中,“寻找ring”方法被作为动作事件(ActionEvent)的资源并对它进行造型送入菜单项,然后得到动作指令字符串,再通过它去贯穿串联组,当然条件是对它进行声明。这些大多数同前面的一样,但请注意如果“Exit”被选中,通过进入封装类对象的指针(MenuNew.this)并创建一个WINDOW_CLOSING事件,一个新的窗口事件就被创建了。新的事件被分配到封装类对象的dispatchEvent()方法,然后结束调用windowsClosing()内部帧的窗口接收器(这个接收器作为一个内部类被创建在main()里),似乎这是“正常”产生消息的方法。通过这种机制,我们可以在任何情况下迅速处理任何的信息,因此,它非常的强大。 FL接收器是很简单尽管它能处理特殊菜单的所有不同的特色。如果我们的逻辑十分的简单明了,这种方法对我们就很有用处,但通常,我们使用这种方法时需要与FooL,BarL和BazL一道使用,它们每个都附加到一个单独的菜单组件上,因此必然无需测试逻辑,并且使我们正确地辨识出谁调用了接收器。这种方法产生了大量的类,内部代码趋向于变得小巧和处理起来简单、安全。 7. 对话框 @@ -2945,13 +2945,13 @@ public class GoodTechnique extends Frame { } } ///:~ -这个程序例子同样证明了各种各样的发现和显示关于它们的信息的事件。这种显示是一种集中显示信息的方法。一组字符串去获取关于每种类型的事件的信息,并且show()方法对任何图像对象都设置了一个句柄,我们采用并直接地写在外观代码上。这种设计是有意的被某种事件重复使用。 -激活面板代表了这种新型的组件。它是一个底部有一个按钮的彩色的面板,并且它由利用接收器类为每一个单独的事件来引发捕捉所有发生在它之上的事件,除了那些在激活面板过载的老式的processEvent()方法(注意它应该同样调用super.processEvent())。利用这种方法的唯一理由是它捕捉发生的每一个事件,因此我们可以观察持续发生的每一事件。processEvent()方法没有更多的展示代表每个事件的字符串,否则它会不得不使用一串条件语句去寻找事件。在其它方面,内嵌接收类早已清晰地知道被发现的事件。(假定我们注册它们到组件,我们不需要任何的控件的逻辑,这将成为我们的目的。)因此,它们不会去检查任何事件;这些事件正好做它们的原材料。 +这个程序例子同样证明了各种各样的发现和显示关于它们的信息的事件。这种显示是一种集中显示信息的方法。一组字符串去获取关于每种类型的事件的信息,并且show()方法对任何图像对象都设置了一个指针,我们采用并直接地写在外观代码上。这种设计是有意的被某种事件重复使用。 +激活面板代表了这种新型的组件。它是一个底部有一个按钮的彩色的面板,并且它由利用接收器类为每一个单独的事件来引发捕捉所有发生在它之上的事件,除了那些在激活面板重载的老式的processEvent()方法(注意它应该同样调用super.processEvent())。利用这种方法的唯一理由是它捕捉发生的每一个事件,因此我们可以观察持续发生的每一事件。processEvent()方法没有更多的展示代表每个事件的字符串,否则它会不得不使用一串条件语句去寻找事件。在其它方面,内嵌接收类早已清晰地知道被发现的事件。(假定我们注册它们到组件,我们不需要任何的控件的逻辑,这将成为我们的目的。)因此,它们不会去检查任何事件;这些事件正好做它们的原材料。 每个接收器修改显示字符串和它的指定事件,并且调用重画方法repaint()因此将显示这个字符串。我们同样能注意到一个通常能消除闪烁的秘诀: public void update(Graphics g) { paint(g); } -我们不会始终需要过载update(),但如果我们写下一些闪烁的程序,并运行它。默认的最新版本的清除背景然后调用paint()方法重新画出一些图画。这个清除动作通常会产生闪烁,但是不必要的,因为paint()重画了整个的外观。 +我们不会始终需要重载update(),但如果我们写下一些闪烁的程序,并运行它。默认的最新版本的清除背景然后调用paint()方法重新画出一些图画。这个清除动作通常会产生闪烁,但是不必要的,因为paint()重画了整个的外观。 我们可以看到许多的接收器——但是,对接收器输入检查指令,但我们却不能接收任何组件不支持的事件。(不像BadTechnuque.java那样我们能时时刻刻看到)。 试验这个程序是十分的有教育意义的,因为我们学习了许多的关于在Java中事件发生的方法。一则它展示了大多数开窗口的系统中设计上的瑕疵:它相当的难以去单击和释放鼠标,除非移动它,并且当我们实际上正试图用鼠标单击在某物体上时开窗口的会常常认为我们是在拖动。一个解决这个问题的方案是使用mousePressed()鼠标按下方法和mouseReleased()鼠标释放方法去代替mouseClicked()鼠标单击方法,然后判断是否去调用我们自己的以时间和4个像素的鼠标滞后作用的“mouseReallyClicked()真实的鼠标单击”方法。 @@ -3207,7 +3207,7 @@ public class BadTechnique extends Frame { 13.17 Java 1.1用户接口API Java 1.1版同样增加了一些重要的新功能,包括焦点遍历,桌面色彩访问,打印“沙箱内”及早期的剪贴板支持。 -焦点遍历十分的简单,因为它显然存在于AWT库里的组件并且我们不必为使它工作而去做任何事。如果我们制造我们自己组件并且想使它们去处理焦点遍历,我们过载isFocusTraversable()以使它返回真值。如果我们想在一个鼠标单击上捕捉键盘焦点,我们可以捕捉鼠标按下事件并且调用requestFocus()需求焦点方法。 +焦点遍历十分的简单,因为它显然存在于AWT库里的组件并且我们不必为使它工作而去做任何事。如果我们制造我们自己组件并且想使它们去处理焦点遍历,我们重载isFocusTraversable()以使它返回真值。如果我们想在一个鼠标单击上捕捉键盘焦点,我们可以捕捉鼠标按下事件并且调用requestFocus()需求焦点方法。 13.17.1 桌面颜色 利用桌面颜色,我们可知道当前用户桌面都有哪些颜色选择。这样一来,就可在必要的时候通过自己的程序来运用那些颜色。颜色都会得以自动初始化,并置于SystemColor的static成员中,所以要做的唯一事情就是读取自己感兴趣的成员。各种名字的意义是不言而喻的:desktop,activeCaption, activeCaptionText,activeCaptionBorder, inactiveCaption, inactiveCaptionText,inactiveCaptionBorder, window, windowBorder, windowText, menu,menuText,text, textText, textHighlight, textHighlightText,textInactiveText,control, controlText, controlHighlight,controlLtHighlight,controlShadow,controlDkShadow, scrollbar, info(用于帮助)以及infoText(用于帮助文字)。 @@ -3428,7 +3428,7 @@ class Plot3 extends Plot { 2. 打印图形 按下“Print graphics”按钮时,GBL接收器会被激活。我们需要打印时,创建的PrintData对象初始化,然后我们简单地为这个组件调用print()打印方法。为强制打印,我们必须为图形对象调用dispose()处理方法,并且为PrintData对象调用end()结束方法(或改变为为PrintJob调用end()结束方法。) 这种工作在绘图对象中继续。我们可以看到基础类绘图是很简单的——它扩展画布并且包括一个中断调用ring来指明多少个集中的ring需要画在这个特殊的画布上。这三个衍生类展示了可达到一个目的的不同的方法:画在屏幕上和打印的页面上。 -Plot1采用最简单的编程方法:忽略绘画和打印的不同,并且过载paint()绘画方法。使用这种工作方法的原因是默认的print()打印方法简单地改变工作方法转而调用Paint()。但是,我们会注意到输出的尺寸依赖于屏幕上画布的大小,因为宽度和高度都是在调用Canvas.getSize()方法时决定是,所以这是合理的。如果我们图像的尺寸一值都是固定不变的,其它的情况都可接受。当画出的外观的大小如此的重要时,我们必须深入了解的尺寸大小的重要性。不凑巧的是,就像我们将在Plot2中看到的一样,这种方法变得很棘手。因为一些我们不知道的好的理由,我们不能简单地要求图形对象以它自己的大小画出外观。这将使整个的处理工作变得十分的优良。相反,如果我们打印而不是绘画,我们必须利用RTTI instanceof关键字(在本书11章中有相应描述)来测试PrintGrapics,然后下溯造型并调用这独特的PrintGraphics方法:getPrintJob()方法。现在我们拥有PrintJob的句柄并且我们可以发现纸张的高度和宽度。这是一种hacky的方法,但也许这对它来说是合理的理由。(在其它方面,到如今我们看到一些其它的库设计,因此,我们可能会得到设计者们的想法。) +Plot1采用最简单的编程方法:忽略绘画和打印的不同,并且重载paint()绘画方法。使用这种工作方法的原因是默认的print()打印方法简单地改变工作方法转而调用Paint()。但是,我们会注意到输出的尺寸依赖于屏幕上画布的大小,因为宽度和高度都是在调用Canvas.getSize()方法时决定是,所以这是合理的。如果我们图像的尺寸一值都是固定不变的,其它的情况都可接受。当画出的外观的大小如此的重要时,我们必须深入了解的尺寸大小的重要性。不凑巧的是,就像我们将在Plot2中看到的一样,这种方法变得很棘手。因为一些我们不知道的好的理由,我们不能简单地要求图形对象以它自己的大小画出外观。这将使整个的处理工作变得十分的优良。相反,如果我们打印而不是绘画,我们必须利用RTTI instanceof关键字(在本书11章中有相应描述)来测试PrintGrapics,然后下溯造型并调用这独特的PrintGraphics方法:getPrintJob()方法。现在我们拥有PrintJob的指针并且我们可以发现纸张的高度和宽度。这是一种hacky的方法,但也许这对它来说是合理的理由。(在其它方面,到如今我们看到一些其它的库设计,因此,我们可能会得到设计者们的想法。) 我们可以注意到Plot2中的paint()绘画方法对打印和绘图的可能性进行审查。但是因为当打印时Print()方法将被调用,那么为什么不使用那种方法呢?这种方法同样也在Plot3中也被使用,并且它消除了对instanceof使用的需求,因为在Print()方法中我们可以假设我们能对一个PrintGraphics对象造型。这样也不坏。这种情况被放置公共绘画代码到一个分离的doGraphics()方法的办法所改进。 2. 在程序片内运行帧 @@ -3548,7 +3548,7 @@ public class CutAndPaste extends Frame { 13.18 可视编程和Beans 迄今为止,我们已看到Java对创建可重复使用的代码片工作而言是多么的有价值。“最大限度地可重复使用”的代码单元拥有类,因为它包含一个紧密结合在一起的单元特性(字段)和单元动作(方法),它们可以直接经过混合或通过继承被重复使用。 -继承和多形态性是面向对象编程的精华,但在大多数情况下当我们创建一个应用程序时,我们真正最想要的恰恰是我们最需要的组件。我们希望在我们的设计中设置这些部件就像电子工程师在电路板上创造集成电路块一样(在使用Java的情况下,就是放到WEB页面上)。这似乎会成为加快这种“模块集合”编制程序方法的发展。 +继承和多态态性是面向对象编程的精华,但在大多数情况下当我们创建一个应用程序时,我们真正最想要的恰恰是我们最需要的组件。我们希望在我们的设计中设置这些部件就像电子工程师在电路板上创造集成电路块一样(在使用Java的情况下,就是放到WEB页面上)。这似乎会成为加快这种“模块集合”编制程序方法的发展。 “可视化编程”最早的成功——非常的成功——要归功于微软公司的Visual Basic(VB,可视化Basic语言),接下来的第二代是Borland公司Delphi(一种客户/服务器数据库应用程序开发工具,也是Java Beans设计的主要灵感)。这些编程工具的组件的像征就是可视化,这是不容置疑的,因为它们通常展示一些类型的可视化组件,例如:一个按惯或一个TextField。事实上,可视化通常表现为组件可以非常精确地访问运行中程序。因此可视化编程方法的一部分包含从一个调色盘从拖放一个组件并将它放置到我们的窗体中。应用程序创建工具像我们所做的一样编写程序代码,该代码将导致正在运行的程序中的组件被创建。 简单地拖放组件到一个窗体中通常不足以构成一个完整的程序。一般情况下,我们需要改变组件的特性,例如组件的色彩,组件的文字,组件连结的数据库,等等。特性可以参照属性在编程时进行修改。我们可以在应用程序构建工具中巧妙处置我们组件的属性,并且当我们创建程序时,构建数据被保存下来,所以当该程序被启动时,数据能被重新恢复。 到如今,我们可能习惯于使用对象的多个特性,这也是一个动作集合。在设计时,可视化组件的动作可由事件部分地代表,意味着“任何事件都可以发生在组件上”。通常,由我们决定想发生的事件,当一个事件发生时,对所发生的事件连接代码。 @@ -3611,10 +3611,10 @@ public class Frog { } ///:~ 首先,我们可看到Bean就是一个类。通常,所有我们的字段会被作为专用,并且可以接近的唯一办法是通过方法。紧接着的是命名规则,属性是jump,color,jumper,spots(注意这些修改是在第一个字母在属性名的情况下进行的)。虽然内部确定的名字同最早的三个例子的属性名一样,在jumper中我们可以看到属性名不会强迫我们使用任何特殊的内部可变的名字(或者,真的拥有一些内部的可变的属性名)。 -Bean事件的句柄是ActionEvent和KeyEvent,这是根据有关接收器的“add”和“remove”命名方法得出的。最后我们可以注意到普通的方法croak()一直是Bean的一部分,仅仅是因为它是一个公共的方法,而不是因为它符合一些命名规则。 +Bean事件的指针是ActionEvent和KeyEvent,这是根据有关接收器的“add”和“remove”命名方法得出的。最后我们可以注意到普通的方法croak()一直是Bean的一部分,仅仅是因为它是一个公共的方法,而不是因为它符合一些命名规则。 13.18.2 用Introspector提取BeanInfo -当我们拖放一个Bean的调色板并将它放入到窗体中时,一个Bean的最关键的部分的规则发生了。应用程序构建工具必须可以创建Bean(如果它是默认的构建器的话,它就可以做)然后,在此范围外访问Bean的源代码,提取所有的必要的信息以创立属性表和事件处理器。 +当我们拖放一个Bean的调色板并将它放入到窗体中时,一个Bean的最关键的部分的规则发生了。应用程序构建工具必须可以创建Bean(如果它是默认的构造器的话,它就可以做)然后,在此范围外访问Bean的源代码,提取所有的必要的信息以创立属性表和事件处理器。 解决方案的一部分在11章结尾部分已经显现出来:Java 1.1版的映象允许一个匿名类的所有方法被发现。这完美地解决了Bean的难题而无需我们使用一些特殊的语言关键字像在其它的可视化编程语言中所需要的那样。事实上,一个主要的原因是映象增加到Java 1.1版中以支持Beans(尽管映象同样支持对象串联和远程方法调用)。因为我们可能希望应用程序构建工具的开发者将不得不映象每个Bean并且通过它们的方法搜索以找到Bean的属性和事件。 这当然是可能的,但是Java的研制者们希望为每个使用它的用户提供一个标准的接口,而不仅仅是使Bean更为简单易用,不过他们也同样提供了一个创建更复杂的Bean的标准方法。这个接口就是Introspector类,在这个类中最重要的方法静态的getBeanInfo()。我们通过一个类处理这个方法并且getBeanInfo()方法全面地对类进行查询,返回一个我们可以进行详细研究以发现其属性、方法和事件的BeanInfo对象。 通常我们不会留意这样的一些事物——我们可能会使用我们大多数的现成的Bean,并且我们不需要了解所有的在底层运行的技术细节。我们会简单地拖放我们的Bean到我们窗体中,然后配置它们的属性并且为事件编写处理器。无论如何它都是一个有趣的并且是有教育意义的使用Introspector来显示关于Bean信息的练习,好啦,闲话少说,这里有一个工具请运行它(我们可以在forgbean子目录中找到它): @@ -4207,7 +4207,7 @@ public class ButtonGroups extends JPanel { } } ///:~ -边框标题由类名剔除了所有的路径信息而来。AbstractButton初始化为一个JButton,JButtonr的标签发生“失效”,因此如果我们忽略这个异常信息,我们会在屏幕上一直看到这个问题。getConstructor()方法产生了一个通过getConstructor()方法安放自变量数组类型到类数组的构建器对象,然后所有我们要做的就是调用newInstance(),通过它一个数组对象包含我们当前的自变量——在这种例子中,就是ids数组中的字符串。 +边框标题由类名剔除了所有的路径信息而来。AbstractButton初始化为一个JButton,JButtonr的标签发生“失效”,因此如果我们忽略这个异常信息,我们会在屏幕上一直看到这个问题。getConstructor()方法产生了一个通过getConstructor()方法安放自变量数组类型到类数组的构造器对象,然后所有我们要做的就是调用newInstance(),通过它一个数组对象包含我们当前的自变量——在这种例子中,就是ids数组中的字符串。 这样增加了一些更复杂的内容到这个简单的程序中。为了使“异或”行为拥有按钮,我们创建一个按钮组并增加每个按钮到我们所需的组中。当我们运行这个程序时,我们会注意到所有的按钮除了JButton都会向我们展示“异或”行为。 13.19.8 图标 @@ -4270,7 +4270,7 @@ public class Faces extends JPanel { } } ///:~ -一个图标可以在许多的构建器中使用,但我们可以使用setIcon()方法增加或更换图标。这个例子同样展示了当事件发生在JButton(或者一些AbstractButton)上时,为什么它可以设置各种各样的显示图标:当JButton被按下时,当它被失效时,或者“滚过”时(鼠标从它上面移动过但并不击它)。我们会注意到那给了按钮一种动画的感觉。 +一个图标可以在许多的构造器中使用,但我们可以使用setIcon()方法增加或更换图标。这个例子同样展示了当事件发生在JButton(或者一些AbstractButton)上时,为什么它可以设置各种各样的显示图标:当JButton被按下时,当它被失效时,或者“滚过”时(鼠标从它上面移动过但并不击它)。我们会注意到那给了按钮一种动画的感觉。 注意工具提示条也同样增加到按钮中。 13.19.9 菜单 @@ -4431,9 +4431,9 @@ public class Menus extends JPanel { } ///:~ 这个程序的目的是允许程序设计者简单地创建表格来描述每个菜单,而不是输入代码行来建立菜单。每个菜单都产生一个菜单,表格中的第一列包含菜单名和键盘快捷键。其余的列包含每个菜单项的数据:字符串存在在菜单项中的位置,菜单的类型,它的快捷键,当菜单项被选中时被激活的动作接收器及菜单是否被激活等信息。如果列开始处是空的,它将被作为一个分隔符来处理。 -为了预防浪费和冗长的多个Boolean创建的对象和类型标志,以下的这些在类开始时就作为static final被创建:bT和bF描述Booleans和哑类MType的不同对象描述标准的菜单项(mi),复选框菜单项(cb),和单选钮菜单项(rb)。请记住一组Object可以拥有单一的Object句柄,并且不再是原来的值。 -这个程序例子同样展示了JLables和JMenuItems(和它们的衍生事物)如何处理图标的。一个图标经由它的构建器置放进JLable中并当对应的菜单项被选中时被改变。 -菜单条数组控制处理所有在文件菜单清单中列出的,我们想显示在菜单条上的文件菜单。我们通过这个数组去使用createMenuBar(),将数组分类成单独的菜单数据数组,再通过每个单独的数组去创建菜单。这种方法依次使用菜单数据的每一行并以该数据创建JMenu,然后为菜单数据中剩下的每一行调用createMenuItem()方法。最后,createMenuItem()方法分析菜单数据的每一行并且判断菜单类型和它的属性,再适当地创建菜单项。终于,像我们在菜单构建器中看到的一样,从表示createMenuBar(menuBar)的表格中创建菜单,而所有的事物都是采用递归方法处理的。 +为了预防浪费和冗长的多个Boolean创建的对象和类型标志,以下的这些在类开始时就作为static final被创建:bT和bF描述Booleans和哑类MType的不同对象描述标准的菜单项(mi),复选框菜单项(cb),和单选钮菜单项(rb)。请记住一组Object可以拥有单一的Object指针,并且不再是原来的值。 +这个程序例子同样展示了JLables和JMenuItems(和它们的衍生事物)如何处理图标的。一个图标经由它的构造器置放进JLable中并当对应的菜单项被选中时被改变。 +菜单条数组控制处理所有在文件菜单清单中列出的,我们想显示在菜单条上的文件菜单。我们通过这个数组去使用createMenuBar(),将数组分类成单独的菜单数据数组,再通过每个单独的数组去创建菜单。这种方法依次使用菜单数据的每一行并以该数据创建JMenu,然后为菜单数据中剩下的每一行调用createMenuItem()方法。最后,createMenuItem()方法分析菜单数据的每一行并且判断菜单类型和它的属性,再适当地创建菜单项。终于,像我们在菜单构造器中看到的一样,从表示createMenuBar(menuBar)的表格中创建菜单,而所有的事物都是采用递归方法处理的。 这个程序不能建立串联的菜单,但我们拥有足够的知识,如果我们需要的话,随时都能增加多级菜单进去。 13.19.10 弹出式菜单 @@ -4495,7 +4495,7 @@ public class Popup extends JPanel { 相同的ActionListener被加入每个JMenuItem中,使其能从菜单标签中取出文字,并将文字插入JTextField。 13.19.11 列表框和组合框 -列表框和组合框在Swing中工作就像它们在老的AWT中工作一样,但如果我们需要它,它们同样被增加功能。另外,它也更加的方便易用。例如,JList中有一个显示String数组的构建器(奇怪的是同样的功能在JComboBox中无效!)。下面的例子显示了它们基本的用法。 +列表框和组合框在Swing中工作就像它们在老的AWT中工作一样,但如果我们需要它,它们同样被增加功能。另外,它也更加的方便易用。例如,JList中有一个显示String数组的构造器(奇怪的是同样的功能在JComboBox中无效!)。下面的例子显示了它们基本的用法。 //: ListCombo.java // List boxes & Combo boxes package c13.swing; @@ -4704,8 +4704,8 @@ public class Table extends JPanel { } } ///:~ -DateModel包括一组数据,但我们同样能从其它的地方得到数据,例如从数据库中。构建器增加了一个TableModelListener用来在每次表格被改变后打印数组。剩下的方法都遵循Bean的命名规则,并且当JTable需要在DateModel中显示信息时调用。AbstractTableModel提供了默认的setValueAt()和isCellEditable()方法以防止修改这些数据,因此如果我们想修改这些数据,就必须过载这些方法。 -一旦我们拥有一个TableModel,我们只需要将它分配给JTable构建器即可。所有有关显示,编辑和更新的详细资料将为我们处理。注意这个程序例子同样将JTable放置在JScrollPane中,这是因为JScrollPane需要一个特殊的JTable方法。 +DateModel包括一组数据,但我们同样能从其它的地方得到数据,例如从数据库中。构造器增加了一个TableModelListener用来在每次表格被改变后打印数组。剩下的方法都遵循Bean的命名规则,并且当JTable需要在DateModel中显示信息时调用。AbstractTableModel提供了默认的setValueAt()和isCellEditable()方法以防止修改这些数据,因此如果我们想修改这些数据,就必须重载这些方法。 +一旦我们拥有一个TableModel,我们只需要将它分配给JTable构造器即可。所有有关显示,编辑和更新的详细资料将为我们处理。注意这个程序例子同样将JTable放置在JScrollPane中,这是因为JScrollPane需要一个特殊的JTable方法。 13.19.15 卡片式对话框 在本章的前部,向我们介绍了老式的CardLayout,并且注意到我们怎样去管理我们所有的卡片开关。有趣的是,有人现在认为这是一种不错的设计。幸运的是,Swing用JTabbedPane对它进行了修补,由JTabbedPane来处理这些卡片,开关和其它的任何事物。对比CardLayout和JTabbedPane,我们会发现惊人的差异。 @@ -4757,7 +4757,7 @@ public class Tabbed extends JPanel { } } ///:~ -再者,我们可以注意到使用的数组构造式样:第一个元素是被置放在卡片上的String,第二个元素是将被显示在对应窗格上JPanel类。在Tabbed()构建器里,我们可以看到两个重要的JTabbedPane方法被使用:addTab()插入一个新的窗格,setSelectedIndex()选择一个窗格并从它开始。(一个在中间被选中的窗格证明我们不必从第一个窗格开始)。 +再者,我们可以注意到使用的数组构造式样:第一个元素是被置放在卡片上的String,第二个元素是将被显示在对应窗格上JPanel类。在Tabbed()构造器里,我们可以看到两个重要的JTabbedPane方法被使用:addTab()插入一个新的窗格,setSelectedIndex()选择一个窗格并从它开始。(一个在中间被选中的窗格证明我们不必从第一个窗格开始)。 当我们调用addTab()方法时,我们为它提供卡片的String和一些组件(也就是说,一个AWT组件,而不是一个来自AWT的JComponent)。这个组件会被显示在窗格中。一旦我们这样做了,自然而然的就不需要更多管理了——JTabbedPane会为我们处理其它的任何事。 makePanel()方法获取我们想创建的类Class对象和用newInstance()去创建并造型为JPanel(当然,假定那些类是必须从JPanel继承才能增加的类,除非在这一节中为程序例子的结构所使用)。它增加了一个包括类名并返回结果的TitledBorder,以作为一个JPanel在addTab()被使用。 当我们运行程序时,我们会发现如果卡片太多,填满了一行,JTabbedPane自动地将它们堆积起来。 @@ -4769,7 +4769,7 @@ makePanel()方法获取我们想创建的类Class对象和用newInstance()去创 这一节意味着唯一向我们介绍的是Swing的强大力量和我们的着手处,因此我们能注意到通过库,我们会感觉到我们的方法何等的简单。到目前为止,我们已看到的可能足够满足我们UI设计需要的一部分。不过,这里有许多有关Swing额外的情况——它有意成为一全功能的UI设计工具箱。如果我们没有发现我们所需要的,请到SUN公司的在线文件中去查找,并搜索WEB。这个方法几乎可以完成我们能想到的任何事。 本节中没有涉及的一些要点: ■更多特殊的组件,例如JColorChooser,JFileChooser,JPasswordField,JHTMLPane(完成简单的HTML格式化和显示)以及JTextPane(一个支持格式化,字处理和图像的文字编辑器)。它们都非常易用。 -■Swing的新的事件类型。在一些方法中,它们看起来像违例:类型非常的重要,名字可以被用来表示除了它们自己之外的任何事物。 +■Swing的新的事件类型。在一些方法中,它们看起来像异常:类型非常的重要,名字可以被用来表示除了它们自己之外的任何事物。 ■新的布局管理:Springs & Struts以及BoxLayout ■分裂控制:一个间隔物式的分裂条,允许我们动态地处理其它组件的位置。 ■JLayeredPane和JInternalFrame被一起用来在当前帧中创建子帧,以产生多文件接口(MDI)应用程序。 @@ -4782,13 +4782,13 @@ makePanel()方法获取我们想创建的类Class对象和用newInstance()去创 13.20 总结 对于AWT而言,Java 1.1到Java 1.2最大的改变就是Java中所有的库。Java 1.0版的AWT曾作为目前见过的最糟糕的一个设计被彻底地批评,并且当它允许我们在创建小巧精致的程序时,产生的GUI“在所有的平台上都同样的平庸”。它与在特殊平台上本地应用程序开发工具相比也是受到限制的,笨拙的并且也是不友好的。当Java 1.1版纳入新的事件模型和Java Beans时,平台被设置——现在它可以被拖放到可视化的应用程序构建工具中,创建GUI组件。另外,事件模型的设计和Bean无疑对轻松的编程和可维护的代码都非常的在意(这些在Java 1.0 AWT中不那么的明显)。但直至GUI组件-JFC/Swing类-显示工作结束它才这样。对于Swing组件而言,交叉平台GUI编程可以变成一种有教育意义的经验。 -现在,唯一的情况是缺乏应用程序构建工具,并且这就是真正的变革的存在之处。微软的Visual Basic和Visual C++需要它们的应用程序构建工具,同样的是Borland的Delphi和C++构建器。如果我们需要应用程序构建工具变得更好,我们不得不交叉我们的指针并且希望自动授权机会给我们所需要的。Java是一个开放的环境,因此不但考虑到同其它的应用程序构建环境竞争,而且Java还促进它们的发展。这些工具被认真地使用,它们必须支持Java Beans。这意味着一个平等的应用领域:如果一个更好的应用程序构建工具出现,我们不需要去约束它就可以使用——我们可以采用并移动到新的工具上工作即可,这会提高我们的工作效率。这种竞争的环境对应用程序构建工具来说从未出现过,这种竞争能真正提高程序设计者的工作效率。 +现在,唯一的情况是缺乏应用程序构建工具,并且这就是真正的变革的存在之处。微软的Visual Basic和Visual C++需要它们的应用程序构建工具,同样的是Borland的Delphi和C++构造器。如果我们需要应用程序构建工具变得更好,我们不得不交叉我们的指针并且希望自动授权机会给我们所需要的。Java是一个开放的环境,因此不但考虑到同其它的应用程序构建环境竞争,而且Java还促进它们的发展。这些工具被认真地使用,它们必须支持Java Beans。这意味着一个平等的应用领域:如果一个更好的应用程序构建工具出现,我们不需要去约束它就可以使用——我们可以采用并移动到新的工具上工作即可,这会提高我们的工作效率。这种竞争的环境对应用程序构建工具来说从未出现过,这种竞争能真正提高程序设计者的工作效率。 13.21 练习 (1)创建一个有文字字段和三个按钮的程序片。当我们按下每个按钮时,使不同的文字显示在文字段中。 (2)增加一个复选框到练习1创建的程序中,捕捉事件,并插入不同的文字到文字字段中。 (3)创建一个程序片并增加所有导致action()被调用的组件,然后捕捉他们的事件并在文字字段中为每个组件显示一个特定的消息。 -(4)增加可以被handleEvent()方法测试事件的组件到练习3中。过载handleEvent()并在文字字段中为每个组件显示特定的消息。 +(4)增加可以被handleEvent()方法测试事件的组件到练习3中。重载handleEvent()并在文字字段中为每个组件显示特定的消息。 (5)创建一个有一个按钮和一个TextField的程序片。编写一个handleEvent(),以便如果按钮有焦点,输入字符到将显示的TextField中。 (6)创建一个应用程序并将本章所有的组件增加主要的帧,包括菜单和对话框。 (7)修改TextNew.java,以便字母在t2中保持输入时的样子,取代自动变成大写。 diff --git "a/\347\254\2544\347\253\240 \345\210\235\345\247\213\345\214\226\345\222\214\346\270\205\351\231\244.md" "b/\347\254\2544\347\253\240 \345\210\235\345\247\213\345\214\226\345\222\214\346\270\205\351\231\244.md" index 8b65d50..1440e62 100644 --- "a/\347\254\2544\347\253\240 \345\210\235\345\247\213\345\214\226\345\222\214\346\270\205\351\231\244.md" +++ "b/\347\254\2544\347\253\240 \345\210\235\345\247\213\345\214\226\345\222\214\346\270\205\351\231\244.md" @@ -5,4 +5,4 @@ “初始化”和“清除”是这些安全问题的其中两个。许多C程序的错误都是由于程序员忘记初始化一个变量造成的。对于现成的库,若用户不知道如何初始化库的一个组件,就往往会出现这一类的错误。清除是另一个特殊的问题,因为用完一个元素后,由于不再关心,所以很容易把它忘记。这样一来,那个元素占用的资源会一直保留下去,极易产生资源(主要是内存)用尽的后果。 -C++为我们引入了“构建器”的概念。这是一种特殊的方法,在一个对象创建之后自动调用。Java也沿用了这个概念,但新增了自己的“垃圾收集器”,能在资源不再需要的时候自动释放它们。本章将讨论初始化和清除的问题,以及Java如何提供它们的支持。 +C++为我们引入了“构造器”的概念。这是一种特殊的方法,在一个对象创建之后自动调用。Java也沿用了这个概念,但新增了自己的“垃圾收集器”,能在资源不再需要的时候自动释放它们。本章将讨论初始化和清除的问题,以及Java如何提供它们的支持。 diff --git "a/\347\254\2547\347\253\240 \345\244\232\345\275\242\346\200\247.md" "b/\347\254\2547\347\253\240 \345\244\232\346\200\201\346\200\247.md" similarity index 73% rename from "\347\254\2547\347\253\240 \345\244\232\345\275\242\346\200\247.md" rename to "\347\254\2547\347\253\240 \345\244\232\346\200\201\346\200\247.md" index dfea50b..3c98254 100644 --- "a/\347\254\2547\347\253\240 \345\244\232\345\275\242\346\200\247.md" +++ "b/\347\254\2547\347\253\240 \345\244\232\346\200\201\346\200\247.md" @@ -1,10 +1,10 @@ -# 第7章 多形性 +# 第7章 多态性 -“对于面向对象的程序设计语言,多型性是第三种最基本的特征(前两种是数据抽象和继承。” +“对于面向对象的程序设计语言,多态性是第三种最基本的特征(前两种是数据抽象和继承。” -“多形性”(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来,亦即实现了“是什么”与“怎样做”两个模块的分离。利用多形性的概念,代码的组织以及可读性均能获得改善。此外,还能创建“易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地“成长”。 +“多态性”(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来,亦即实现了“是什么”与“怎样做”两个模块的分离。利用多态性的概念,代码的组织以及可读性均能获得改善。此外,还能创建“易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地“成长”。 -通过合并各种特征与行为,封装技术可创建出新的数据类型。通过对具体实施细节的隐藏,可将接口与实施细节分离,使所有细节成为“private”(私有)。这种组织方式使那些有程序化编程背景人感觉颇为舒适。但多形性却涉及对“类型”的分解。通过上一章的学习,大家已知道通过继承可将一个对象当作它自己的类型或者它自己的基础类型对待。这种能力是十分重要的,因为多个类型(从相同的基础类型中衍生出来)可被当作同一种类型对待。而且只需一段代码,即可对所有不同的类型进行同样的处理。利用具有多形性的方法调用,一种类型可将自己与另一种相似的类型区分开,只要它们都是从相同的基础类型中衍生出来的。这种区分是通过各种方法在行为上的差异实现的,可通过基础类实现对那些方法的调用。 +通过合并各种特征与行为,封装技术可创建出新的数据类型。通过对具体实施细节的隐藏,可将接口与实施细节分离,使所有细节成为“private”(私有)。这种组织方式使那些有程序化编程背景人感觉颇为舒适。但多态性却涉及对“类型”的分解。通过上一章的学习,大家已知道通过继承可将一个对象当作它自己的类型或者它自己的基础类型对待。这种能力是十分重要的,因为多个类型(从相同的基础类型中衍生出来)可被当作同一种类型对待。而且只需一段代码,即可对所有不同的类型进行同样的处理。利用具有多态性的方法调用,一种类型可将自己与另一种相似的类型区分开,只要它们都是从相同的基础类型中衍生出来的。这种区分是通过各种方法在行为上的差异实现的,可通过基础类实现对那些方法的调用。 -在这一章中,大家要由浅入深地学习有关多形性的问题(也叫作动态绑定、推迟绑定或者运行期绑定)。同时举一些简单的例子,其中所有无关的部分都已剥除,只保留与多形性有关的代码。 +在这一章中,大家要由浅入深地学习有关多态性的问题(也叫作动态绑定、推迟绑定或者运行期绑定)。同时举一些简单的例子,其中所有无关的部分都已剥除,只保留与多态性有关的代码。 diff --git "a/\347\254\2548\347\253\240 \345\257\271\350\261\241\347\232\204\345\256\271\347\272\263.md" "b/\347\254\2548\347\253\240 \345\257\271\350\261\241\347\232\204\345\256\271\347\272\263.md" index 3827fa5..3cf8cc2 100644 --- "a/\347\254\2548\347\253\240 \345\257\271\350\261\241\347\232\204\345\256\271\347\272\263.md" +++ "b/\347\254\2548\347\253\240 \345\257\271\350\261\241\347\232\204\345\256\271\347\272\263.md" @@ -3,12 +3,12 @@ “如果一个程序只含有数量固定的对象,而且已知它们的存在时间,那么这个程序可以说是相当简单的。” -通常,我们的程序需要根据程序运行时才知道的一些标准创建新对象。若非程序正式运行,否则我们根本不知道自己到底需要多少数量的对象,甚至不知道它们的准确类型。为了满足常规编程的需要,我们要求能在任何时候、任何地点创建任意数量的对象。所以不可依赖一个已命名的句柄来容纳自己的每一个对象,就象下面这样: +通常,我们的程序需要根据程序运行时才知道的一些标准创建新对象。若非程序正式运行,否则我们根本不知道自己到底需要多少数量的对象,甚至不知道它们的准确类型。为了满足常规编程的需要,我们要求能在任何时候、任何地点创建任意数量的对象。所以不可依赖一个已命名的指针来容纳自己的每一个对象,就象下面这样: -``` +``` java MyObject myHandle; ``` 因为根本不知道自己实际需要多少这样的东西。 -为解决这个非常关键的问题,Java提供了容纳对象(或者对象的句柄)的多种方式。其中内建的类型是数组,我们之前已讨论过它,本章准备加深大家对它的认识。此外,Java的工具(实用程序)库提供了一些“集合类”(亦称作“容器类”,但该术语已由AWT使用,所以这里仍采用“集合”这一称呼)。利用这些集合类,我们可以容纳乃至操纵自己的对象。本章的剩余部分会就此进行详细讨论。 +为解决这个非常关键的问题,Java提供了容纳对象(或者对象的指针)的多种方式。其中内建的类型是数组,我们之前已讨论过它,本章准备加深大家对它的认识。此外,Java的工具(实用程序)库提供了一些“集合类”(亦称作“容器类”,但该术语已由AWT使用,所以这里仍采用“集合”这一称呼)。利用这些集合类,我们可以容纳乃至操纵自己的对象。本章的剩余部分会就此进行详细讨论。 diff --git "a/\347\254\2549\347\253\240 \350\277\235\344\276\213\345\267\256\351\224\231\346\216\247\345\210\266.md" "b/\347\254\2549\347\253\240 \345\274\202\345\270\270\345\267\256\351\224\231\346\216\247\345\210\266.md" similarity index 83% rename from "\347\254\2549\347\253\240 \350\277\235\344\276\213\345\267\256\351\224\231\346\216\247\345\210\266.md" rename to "\347\254\2549\347\253\240 \345\274\202\345\270\270\345\267\256\351\224\231\346\216\247\345\210\266.md" index 490b856..9b3f405 100644 --- "a/\347\254\2549\347\253\240 \350\277\235\344\276\213\345\267\256\351\224\231\346\216\247\345\210\266.md" +++ "b/\347\254\2549\347\253\240 \345\274\202\345\270\270\345\267\256\351\224\231\346\216\247\345\210\266.md" @@ -1,4 +1,4 @@ -# 第9章 违例差错控制 +# 第9章 异常差错控制 Java的基本原理就是“形式错误的代码不会运行”。 @@ -8,10 +8,10 @@ Java的基本原理就是“形式错误的代码不会运行”。 ①:C程序员研究一下printf()的返回值便知端详。 -解决的方法是在错误控制中排除所有偶然性,强制格式的正确。这种方法实际已有很长的历史,因为早在60年代便在操作系统里采用了“违例控制”手段;甚至可以追溯到BASIC语言的on error goto语句。但C++的违例控制建立在Ada的基础上,而Java又主要建立在C++的基础上(尽管它看起来更象Object Pascal)。 +解决的方法是在错误控制中排除所有偶然性,强制格式的正确。这种方法实际已有很长的历史,因为早在60年代便在操作系统里采用了“异常控制”手段;甚至可以追溯到BASIC语言的on error goto语句。但C++的异常控制建立在Ada的基础上,而Java又主要建立在C++的基础上(尽管它看起来更象Object Pascal)。 -“违例”(Exception)这个词表达的是一种“例外”情况,亦即正常情况之外的一种“异常”。在问题发生的时候,我们可能不知具体该如何解决,但肯定知道已不能不顾一切地继续下去。此时,必须坚决地停下来,并由某人、某地指出发生了什么事情,以及该采取何种对策。但为了真正解决问题,当地可能并没有足够多的信息。因此,我们需要将其移交给更级的负责人,令其作出正确的决定(类似一个命令链)。 +“异常”(Exception)这个词表达的是一种“例外”情况,亦即正常情况之外的一种“异常”。在问题发生的时候,我们可能不知具体该如何解决,但肯定知道已不能不顾一切地继续下去。此时,必须坚决地停下来,并由某人、某地指出发生了什么事情,以及该采取何种对策。但为了真正解决问题,当地可能并没有足够多的信息。因此,我们需要将其移交给更级的负责人,令其作出正确的决定(类似一个命令链)。 -违例机制的另一项好处就是能够简化错误控制代码。我们再也不用检查一个特定的错误,然后在程序的多处地方对其进行控制。此外,也不需要在方法调用的时候检查错误(因为保证有人能捕获这里的错误)。我们只需要在一个地方处理问题:“违例控制模块”或者“违例控制器”。这样可有效减少代码量,并将那些用于描述具体操作的代码与专门纠正错误的代码分隔开。一般情况下,用于读取、写入以及调试的代码会变得更富有条理。 +异常机制的另一项好处就是能够简化错误控制代码。我们再也不用检查一个特定的错误,然后在程序的多处地方对其进行控制。此外,也不需要在方法调用的时候检查错误(因为保证有人能捕获这里的错误)。我们只需要在一个地方处理问题:“异常控制模块”或者“异常控制器”。这样可有效减少代码量,并将那些用于描述具体操作的代码与专门纠正错误的代码分隔开。一般情况下,用于读取、写入以及调试的代码会变得更富有条理。 -由于违例控制是由Java编译器强行实施的,所以毋需深入学习违例控制,便可正确使用本书编写的大量例子。本章向大家介绍了用于正确控制违例所需的代码,以及在某个方法遇到麻烦的时候,该如何生成自己的违例。 +由于异常控制是由Java编译器强行实施的,所以毋需深入学习异常控制,便可正确使用本书编写的大量例子。本章向大家介绍了用于正确控制异常所需的代码,以及在某个方法遇到麻烦的时候,该如何生成自己的异常。 diff --git "a/\351\231\204\345\275\225A \344\275\277\347\224\250\351\235\236JAVA\344\273\243\347\240\201.md" "b/\351\231\204\345\275\225A \344\275\277\347\224\250\351\235\236JAVA\344\273\243\347\240\201.md" index 0cca437..7b59d47 100644 --- "a/\351\231\204\345\275\225A \344\275\277\347\224\250\351\235\236JAVA\344\273\243\347\240\201.md" +++ "b/\351\231\204\345\275\225A \344\275\277\347\224\250\351\235\236JAVA\344\273\243\347\240\201.md" @@ -31,13 +31,13 @@ A.1.1 调用固有方法 我们先从一个简单的例子开始:一个Java程序调用固有方法,后者再调用Win32的API函数MessageBox(),显示出一个图形化的文 本框。这个例子稍后也会与J/Direct一志使用。若您的平台不是Win32,只需将包含了下述内容的C头: -``` +``` java #include ``` 替换成: -``` +``` java #include ``` @@ -45,7 +45,7 @@ A.1.1 调用固有方法 第一步是写出对固有方法及它的自变量进行声明的Java代码: -``` +``` java class ShowMsgBox { public static void main(String [] args) { ShowMsgBox app = new ShowMsgBox(); @@ -63,13 +63,13 @@ class ShowMsgBox { ##### 1. C头文件生成器:javah 现在编译您的Java源文件,并对编译出来的.class文件运行javah。javah是在1.0版里提供的,但由于我们要使用Java 1.1 JNI,所以必须指定-jni参数: -``` +``` java javah -jni ShowMsgBox ``` javah会读入类文件,并为每个固有方法声明在C或C++头文件里生成一个函数原型。下面是输出结果——ShowMsgBox.h源文件(为符合本书的要求,稍微进行了一下修改): -``` +``` java /* DO NOT EDIT THIS FILE - it is machine generated */ #include @@ -98,12 +98,12 @@ Java_ShowMsgBox_ShowMessage 从“#ifdef_cplusplus”这个预处理引导命令可以看出,该文件既可由C编译器编译,亦可由C++编译器编译。第一个#include命令包括jni.h——一个头文件,作用之一是定义在文件其余部分用到的类型;JNIEXPORT和JNICALL是一些宏,它们进行了适当的扩充,以便与那些不同平台专用的引导命令配合;JNIEnv,jobject以及jstring则是JNI数据类型定义。 ##### 2. 名称管理和函数签名 -JNI统一了固有方法的命名规则;这一点是非常重要的,因为它属于虚拟机将Java调用与固有方法链接起来的机制的一部分。从根本上说,所有固有方法都要以一个“Java”起头,后面跟随Java方法的名字;下划线字符则作为分隔符使用。若Java固有方法“过载”(即命名重复),那么也把函数签名追加到名字后面。在原型前面的注释里,大家可看到固有的签名。欲了解命名规则和固有方法签名更详细的情况,请参考相应的JNI文档。 +JNI统一了固有方法的命名规则;这一点是非常重要的,因为它属于虚拟机将Java调用与固有方法链接起来的机制的一部分。从根本上说,所有固有方法都要以一个“Java”起头,后面跟随Java方法的名字;下划线字符则作为分隔符使用。若Java固有方法“重载”(即命名重复),那么也把函数签名追加到名字后面。在原型前面的注释里,大家可看到固有的签名。欲了解命名规则和固有方法签名更详细的情况,请参考相应的JNI文档。 ##### 3. 实现自己的DLL 此时,我们要做的全部事情就是写一个C或C++源文件,在其中包含由javah生成的头文件;并实现固有方法;然后编译它,生成一个动态链接库。这一部分的工作是与平台有关的,所以我假定读者已经知道如何创建一个DLL。通过调用一个Win32 API,下面的代码实现了固有方法。随后,它会编译和链接到一个名为MsgImpl.dll的文件里: -``` +``` java #include #include "ShowMsgBox.h" @@ -135,7 +135,7 @@ A.1.2 访问JNI函数:JNIEnv自变量 利用JNIEnv自变量,程序员可访问一系列函数。这些函数可划分为下述类别: -``` +``` java ■获取版本信息 ■进行类和对象操作 ■控制对Java对象的全局和局部引用 @@ -149,13 +149,13 @@ JNI函数的数量相当多,这里不再详述。相反,我会向大家揭 若观察一下jni.h头文件,就会发现在#ifdef _cplusplus预处理器条件的内部,当由C++编译器编译时,JNIEnv_结构被定义成一个类。这个类包含了大量内嵌函数。通过一种简单而且熟悉的语法,这些函数让我们可以从容访问JNI函数。例如,前例包含了下面这行代码: -``` +``` java (*jEnv)->ReleaseStringUTFChars(jEnv, jMsg,msg); ``` 它在C++里可改写成下面这个样子: -``` +``` java jEnv->ReleaseStringUTFChars(jMsg,msg); ``` @@ -172,7 +172,7 @@ A.1.3 传递和使用Java对象 为传递对象,声明固有方法时要采用原始的Java语法。如下例所示,MyJavaClass有一个public(公共)字段,以及一个public方法。UseObjects类声明了一个固有方法,用于接收MyJavaClass类的一个对象。为调查固有方法是否能控制自己的自变量,我们设置了自变量的public字段,调用固有方法,然后打印出public字段的值。 -``` +``` java class MyJavaClass { public void divByTwo() { aValue /= 2; } public int aValue; @@ -196,7 +196,7 @@ public class UseObjects { 编译好代码,并将.class文件传递给javah后,就可以实现固有方法。在下面这个例子中,一旦取得字段和方法ID,就会通过JNI函数访问它们。 -``` +``` java JNIEXPORT void JNICALL Java_UseObjects_changeObject( JNIEnv * env, jobject jThis, jobject obj) { @@ -232,11 +232,11 @@ A.1.4 JNI和Java异常 利用JNI,可丢弃、捕捉、打印以及重新丢弃Java异常,就象在一个Java程序里那样。但对程序员来说,需自行调用专用的JNI函数,以便对异常进行处理。下面列出用于异常处理的一些JNI函数: -``` +``` java ■Throw():丢弃一个现有的异常对象;在固有方法中用于重新丢弃一个异常。 ■ThrowNew():生成一个新的异常对象,并将其丢弃。 ■ExceptionOccurred():判断一个异常是否已被丢弃,但尚未清除。 -■ExceptionDescribe():打印一个异常和堆栈跟踪信息。 +■ExceptionDescribe():打印一个异常和栈跟踪信息。 ■ExceptionClear():清除一个待决的异常。 ■FatalError():造成一个严重错误,不返回。 ``` @@ -273,7 +273,7 @@ A.3 J/Direct J/Direct是调用Win32 DLL函数最简单的方式。它的主要设计目标是与Win32API打交道,但完全可用它调用其他任何API。但是,尽管这一特性非常方便,但它同时也造成了某些限制,且降低了性能(与RNI相比)。但J/Direct也有一些明显的优点。首先,除希望调用的那个DLL里的代码之外,没有必要再编写额外的非Java代码,换言之,我们不需要一个封装器或者代理/存根DLL。其次,函数自变量与标准数据类型之间实现了自动转换。若必须传递用户自定义的数据类型,那么J/Direct可能不按我们的希望工作。第三,就象下例展示的那样,它非常简单和直接。只需少数几行,这个例子便能调用Win32 API函数MessageBox(),它能弹出一个小的模态窗口,并带有一个标题、一条消息、一个可选的图标以及几个按钮。 -``` +``` java public class ShowMsgBox { public static void main(String args[]) throws UnsatisfiedLinkError { @@ -300,7 +300,7 @@ A.3.1 @dll.import引导命令 ##### 1. 别名处理和按顺序链接 为了使@dll.import引导命令能象上面显示的那样工作,DLL内的函数必须按名字导出。然而,我们有时想使用与DLL里原始名字不同的一个名字(别名处理),否则函数就可能按编号(比如按顺序)导出,而不是按名字导出。下面这个例子声明了FinestraDiMessaggio()(用意大利语说的“MessageBox”)。正如大家看到的那样,使用的语法是非常简单的。 -``` +``` java public class Aliasing { public static void main(String args[]) throws UnsatisfiedLinkError { @@ -318,7 +318,7 @@ public class Aliasing { 下面这个例子展示了如何同DLL里并非按名字导出的一个函数建立链接,那个函数事实是按它们在DLL里的位置导出的。这个例子假设有一个名为MYMATH的DLL,这个DLL在位置编号3处包含了一个函数。那个函数获取两个整数作为自变量,并返回两个整数的和。 -``` +``` java public class ByOrdinal { public static void main(String args[]) throws UnsatisfiedLinkError { @@ -337,7 +337,7 @@ public class ByOrdinal { @dll.import引导命令可应用于整个类。也就是说,那个类的所有方法都是在相同的DLL里实现的,并具有相同的链接属性。引导命令不会由子类继承;考虑到这个原因,而且由于DLL里的函数是自然的static函数,所以更佳的设计方案是将API函数封装到一个独立的类里,如下所示: -``` +``` java /** @dll.import("USER32") */ class MyUser32Access { public static native int @@ -364,7 +364,7 @@ A.3.2 com.ms.win32包 Win32 API的体积相当庞大——包含了数以千计的函数、常数以及数据类型。当然,我们并不想将每个Win32 API函数都写成对应Java形式。微软考虑到了这个问题,发行了一个Java包,可通过J/Direct将Win32 API映射成Java类。这个包的名字叫作com.ms.win32。安装Java SDK 2.0时,若在安装选项中进行了相应的设置,这个包就会安装到我们的类路径中。这个包由大量Java类构成,它们完整再现了Win32 API的常数、数据类型以及函数。包容能力最大的三个类是User32.class,Kernel.class以及Gdi32.class。它们包含的是Win32 API的核心内容。为使用它们,只需在自己的Java代码里导入即可。前面的ShowMsgBox示例可用com.ms.win32改写成下面这个样子(这里也考虑到了用更恰当的方式使用UnsatisfiedLinkError): -``` +``` java import com.ms.win32.*; public class UseWin32Package { @@ -393,7 +393,7 @@ A.3.3 汇集 “汇集”(Marshaling)是指将一个函数自变量从它原始的二进制形式转换成与语言无关的某种形式,再将这种通用形式转换成适合调用函数采用的二进制格式。在前面的例子中,我们调用了MessageBox()函数,并向它传递了两个字串。MessageBox()是个C函数,而且Java字串的二进制布局与C字串并不相同。但尽管如此,自变量仍获得了正确的传递。这是由于在调用C代码前,J/Direct已帮我们考虑到了将Java字串转换成C字串的问题。这种情况适合所有标准的Java类型。下面这张表格总结了简单数据类型的默认对应关系: -``` +``` java Java C byte BYTE或CHAR @@ -417,14 +417,14 @@ A.3.4 编写回调函数 有些Win32 API函数要求将一个函数指针作为自己的参数使用。Windows API函数随后就可以调用自变量函数(通常是在以后发生特定的事件时)。这一技术就叫作“回调函数”。回调函数的例子包括窗口进程以及我们在打印过程中设置的回调(为后台打印程序提供回调函数的地址,使其能更新状态,并在必要的时候中止打印)。 -另一个例子是API函数EnumWindows(),它能枚举目前系统内所有顶级窗口。EnumWindows()要求获取一个函数指针作为自己的参数,然后搜索由Windows内部维护的一个列表。对于列表内的每个窗口,它都会调用回调函数,将窗口句柄作为一个自变量传给回调。 +另一个例子是API函数EnumWindows(),它能枚举目前系统内所有顶级窗口。EnumWindows()要求获取一个函数指针作为自己的参数,然后搜索由Windows内部维护的一个列表。对于列表内的每个窗口,它都会调用回调函数,将窗口指针作为一个自变量传给回调。 为了在Java里达到同样的目的,必须使用com.ms.dll包里的Callback类。我们从Callback里继承,并取消callback()。这个方法只能接近int参数,并会返回int或void。方法签名和具体的实施取决于使用这个回调的Windows API函数。 现在,我们要进行的全部工作就是创建这个Callback衍生类的一个实例,并将其作为函数指针传递给API函数。随后,J/Direct会帮助我们自动完成剩余的工作。 -下面这个例子调用了Win32 API函数EnumWindows();EnumWindowsProc类里的callback()方法会获取每个顶级窗口的句柄,获取标题文字,并将其打印到控制台窗口。 +下面这个例子调用了Win32 API函数EnumWindows();EnumWindowsProc类里的callback()方法会获取每个顶级窗口的指针,获取标题文字,并将其打印到控制台窗口。 -``` +``` java import com.ms.dll.*; import com.ms.win32.*; @@ -471,7 +471,7 @@ RNI在概念上类似Sun公司的JNI。考虑到这个原因,而且由于该 JNI和RNI之间存在几方面引人注目的差异。下面列出的是由msjavah生成的C头文件(微软提供的msjavah在功能上相当于Sun的javah),应用于前面在JNI例子里使用的Java类文件ShowMsgBox。 -``` +``` java /* DO NOT EDIT - automatically generated by msjavah */ #include @@ -518,7 +518,7 @@ ShowMsgBox_ShowMessage (struct HShowMsgBox *, 在JNI中,通过JNIEnv自变量可访问大量函数,以便同JVM打交道。在RNI中,用于控制JVM运作的函数变成了可直接调用。它们中的某一些(如控制异常的那一个)类似于它们的JNI“兄弟”。但大多数RNI函数都有与JNI中不同的名字和用途。 JNI和RNI最重大的一个区别是“垃圾收集”的模型。在JNI中,垃圾收集在固有方法执行期间遵守与Java代码执行时相同的规则。而在RNI中,要由程序员在固有方法活动期间自行负责“垃圾收集器”器的启动与中止。默认情况下,垃圾收集器在进入固有方法前处于不活动状态;这样一来,程序员就可假定准备使用的对象用不着在那个时间段内进行垃圾收集。然而一旦固有方法准备长时间执行,程序员就应考虑激活垃圾收集器——通过调用GCEnable()这个RNI函数(GC是“Garbage Collector”的缩写,即“垃圾收集”)。 -也存在与全局句柄特性类似的机制——程序员可利用可保证特定的对象在GC活动期间不至于被当作“垃圾”收掉。概念是类似的,但名称有所差异——在RNI中,人们把它叫作GCFrames。 +也存在与全局指针特性类似的机制——程序员可利用可保证特定的对象在GC活动期间不至于被当作“垃圾”收掉。概念是类似的,但名称有所差异——在RNI中,人们把它叫作GCFrames。 A.4.1 RNI总结 @@ -599,7 +599,7 @@ A.5.3 用Java设计COM服务器 本节将介绍ActiveX控件、Automation服务器或者其他任何符合COM规范的服务器的开发过程。下面这个例子实现了一个简单的Automation服务器,它能执行整数加法。我们用setAddend()方法设置addend的值。每次调用sum()方法的时候,addend就会添加到当前result里。我们用getResult()获得result值,并用clear()重新设置值。用于实现这一行为的Java类是非常简单的: -``` +``` java public class Adder { private int addend; private int result; @@ -616,7 +616,7 @@ public class Adder { 为了将这个Java类作为一个COM对象使用,我们将Javareg工具应用于编译好的Adder.class文件。这个工具提供了一系列选项;在这种情况下,我们指定Java类文件名("Adder"),想为这个服务器在注册表里置入的ProgID("JavaAdder.Adder.1"),以及想为即将生成的类型库指定的名字("JavaAdder.tlb")。由于尚未给出CLSID,所以Javareg会自动生成一个。若我们再次对同样的服务器调用Javareg,就会直接使用现成的CLSID。 -``` +``` java javareg /register /class:Adder /progid:JavaAdder.Adder.1 /typelib:JavaAdder.tlb @@ -626,7 +626,7 @@ Javareg也会将新服务器注册到Windows注册表。此时,我们必须记 现在,我们已在自己的系统中安装了一个新的Automation服务器。为进行测试,我们需要一个Automation控制器,而Automation控制器就是Visual Basic(VB)。在下面,大家会看到几行VB代码。按照VB的格式,我设置了一个文本框,用它从用户那里接收要相加的值。并用一个标签显示结果,用两个下推按钮分别调用sum()和clear()方法。最开始,我们声明了一个名为Adder的对象变量。在Form_Load子例程中(在窗体首次显示时载入),会调用Adder自动服务器的一个新实例,并对窗体的文本字段进行初始化。一旦用户按下“Sum”或者“Clear”按钮,就会调用服务器中对应的方法。 -``` +``` java Dim Adder As Object Private Sub Form_Load() @@ -658,7 +658,7 @@ A.5.4 用Java设计COM客户 首先,我们将Jactivex工具应用于服务器的类型库。Jactivex有一系列选项和开关可供选择。但它最基本的形式是读取一个类型库,并生成Java源文件。这个源文件保存于我们的windows/java/trustlib目录中。通过下面这行代码,它应用于为外部COM Automation服务器生成的类型库: -``` +``` java jactivex /javatlb JavaAdder.tlb ``` @@ -670,7 +670,7 @@ Jactivex完成以后,我们再来看看自己的windows/java/trustlib目录。 “例示”Adder类的一个Java对象以后,就相当于指示COM激活服务器,并创建这个COM对象的一个实例。但我们随后必须指定自己想使用哪个接口,在由服务器实现的接口中挑选一个。这正是类型模型完成的工作。这儿使用的是“默认遣送”接口,它是Automation控制器用于同一个Automation服务器通信的标准接口。欲了解这方面的细节,请参考由Ibid编著的《Inside COM》。请注意激活服务器并选择一个COM接口是多么容易! -``` +``` java import javaadder.*; public class JavaClient { @@ -722,11 +722,11 @@ ORB是对象间相互请求的一条通信总线。进行请求时,毋需关 CORBA是面向语言的透明而设计的:一个客户对象可调用属于不同类的服务器对象方法,无论对方是用何种语言实现的。当然,客户对象事先必须知道由服务器对象揭示的方法名称及签名。这时便要用到IDL。CORBA IDL是一种与语言无关的设计方法,可用它指定数据类型、属性、操作、接口以及更多的东西。IDL的语法类似于C++或Java语法。下面这张表格为大家总结了三种语言一些通用概念,并展示了它们的对应关系。 -``` +``` java CORBA IDL Java C++ ``` -``` +``` java 模块(Module) 包(Package) 命名空间(Namespace) 接口(Interface) 接口(Interface) 纯抽象类(Pure abstract class) 方法(Method) 方法(Method) 成员函数(Member function) @@ -754,7 +754,7 @@ A.6.2 一个例子 IDL文件已分发给客户端的程序员,并成为两种语言间的桥梁。 下面这个例子展示了时间服务器的IDL描述情况: -``` +``` java module RemoteTime { interface ExactTime { string getTime(); @@ -768,7 +768,7 @@ module RemoteTime { 第二步是编译IDL,创建Java根干代码。我们将利用这些代码实现客户和服务器。与JavaIDL产品配套提供的工具是idltojava: -``` +``` java idltojava -fserver -fclient RemoteTime.idl ``` @@ -780,7 +780,7 @@ idltojava -fserver -fclient RemoteTime.idl 大家在下面看到的是服务器端使用的代码。服务器对象是在ExactTimeServer类里实现的。RemoteTimeServer这个应用的作用是:创建一个服务器对象,通过ORB为其注册,指定对象引用时采用的名称,然后“安静”地等候客户发出请求。 -``` +``` java import RemoteTime.*; import org.omg.CosNaming.*; @@ -851,7 +851,7 @@ public class RemoteTimeServer { 服务对象最好准备好由客户使用。此时,服务器进程会进入一种等候状态。同样地,由于它是一种“短期服务”,所以生存时间要受服务器进程的限制。JavaIDL目前尚未提供对“持久对象”(只要创建它们的进程保持运行状态,对象就会一直存在下去)的支持。 现在,我们已对服务器代码的工作有了一定的认识。接下来看看客户代码: -``` +``` java import RemoteTime.*; import org.omg.CosNaming.*; import org.omg.CORBA.*; diff --git "a/\351\231\204\345\275\225B \345\257\271\346\257\224C++\345\222\214Java.md" "b/\351\231\204\345\275\225B \345\257\271\346\257\224C++\345\222\214Java.md" index 82316e0..e8dbc9c 100644 --- "a/\351\231\204\345\275\225B \345\257\271\346\257\224C++\345\222\214Java.md" +++ "b/\351\231\204\345\275\225B \345\257\271\346\257\224C++\345\222\214Java.md" @@ -15,7 +15,7 @@ (5) 在Java中,类定义采取几乎和C++一样的形式。但没有标志结束的分号。没有class foo这种形式的类声明,只有类定义。 -``` +``` java class aType() void aMethod() {/* 方法主体 */} } @@ -23,7 +23,7 @@ void aMethod() {/* 方法主体 */} (6) Java中没有作用域范围运算符“::”。Java利用点号做所有的事情,但可以不用考虑它,因为只能在一个类里定义元素。即使那些方法定义,也必须在一个类的内部,所以根本没有必要指定作用域的范围。我们注意到的一项差异是对static方法的调用:使用ClassName.methodName()。除此以外,package(包)的名字是用点号建立的,并能用import关键字实现C++的“#include”的一部分功能。例如下面这个语句: -``` +``` java import java.awt.*; ``` @@ -41,9 +41,9 @@ import java.awt.*; (10) Java增添了三个右移位运算符“>>>”,具有与“逻辑”右移位运算符类似的功用,可在最末尾插入零值。“>>”则会在移位的同时插入符号位(即“算术”移位)。 -(11) 尽管表面上类似,但与C++相比,Java数组采用的是一个颇为不同的结构,并具有独特的行为。有一个只读的length成员,通过它可知道数组有多大。而且一旦超过数组边界,运行期检查会自动丢弃一个异常。所有数组都是在内存“堆”里创建的,我们可将一个数组分配给另一个(只是简单地复制数组句柄)。数组标识符属于第一级对象,它的所有方法通常都适用于其他所有对象。 +(11) 尽管表面上类似,但与C++相比,Java数组采用的是一个颇为不同的结构,并具有独特的行为。有一个只读的length成员,通过它可知道数组有多大。而且一旦超过数组边界,运行期检查会自动丢弃一个异常。所有数组都是在内存“堆”里创建的,我们可将一个数组分配给另一个(只是简单地复制数组指针)。数组标识符属于第一级对象,它的所有方法通常都适用于其他所有对象。 -(12) 对于所有不属于主类型的对象,都只能通过new命令创建。和C++不同,Java没有相应的命令可以“在堆栈上”创建不属于主类型的对象。所有主类型都只能在堆栈上创建,同时不使用new命令。所有主要的类都有自己的“封装(器)”类,所以能够通过new创建等价的、以内存“堆”为基础的对象(主类型数组是一个例外:它们可象C++那样通过集合初始化进行分配,或者使用new)。 +(12) 对于所有不属于主类型的对象,都只能通过new命令创建。和C++不同,Java没有相应的命令可以“在栈上”创建不属于主类型的对象。所有主类型都只能在栈上创建,同时不使用new命令。所有主要的类都有自己的“封装(器)”类,所以能够通过new创建等价的、以内存“堆”为基础的对象(主类型数组是一个例外:它们可象C++那样通过集合初始化进行分配,或者使用new)。 (13) Java中不必进行提前声明。若想在定义前使用一个类或方法,只需直接使用它即可——编译器会保证使用恰当的定义。所以和在C++中不同,我们不会碰到任何涉及提前引用的问题。 @@ -51,18 +51,18 @@ import java.awt.*; (15) Java用包代替了命名空间。由于将所有东西都置入一个类,而且由于采用了一种名为“封装”的机制,它能针对类名进行类似于命名空间分解的操作,所以命名的问题不再进入我们的考虑之列。数据包也会在单独一个库名下收集库的组件。我们只需简单地“import”(导入)一个包,剩下的工作会由编译器自动完成。 -(16) 被定义成类成员的对象句柄会自动初始化成null。对基本类数据成员的初始化在Java里得到了可靠的保障。若不明确地进行初始化,它们就会得到一个默认值(零或等价的值)。可对它们进行明确的初始化(显式初始化):要么在类内定义它们,要么在构建器中定义。采用的语法比C++的语法更容易理解,而且对于static和非static成员来说都是固定不变的。我们不必从外部定义static成员的存储方式,这和C++是不同的。 +(16) 被定义成类成员的对象指针会自动初始化成null。对基本类数据成员的初始化在Java里得到了可靠的保障。若不明确地进行初始化,它们就会得到一个默认值(零或等价的值)。可对它们进行明确的初始化(显式初始化):要么在类内定义它们,要么在构造器中定义。采用的语法比C++的语法更容易理解,而且对于static和非static成员来说都是固定不变的。我们不必从外部定义static成员的存储方式,这和C++是不同的。 -(17) 在Java里,没有象C和C++那样的指针。用new创建一个对象的时候,会获得一个引用(本书一直将其称作“句柄”)。例如: +(17) 在Java里,没有象C和C++那样的指针。用new创建一个对象的时候,会获得一个引用(本书一直将其称作“指针”)。例如: String s = new String("howdy"); 然而,C++引用在创建时必须进行初始化,而且不可重定义到一个不同的位置。但Java引用并不一定局限于创建时的位置。它们可根据情况任意定义,这便消除了对指针的部分需求。在C和C++里大量采用指针的另一个原因是为了能指向任意一个内存位置(这同时会使它们变得不安全,也是Java不提供这一支持的原因)。指针通常被看作在基本变量数组中四处移动的一种有效手段。Java允许我们以更安全的形式达到相同的目标。解决指针问题的终极方法是“固有方法”(已在附录A讨论)。将指针传递给方法时,通常不会带来太大的问题,因为此时没有全局函数,只有类。而且我们可传递对对象的引用。Java语言最开始声称自己“完全不采用指针!”但随着许多程序员都质问没有指针如何工作?于是后来又声明“采用受到限制的指针”。大家可自行判断它是否“真”的是一个指针。但不管在何种情况下,都不存在指针“算术”。 -(18) Java提供了与C++类似的“构建器”(Constructor)。如果不自己定义一个,就会获得一个默认构建器。而如果定义了一个非默认的构建器,就不会为我们自动定义默认构建器。这和C++是一样的。注意没有复制构建器,因为所有自变量都是按引用传递的。 +(18) Java提供了与C++类似的“构造器”(Constructor)。如果不自己定义一个,就会获得一个默认构造器。而如果定义了一个非默认的构造器,就不会为我们自动定义默认构造器。这和C++是一样的。注意没有复制构造器,因为所有自变量都是按引用传递的。 (19) Java中没有“破坏器”(Destructor)。变量不存在“作用域”的问题。一个对象的“存在时间”是由对象的存在时间决定的,并非由垃圾收集器决定。有个finalize()方法是每一个类的成员,它在某种程度上类似于C++的“破坏器”。但finalize()是由垃圾收集器调用的,而且只负责释放“资源”(如打开的文件、套接字、端口、URL等等)。如需在一个特定的地点做某样事情,必须创建一个特殊的方法,并调用它,不能依赖finalize()。而在另一方面,C++中的所有对象都会(或者说“应该”)破坏,但并非Java中的所有对象都会被当作“垃圾”收集掉。由于Java不支持破坏器的概念,所以在必要的时候,必须谨慎地创建一个清除方法。而且针对类内的基础类以及成员对象,需要明确调用所有清除方法。 -(20) Java具有方法“过载”机制,它的工作原理与C++函数的过载几乎是完全相同的。 +(20) Java具有方法“重载”机制,它的工作原理与C++函数的重载几乎是完全相同的。 (21) Java不支持默认自变量。 @@ -70,7 +70,7 @@ String s = new String("howdy"); (23) Java采用了一种单根式的分级结构,因此所有对象都是从根类Object统一继承的。而在C++中,我们可在任何地方启动一个新的继承树,所以最后往往看到包含了大量树的“一片森林”。在Java中,我们无论如何都只有一个分级结构。尽管这表面上看似乎造成了限制,但由于我们知道每个对象肯定至少有一个Object接口,所以往往能获得更强大的能力。C++目前似乎是唯一没有强制单根结构的唯一一种OO语言。 -(24) Java没有模板或者参数化类型的其他形式。它提供了一系列集合:Vector(向量),Stack(堆栈)以及Hashtable(散列表),用于容纳Object引用。利用这些集合,我们的一系列要求可得到满足。但这些集合并非是为实现象C++“标准模板库”(STL)那样的快速调用而设计的。Java 1.2中的新集合显得更加完整,但仍不具备正宗模板那样的高效率使用手段。 +(24) Java没有模板或者参数化类型的其他形式。它提供了一系列集合:Vector(向量),Stack(栈)以及Hashtable(散列表),用于容纳Object引用。利用这些集合,我们的一系列要求可得到满足。但这些集合并非是为实现象C++“标准模板库”(STL)那样的快速调用而设计的。Java 1.2中的新集合显得更加完整,但仍不具备正宗模板那样的高效率使用手段。 (25) “垃圾收集”意味着在Java中出现内存漏洞的情况会少得多,但也并非完全不可能(若调用一个用于分配存储空间的固有方法,垃圾收集器就不能对其进行跟踪监视)。然而,内存漏洞和资源漏洞多是由于编写不当的finalize()造成的,或是由于在已分配的一个块尾释放一种资源造成的(“破坏器”在此时显得特别方便)。垃圾收集器是在C++基础上的一种极大进步,使许多编程问题消弥于无形之中。但对少数几个垃圾收集器力有不逮的问题,它却是不大适合的。但垃圾收集器的大量优点也使这一处缺点显得微不足道。 @@ -78,15 +78,15 @@ String s = new String("howdy"); (27) 我们不是象C++那样控制声明代码块,而是将访问限定符(public,private和protected)置入每个类成员的定义里。若未规定一个“显式”(明确的)限定符,就会默认为“友好的”(friendly)。这意味着同一个包里的其他元素也可以访问它(相当于它们都成为C++的“friends”——朋友),但不可由包外的任何元素访问。类——以及类内的每个方法——都有一个访问限定符,决定它是否能在文件的外部“可见”。private关键字通常很少在Java中使用,因为与排斥同一个包内其他类的访问相比,“友好的”访问通常更加有用。然而,在多线程的环境中,对private的恰当运用是非常重要的。Java的protected关键字意味着“可由继承者访问,亦可由包内其他元素访问”。注意Java没有与C++的protected关键字等价的元素,后者意味着“只能由继承者访问”(以前可用“private protected”实现这个目的,但这一对关键字的组合已被取消了)。 -(28) 嵌套的类。在C++中,对类进行嵌套有助于隐藏名称,并便于代码的组织(但C++的“命名空间”已使名称的隐藏显得多余)。Java的“封装”或“打包”概念等价于C++的命名空间,所以不再是一个问题。Java 1.1引入了“内部类”的概念,它秘密保持指向外部类的一个句柄——创建内部类对象的时候需要用到。这意味着内部类对象也许能访问外部类对象的成员,毋需任何条件——就好象那些成员直接隶属于内部类对象一样。这样便为回调问题提供了一个更优秀的方案——C++是用指向成员的指针解决的。 +(28) 嵌套的类。在C++中,对类进行嵌套有助于隐藏名称,并便于代码的组织(但C++的“命名空间”已使名称的隐藏显得多余)。Java的“封装”或“打包”概念等价于C++的命名空间,所以不再是一个问题。Java 1.1引入了“内部类”的概念,它秘密保持指向外部类的一个指针——创建内部类对象的时候需要用到。这意味着内部类对象也许能访问外部类对象的成员,毋需任何条件——就好象那些成员直接隶属于内部类对象一样。这样便为回调问题提供了一个更优秀的方案——C++是用指向成员的指针解决的。 (29) 由于存在前面介绍的那种内部类,所以Java里没有指向成员的指针。 (30) Java不存在“嵌入”(inline)方法。Java编译器也许会自行决定嵌入一个方法,但我们对此没有更多的控制权力。在Java中,可为一个方法使用final关键字,从而“建议”进行嵌入操作。然而,嵌入函数对于C++的编译器来说也只是一种建议。 -(31) Java中的继承具有与C++相同的效果,但采用的语法不同。Java用extends关键字标志从一个基础类的继承,并用super关键字指出准备在基础类中调用的方法,它与我们当前所在的方法具有相同的名字(然而,Java中的super关键字只允许我们访问父类的方法——亦即分级结构的上一级)。通过在C++中设定基础类的作用域,我们可访问位于分级结构较深处的方法。亦可用super关键字调用基础类构建器。正如早先指出的那样,所有类最终都会从Object里自动继承。和C++不同,不存在明确的构建器初始化列表。但编译器会强迫我们在构建器主体的开头进行全部的基础类初始化,而且不允许我们在主体的后面部分进行这一工作。通过组合运用自动初始化以及来自未初始化对象句柄的异常,成员的初始化可得到有效的保证。 +(31) Java中的继承具有与C++相同的效果,但采用的语法不同。Java用extends关键字标志从一个基础类的继承,并用super关键字指出准备在基础类中调用的方法,它与我们当前所在的方法具有相同的名字(然而,Java中的super关键字只允许我们访问父类的方法——亦即分级结构的上一级)。通过在C++中设定基础类的作用域,我们可访问位于分级结构较深处的方法。亦可用super关键字调用基础类构造器。正如早先指出的那样,所有类最终都会从Object里自动继承。和C++不同,不存在明确的构造器初始化列表。但编译器会强迫我们在构造器主体的开头进行全部的基础类初始化,而且不允许我们在主体的后面部分进行这一工作。通过组合运用自动初始化以及来自未初始化对象指针的异常,成员的初始化可得到有效的保证。 -``` +``` java public class Foo extends Bar { public Foo(String msg) { super(msg); // Calls base constructor @@ -104,7 +104,7 @@ public class Foo extends Bar { 为创建可进行“例示”(即创建一个实例)的一个interface(接口)的版本,需使用implements关键字。它的语法类似于继承的语法,如下所示: -``` +``` java public interface Face { public void smile(); } @@ -119,23 +119,23 @@ public class Baz extends Bar implements Face { (35) Java不提供多重继承机制(MI),至少不象C++那样做。与protected类似,MI表面上是一个很不错的主意,但只有真正面对一个特定的设计问题时,才知道自己需要它。由于Java使用的是“单根”分级结构,所以只有在极少的场合才需要用到MI。interface关键字会帮助我们自动完成多个接口的合并工作。 -(36) 运行期的类型标识功能与C++极为相似。例如,为获得与句柄X有关的信息,可使用下述代码: +(36) 运行期的类型标识功能与C++极为相似。例如,为获得与指针X有关的信息,可使用下述代码: -``` +``` java X.getClass().getName(); ``` 为进行一个“类型安全”的紧缩造型,可使用: -``` +``` java derived d = (derived)base; ``` 这与旧式风格的C造型是一样的。编译器会自动调用动态造型机制,不要求使用额外的语法。尽管它并不象C++的“new casts”那样具有易于定位造型的优点,但Java会检查使用情况,并丢弃那些“异常”,所以它不会象C++那样允许坏造型的存在。 -(37) Java采取了不同的异常控制机制,因为此时已经不存在构建器。可添加一个finally从句,强制执行特定的语句,以便进行必要的清除工作。Java中的所有异常都是从基础类Throwable里继承而来的,所以可确保我们得到的是一个通用接口。 +(37) Java采取了不同的异常控制机制,因为此时已经不存在构造器。可添加一个finally从句,强制执行特定的语句,以便进行必要的清除工作。Java中的所有异常都是从基础类Throwable里继承而来的,所以可确保我们得到的是一个通用接口。 -``` +``` java public void f(Obj b) throws IOException { myresource mr = b.createResource(); try { @@ -152,11 +152,11 @@ public void f(Obj b) throws IOException { (38) Java的异常规范比C++的出色得多。丢弃一个错误的异常后,不是象C++那样在运行期间调用一个函数,Java异常规范是在编译期间检查并执行的。除此以外,被取代的方法必须遵守那一方法的基础类版本的异常规范:它们可丢弃指定的异常或者从那些异常衍生出来的其他异常。这样一来,我们最终得到的是更为“健壮”的异常控制代码。 -(39) Java具有方法过载的能力,但不允许运算符过载。String类不能用+和+=运算符连接不同的字串,而且String表达式使用自动的类型转换,但那是一种特殊的内建情况。 +(39) Java具有方法重载的能力,但不允许运算符重载。String类不能用+和+=运算符连接不同的字串,而且String表达式使用自动的类型转换,但那是一种特殊的内建情况。 -(40) 通过事先的约定,C++中经常出现的const问题在Java里已得到了控制。我们只能传递指向对象的句柄,本地副本永远不会为我们自动生成。若希望使用类似C++按值传递那样的技术,可调用clone(),生成自变量的一个本地副本(尽管clone()的设计依然尚显粗糙——参见第12章)。根本不存在被自动调用的副本构建器。为创建一个编译期的常数值,可象下面这样编码: +(40) 通过事先的约定,C++中经常出现的const问题在Java里已得到了控制。我们只能传递指向对象的指针,本地副本永远不会为我们自动生成。若希望使用类似C++按值传递那样的技术,可调用clone(),生成自变量的一个本地副本(尽管clone()的设计依然尚显粗糙——参见第12章)。根本不存在被自动调用的副本构造器。为创建一个编译期的常数值,可象下面这样编码: -``` +``` java static final int SIZE = 255 static final int BSIZE = 8 * SIZE ``` @@ -185,15 +185,15 @@ static final int BSIZE = 8 * SIZE (45) Java 1.1包含了Java Beans标准,后者可创建在可视编程环境中使用的组件。由于遵守同样的标准,所以可视组件能够在所有厂商的开发环境中使用。由于我们并不依赖一家厂商的方案进行可视组件的设计,所以组件的选择余地会加大,并可提高组件的效能。除此之外,Java Beans的设计非常简单,便于程序员理解;而那些由不同的厂商开发的专用组件框架则要求进行更深入的学习。 -(46) 若访问Java句柄失败,就会丢弃一次异常。这种丢弃测试并不一定要正好在使用一个句柄之前进行。根据Java的设计规范,只是说异常必须以某种形式丢弃。许多C++运行期系统也能丢弃那些由于指针错误造成的异常。 +(46) 若访问Java指针失败,就会丢弃一次异常。这种丢弃测试并不一定要正好在使用一个指针之前进行。根据Java的设计规范,只是说异常必须以某种形式丢弃。许多C++运行期系统也能丢弃那些由于指针错误造成的异常。 (47) Java通常显得更为健壮,为此采取的手段如下: -■对象句柄初始化成null(一个关键字) +■对象指针初始化成null(一个关键字) -■句柄肯定会得到检查,并在出错时丢弃异常 +■指针肯定会得到检查,并在出错时丢弃异常 -■所有数组访问都会得到检查,及时发现边界违例情况 +■所有数组访问都会得到检查,及时发现边界异常情况 ■自动垃圾收集,防止出现内存漏洞 diff --git "a/\351\231\204\345\275\225C Java\347\274\226\347\250\213\350\247\204\345\210\231.md" "b/\351\231\204\345\275\225C Java\347\274\226\347\250\213\350\247\204\345\210\231.md" index 852d560..b9657ea 100644 --- "a/\351\231\204\345\275\225C Java\347\274\226\347\250\213\350\247\204\345\210\231.md" +++ "b/\351\231\204\345\275\225C Java\347\274\226\347\250\213\350\247\204\345\210\231.md" @@ -3,9 +3,9 @@ 本附录包含了大量有用的建议,帮助大家进行低级程序设计,并提供了代码编写的一般性指导: -(1) 类名首字母应该大写。字段、方法以及对象(句柄)的首字母应小写。对于所有标识符,其中包含的所有单词都应紧靠在一起,而且大写中间单词的首字母。例如: +(1) 类名首字母应该大写。字段、方法以及对象(指针)的首字母应小写。对于所有标识符,其中包含的所有单词都应紧靠在一起,而且大写中间单词的首字母。例如: -``` +``` java ThisIsAClassName thisIsMethodOrFieldName ``` @@ -16,7 +16,7 @@ Java包(Package)属于一种特殊情况:它们全都是小写字母,即 (2) 为了常规用途而创建一个类时,请采取“经典形式”,并包含对下述元素的定义: -``` +``` java equals() hashCode() toString() @@ -32,7 +32,7 @@ implement Serializable (6) 使类尽可能短小精悍,而且只解决一个特定的问题。下面是对类设计的一些建议: -■一个复杂的开关语句:考虑采用“多形”机制 +■一个复杂的开关语句:考虑采用“多态”机制 ■数量众多的方法涉及到类型差别极大的操作:考虑用几个类来分别实现 @@ -50,7 +50,7 @@ implement Serializable (12) 避免使用“魔术数字”,这些数字很难与代码很好地配合。如以后需要修改它,无疑会成为一场噩梦,因为根本不知道“100”到底是指“数组大小”还是“其他全然不同的东西”。所以,我们应创建一个常数,并为其使用具有说服力的描述性名称,并在整个程序中都采用常数标识符。这样可使程序更易理解以及更易维护。 -(13) 涉及构建器和异常的时候,通常希望重新丢弃在构建器中捕获的任何异常——如果它造成了那个对象的创建失败。这样一来,调用者就不会以为那个对象已正确地创建,从而盲目地继续。 +(13) 涉及构造器和异常的时候,通常希望重新丢弃在构造器中捕获的任何异常——如果它造成了那个对象的创建失败。这样一来,调用者就不会以为那个对象已正确地创建,从而盲目地继续。 (14) 当客户程序员用完对象以后,若你的类要求进行任何清除工作,可考虑将清除代码置于一个良好定义的方法里,采用类似于cleanup()这样的名字,明确表明自己的用途。除此以外,可在类内放置一个boolean(布尔)标记,指出对象是否已被清除。在类的finalize()方法里,请确定对象已被清除,并已丢弃了从RuntimeException继承的一个类(如果还没有的话),从而指出一个编程错误。在采取象这样的方案之前,请确定finalize()能够在自己的系统中工作(可能需要调用System.runFinalizersOnExit(true),从而确保这一行为)。 @@ -62,7 +62,7 @@ implement Serializable (18) 尽量使用interfaces,不要使用abstract类。若已知某样东西准备成为一个基础类,那么第一个选择应是将其变成一个interface(接口)。只有在不得不使用方法定义或者成员变量的时候,才需要将其变成一个abstract(抽象)类。接口主要描述了客户希望做什么事情,而一个类则致力于(或允许)具体的实施细节。 -(19) 在构建器内部,只进行那些将对象设为正确状态所需的工作。尽可能地避免调用其他方法,因为那些方法可能被其他人覆盖或取消,从而在构建过程中产生不可预知的结果(参见第7章的详细说明)。 +(19) 在构造器内部,只进行那些将对象设为正确状态所需的工作。尽可能地避免调用其他方法,因为那些方法可能被其他人覆盖或取消,从而在构建过程中产生不可预知的结果(参见第7章的详细说明)。 (20) 对象不应只是简单地容纳一些数据;它们的行为也应得到良好的定义。 diff --git "a/\351\231\204\345\275\225D \346\200\247\350\203\275.md" "b/\351\231\204\345\275\225D \346\200\247\350\203\275.md" index a327daa..0b0f833 100644 --- "a/\351\231\204\345\275\225D \346\200\247\350\203\275.md" +++ "b/\351\231\204\345\275\225D \346\200\247\350\203\275.md" @@ -27,7 +27,7 @@ D.2.1 安插自己的测试代码 插入下述“显式”计时代码,对程序进行评测: -``` +``` java long start = System.currentTimeMillis(); // 要计时的运算代码放在这儿 long time = System.currentTimeMillis() - start; @@ -42,13 +42,13 @@ JDK配套提供了一个内建的评测程序,能跟踪花在每个例程上 为运行评测程序,请在调用Java解释器的未优化版本时加上-prof选项。例如: -``` +``` java java_g -prof myClass ``` 或加上一个程序片(Applet): -``` +``` java java_g -prof sun.applet.AppletViewer applet.html ``` @@ -84,7 +84,7 @@ D.3.2 依赖语言的方法 运算 示例 标准时间 -``` +``` java 本地赋值 i=n; 1.0 实例赋值 this.i=n; 1.2 int增值 i++; 1.5 @@ -124,7 +124,7 @@ D.3.3 特殊情况 ■字串的开销:字串连接运算符+看似简单,但实际需要消耗大量系统资源。编译器可高效地连接字串,但变量字串却要求可观的处理器时间。例如,假设s和t是字串变量: -``` +``` java System.out.println("heading" + s + "trailer" + t); ``` diff --git "a/\351\231\204\345\275\225E \345\205\263\344\272\216\345\236\203\345\234\276\346\224\266\351\233\206\347\232\204\344\270\200\344\272\233\350\257\235.md" "b/\351\231\204\345\275\225E \345\205\263\344\272\216\345\236\203\345\234\276\346\224\266\351\233\206\347\232\204\344\270\200\344\272\233\350\257\235.md" index eb0c00d..58aa4ee 100644 --- "a/\351\231\204\345\275\225E \345\205\263\344\272\216\345\236\203\345\234\276\346\224\266\351\233\206\347\232\204\344\270\200\344\272\233\350\257\235.md" +++ "b/\351\231\204\345\275\225E \345\205\263\344\272\216\345\236\203\345\234\276\346\224\266\351\233\206\347\232\204\344\270\200\344\272\233\350\257\235.md" @@ -7,27 +7,27 @@ 我之所以想到速度,部分原因是由于C++模型。C++将自己的主要精力放在编译期间“静态”发生的所有事情上,所以程序的运行期版本非常短小和快速。C++也直接建立在C模型的基础上(主要为了向后兼容),但有时仅仅由于它在C中能按特定的方式工作,所以也是C++中最方便的一种方法。最重要的一种情况是C和C++对内存的管理方式,它是某些人觉得Java速度肯定慢的重要依据:在Java中,所有对象都必须在内存“堆”里创建。 -而在C++中,对象是在堆栈中创建的。这样可达到更快的速度,因为当我们进入一个特定的作用域时,堆栈指针会向下移动一个单位,为那个作用域内创建的、以堆栈为基础的所有对象分配存储空间。而当我们离开作用域的时候(调用完毕所有局部构建器后),堆栈指针会向上移动一个单位。然而,在C++里创建“内存堆”(Heap)对象通常会慢得多,因为它建立在C的内存堆基础上。这种内存堆实际是一个大的内存池,要求必须进行再循环(再生)。在C++里调用delete以后,释放的内存会在堆里留下一个洞,所以再调用new的时候,存储分配机制必须进行某种形式的搜索,使对象的存储与堆内任何现成的洞相配,否则就会很快用光堆的存储空间。之所以内存堆的分配会在C++里对性能造成如此重大的性能影响,对可用内存的搜索正是一个重要的原因。所以创建基于堆栈的对象要快得多。 +而在C++中,对象是在栈中创建的。这样可达到更快的速度,因为当我们进入一个特定的作用域时,栈指针会向下移动一个单位,为那个作用域内创建的、以栈为基础的所有对象分配存储空间。而当我们离开作用域的时候(调用完毕所有局部构造器后),栈指针会向上移动一个单位。然而,在C++里创建“内存堆”(Heap)对象通常会慢得多,因为它建立在C的内存堆基础上。这种内存堆实际是一个大的内存池,要求必须进行再循环(再生)。在C++里调用delete以后,释放的内存会在堆里留下一个洞,所以再调用new的时候,存储分配机制必须进行某种形式的搜索,使对象的存储与堆内任何现成的洞相配,否则就会很快用光堆的存储空间。之所以内存堆的分配会在C++里对性能造成如此重大的性能影响,对可用内存的搜索正是一个重要的原因。所以创建基于栈的对象要快得多。 -同样地,由于C++如此多的工作都在编译期间进行,所以必须考虑这方面的因素。但在Java的某些地方,事情的发生却要显得“动态”得多,它会改变模型。创建对象的时候,垃圾收集器的使用对于提高对象创建的速度产生了显著的影响。从表面上看,这种说法似乎有些奇怪——存储空间的释放会对存储空间的分配造成影响,但它正是JVM采取的重要手段之一,这意味着在Java中为堆对象分配存储空间几乎能达到与C++中在堆栈里创建存储空间一样快的速度。 +同样地,由于C++如此多的工作都在编译期间进行,所以必须考虑这方面的因素。但在Java的某些地方,事情的发生却要显得“动态”得多,它会改变模型。创建对象的时候,垃圾收集器的使用对于提高对象创建的速度产生了显著的影响。从表面上看,这种说法似乎有些奇怪——存储空间的释放会对存储空间的分配造成影响,但它正是JVM采取的重要手段之一,这意味着在Java中为堆对象分配存储空间几乎能达到与C++中在栈里创建存储空间一样快的速度。 -可将C++的堆(以及更慢的Java堆)想象成一个庭院,每个对象都拥有自己的一块地皮。在以后的某个时间,这种“不动产”会被抛弃,而且必须再生。但在某些JVM里,Java堆的工作方式却是颇有不同的。它更象一条传送带:每次分配了一个新对象后,都会朝前移动。这意味着对象存储空间的分配可以达到非常快的速度。“堆指针”简单地向前移至处女地,所以它与C++的堆栈分配方式几乎是完全相同的(当然,在数据记录上会多花一些开销,但要比搜索存储空间快多了)。 +可将C++的堆(以及更慢的Java堆)想象成一个庭院,每个对象都拥有自己的一块地皮。在以后的某个时间,这种“不动产”会被抛弃,而且必须再生。但在某些JVM里,Java堆的工作方式却是颇有不同的。它更象一条传送带:每次分配了一个新对象后,都会朝前移动。这意味着对象存储空间的分配可以达到非常快的速度。“堆指针”简单地向前移至处女地,所以它与C++的栈分配方式几乎是完全相同的(当然,在数据记录上会多花一些开销,但要比搜索存储空间快多了)。 现在,大家可能注意到了堆事实并非一条传送带。如按那种方式对待它,最终就要求进行大量的页交换(这对性能的发挥会产生巨大干扰),这样终究会用光内存,出现内存分页错误。所以这儿必须采取一个技巧,那就是著名的“垃圾收集器”。它在收集“垃圾”的同时,也负责压缩堆里的所有对象,将“堆指针”移至尽可能靠近传送带开头的地方,远离发生(内存)分页错误的地点。垃圾收集器会重新安排所有东西,使其成为一个高速、无限自由的堆模型,同时游刃有余地分配存储空间。 -为真正掌握它的工作原理,我们首先需要理解不同垃圾收集器(GC)采取的工作方案。一种简单、但速度较慢的GC技术是引用计数。这意味着每个对象都包含了一个引用计数器。每当一个句柄同一个对象连接起来时,引用计数器就会增值。每当一个句柄超出自己的作用域,或者设为null时,引用计数就会减值。这样一来,只要程序处于运行状态,就需要连续进行引用计数管理——尽管这种管理本身的开销比较少。垃圾收集器会在整个对象列表中移动巡视,一旦它发现其中一个引用计数成为0,就释放它占据的存储空间。但这样做也有一个缺点:若对象相互之间进行循环引用,那么即使引用计数不是0,仍有可能属于应收掉的“垃圾”。为了找出这种自引用的组,要求垃圾收集器进行大量额外的工作。引用计数属于垃圾收集的一种类型,但它看起来并不适合在所有JVM方案中采用。 +为真正掌握它的工作原理,我们首先需要理解不同垃圾收集器(GC)采取的工作方案。一种简单、但速度较慢的GC技术是引用计数。这意味着每个对象都包含了一个引用计数器。每当一个指针同一个对象连接起来时,引用计数器就会增值。每当一个指针超出自己的作用域,或者设为null时,引用计数就会减值。这样一来,只要程序处于运行状态,就需要连续进行引用计数管理——尽管这种管理本身的开销比较少。垃圾收集器会在整个对象列表中移动巡视,一旦它发现其中一个引用计数成为0,就释放它占据的存储空间。但这样做也有一个缺点:若对象相互之间进行循环引用,那么即使引用计数不是0,仍有可能属于应收掉的“垃圾”。为了找出这种自引用的组,要求垃圾收集器进行大量额外的工作。引用计数属于垃圾收集的一种类型,但它看起来并不适合在所有JVM方案中采用。 -在速度更快的方案里,垃圾收集并不建立在引用计数的基础上。相反,它们基于这样一个原理:所有非死锁的对象最终都肯定能回溯至一个句柄,该句柄要么存在于堆栈中,要么存在于静态存储空间。这个回溯链可能经历了几层对象。所以,如果从堆栈和静态存储区域开始,并经历所有句柄,就能找出所有活动的对象。对于自己找到的每个句柄,都必须跟踪到它指向的那个对象,然后跟随那个对象中的所有句柄,“跟踪追击”到它们指向的对象……等等,直到遍历了从堆栈或静态存储区域中的句柄发起的整个链接网路为止。中途移经的每个对象都必须仍处于活动状态。注意对于那些特殊的自引用组,并不会出现前述的问题。由于它们根本找不到,所以会自动当作垃圾处理。 +在速度更快的方案里,垃圾收集并不建立在引用计数的基础上。相反,它们基于这样一个原理:所有非死锁的对象最终都肯定能回溯至一个指针,该指针要么存在于栈中,要么存在于静态存储空间。这个回溯链可能经历了几层对象。所以,如果从栈和静态存储区域开始,并经历所有指针,就能找出所有活动的对象。对于自己找到的每个指针,都必须跟踪到它指向的那个对象,然后跟随那个对象中的所有指针,“跟踪追击”到它们指向的对象……等等,直到遍历了从栈或静态存储区域中的指针发起的整个链接网路为止。中途移经的每个对象都必须仍处于活动状态。注意对于那些特殊的自引用组,并不会出现前述的问题。由于它们根本找不到,所以会自动当作垃圾处理。 在这里阐述的方法中,JVM采用一种“自适应”的垃圾收集方案。对于它找到的那些活动对象,具体采取的操作取决于当前正在使用的是什么变体。其中一个变体是“停止和复制”。这意味着由于一些不久之后就会非常明显的原因,程序首先会停止运行(并非一种后台收集方案)。随后,已找到的每个活动对象都会从一个内存堆复制到另一个,留下所有的垃圾。除此以外,随着对象复制到新堆,它们会一个接一个地聚焦在一起。这样可使新堆显得更加紧凑(并使新的存储区域可以简单地抽离末尾,就象前面讲述的那样)。 -当然,将一个对象从一处挪到另一处时,指向那个对象的所有句柄(引用)都必须改变。对于那些通过跟踪内存堆的对象而获得的句柄,以及那些静态存储区域,都可以立即改变。但在“遍历”过程中,还有可能遇到指向这个对象的其他句柄。一旦发现这个问题,就当即进行修正(可想象一个散列表将老地址映射成新地址)。 +当然,将一个对象从一处挪到另一处时,指向那个对象的所有指针(引用)都必须改变。对于那些通过跟踪内存堆的对象而获得的指针,以及那些静态存储区域,都可以立即改变。但在“遍历”过程中,还有可能遇到指向这个对象的其他指针。一旦发现这个问题,就当即进行修正(可想象一个散列表将老地址映射成新地址)。 有两方面的问题使复制收集器显得效率低下。第一个问题是我们拥有两个堆,所有内存都在这两个独立的堆内来回移动,要求付出的管理量是实际需要的两倍。为解决这个问题,有些JVM根据需要分配内存堆,并将一个堆简单地复制到另一个。 第二个问题是复制。随着程序变得越来越“健壮”,它几乎不产生或产生很少的垃圾。尽管如此,一个副本收集器仍会将所有内存从一处复制到另一处,这显得非常浪费。为避免这个问题,有些JVM能侦测是否没有产生新的垃圾,并随即改换另一种方案(这便是“自适应”的缘由)。另一种方案叫作“标记和清除”,Sun公司的JVM一直采用的都是这种方案。对于常规性的应用,标记和清除显得非常慢,但一旦知道自己不产生垃圾,或者只产生很少的垃圾,它的速度就会非常快。 -标记和清除采用相同的逻辑:从堆栈和静态存储区域开始,并跟踪所有句柄,寻找活动对象。然而,每次发现一个活动对象的时候,就会设置一个标记,为那个对象作上“记号”。但此时尚不收集那个对象。只有在标记过程结束,清除过程才正式开始。在清除过程中,死锁的对象会被释放然而,不会进行任何形式的复制,所以假若收集器决定压缩一个断续的内存堆,它通过移动周围的对象来实现。 +标记和清除采用相同的逻辑:从栈和静态存储区域开始,并跟踪所有指针,寻找活动对象。然而,每次发现一个活动对象的时候,就会设置一个标记,为那个对象作上“记号”。但此时尚不收集那个对象。只有在标记过程结束,清除过程才正式开始。在清除过程中,死锁的对象会被释放然而,不会进行任何形式的复制,所以假若收集器决定压缩一个断续的内存堆,它通过移动周围的对象来实现。 “停止和复制”向我们表明这种类型的垃圾收集并不是在后台进行的;相反,一旦发生垃圾收集,程序就会停止运行。在Sun公司的文档库中,可发现许多地方都将垃圾收集定义成一种低优先级的后台进程,但它只是一种理论上的实验,实际根本不能工作。在实际应用中,Sun的垃圾收集器会在内存减少时运行。除此以外,“标记和清除”也要求程序停止运行。