提交 b639a8e0 编写于 作者: W wizardforcel

compose 9~12

上级 e607b888
......@@ -57,10 +57,10 @@ As a source of data. Connect it to a FilterInputStream object to provide a usefu
| 类 | 功能 | 构造器参数/如何使用 |
| --- | --- | --- |
| `ByteArrayInputStream |` 允许内存中的一个缓冲区作为InputStream使用 | 从中提取字节的缓冲区/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口 |
| `StringBufferInputStream` | 将一个String转换成InputStream | 一个String(字符串)。基础的实现方案实际采用一个 |
| `StringBuffer`(字符串缓冲)/作为一个数据源使用。 |通过将其同一个FilterInputStream对象连接,可提供一个有用的接口 |
| `FileInputStream` | 用于从文件读取信息 | 代表文件名的一个String,或者一个File或FileDescriptor对象/作为一个数据源使用。通过将其同一个FilterInputStream对象连接,可提供一个有用的接口 |
| `ByteArrayInputStream |` 允许内存中的一个缓冲区作为`InputStream`使用 | 从中提取字节的缓冲区/作为一个数据源使用。通过将其同一个`FilterInputStream`对象连接,可提供一个有用的接口 |
| `StringBufferInputStream` | 将一个`String`转换成`InputStream` | 一个`String`(字符串)。基础的实现方案实际采用一个 |
| `StringBuffer`(字符串缓冲)/作为一个数据源使用。 | 通过将其同一个FilterInputStream对象连接,可提供一个有用的接口 |
| `FileInputStream` | 用于从文件读取信息 | 代表文件名的一个`String`,或者一个`File``FileDescriptor`对象/作为一个数据源使用。通过将其同一个`FilterInputStream`对象连接,可提供一个有用的接口 |
```
......
......@@ -3,6 +3,7 @@
Java IO流库能满足我们的许多基本要求:可以通过控制台、文件、内存块甚至因特网(参见第15章)进行读写。可以创建新的输入和输出对象类型(通过从`InputStream``OutputStream`继承)。向一个本来预期为收到字符串的方法传递一个对象时,由于Java已限制了“自动类型转换”,所以会自动调用`toString()`方法。而我们可以重新定义这个`toString()`,扩展一个数据流能接纳的对象种类。
在IO数据流库的联机文档和设计过程中,仍有些问题没有解决。比如当我们打开一个文件以便输出时,完全可以指定一旦有人试图覆盖该文件就“抛”出一个异常——有的编程系统允许我们自行指定想打开一个输出文件,但唯一的前提是它尚不存在。但在Java中,似乎必须用一个`File`对象来判断某个文件是否存在,因为假如将其作为`FileOutputStream`或者`FileWriter`打开,那么肯定会被覆盖。若同时指定文件和目录路径,`File`类设计上的一个缺陷就会暴露出来,因为它会说“不要试图在单个类里做太多的事情”!
IO流库易使我们混淆一些概念。它确实能做许多事情,而且也可以移植。但假如假如事先没有吃透装饰器方案的概念,那么所有的设计都多少带有一点盲目性质。所以不管学它还是教它,都要特别花一些功夫才行。而且它并不完整:没有提供对输出格式化的支持,而其他几乎所有语言的IO包都提供了这方面的支持(这一点没有在Java 1.1里得以纠正,它完全错失了改变库设计模式的机会,反而增添了更特殊的一些情况,使复杂程度进一步提高)。Java 1.1转到那些尚未替换的IO库,而不是增加新库。而且库的设计人员似乎没有很好地指出哪些特性是不赞成的,哪些是首选的,造成库设计中经常都会出现一些令人恼火的反对消息。
然而,一旦掌握了装饰器方案,并开始在一些较为灵活的环境使用库,就会认识到这种设计的好处。到那个时候,为此多付出的代码行应该不至于使你觉得太生气。
......@@ -10,6 +10,7 @@
10.2.1 通过`FilterInputStream``InputStream`里读入数据
`FilterInputStream`类要完成两件全然不同的事情。其中,`DataInputStream`允许我们读取不同的基本类型数据以及`String`对象(所有方法都以`read`开头,比如`readByte()``readFloat()`等等)。伴随对应的`DataOutputStream`,我们可通过数据“流”将基本类型的数据从一个地方搬到另一个地方。这些“地方”是由表10.1总结的那些类决定的。若读取块内的数据,并自己进行解析,就不需要用到`DataInputStream`。但在其他许多情况下,我们一般都想用它对自己读入的数据进行自动格式化。
剩下的类用于修改`InputStream`的内部行为方式:是否进行缓冲,是否跟踪自己读入的数据行,以及是否能够推回一个字符等等。后两种类看起来特别象提供对构建一个编译器的支持(换言之,添加它们为了支持Java编译器的构建),所以在常规编程中一般都用不着它们。
也许几乎每次都要缓冲自己的输入,无论连接的是哪个IO设备。所以IO库最明智的做法就是将未缓冲输入作为一种特殊情况处理,同时将缓冲输入接纳为标准做法。
......
......@@ -135,6 +135,7 @@ public class IOStreamDemo {
10.5.1 输入流
当然,我们经常想做的一件事情是将格式化的输出打印到控制台,但那已在第5章创建的`com.bruceeckel.tools`中得到了简化。
第1到第4部分演示了输入流的创建与使用(尽管第4部分展示了将输出流作为一个测试工具的简单应用)。
1. 缓冲的输入文件
......@@ -184,6 +185,7 @@ public class TestEOF {
这个例子展示了如何`LineNumberInputStream`来跟踪输入行的编号。在这里,不可简单地将所有构造器都组合起来,因为必须保持`LineNumberInputStream`的一个引用(注意这并非一种继承环境,所以不能简单地将`in4`转换到一个`LineNumberInputStream`)。因此,`li`容纳了指向`LineNumberInputStream`的引用,然后在它的基础上创建一个`DataInputStream`,以便读入数据。
这个例子也展示了如何将格式化数据写入一个文件。首先创建了一个`FileOutputStream`,用它同一个文件连接。考虑到效率方面的原因,它生成了一个`BufferedOutputStream`。这几乎肯定是我们一般的做法,但却必须明确地这样做。随后为了进行格式化,它转换成一个`PrintStream`。用这种方式创建的数据文件可作为一个原始的文本文件读取。
标志`DataInputStream`何时结束的一个方法是`readLine()`。一旦没有更多的字符串可以读取,它就会返回`null`。每个行都会伴随自己的行号打印到文件里。该行号可通过`li`查询。
可看到用于`out1`的、一个明确指定的`close()`。若程序准备掉转头来,并再次读取相同的文件,这种做法就显得相当有用。然而,该程序直到结束也没有检查文件`IODemo.txt`。正如以前指出的那样,如果不为自己的所有输出文件调用`close()`,就可能发现缓冲区不会得到刷新,造成它们不完整。。
......
......@@ -11,6 +11,7 @@
所以与原来的IO流库相比,经常都要对新IO流进行层次更多的封装。同样地,这也属于装饰器方案的一个缺点——需要为额外的灵活性付出代价。
之所以在Java 1.1里添加了`Reader``Writer`层次,最重要的原因便是国际化的需求。老式IO流层次结构只支持8位字节流,不能很好地控制16位Unicode字符。由于Unicode主要面向的是国际化支持(Java内含的`char`是16位的Unicode),所以添加了`Reader``Writer`层次,以提供对所有IO操作中的Unicode的支持。除此之外,新库也对速度进行了优化,可比旧库更快地运行。
与本书其他地方一样,我会试着提供对类的一个概述,但假定你会利用联机文档搞定所有的细节,比如方法的详尽列表等。
10.7.1 数据的发起与接收
......
......@@ -98,6 +98,7 @@ public class Worm implements Serializable {
```
更有趣的是,`Worm`内的`Data`对象数组是用随机数字初始化的(这样便不用怀疑编译器保留了某种原始信息)。每个`Worm`段都用一个`Char`标记。这个`Char`是在重复生成链接的`Worm`列表时自动产生的。创建一个`Worm`时,需告诉构造器希望它有多长。为产生下一个引用(`next`),它总是用减去1的长度来调用`Worm`构造器。最后一个`next`引用则保持为`null`(空),表示已抵达`Worm`的尾部。
上面的所有操作都是为了加深事情的复杂程度,加大对象序列化的难度。然而,真正的序列化过程却是非常简单的。一旦从另外某个流里创建了`ObjectOutputStream``writeObject()`就会序列化对象。注意也可以为一个`String`调用`writeObject()`。亦可使用与`DataOutputStream`相同的方法写入所有基本数据类型(它们有相同的接口)。
有两个单独的`try`块看起来是类似的。第一个读写的是文件,而另一个读写的是一个`ByteArray`(字节数组)。可利用对任何`DataInputStream`或者`DataOutputStream`的序列化来读写特定的对象;正如在关于连网的那一章会讲到的那样,这些对象甚至包括网络。一次循环后的输出结果如下:
......@@ -123,6 +124,7 @@ Worm storage, w3 = :a(262):b(100):c(396):d(480):e(316):f(398)
10.9.1 寻找类
读者或许会奇怪为什么需要一个对象从它的序列化状态中恢复。举个例子来说,假定我们序列化一个对象,并通过网络将其作为文件传送给另一台机器。此时,位于另一台机器的程序可以只用文件目录来重新构造这个对象吗?
回答这个问题的最好方法就是做一个实验。下面这个文件位于本章的子目录下:
```
......@@ -155,6 +157,7 @@ public class FreezeAlien {
```
该程序并不是捕获和控制异常,而是将异常简单、直接地传递到`main()`外部,这样便能在命令行报告它们。
程序编译并运行后,将结果产生的`file.x`复制到名为`xfiles`的子目录,代码如下:
```
......
......@@ -135,6 +135,7 @@ After creating Cookie
```
可以看到,每个`Class`只有在它需要的时候才会载入,而`static`初始化工作是在类载入时执行的。
非常有趣的是,另一个JVM的输出变成了另一个样子:
```
......@@ -158,6 +159,7 @@ Gum.class;
```
这样做不仅更加简单,而且更安全,因为它会在编译期间得到检查。由于它取消了对方法调用的需要,所以执行的效率也会更高。
类标记不仅可以应用于普通类,也可以应用于接口、数组以及基本数据类型。除此以外,针对每种基本数据类型的包装器类,它还存在一个名为`TYPE`的标准字段。`TYPE`字段的作用是为相关的基本数据类型产生`Class`对象的一个引用,如下所示:
| …… | 等价于…… |
......
......@@ -10,6 +10,7 @@
在运行期查询类信息的另一个原动力是通过网络创建与执行位于远程系统上的对象。这就叫作“远程方法调用”(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`类有关的更多的资料)。因此,匿名对象的类信息可在运行期被完整的揭露出来,而在编译期间不需要知道任何东西。
大家要认识的很重要的一点是“反射”并没有什么神奇的地方。通过“反射”同一个未知类型的对象打交道时,JVM只是简单地检查那个对象,并调查它从属于哪个特定的类(就象以前的RTTI那样)。但在这之后,在我们做其他任何事情之前,`Class`对象必须载入。因此,用于那种特定类型的`.class`文件必须能由JVM调用(要么在本地机器内,要么可以通过网络取得)。所以RTTI和“反射”之间唯一的区别就是对RTTI来说,编译器会在编译期打开和检查`.class`文件。换句话说,我们可以用“普通”方式调用一个对象的所有方法;但对“反射”来说,`.class`文件在编译期间是不可使用的,而是由运行期环境打开和检查。
11.3.1 一个类方法提取器
......
# 11.4 总结
利用RTTI可根据一个匿名的基类引用调查出类型信息。但正是由于这个原因,新手们极易误用它,因为有些时候多态性方法便足够了。对那些以前习惯程序化编程的人来说,极易将他们的程序组织成一系列`switch`语句。他们可能用RTTI做到这一点,从而在代码开发和维护中损失多态性技术的重要价值。Java的要求是让我们尽可能地采用多态性,只有在极特别的情况下才使用RTTI。
但为了利用多态性,要求我们拥有对基类定义的控制权,因为有些时候在程序范围之内,可能发现基类并未包括我们想要的方法。若基类来自一个库,或者由别的什么东西控制着,RTTI便是一种很好的解决方案:可继承一个新类型,然后添加自己的额外方法。在代码的其他地方,可以侦测自己的特定类型,并调用那个特殊的方法。这样做不会破坏多态性以及程序的扩展能力,因为新类型的添加不要求查找程序中的`switch`语句。但在需要新特性的主体中添加新代码时,就必须用RTTI侦测自己特定的类型。
从某个特定类的利益的角度出发,在基类里加入一个特性后,可能意味着从那个基类派生的其他所有类都必须获得一些无意义的“鸡肋”。这使得接口变得含义模糊。若有人从那个基类继承,且必须覆盖抽象方法,这一现象便会使他们陷入困扰。比如现在用一个类结构来表示乐器(`Instrument`)。假定我们想清洁管弦乐队中所有适当乐器的通气音栓(Spit Valve),此时的一个办法是在基类`Instrument`中置入一个`ClearSpitValve()`方法。但这样做会造成一个误区,因为它暗示着打击乐器和电子乐器中也有音栓。针对这种情况,RTTI提供了一个更合理的解决方案,可将方法置入特定的类中(此时是`Wind`,即“通气口”)——这样做是可行的。但事实上一种更合理的方案是将`prepareInstrument()`置入基类中。初学者刚开始时往往看不到这一点,一般会认定自己必须使用RTTI。
......
......@@ -79,6 +79,7 @@ v: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
1. 使用`protected`时的技巧
为避免我们创建的每个类都默认具有克隆能力,`clone()`方法在基类`Object`里得到了“保留”(设为`protected`)。这样造成的后果就是:对那些简单地使用一下这个类的客户程序员来说,他们不会默认地拥有这个方法;其次,我们不能利用指向基类的一个引用来调用`clone()`(尽管那样做在某些情况下特别有用,比如用多态性的方式克隆一系列对象)。在编译期的时候,这实际是通知我们对象不可克隆的一种方式——而且最奇怪的是,Java库中的大多数类都不能克隆。因此,假如我们执行下述代码:
```
Integer x = new Integer(l);
x = x.clone();
......@@ -337,9 +338,11 @@ public class DeepCopy {
```
`DepthReading``TemperatureReading`非常相似;它们都只包含了基本数据类型。所以`clone()`方法能够非常简单:调用`super.clone()`并返回结果即可。注意两个类使用的`clone()`代码是完全一致的。
`OceanReading`是由`DepthReading``TemperatureReading`对象合并而成的。为了对其进行深层复制,`clone()`必须同时克隆`OceanReading`内的引用。为达到这个目标,`super.clone()`的结果必须转换成一个`OceanReading`对象(以便访问`depth``temperature`引用)。
## 12.2.7 用`Vector`进行深层复制
下面让我们复习一下本章早些时候提出的`Vector`例子。这一次`Int2`类是可以克隆的,所以能对`Vector`进行深层复制:
```
......@@ -405,7 +408,9 @@ public class AddingClone {
```
`Int3``Int2`继承而来,并添加了一个新的基本类型成员`int j`。大家也许认为自己需要再次覆盖`clone()`,以确保`j`得到复制,但实情并非如此。将`Int2``clone()`当作`Int3``clone()`调用时,它会调用`Object.clone()`,判断出当前操作的是`Int3`,并复制`Int3`内的所有二进制位。只要没有新增需要克隆的引用,对`Object.clone()`的一个调用就能完成所有必要的复制——无论`clone()`是在层次结构多深的一级定义的。
至此,大家可以总结出对`Vector`进行深层复制的先决条件:在克隆了`Vector`后,必须在其中遍历,并克隆由`Vector`指向的每个对象。为了对`Hashtable`(散列表)进行深层复制,也必须采取类似的处理。
这个例子剩余的部分显示出克隆已实际进行——证据就是在克隆了对象以后,可以自由改变它,而原来那个对象不受任何影响。
## 12.2.8 通过序列化进行深层复制
......@@ -556,6 +561,7 @@ public class HorrorFlick {
## 12.2.10 为什么有这个奇怪的设计
之所以感觉这个方案的奇特,因为它事实上的确如此。也许大家会奇怪它为什么要象这样运行,而该方案背后的真正含义是什么呢?后面讲述的是一个未获证实的故事——大概是由于围绕Java的许多买卖使其成为一种设计优良的语言——但确实要花许多口舌才能讲清楚这背后发生的所有事情。
最初,Java只是作为一种用于控制硬件的语言而设计,与因特网并没有丝毫联系。象这样一类面向大众的语言一样,其意义在于程序员可以对任意一个对象进行克隆。这样一来,`clone()`就放置在根类`Object`里面,但因为它是一种公用方式,因而我们通常能够对任意一个对象进行克隆。看来这是最灵活的方式了,毕竟它不会带来任何害处。
正当Java看起来象一种终级因特网程序设计语言的时候,情况却发生了变化。突然地,人们提出了安全问题,而且理所当然,这些问题与使用对象有关,我们不愿望任何人克隆自己的保密对象。所以我们最后看到的是为原来那个简单、直观的方案添加的大量补丁:`clone()``Object`里被设置成`protected`。必须将其覆盖,并使用`implement Cloneable`,同时解决异常的问题。
......
......@@ -24,6 +24,7 @@ public class ImmutableInteger {
```
`Integer`类(以及基本的“包装器”类)用简单的形式实现了“不变性”:它们没有提供可以修改对象的方法。
若确实需要一个容纳了基本数据类型的对象,并想对基本数据类型进行修改,就必须亲自创建它们。幸运的是,操作非常简单:
```
......@@ -179,7 +180,9 @@ public class Immutable2 {
这一方法特别适合在下述场合应用:
(1) 需要不可变的对象,而且
(2) 经常需要进行大量修改,或者
(3) 创建新的不变对象代价太高
## 12.4.3 不变字符串
......
......@@ -155,6 +155,7 @@ throw e;
```
重新“抛”出一个异常导致异常进入更高一级环境的异常控制器中。用于同一个`try`块的任何更进一步的`catch`从句仍然会被忽略。此外,与异常对象有关的所有东西都会得到保留,所以用于捕获特定异常类型的更高一级的控制器可以从那个对象里提取出所有信息。
若只是简单地重新抛出当前异常,我们打印出来的、与`printStackTrace()`内的那个异常有关的信息会与异常的起源地对应,而不是与重新抛出它的地点对应。若想安装新的栈跟踪信息,可调用`fillInStackTrace()`,它会返回一个特殊的异常对象。这个异常的创建过程如下:将当前栈的信息填充到原来的异常对象里。下面列出它的形式:
```
......
......@@ -2,6 +2,7 @@
覆盖一个方法时,只能产生已在方法的基类版本中定义的异常。这是一个重要的限制,因为它意味着与基类协同工作的代码也会自动应用于从基类派生的任何对象(当然,这属于基本的OOP概念),其中包括异常。
下面这个例子演示了强加在异常身上的限制类型(在编译期):
```
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册