Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
OpenDocCN
thinking-in-java-zh
提交
b6cbdcf4
T
thinking-in-java-zh
项目概览
OpenDocCN
/
thinking-in-java-zh
通知
0
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
T
thinking-in-java-zh
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
前往新版Gitcode,体验更适合开发者的 AI 搜索 >>
提交
b6cbdcf4
编写于
10月 02, 2017
作者:
W
wizardforcel
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
compose 14~17
上级
b639a8e0
变更
17
隐藏空白更改
内联
并排
Showing
17 changed file
with
26 addition
and
8 deletion
+26
-8
14.1 反应灵敏的用户界面.md
14.1 反应灵敏的用户界面.md
+1
-2
14.2 共享有限的资源.md
14.2 共享有限的资源.md
+1
-0
14.3 堵塞.md
14.3 堵塞.md
+1
-0
14.4 优先级.md
14.4 优先级.md
+1
-0
14.5 回顾runnable.md
14.5 回顾runnable.md
+1
-2
14.6 总结.md
14.6 总结.md
+1
-0
15.1 机器的标识.md
15.1 机器的标识.md
+1
-0
15.5 一个Web应用.md
15.5 一个Web应用.md
+1
-0
15.6 Java与CGI的沟通.md
15.6 Java与CGI的沟通.md
+3
-0
15.7 用JDBC连接数据库.md
15.7 用JDBC连接数据库.md
+1
-0
15.8 远程方法.md
15.8 远程方法.md
+2
-2
16.3 模拟垃圾回收站.md
16.3 模拟垃圾回收站.md
+1
-2
16.4 改进设计.md
16.4 改进设计.md
+5
-0
16.6 多重分发.md
16.6 多重分发.md
+2
-0
16.8 RTTI真的有害吗.md
16.8 RTTI真的有害吗.md
+1
-0
17.1 文字处理.md
17.1 文字处理.md
+2
-0
17.2 方法查找工具.md
17.2 方法查找工具.md
+1
-0
未找到文件。
14.1 反应灵敏的用户界面.md
浏览文件 @
b6cbdcf4
...
...
@@ -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属性。
...
...
14.2 共享有限的资源.md
浏览文件 @
b6cbdcf4
...
...
@@ -353,6 +353,7 @@ synchronized(syncObject) {
```
在能进入同步块之前,必须在
`synchObject`
上取得锁。如果已有其他线程取得了这把锁,块便不能进入,必须等候那把锁被释放。
可从整个
`run()`
中删除
`synchronized`
关键字,换成用一个同步块包围两个关键行,从而完成对
`Sharing2`
例子的修改。但什么对象应作为锁来使用呢?那个对象已由
`synchTest()`
标记出来了——也就是当前对象(
`this`
)!所以修改过的
`run()`
方法象下面这个样子:
```
...
...
14.3 堵塞.md
浏览文件 @
b6cbdcf4
...
...
@@ -396,6 +396,7 @@ public class Blocking extends Applet {
14.
3.2 死锁
由于线程可能进入堵塞状态,而且由于对象可能拥有“同步”方法——除非同步锁定被解除,否则线程不能访问那个对象——所以一个线程完全可能等候另一个对象,而另一个对象又在等候下一个对象,以此类推。这个“等候”链最可怕的情形就是进入封闭状态——最后那个对象等候的是第一个对象!此时,所有线程都会陷入无休止的相互等待状态,大家都动弹不得。我们将这种情况称为“死锁”。尽管这种情况并非经常出现,但一旦碰到,程序的调试将变得异常艰难。
就语言本身来说,尚未直接提供防止死锁的帮助措施,需要我们通过谨慎的设计来避免。如果有谁需要调试一个死锁的程序,他是没有任何窍门可用的。
1.
Java 1.2对
`stop()`
,
`suspend()`
,
`resume()`
以及
`destroy()`
的反对
...
...
14.4 优先级.md
浏览文件 @
b6cbdcf4
...
...
@@ -159,6 +159,7 @@ public class Counter5 extends Applet {
`Counter5`
中的
`init()`
创建了由10个
`Ticker2`
构成的一个数组;它们的按钮以及输入字段(文本字段)由
`Ticker2`
构造器置入窗体。
`Counter5`
增加了新的按钮,用于启动一切,以及用于提高和降低线程组的最大优先级。除此以外,还有一些标签用于显示一个线程可以采用的最大及最小优先级;以及一个特殊的文本字段,用于显示线程组的最大优先级(在下一节里,我们将全面讨论线程组的问题)。最后,父线程组的优先级也作为标签显示出来。
按下
`up`
(上)或
`down`
(下)按钮的时候,会先取得
`Ticker2`
当前的优先级,然后相应地提高或者降低。
运行该程序时,我们可注意到几件事情。首先,线程组的默认优先级是5。即使在启动线程之前(或者在创建线程之前,这要求对代码进行适当的修改)将最大优先级降到5以下,每个线程都会有一个5的默认优先级。
最简单的测试是获取一个计数器,将它的优先级降低至1,此时应观察到它的计数频率显著放慢。现在试着再次提高优先级,可以升高回线程组的优先级,但不能再高了。现在将线程组的优先级降低两次。线程的优先级不会改变,但假若试图提高或者降低它,就会发现这个优先级自动变成线程组的优先级。此外,新线程仍然具有一个默认优先级,即使它比组的优先级还要高(换句话说,不要指望利用组优先级来防止新线程拥有比现有的更高的优先级)。
...
...
14.5 回顾runnable.md
浏览文件 @
b6cbdcf4
...
...
@@ -187,8 +187,7 @@ public class ColorBoxes2 extends Frame {
和以前一样,在我们实现
`Runnable`
的时候,并没有获得与
`Thread`
配套提供的所有功能,所以必须创建一个新的
`Thread`
,并将自己传递给它的构造器,以便正式“启动”——
`start()`
——一些东西。大家在
`CBoxVector`
构造器和
`go()`
里都可以体会到这一点。
`run()`
方法简单地选择
`Vector`
里的一个随机元素编号,并为那个元素调用
`nextColor()`
,令其挑选一种新的随机颜色。
运行这个程序时,大家会发现它确实变得更快,响应也更迅速(比如在中断它的时候,它能更快地停下来)。而且随着网格尺寸的壮
大,它也不会经常性地陷于“停顿”状态。因此,线程的处理又多了一项新的考虑因素:必须随时检查自己有没有“太多的线程”(无论对什么程序和运行平台)。若线程太多,必须试着使用上面介绍的技术,对程序中的线程数量进行“平衡”。如果在一个多线程的程序中遇到了性能上的问题,那么现在有许多因素需要检查:
运行这个程序时,大家会发现它确实变得更快,响应也更迅速(比如在中断它的时候,它能更快地停下来)。而且随着网格尺寸的壮大,它也不会经常性地陷于“停顿”状态。因此,线程的处理又多了一项新的考虑因素:必须随时检查自己有没有“太多的线程”(无论对什么程序和运行平台)。若线程太多,必须试着使用上面介绍的技术,对程序中的线程数量进行“平衡”。如果在一个多线程的程序中遇到了性能上的问题,那么现在有许多因素需要检查:
(1) 对
`sleep`
,
`yield()`
以及/或者
`wait()`
的调用足够多吗?
...
...
14.6 总结.md
浏览文件 @
b6cbdcf4
...
...
@@ -13,6 +13,7 @@
(4) 漫长的等待、浪费精力的资源竞争以及死锁等多线程症状。
线程另一个优点是它们用“轻度”执行切换(100条指令的顺序)取代了“重度”进程场景切换(1000条指令)。由于一个进程内的所有线程共享相同的内存空间,所以“轻度”场景切换只改变程序的执行和本地变量。而在“重度”场景切换时,一个进程的改变要求必须完整地交换内存空间。
线程处理看来好象进入了一个全新的领域,似乎要求我们学习一种全新的程序设计语言——或者至少学习一系列新的语言概念。由于大多数微机操作系统都提供了对线程的支持,所以程序设计语言或者库里也出现了对线程的扩展。不管在什么情况下,涉及线程的程序设计:
(1) 刚开始会让人摸不着头脑,要求改换我们传统的编程思路;
...
...
15.1 机器的标识.md
浏览文件 @
b6cbdcf4
...
...
@@ -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中进行了测试,但大家可以依次进入自己的“开始”、“设置”、“控制面板”、“网络”,然后进入“标识”卡片。其中,“计算机名称”就是应在命令行输入的内容。
```
...
...
15.5 一个Web应用.md
浏览文件 @
b6cbdcf4
...
...
@@ -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 服务器应用
...
...
15.6 Java与CGI的沟通.md
浏览文件 @
b6cbdcf4
...
...
@@ -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`
里,以便能按传统方式同它通信。下面列出的便是一个用于完成上述工作的程序片,必须在从它的各个字段里收集了数据之后再执行它:
...
...
15.7 用JDBC连接数据库.md
浏览文件 @
b6cbdcf4
...
...
@@ -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代码做自己想做的任何事情了。
...
...
15.8 远程方法.md
浏览文件 @
b6cbdcf4
...
...
@@ -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`
都无法找到文件。这是由于它搜索的是类路径。因此,我们必须同时指定类路径,就象下面这样:
...
...
16.3 模拟垃圾回收站.md
浏览文件 @
b6cbdcf4
...
...
@@ -120,7 +120,6 @@ package c16.recyclea;
表面上持,先把
`Trash`
的类型向上转换到一个集合容纳基类型的引用,再回过头重新向下转换,这似乎是一种非常愚蠢的做法。为什么不只是一开始就将垃圾置入适当的容器里呢?(事实上,这正是拨开“回收”一团迷雾的关键)。在这个程序中,我们很容易就可以换成这种做法,但在某些情况下,系统的结构及灵活性都能从向下转换中得到极大的好处。
该程序已满足了设计的初衷:它能够正常工作!只要这是个一次性的方案,就会显得非常出色。但是,真正有用的程序应该能够在任
何时候解决问题。所以必须问自己这样一个问题:“如果情况发生了变化,它还能工作吗?”举个例子来说,厚纸板现在是一种非常有价值的可回收物品,那么如何把它集成到系统中呢(特别是程序很大很复杂的时候)?由于前面在
`switch`
语句中的类型检查编码可能散布于整个程序,所以每次加入一种新类型时,都必须找到所有那些编码。若不慎遗漏一个,编译器除了指出存在一个错误之外,不能再提供任何有价值的帮助。
该程序已满足了设计的初衷:它能够正常工作!只要这是个一次性的方案,就会显得非常出色。但是,真正有用的程序应该能够在任何时候解决问题。所以必须问自己这样一个问题:“如果情况发生了变化,它还能工作吗?”举个例子来说,厚纸板现在是一种非常有价值的可回收物品,那么如何把它集成到系统中呢(特别是程序很大很复杂的时候)?由于前面在
`switch`
语句中的类型检查编码可能散布于整个程序,所以每次加入一种新类型时,都必须找到所有那些编码。若不慎遗漏一个,编译器除了指出存在一个错误之外,不能再提供任何有价值的帮助。
RTTI在这里使用不当的关键是“每种类型都进行了测试”。如果由于类型的子集需要特殊的对待,所以只寻找那个子集,那么情况就会变得好一些。但假如在一个
`switch`
语句中查找每一种类型,那么很可能错过一个重点,使最终的代码很难维护。在下一节中,大家会学习如何逐步对这个程序进行改进,使其显得越来越灵活。这是在程序设计中一种非常有意义的例子。
16.4 改进设计.md
浏览文件 @
b6cbdcf4
...
...
@@ -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是很有必要的,这并不仅仅是为了表面好看——也是为了产生更易维护的代码。
16.6 多重分发.md
浏览文件 @
b6cbdcf4
...
...
@@ -18,7 +18,9 @@
![](
16-3.gif
)
新建立的分级结构是
`TypeBin`
,其中包含了它自己的一个方法,名为
`add()`
,而且也应用了多态性。但要注意一个新特点:
`add()`
已进行了“重载”处理,可接受不同的垃圾类型作为参数。因此,双重满足机制的一个关键点是它也要涉及到重载。
程序的重新设计也带来了一个问题:现在的基类
`Trash`
必须包含一个
`addToBin()`
方法。为解决这个问题,一个最直接的办法是复制所有代码,并修改基类。然而,假如没有对源码的控制权,那么还有另一个办法可以考虑:将
`addToBin()`
方法置入一个接口内部,保持
`Trash`
不变,并继承新的、特殊的类型
`Aluminum`
,
`Paper`
,
`Glass`
以及
`Cardboard`
。我们在这里准备采取后一个办法。
这个设计模式中用到的大多数类都必须设为
`public`
(公用)属性,所以它们放置于自己的类内。下面列出接口代码:
```
...
...
16.8 RTTI真的有害吗.md
浏览文件 @
b6cbdcf4
...
...
@@ -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()`
,就象下面这样:
...
...
17.1 文字处理.md
浏览文件 @
b6cbdcf4
...
...
@@ -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.
从打包文件中提取
...
...
17.2 方法查找工具.md
浏览文件 @
b6cbdcf4
...
...
@@ -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.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录