提交 b6cbdcf4 编写于 作者: W wizardforcel

compose 14~17

上级 b639a8e0
......@@ -522,8 +522,7 @@ Ticker[] s = new Ticker[size]
## 14.1.5 Daemon线程
“Daemon”线程的作用是在程序的运行期间于后台提供一种“常规”服务,但它并不属于程序的一个基本部分。因此,一旦所有非
Daemon线程完成,程序也会中止运行。相反,假若有任何非Daemon线程仍在运行(比如还有一个正在运行`main()`的线程),则程序的运行不会中止。
“Daemon”线程的作用是在程序的运行期间于后台提供一种“常规”服务,但它并不属于程序的一个基本部分。因此,一旦所有非Daemon线程完成,程序也会中止运行。相反,假若有任何非Daemon线程仍在运行(比如还有一个正在运行`main()`的线程),则程序的运行不会中止。
通过调用`isDaemon()`,可调查一个线程是不是一个Daemon,而且能用`setDaemon()`打开或者关闭一个线程的Daemon状态。如果是一个Daemon线程,那么它创建的任何线程也会自动具备Daemon属性。
......
......@@ -353,6 +353,7 @@ synchronized(syncObject) {
```
在能进入同步块之前,必须在`synchObject`上取得锁。如果已有其他线程取得了这把锁,块便不能进入,必须等候那把锁被释放。
可从整个`run()`中删除`synchronized`关键字,换成用一个同步块包围两个关键行,从而完成对`Sharing2`例子的修改。但什么对象应作为锁来使用呢?那个对象已由`synchTest()`标记出来了——也就是当前对象(`this`)!所以修改过的`run()`方法象下面这个样子:
```
......
......@@ -396,6 +396,7 @@ public class Blocking extends Applet {
14.3.2 死锁
由于线程可能进入堵塞状态,而且由于对象可能拥有“同步”方法——除非同步锁定被解除,否则线程不能访问那个对象——所以一个线程完全可能等候另一个对象,而另一个对象又在等候下一个对象,以此类推。这个“等候”链最可怕的情形就是进入封闭状态——最后那个对象等候的是第一个对象!此时,所有线程都会陷入无休止的相互等待状态,大家都动弹不得。我们将这种情况称为“死锁”。尽管这种情况并非经常出现,但一旦碰到,程序的调试将变得异常艰难。
就语言本身来说,尚未直接提供防止死锁的帮助措施,需要我们通过谨慎的设计来避免。如果有谁需要调试一个死锁的程序,他是没有任何窍门可用的。
1. Java 1.2对`stop()``suspend()``resume()`以及`destroy()`的反对
......
......@@ -159,6 +159,7 @@ public class Counter5 extends Applet {
`Counter5`中的`init()`创建了由10个`Ticker2`构成的一个数组;它们的按钮以及输入字段(文本字段)由`Ticker2`构造器置入窗体。`Counter5`增加了新的按钮,用于启动一切,以及用于提高和降低线程组的最大优先级。除此以外,还有一些标签用于显示一个线程可以采用的最大及最小优先级;以及一个特殊的文本字段,用于显示线程组的最大优先级(在下一节里,我们将全面讨论线程组的问题)。最后,父线程组的优先级也作为标签显示出来。
按下`up`(上)或`down`(下)按钮的时候,会先取得`Ticker2`当前的优先级,然后相应地提高或者降低。
运行该程序时,我们可注意到几件事情。首先,线程组的默认优先级是5。即使在启动线程之前(或者在创建线程之前,这要求对代码进行适当的修改)将最大优先级降到5以下,每个线程都会有一个5的默认优先级。
最简单的测试是获取一个计数器,将它的优先级降低至1,此时应观察到它的计数频率显著放慢。现在试着再次提高优先级,可以升高回线程组的优先级,但不能再高了。现在将线程组的优先级降低两次。线程的优先级不会改变,但假若试图提高或者降低它,就会发现这个优先级自动变成线程组的优先级。此外,新线程仍然具有一个默认优先级,即使它比组的优先级还要高(换句话说,不要指望利用组优先级来防止新线程拥有比现有的更高的优先级)。
......
......@@ -187,8 +187,7 @@ public class ColorBoxes2 extends Frame {
和以前一样,在我们实现`Runnable`的时候,并没有获得与`Thread`配套提供的所有功能,所以必须创建一个新的`Thread`,并将自己传递给它的构造器,以便正式“启动”——`start()`——一些东西。大家在`CBoxVector`构造器和`go()`里都可以体会到这一点。`run()`方法简单地选择`Vector`里的一个随机元素编号,并为那个元素调用`nextColor()`,令其挑选一种新的随机颜色。
运行这个程序时,大家会发现它确实变得更快,响应也更迅速(比如在中断它的时候,它能更快地停下来)。而且随着网格尺寸的壮
大,它也不会经常性地陷于“停顿”状态。因此,线程的处理又多了一项新的考虑因素:必须随时检查自己有没有“太多的线程”(无论对什么程序和运行平台)。若线程太多,必须试着使用上面介绍的技术,对程序中的线程数量进行“平衡”。如果在一个多线程的程序中遇到了性能上的问题,那么现在有许多因素需要检查:
运行这个程序时,大家会发现它确实变得更快,响应也更迅速(比如在中断它的时候,它能更快地停下来)。而且随着网格尺寸的壮大,它也不会经常性地陷于“停顿”状态。因此,线程的处理又多了一项新的考虑因素:必须随时检查自己有没有“太多的线程”(无论对什么程序和运行平台)。若线程太多,必须试着使用上面介绍的技术,对程序中的线程数量进行“平衡”。如果在一个多线程的程序中遇到了性能上的问题,那么现在有许多因素需要检查:
(1) 对`sleep``yield()`以及/或者`wait()`的调用足够多吗?
......
......@@ -13,6 +13,7 @@
(4) 漫长的等待、浪费精力的资源竞争以及死锁等多线程症状。
线程另一个优点是它们用“轻度”执行切换(100条指令的顺序)取代了“重度”进程场景切换(1000条指令)。由于一个进程内的所有线程共享相同的内存空间,所以“轻度”场景切换只改变程序的执行和本地变量。而在“重度”场景切换时,一个进程的改变要求必须完整地交换内存空间。
线程处理看来好象进入了一个全新的领域,似乎要求我们学习一种全新的程序设计语言——或者至少学习一系列新的语言概念。由于大多数微机操作系统都提供了对线程的支持,所以程序设计语言或者库里也出现了对线程的扩展。不管在什么情况下,涉及线程的程序设计:
(1) 刚开始会让人摸不着头脑,要求改换我们传统的编程思路;
......
......@@ -10,6 +10,7 @@
①:这意味着最多只能得到40亿左右的数字组合,全世界的人很快就会把它用光。但根据目前正在研究的新IP编址方案,它将采用128 bit的数字,这样得到的唯一性IP地址也许在几百年的时间里都不会用完。
作为运用`InetAddress.getByName()`一个简单的例子,请考虑假设自己有一家拨号连接因特网服务提供者(ISP),那么会发生什么情况。每次拨号连接的时候,都会分配得到一个临时IP地址。但在连接期间,那个IP地址拥有与因特网上其他IP地址一样的有效性。如果有人按照你的IP地址连接你的机器,他们就有可能使用在你机器上运行的Web或者FTP服务器程序。当然这有个前提,对方必须准确地知道你目前分配到的IP。由于每次拨号连接获得的IP都是随机的,怎样才能准确地掌握你的IP呢?
下面这个程序利用`InetAddress.getByName()`来产生你的IP地址。为了让它运行起来,事先必须知道计算机的名字。该程序只在Windows 95中进行了测试,但大家可以依次进入自己的“开始”、“设置”、“控制面板”、“网络”,然后进入“标识”卡片。其中,“计算机名称”就是应在命令行输入的内容。
```
......
......@@ -7,6 +7,7 @@
作为Java程序员,上述解决问题的方法显得非常笨拙。而且很自然地,我们希望一切工作都用Java完成。首先,我们会用一个Java程序片负责客户端的数据有效性校验,避免数据在服务器和客户之间传来传去,浪费时间和带宽,同时减轻服务器额外构建HTML页的负担。然后跳过Perl CGI脚本,换成在服务器上运行一个Java应用。事实上,我们在这儿已完全跳过了Web服务器,仅仅需要从程序片到服务器上运行的Java应用之间建立一个连接即可。
正如大家不久就会体验到的那样,尽管看起来非常简单,但实际上有一些意想不到的问题使局面显得稍微有些复杂。用Java 1.1写程序片是最理想的,但实际上却经常行不通。到本书写作的时候,拥有Java 1.1能力的浏览器仍为数不多,而且即使这类浏览器现在非常流行,仍需考虑照顾一下那些升级缓慢的人。所以从安全的角度看,程序片代码最好只用Java 1.0编写。基于这一前提,我们不能用JAR文件来合并(压缩)程序片中的`.class`文件。所以,我们应尽可能减少`.class`文件的使用数量,以缩短下载时间。
好了,再来说说我用的Web服务器(写这个示范程序时用的就是它)。它确实支持Java,但仅限于Java 1.0!所以服务器应用也必须用Java 1.0编写。
15.5.1 服务器应用
......
......@@ -280,6 +280,7 @@ URL类的最大的特点就是有效地保护了我们的安全。可以同一
在这儿使用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++中,几乎一刻都不能放松对这些问题的关注。
这个项目首先创建一个可以重复使用的部分,由C++头文件中的`Pair``CGI_vector`构成。从技术角度看,确实不应把这些东西都塞到一个头文件里。但就目前的例子来说,这样做不会造成任何方面的损害,而且更具有Java风格,所以大家阅读理解代码时要显得轻松一些:
```
......@@ -568,6 +569,7 @@ void main() {
在目前这种情况下,我们希望服务器将所有信息都直接反馈回客户程序(亦即我们的程序片,它们正在等候给自己的回复)。信息应该原封不动,所以`content-type`设为`text/plain`(纯文本)。一旦服务器看到这个头,就会将所有字符串都直接发还给客户。所以每个字符串(三个用于出错条件,一个用于成功的加入)都会返回程序片。
我们用相同的代码添加电子函件名称(用户的姓名)。但在CGI脚本的情况下,并不存在无限循环——程序只是简单地响应,然后就中断。每次有一个CGI请求抵达时,程序都会启动,对那个请求作出反应,然后自行关闭。所以CPU不可能陷入空等待的尴尬境地,只有启动程序和打开文件时才存在性能上的隐患。Web服务器对CGI请求进行控制时,它的开销会将这种隐患减轻到最低程度。
这种设计的另一个好处是由于`Pair``CGI_vector`都得到了定义,大多数工作都帮我们自动完成了,所以只需修改`main()`即可轻松创建自己的CGI程序。尽管小服务程序(`Servlet`)最终会变得越来越流行,但为了创建快速的CGI程序,C++仍然显得非常方便。
15.6.4 POST的概念
......@@ -640,6 +642,7 @@ VALUE = "" size = "40"></p>
```
填好这个表单并提交出去以后,会得到一个简单的文本页,其中包含了解析出来的结果。从中可知道CGI程序是否在正常工作。
当然,用一个程序片来提交数据显得更有趣一些。然而,POST数据的提交属于一个不同的过程。在用常规方式调用了CGI程序以后,必须另行建立与服务器的一个连接,以便将查询字符串反馈给它。服务器随后会进行一番处理,再通过标准输入将查询字符串反馈回CGI程序。
为建立与服务器的一个直接连接,必须取得自己创建的URL,然后调用`openConnection()`创建一个`URLConnection`。但是,由于`URLConnection`一般不允许我们把数据发给它,所以必须很可笑地调用`setDoOutput(true`)函数,同时调用的还包括`setDoInput(true)`以及`setAllowUserInteraction(false)`——注释⑥。最后,可调用`getOutputStream()`来创建一个`OutputStream`(输出数据流),并把它封装到一个`DataOutputStream`里,以便能按传统方式同它通信。下面列出的便是一个用于完成上述工作的程序片,必须在从它的各个字段里收集了数据之后再执行它:
......
......@@ -72,6 +72,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代码做自己想做的任何事情了。
......
......@@ -139,6 +139,7 @@ Naming.bind("PerfectTime", pt);
```
服务名是任意的;它在这里正好为`PerfectTime`,和类名一样,但你可以根据情况任意修改。最重要的是确保它在注册表里是个独一无二的名字,以便客户正常地获取远程对象。若这个名字已在注册表里了,就会得到一个`AlreadyBoundException`异常。为防止这个问题,可考虑坚持使用`rebind()`,放弃`bind()`。这是由于`rebind()`要么会添加一个新条目,要么将同名的条目替换掉。
尽管`main()`退出,我们的对象已经创建并注册,所以会由注册表一直保持活动状态,等候客户到达并发出对它的请求。只要`rmiregistry`处于运行状态,而且我们没有为名字调用`Naming.unbind()`方法,对象就肯定位于那个地方。考虑到这个原因,在我们设计自己的代码时,需要先关闭`rmiregistry`,并在编译远程对象的一个新版本时重新启动它。
并不一定要将`rmiregistry`作为一个外部进程启动。若事前知道自己的是要求用以注册表的唯一一个应用,就可在程序内部启动它,使用下述代码:
......@@ -153,8 +154,7 @@ LocateRegistry.createRegistry(2005);
若编译和运行`PerfectTime.java`,即使`rmiregistry`正确运行,它也无法工作。这是由于RMI的框架尚未就位。首先必须创建根和干,以便提供网络连接操作,并使我们将远程对象伪装成自己机器内的某个本地对象。
所有这些幕后的工作都是相当复杂的。我们从远程对象传入、传出的任何对象都必须`implement Serializable`(如果想传递
远程引用,而非整个对象,对象的参数就可以`implement Remote`)。因此可以想象,当根和干通过网络“汇集”所有参数并返回结果的时候,会自动进行序列化以及数据的重新装配。幸运的是,我们根本没必要了解这些方面的任何细节,但根和干却是必须创建的。一个简单的过程如下:在编译好的代码中调用`rmic`,它会创建必需的一些文件。所以唯一要做的事情就是为编译过程新添一个步骤。
所有这些幕后的工作都是相当复杂的。我们从远程对象传入、传出的任何对象都必须`implement Serializable`(如果想传递远程引用,而非整个对象,对象的参数就可以`implement Remote`)。因此可以想象,当根和干通过网络“汇集”所有参数并返回结果的时候,会自动进行序列化以及数据的重新装配。幸运的是,我们根本没必要了解这些方面的任何细节,但根和干却是必须创建的。一个简单的过程如下:在编译好的代码中调用`rmic`,它会创建必需的一些文件。所以唯一要做的事情就是为编译过程新添一个步骤。
然而,`rmic`工具与特定的包和类路径有很大的关联。`PerfectTime.java`位于包`c15.Ptime`中,即使我们调用与`PerfectTime.class`同一目录内的`rmic``rmic`都无法找到文件。这是由于它搜索的是类路径。因此,我们必须同时指定类路径,就象下面这样:
......
......@@ -120,7 +120,6 @@ package c16.recyclea;
表面上持,先把`Trash`的类型向上转换到一个集合容纳基类型的引用,再回过头重新向下转换,这似乎是一种非常愚蠢的做法。为什么不只是一开始就将垃圾置入适当的容器里呢?(事实上,这正是拨开“回收”一团迷雾的关键)。在这个程序中,我们很容易就可以换成这种做法,但在某些情况下,系统的结构及灵活性都能从向下转换中得到极大的好处。
该程序已满足了设计的初衷:它能够正常工作!只要这是个一次性的方案,就会显得非常出色。但是,真正有用的程序应该能够在任
何时候解决问题。所以必须问自己这样一个问题:“如果情况发生了变化,它还能工作吗?”举个例子来说,厚纸板现在是一种非常有价值的可回收物品,那么如何把它集成到系统中呢(特别是程序很大很复杂的时候)?由于前面在`switch`语句中的类型检查编码可能散布于整个程序,所以每次加入一种新类型时,都必须找到所有那些编码。若不慎遗漏一个,编译器除了指出存在一个错误之外,不能再提供任何有价值的帮助。
该程序已满足了设计的初衷:它能够正常工作!只要这是个一次性的方案,就会显得非常出色。但是,真正有用的程序应该能够在任何时候解决问题。所以必须问自己这样一个问题:“如果情况发生了变化,它还能工作吗?”举个例子来说,厚纸板现在是一种非常有价值的可回收物品,那么如何把它集成到系统中呢(特别是程序很大很复杂的时候)?由于前面在`switch`语句中的类型检查编码可能散布于整个程序,所以每次加入一种新类型时,都必须找到所有那些编码。若不慎遗漏一个,编译器除了指出存在一个错误之外,不能再提供任何有价值的帮助。
RTTI在这里使用不当的关键是“每种类型都进行了测试”。如果由于类型的子集需要特殊的对待,所以只寻找那个子集,那么情况就会变得好一些。但假如在一个`switch`语句中查找每一种类型,那么很可能错过一个重点,使最终的代码很难维护。在下一节中,大家会学习如何逐步对这个程序进行改进,使其显得越来越灵活。这是在程序设计中一种非常有意义的例子。
......@@ -29,6 +29,7 @@
```
这些代码显然“过于复杂”,也是新类型加入时必须改动代码的场所之一。如果经常都要加入新类型,那么更好的方案就是建立一个独立的方法,用它获取所有必需的信息,并创建一个引用,指向正确类型的一个对象——已经向上转换到一个`Trash`对象。在《设计模式》中,它被粗略地称呼为“创建模式”。要在这里应用的特殊模式是`Factory`方法的一种变体。在这里,`Factory`方法属于`Trash`的一名`static`(静态)成员。但更常见的一种情况是:它属于派生类中一个被重载的方法。
`Factory`方法的基本原理是我们将创建对象所需的基本信息传递给它,然后返回并等候引用(已经向上转换至基类型)作为返回值出现。从这时开始,就可以按多态性的方式对待对象了。因此,我们根本没必要知道所创建对象的准确类型是什么。事实上,`Factory`方法会把自己隐藏起来,我们是看不见它的。这样做可防止不慎的误用。如果想在没有多态性的前提下使用对象,必须明确地使用RTTI和指定转换。
但仍然存在一个小问题,特别是在基类中使用更复杂的方法(不是在这里展示的那种),且在派生类里重载(覆盖)了它的前提下。如果在派生类里请求的信息要求更多或者不同的参数,那么该怎么办呢?“创建更多的对象”解决了这个问题。为实现`Factory`方法,`Trash`类使用了一个新的方法,名为`factory`。为了将创建数据隐藏起来,我们用一个名为`Info`的新类包含`factory`方法创建适当的`Trash`对象时需要的全部信息。下面是`Info`一种简单的实现方式:
......@@ -46,6 +47,7 @@ class Info {
}
```
`Info`对象唯一的任务就是容纳用于`factory()`方法的信息。现在,假如出现了一种特殊情况,`factory()`需要更多或者不同的信息来新建一种类型的`Trash`对象,那么再也不需要改动`factory()`了。通过添加新的数据和构造器,我们可以修改`Info`类,或者采用子类处理更典型的面向对象形式。
用于这个简单示例的`factory()`方法如下:
......@@ -196,6 +198,7 @@ new Class[] {double.class}
```
这个代码假定所有`Trash`类型都有一个需要`double`数值的构造器(注意`double.class``Double.class`是不同的)。若考虑一种更灵活的方案,亦可调用`getConstructors()`,令其返回可用构造器的一个数组。
`getConstructors()`返回的是指向一个`Constructor`对象的引用(该对象是`java.lang.reflect`的一部分)。我们用方法`newInstance()`动态地调用构造器。该方法需要获取包含了实际参数的一个`Object`数组。这个数组同样是按Java 1.1的语法创建的:
```
......@@ -211,6 +214,7 @@ new Object[] {new Double(info.data)}
1. Trash子类
为了与原型机制相适应,对`Trash`每个新子类唯一的要求就是在其中包含了一个构造器,指示它获取一个`double`参数。Java 1.1的“反射”机制可负责剩下的所有工作。
下面是不同类型的`Trash`,每种类型都有它们自己的文件里,但都属于`Trash`包的一部分(同样地,为了方便在本章内重复使用):
```
......@@ -420,6 +424,7 @@ public class RecycleAP {
所有`Trash`对象——以及`ParseTrash`及支撑类——现在都成为名为`c16.trash`的一个包的一部分,所以它们可以简单地导入。
无论打开包含了`Trash`描述信息的数据文件,还是对那个文件进行解析,所有涉及到的操作均已封装到`static`(静态)方法`ParseTrash.fillBin()`里。所以它现在已经不是我们设计过程中要注意的一个重点。在本章剩余的部分,大家经常都会看到无论添加的是什么类型的新类,`ParseTrash.fillBin()`都会持续工作,不会发生改变,这无疑是一种优良的设计模式。
提到对象的创建,这一方案确实已将新类型加入系统所需的变动严格地“本地化”了。但在使用RTTI的过程中,却存在着一个严重的问题,这里已明确地显露出来。程序表面上工作得很好,但却永远侦测到不能“硬纸板”(`Cardboard`)这种新的废品类型——即使列表里确实有一个硬纸板类型!之所以会出现这种情况,完全是由于使用了RTTI的缘故。RTTI只会查找那些我们告诉它查找的东西。RTTI在这里错误的用法是“系统中的每种类型”都进行了测试,而不是仅测试一种类型或者一个类型子集。正如大家以后会看到的那样,在测试每一种类型时可换用其他方式来运用多态性特征。但假如以这种形式过多地使用RTTI,而且又在自己的系统里添加了一种新类型,很容易就会忘记在程序里作出适当的改动,从而埋下以后难以发现的Bug。因此,在这种情况下避免使用RTTI是很有必要的,这并不仅仅是为了表面好看——也是为了产生更易维护的代码。
......@@ -18,7 +18,9 @@
![](16-3.gif)
新建立的分级结构是`TypeBin`,其中包含了它自己的一个方法,名为`add()`,而且也应用了多态性。但要注意一个新特点:`add()`已进行了“重载”处理,可接受不同的垃圾类型作为参数。因此,双重满足机制的一个关键点是它也要涉及到重载。
程序的重新设计也带来了一个问题:现在的基类`Trash`必须包含一个`addToBin()`方法。为解决这个问题,一个最直接的办法是复制所有代码,并修改基类。然而,假如没有对源码的控制权,那么还有另一个办法可以考虑:将`addToBin()`方法置入一个接口内部,保持`Trash`不变,并继承新的、特殊的类型`Aluminum``Paper``Glass`以及`Cardboard`。我们在这里准备采取后一个办法。
这个设计模式中用到的大多数类都必须设为`public`(公用)属性,所以它们放置于自己的类内。下面列出接口代码:
```
......
......@@ -57,6 +57,7 @@ public class DynaTrash {
```
尽管功能很强,但对`TypeMap`的定义是非常简单的。它只是包含了一个散列表,同时`add()`负担了大部分的工作。添加一个新类型时,那种类型的`Class`对象的引用会被提取出来。随后,利用这个引用判断容纳了那类对象的一个`Vector`是否已存在于散列表中。如答案是肯定的,就提取出那个`Vector`,并将对象加入其中;反之,就将`Class`对象及新`Vector`作为一个“键-值”对加入。
利用`keys()`,可以得到对所有`Class`对象的一个“枚举”(`Enumeration`),而且可用`get()`,可通过`Class`对象获取对应的`Vector`
`filler()`方法非常有趣,因为它利用了`ParseTrash.fillBin()`的设计——不仅能尝试填充一个`Vector`,也能用它的`addTrash()`方法试着填充实现了`Fillable`(可填充)接口的任何东西。`filter()`需要做的全部事情就是将一个引用返回给实现了`Fillable`的一个接口,然后将这个引用作为参数传递给`fillBin()`,就象下面这样:
......
......@@ -12,6 +12,7 @@
我首先将整本书都以ASCII文本格式保存成一个独立的文件。`CodePackager`程序有两种运行模式(在`usageString`有相应的描述):如果使用`-p`标志,程序就会检查一个包含了ASCII文本(即本书的内容)的一个输入文件。它会遍历这个文件,按照注释记号提取出代码,并用位于第一行的文件名来决定创建文件使用什么名字。除此以外,在需要将文件置入一个特殊目录的时候,它还会检查`package`语句(根据由`package`语句指定的路径选择)。
但这样还不够。程序还要对包(`package`)名进行跟踪,从而监视章内发生的变化。由于每一章使用的所有包都以`c02``c03``c04`等等起头,用于标记它们所属的是哪一章(除那些以`com`起头的以外,它们在对不同的章进行跟踪的时候会被忽略)——只要每一章的第一个代码列表包含了一个`package`,所以`CodePackager`程序能知道每一章发生的变化,并将后续的文件放到新的子目录里。
每个文件提取出来时,都会置入一个`SourceCodeFile`对象,随后再将那个对象置入一个集合(后面还会详尽讲述这个过程)。这些`SourceCodeFile`对象可以简单地保存在文件中,那正是本项目的第二个用途。如果直接调用`CodePackager`,不添加`-p`标志,它就会将一个“打包”文件作为输入。那个文件随后会被提取(释放)进入单独的文件。所以`-p`标志的意思就是提取出来的文件已被“打包”(`packed`)进入这个单一的文件。
但为什么还要如此麻烦地使用打包文件呢?这是由于不同的计算机平台用不同的方式在文件里保存文本信息。其中最大的问题是换行字符的表示方法;当然,还有可能存在另一些问题。然而,Java有一种特殊类型的IO数据流——`DataOutputStream`——它可以保证“无论数据来自何种机器,只要使用一个`DataInputStream`收取这些数据,就可用本机正确的格式保存它们”。也就是说,Java负责控制与不同平台有关的所有细节,而这正是Java最具魅力的一点。所以`-p`标志能将所有东西都保存到单一的文件里,并采用通用的格式。用户可从Web下载这个文件以及Java程序,然后对这个文件运行`CodePackager`,同时不指定`-p`标志,文件便会释放到系统中正确的场所(亦可指定另一个子目录;否则就在当前目录创建子目录)。为确保不会留下与特定平台有关的格式,凡是需要描述一个文件或路径的时候,我们就使用File对象。除此以外,还有一项特别的安全措施:在每个子目录里都放入一个空文件;那个文件的名字指出在那个子目录里应找到多少个文件。
......@@ -478,6 +479,7 @@ public class CodePackager {
解析出并保存好文件名后,第一行会被置入字符串`contents`中(该字符串用于保存源码清单的完整正文)。随后,将剩余的代码行读入,并合并进入`contents`字符串。当然事情并没有想象的那么简单,因为特定的情况需加以特别的控制。一种情况是错误检查:若直接遇到一个`startMarker`(起始标记),表明当前操作的这个代码列表没有设置一个结束标记。这属于一个出错条件,需要退出程序。
另一种特殊情况与`package`关键字有关。尽管Java是一种自由形式的语言,但这个程序要求`package`关键字必须位于行首。若发现`package`关键字,就通过检查位于开头的空格以及位于末尾的分号,从而提取出包名(注意亦可一次单独的操作实现,方法是使用重载的`substring()`,令其同时检查起始和结束索引位置)。随后,将包名中的点号替换成特定的文件分隔符——当然,这里要假设文件分隔符仅有一个字符的长度。尽管这个假设可能对目前的所有系统都是适用的,但一旦遇到问题,一定不要忘了检查一下这里。
默认操作是将每一行都连接到`contents`里,同时还有换行字符,直到遇到一个`endMarker`(结束标记)为止。该标记指出构造器应当停止了。若在`endMarker`之前遇到了文件结尾,就认为存在一个错误。
2. 从打包文件中提取
......
......@@ -2,6 +2,7 @@
第11章介绍了Java 1.1新的“反射”概念,并利用这个概念查询一个特定类的方法——要么是由所有方法构成的一个完整列表,要么是这个列表的一个子集(名字与我们指定的关键字相符)。那个例子最大的好处就是能自动显示出所有方法,不强迫我们在继承结构中遍历,检查每一级的基类。所以,它实际是我们节省编程时间的一个有效工具:因为大多数Java方法的名字都规定得非常全面和详尽,所以能有效地找出那些包含了一个特殊关键字的方法名。若找到符合标准的一个名字,便可根据它直接查阅联机帮助文档。
但第11的那个例子也有缺陷,它没有使用AWT,仅是一个纯命令行的应用。在这儿,我们准备制作一个改进的GUI版本,能在我们键入字符的时候自动刷新输出,也允许我们在输出结果中进行剪切和粘贴操作:
```
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册