(译)Java-RMI 第一章

来源:互联网 发布:淘宝seo教程 编辑:程序博客网 时间:2024/06/03 23:49

第一篇:设计与实现:RMI应用程序基础

第一章

本章讨论的重点是JAVA流类。它们定义于JDKjava.io.* 包中。虽然流技术与RMI框架应用技术关系不大,但是JAVA流类的工作原理对每一位RMI程序员来说都是至关重要的。特别强调的是,本章提及的诸多知识点都是学习书中后续章节基础。比如,读者对后续的Socket技术和对象序列化技术的学习效果就直接依赖于对本章内容的理解。

1.1 内核类

       从概念上讲,流就是有序的字节序列。但是,把流看作是“可以依次执行写入与读出操作的数据结构”会更形象。从定义来看, 对流的写和读操作是依次进行的(而不是并发执行的),也就是说,客户程序能够逐个字节地依次向流中写入数据或者逐个字节地依次从流中读取数据。需要明确指出的,绝大多数类型的流都不支持“回退”(或者说是“后悔”)功能,即一旦客户程序从流中读出了若干数据,那就它只能接着向后检索剩余的数据了。同样地,一旦客户程序向流中写入了数据,那么已经被写入的数据就不可能再被抹除了。

       乍看来,流的确是一个非常贫瘠的数据结构。对大多数任务来说,能直接存取对象的HashMapArrayList的确要比字节序列更受气重的多。但是,流拥有着独一无二的优点:对于计算机设备而言,流具备杰出的简易性和正确性。所谓正确性能够这样理解:无论客户程序发送数据至打印机还是客户程序发送数据至调制解调器,它们的代码实现机制都是相同的。这个机制被概括为“信息被逐个字节地发送至目的地,并且已经被发送的数据就绝不能再被收回了[1]”。因此,流就自然地成为了客户程序与计算机设备之间的理想抽象层。通过该抽象层,在数据交换过程中,客户程序就不必过多地关心计算机设备的各种细节信息了。

[1]  在现实生活中,虽然用户可以向打印机发送“取消打印请求”来撤消之前发送的“打印请求”,但是“打印请求”消息已经被发送的事实是无法改变的。

流的使用步骤可以被归纳为三个阶段:

1.       创建一个与设备相关的流实例。这个步骤被称为“打开”流。

2.       从流实例中读出数据或向流实例中写入数据。这个步骤是设备无关的,它仅与流接口定义有关。

3.       关闭流。

下面让我们来看一看由JDK提供的所有输入流与输出流的超类InputStreamOutputStream.

1.1.1 InputStream

InputStream既是一个抽象类也是所有输入流类的超类。它可以被看作是数据源。一旦输入流被打开,客户程序就可以从流中读取数据了。InputStream一般包括如下方法:

public int available( ) throws IOException

public void close( ) throws IOException

public void mark(int numberOfBytes) throws IOException

public boolean markSupported( ) throws IOException

public abstract int read( ) throws IOException

public int read(byte[] buffer) throws IOException

public int read(byte[] buffer, int startingOffset, int numberOfBytes) throws IOException

public void reset( ) throws IOException

public long skip(long numberOfBytes) throws IOException

这些方法按照用途不同可以分为三类,分别是:

1.       数据读取

2.       流导航

3.       资源管理

1.1.1.1 数据读取

数据读取方法是InputStream定义的所有方法中最重要的方法。属于这一类的方法有三个,分别是:

public int read() throws IOException

public int read(byte[] buffer) throws IOException

public int read(byte[] buffer, int startingOffset, int numberOfBytes) throws IOException

其中,第一个方法,read(),返回位于流中当前位置紧后一个字节的字节值。此字节返回值是以0255之间整数来表示的。如果返回值为-1,则表示从流中不能再获得任何的数据了。通常,当客户程序到达文件尾时,read()会返回-1。另一方面,倘若在read()方法执行过程中出现数据临时无法得到的情况,read()方法会被阻塞。

旁白当一段程序为了等待获得某个资源的控制权而不能正常地结束时,它就处于阻塞状态了。通常,从文件中读取数据时,read()方法就有可能为了等待获得对目标硬盘驱动器的控制权而被迫停止执行进入阻塞状态。阻塞常常会给我们带来许多的麻烦。比如说,当客户程序一直在等待一个永远都不会到来的字节时,这段程序就已经崩溃了。

另外两个方法是无参数read()方法的高级版本(它们更有效率)。例如,一段程序要从计算机设备中读取65000个字节。如果客户程序逐个字节地执行读操作,那么这段程序的运行效率会是非常地低下。但是,如果你已经事先知道要读取数据的数据量大小,那最好的做法就是一次性地把所有的数据都读出来放到内存里,而不是一个字节接一个字节地提取,具体方法如下:

byte buffer = new byte[1000];

read(buffer);

read(byte[] buffer)方法的作用是从流中一次性地读取缓冲区大小的字节序列(在本例中就是buffer.length个字节)。read(byte[] buffer)方法的返回值是一个整数表示被实际读取的字节个数。如果方法的返回值是-1,就表示没有数据被读入。

       最后,read(byte[] buffer, int startingOffset, int numberOfBytes)方法的作用是从流中读取numberOfBytes个字节,然后将它们放置在缓冲区内以buffer[startingOffset]开始(包括buffer[startingOffset])的后续空间里。例如,

read(buffer, 2, 7);

这条语句将从流中读取7个字节,然后将它们分别放置在buffer[2], buffer[3]… buffer[8]中。像前面的方法一样,它也会返回一个整数返回值,以代表实际从流中读取的字节个数。如果返回值为-1,则表示没有数据被读入。

1.1.1.2 流导航

流导航方法用来前后移动漂浮于流中的游标。它们分别为:
public int available( ) throws IOException
public long skip(long numberOfBytes) throws IOException
public void mark(int numberOfBytes) throws IOException
public boolean markSupported( ) throws IOException
public void reset( ) throws IOException

首先,available()方法用来统计客户程序能够立即从流中检索出的字节个数。通常来说,在调用read()方法进行实际数据读取操作之前,客户程序都会先使用available()方法来判断此时流中是否有数据可以获得,以避免出现阻塞现象。代码片段如下,
while (stream.available( ) >0 ) {
    processNextByte(stream.read( ));
}

旁白关于available()方法的用法有两个注意事项:

1.  你要确定在流定义中已经正确地实现了available()方法。在InputStream中,available()方法的默认实现是仅只返回0值的。如果在它的子类中没有正确地重载此方法的话,其它相关模块内的程序就会被误导(例如,在上面的代码片段中,如果available()方法仅只返回0值的话,那么循环体内的程序是永远也不会被执行的)。

2.  你一定要使用缓存。至于如何通过缓存来提高读操作的效率,我们将在1.3节以后再进行详细地讲解。

其次,skip()方法将向前推进游标numberOfBytes个字节。对于大多数InputStream子类的实现来说,skip()方法都是以不断地读入字节的方式向前推动游标。

旁白事实上,大多数skip()方法的实现都是以连续地从流中读入字节的方式向前推动游标。因此,如果skip()方法正要读入的字节由于某些原因不能被立即获得,skip()方法和read()方法一样会出现阻塞的现象。而这是值得所有程序开发人员注意的。

许多输入流都只支持“仅进游标”,即游标只能向前推进不能后退。为支持“双向游标”,在流定义中就必须实现“标记(marking)”功能。而“标记”功能的原理其实十分简单,就是:在客户程序从流中读入字节数据的同时标记当前位置点,以备今后将游标再重新置回此位置。客户程序使用的输入流对象是否支持“标记”功能可以通过markSupported()方法的返回值来判断。如果返回值为true,表示支持;否则表示不支持。假设客户程序使用的输入流对象支持“标记”功能,为了实现游标的“后退”,你就需要使用mark()方法来标记游标在流中当前位置。然后,在以后的某个时间点,客户程序就能够再调用reset()方法将游标重置到被标记的位置。mark()方法的唯一的参数numberOfBytes是用来指定标记点的过期条件的。具体地说,就是当游标从标记点开始再往前推进多少个字节之后系统就会自动将标记点从记忆中抹除。

旁白InputStream和其子类最多只可以记忆一个标记点。因此,如果你在输入流中记录第二个标记点的话,系统会自动地抹除第一个标记点。

1.1.1.3 资源管理

因为流通常要关联计算机设备(文件或网络连接),所以使用流就要求操作系统分配非内存的资源。但是,出于性能方面的考量,绝大多数操作系统都会限制一个程序(或者说,进程)能够同时打开的文件与网络连接的个数。在InputStream中定义的资源管理方法是通过调用本地API来实现对系统资源的管理。

InputStream抽象类中定义的唯一的资源管理方法是close()。无论何时客户程序结束了对流操作之后,它都必须以显示地调用close()方法来关闭流,释放系统资源(比如说,文件句柄)。

乍看来,这种作法有些让人费解。毕竟,JAVA语言的一个突出的卖点就是它的内置于语言规范中的垃圾回收机制。为什么不能由垃圾收集器来自动地释放系统资源呢?

原因是:垃圾收集器不可靠。JAVA语言规范的确清晰地要求JVM必须拥有自动的垃圾回收机制,但是它并没有保证当某个对象开始不被任何引用指向时,它就会被立即回收。JAVA语言规范甚至也没有明确地要求当某个对象开始不被任何引用指向时,垃圾收集器会立刻启动运行。而事实上,我们唯一能够确定的事实是:如果某个客户程序快要耗尽它的配额内存时,垃圾收集器才会被激活来回收那些已经没有任何引用指向的对象并且释放相应的内存空间。这种懒惰的垃圾回收机制是完全不足以管理稀缺的系统资源的。综上所述可以归纳为三点:

l         你不能控制一个对象从它应该被回收和实际被回收之前的时间间隔。

l         你不能控制对象被回收的次序。[2

l         你能支配的文件句柄的个数与你未占用的内存余量之间没有必然的因果联系。通常,在耗尽内存之前,你就已经早早地用光了所有可用的文件句柄了。而这时,垃圾收集器还在后台待命呢。

[2] 客户程序可以使用软引用(SoftReference)来最低程度地干预垃圾收集器回收对象的次序。SoftReference是在java.lang.ref包中被定义

简而言之,垃圾收集器对于系统资源的管理是不可靠的。无论何时使用稀缺的系统资源,客户程序都有义务显示地释放资源。请记住,关闭被你打开的流是对你最低的要求。

1.1.2 输入输出异常(IOException)

InputStream抽象类中定义的所有方法都会抛出IOException异常。IOException异常是一个必须被客户程序捕获的异常。从代码层面来说,所有的流操作程序都必须出现在try/catch块内,请看下面的代码片段:
      
try{
        while( -1 != (nextByte = bufferedStream.read( ))) {
            char nextChar = (char) nextByte;
            ...
        }
    }catch (IOException e) {
        ...

      
}

由于流总是被用来与计算机设备进行数据交换,所以IOException被引入以便当设备出现了故障时,可以将故障信息立即通报给用户。

例如,当打印机由于缺纸而暂停打印任务时,它就会向提交打印请求的客户程序抛出一段异常信息。但是,由于客户程序是不可能在没有人为干预的情况下自动向打印机的托盘中塞满打印纸,所以这个打印异常信息应该被立即地发送给终端用户。

大部分流异常发生的场景都与上面的那个例子类似。它们都或多或少地需要一定程度的用户干预(至少,要求用户知情)。因此,这些异常都要求被实时地处理。基于这些考虑,流程序库的设计师们把IOException异常设计成为必须被客户程序捕获的异常类型,以强制客户程序的开发程序员显示地、明确地处理可能会出现失败。

旁白预告:RMI框架也遵循了相类似的设计哲学。在远程接口中定义的所有的远程方法都被强制要求抛出RemoteException异常(客户程序必须显示地捕获与处理它)。RemoteException异常表示“在JVM之外某些设备出现了故障”。 

1.1.3 输出流(OutputStream

OutputStream也是一个抽象类。它可以被看作是数据宿。输出流一旦被打开,客户程序就可以向输出流中发送数据了。OutputStream中包括的方法有:
public void close( ) throws IOException
public void flush( ) throws IOException
public void write(byte[] buffer) throws IOException
public void write(byte[] buffer, int startingOffset, int numberOfBytes)  throws IOException
public void write(int value) throws IOException

OutputStream抽象类有一点类似于InputStream抽象类,但是它并不支持流导航方法。因为数据一旦已经被发送,你就不可能再把它给追回来了。从功能的角度分析,OutputStream抽象类定义的方法可以被分成两类,分别是:

1.       写数据

2.       资源管理

1.1.3.1 写数据

OutputStream抽象类定义了三个写数据方法:
public void write(byte[] buffer) throws IOException
public void write(byte[] buffer, int startingOffset, int numberOfBytes) throws IOException
public void write(int value) throws IOException

这些写数据方法类似于InputStream抽象类中的read()方法。write(int value)方法一次只能向流中写入一个字节。它唯一的输入参数是一个取值范围在0255之间的整数。如果参数值大于255,系统将会自动地将其对256的模作为方法的输入参数。

write(int value)方法也有两个基于数组的变体。write(byte[] buffer)方法将把缓冲区内的所有字节都一次性地写入流中。write(byte[] buffer, int startingOffset, int numberOfBytes)方法将缓冲区内从buffer[startingOffset]字节开始向后numberOfBytes个字节写入流中。

旁白大家可能会很费解为什么write()方法的输入参数也是一个整数呢?read()方法返回整数而不是直接返回一个字节是为了方便向外界通告状态信息。而把write()方法的输入参数也设计成一个整数,则是考虑到了write()方法与read()方法的设计对称性。这样一来,客户程序从输入流出读取的整数只要不等于-1,就可以在不做任何类型转换的条件下直接地写入到输出流中了。

1.1.3.2 资源管理

       OutputStream抽象类定义了两个资源管理方法,分别是:
public void close( )
public void flush( )

       无论何时结束了对输出流的操作之后,客户程序都有义务显示地调用close()方法关闭流和释放系统资源。

       因为缓存技术在文件操作和网络通信中广泛使用,所以flush()方法专门用来将缓冲区内保存的所有待发送数据一次性地发送到底层流或计算机设备中,然后再清空缓冲区。之所以要在Java虚拟机中缓存待发送的数据是因为在客户程序与操作系统之间逐个字节的交换数据是昂贵的也是低效的。

1.2 浏览一个文件

       为了使上面提到的知识更加具体,本节举一个被称为ViewFile应用程序的例子。此案例应用程序能够把用户指定文件中的内容打印输出到JTextArea文本域中。应用程序入口main()方法定义于com.ora.rmibook.chapter1.ViewFile类中[3]ViewFile应用程序的编号是“Example 1-1运行结果快照图片,如下。

       [3] 这个例子使用了Java Swing库中的类。如果你想学习更多关于Swing方面的知识。请您看Java Swing (O'Reilly)Java Foundation Classes in a Nutshell (O'Reilly)

图片 1-1 ViewFile应用程序

 viewfile

Example 1-1. ViewFile.java

public class ViewfileFrame extends ExitingFrame{

    // 许多启动图形用户界面的代码

    // View按钮的事件监听器是一个内部类。

    private void copyStreamToViewingArea(InputStream fileInputStream) throws IOException {
        BufferedInputStream bufferedStream = new BufferedInputStream(fileInputStream);
        int nextByte;
        _fileViewingArea.setText("");
        StringBuffer localBuffer = new StringBuffer( );
        while( -1 != (nextByte = bufferedStream.read( ))) {
            char nextChar = (char) nextByte;
            localBuffer.append(nextChar);
        }
        _fileViewingArea.append(localBuffer.toString( ));
    }
    private class ViewFileAction extends AbstractAction {
        public ViewFileAction( ) {
            putValue(Action.NAME, "View");
            putValue(Action.SHORT_DESCRIPTION, "View file contents in main text area.");
    }
    public void actionPerformed(ActionEvent event) {
        FileInputStream fileInputStream =_fileTextField.getFileInputStream( );
        if (null==fileInputStream) {
            _fileViewingArea.setText("Invalid file name");

        } else {
            try {
                copyStreamToViewingArea(fileInputStream);
                fileInputStream.close( );
            }catch (java.io.IOException ioException){
                _fileViewingArea.setText("/nError occured while reading file");
            }
        }
    }

}

       程序中两个最重要的部分是View按钮的单击事件处理程序和copyStreamToViewingArea()方法。copyStreamToViewingArea()方法接受一个InputStream实例为输入参数,然后将输入流中的所有数据都复制到图形用户界面中的JTextArea文本域中去。当用户单击了View按钮时到底发生了什么事情呢?假设一切都正常地执行且没有任何异常被抛出的话,View按钮的单击事件处理程序中关键的三行代码会被依次被执行,如:
FileInputStream fileInputStream = _fileTextField.getFileInputStream( );
copyStreamToViewingArea(fileInputStream);
fileInputStream.close( );

第一行语句从文本框中读入文件名,然后打开该文件名指向文件的文件输入流FileInputStreamFileInputStreamjava.io.*包中定义。它是InputStream抽象类的子类且专门用来读取文件中的内容。

       一旦输入流被打开,客户程序就会调用copyStreamToViewingArea( )方法。copyStreamToViewingArea( )方法接收输入流实例为参数,然后给它包装一层缓存功能,最后从带有缓存功能的输入流包装中逐个字节地读取数据。在此案例应用程序中有两个要点需要格外地强调以引起注意:

l         客户程序必须显示地检查nextByte中的值是否等于-1(以确定,客户程序是否已经读到了文件尾了)。如果在程序中不检查nextByte的值,那么循环操作将永远执行下去,直到程序崩溃或者有异常被抛出。同时,在文本域被显示内容的末尾处将被追加上无以计数的(char) -1

l         客户程序给FileInputStream实例包装了一层BufferedInputStream实例,而不是赤裸地直接使用FileInputStream实例。从内部机制来说,BufferedInputStream内部维护了一个缓冲区,从而使它可以从底层流中一次读取多个字节,最终达到优化读操作的目的。更形象地说,BufferedInputStream把客户程序对它的read()方法的调用翻译成了客户程序对FileInputStreamread(byte[] buffer)方法的调用。请注意缓存功能给底层流带来的另一个好处就是“它使得流可以支持流导航功能”。

旁白当然,操作系统通常也会实现对文件读写的缓冲功能。既便如此,使用本地API实现客户程序与操作系统之间的数据交换仍然是昂贵的且需要在JVM中提供缓存功能提高数据交换效率。

1.3 流的层次化

Example 1-1中,BufferedInputStream的用法说明了流编程的核心设计思想就是:通过将一个流实例包装在另一个流实例中可以为原有的底层流实例提供更加强大的功能。根据这个概念,我们就可以把所有的流分为两大类,即 原始流 中间层流。

原始流

       只有原始流才能够透过本地方法与计算机设备进行数据交换。他们所做的全部事情就是利用本地API在客户程序与计算机设备之间进行准确的数据转发。例如,FileInputStreamFileOuputStream都是原始流。

中间层流

       中间层流不能够直接连通到计算机设备。他只能作为其它流的包装器,以便给被包装的流对象更多更丰富的功能。那些被中间层流包装的流(无论它是原始流或也是中间层流),我都统称它们为底层流。在代码设计上,通过构造函数参数的方式把底层流实例传输给中间层流实例。中间层流的read()write()方法中实现了更复杂的逻辑,比如缓存、压缩与加密等。中间层流也会将客户程序对flush()方法和close()方法的调用传递给底层流。BufferedInputStreamBufferedOutputStream就是典型的中间层流。

流,重用 测试

InputStreamOutputStream都是抽象类。FileInputStreamFileOutputStream都是它们具体的子类。在软件设计领域,一个非常有争议的问题就是“方法签名”。例如,下面有四个方法签名,它们分别是:
      
parseObjectsFromFile(String filename)
   parseObjectsFromFile(File file)
   parseObjectsFromFile(FileInputStream fileInputStream)
   parseObjectsFromStream(InputStream inputStream)

显而易见,前三个方法签名有更优秀的自我描述性;读者可以很轻松地知道在前三个方法中使用的数据是来自于文件的。并且,正是因为这种硬编码式的方法签名明确地指定了许多细节信息,所以程序员就可以对输入数据做出更多的假设与猜测。(例如,FileInputStream中定义的skip()方法通常不容易阻塞,所以能够被安全地调用)。

然而,在最后一个方法签名中由于指定了更少的细节信息,从而使它更容易被重用。比方说,第四个方法签名能够兼容除了文件流以外其它类型的数据源的情况(像与Socket关联的流或与内存关联的流)。

此外,最后一个方法签名还更易于测试。由于JAVA支持基于内存的流类型(ByteArrayInputStream.),所以给第四种签名的方法写测试用例更简单更方便。下面我就给大家演示一下:
      
public boolean testParsing( ) {
      String testString = "A string whose parse results are easily checked for" + "correctness."
      ByteArrayInputStream testStream = new ByteArrayInputStream(testString.getBytes( ));
      parseObjectsFromStream(testStream);
              // 打印显示解析结果的代码。
      
}

这种的微型测试用例称为单元测试。定期地进行单元测试能够给你和你的项目带来许多的好处。其中,比较显著的益处有:

l         单元测试用例本身就是一份最清晰的程序说明文档。

l         单元测试使你能够更自信地面对代码变动,因为单元测试就能够捕获在编码过程出现的各种逻辑错误与功能遗漏。

最后,为了进一步学习单元测试、单元测试框架以及如何在你的代码中无缝地整合单元测试技术的内容,请读Extreme Programming Explained: Embrace Change by Kent Beck (Addison Wesley)

旁白“对close()flush()的方法调用也会传递给Socket”,乍听起来有些难于理解,但是它实际要表达的核心知识点就是:如果客户程序关闭了一个与Socket关联的流的话(无论是原始流,还是中间层流),那么底层的Socket也会被关闭。

       这种流设计虽然合情合理,但是可能还是会让你大吃一惊。

1.3.1 压缩文件

为了更进一步地解释“层次化”的概念我们来举个例子,我将用CompressFile案例演示的GZIPOutputStream用法。GZIPOutputStream定义于java.util.zip包中。这个例子的编号是Example 1-2

       CompressFile应用程序要求用户首先选择一个文件,然后制作那个文件的压缩拷贝。在制作压缩复本的过程中,CompressFile应用程序层次化地整合了三个输出流。具体作法如下,它先创建了一个FileOutputStream实例,然后把FileOutputStream实例作为BufferedOutputStream构造函数参数创建一个BufferedOutputStream实例,接着把BufferedOutputStream实例作为GZIPOutputStream构造函数参数创建GZIPOutputStream实例。最后,所有的数据都通过最外层的GZIPOutputStream.实例写入流中。顺便提一下,整个应用程序的入口main()方法是被定义于com.ora.rmibook.chapter1.CompressFile类中。

       整个应用程序实现中最重要的代码基本上都集中在copy()方法和Compress按钮的单击事件处理程序中了。其中,copy()方法用于从InputStreamOutputStream逐一地复制和压缩数据。CompressFile应用程序的运行时快照图片Figure 1-2如下:

Figure 1-2. CompressFile应用程序

CompressFile 

Example 1-2. CompressFile.java

private int copy(InputStream source, OutputStream destination) throws IOException {
    int nextByte;
    int numberOfBytesCopied = 0;
    while(-1!= (nextByte = source.read( ))) {
        destination.write(nextByte);
        numberOfBytesCopied++;
    }
    destination.flush( );
    return numberOfBytesCopied;
}

private class CompressFileAction extends AbstractAction {
//
省略启动代码
    public void actionPerformed(ActionEvent event) {
        InputStream source = _startingFileTextField.getFileInputStream( );
        OutputStream destination = _destinationFileTextField.getFileOutputStream( );
        if ((null!=source) && (null!=destination)) {
            try {
                BufferedInputStream bufferedSource = new BufferedInputStream(source);
                BufferedOutputStream bufferedDestination = new

BufferedOutputStream(destination);
                GZIPOutputStream zippedDestination = new GZIPOutputStream(bufferedDestination);
                copy(bufferedSource, zippedDestination);
                bufferedSource.close( );
                zippedDestination.close( );
            }catch (IOException e){}
        }
    }
}

1.3.1.1 工作原理

       当用户单击Compress按钮时,两个输入流和三个输出流会被创建。其中,输入流的用法与ViewFile案例应用程序中输入流的用法相同,即利用缓存功能提高文件的读取效率,而输出流就是一个全新的内容了。首先,我们创建了一个FileOutputStream实例,然后给它包装上BufferedOutputStream实例,最后再在最外层包装上GZIPOutputStream实例。为了理解层次化输出流的原理,我们就必须搞明白当客户程序向最外层的输出流GZIPOutputStream写入数据时到底发生了什么。

1.       zippedDestinationwrite(nextByte)方法会被反复地调用。

2.       zippedDestination并不会立即将接收到的数据转发给底层输出流实例bufferedDestination。相反,它会先压缩接收到的数据,然后再使用zippedDestinationwrite(int value)方法将压缩后的数据向底层输出流bufferedDestination传送。

3.       bufferedDestination也不会立即将接收的数据转发给底层的输出流实例destination,而是将被接收的数据缓存起来,当被缓存的数据量足够多时,再调用destinationwrite(byte[] buffer)方法一次性地将数据写入物理文件中。

copy()方法中,当所有的数据都已经被读入之后,zippedDestinationflush()方法就会被调用。于是,bufferedDestination将缓存的所有数据全部推送到destination中,接着destination再将数据持久化到物理文件内。最后,在actionPerformed()方法中,所有的输出流会被依次关闭且将宝贵的系统资源交还给操作系统。

1.3.2 常用的中间层流

在讨论流技术的末篇,我将介绍几个在日常工作中最常用的中间层流。除了缓存与压缩之外,还有两组常用的中间层流分别是DataInputStream/DataOutputStreamObjectInputStream/ObjectOutputStream。后者,我们将在第十章详细地讨论。

压缩流

DeflaterOutputStream是一个抽象类。同时,它也是所有具有数据压缩功能输出流的超类。GZIPOutputStream是由JDK提供的默认压缩类。同样地,DeflaterInputStream是一个抽象类。同时,它也是所有具有数据解压缩功能的输入流的超类。GZIPInputStream是由JDK提供的默认解压缩类。

在大部分情况下,它们的用法与其它类型的流无异。但是,存在着一个例外,就是“在DeflaterOutputStream类定义中,flush()方法的实现是有违惯例的”。在其它类型的流定义中,flush()方法将提交本地缓存中的所有数据到底层流或计算机设备,然后清空本地缓存。这种做法可以被总结为“最多处理数据原则”。

但是,在DeflaterOutputStream定义中的flush()方法仅仅调用底层流中的flush()方法,而不做其它的任何事情。请见代码:
public void flush( ) throws IOException {
   out.flush( );
}

这就意为着本地缓存数据根本就没有被提交。比如说,字符串“Roy Rogers”需要被压缩为长度为51个比特的字节序列,而且随着的程序的执行已经有48个比特的字节序列(6个字节)被提交到(有缓存功能的)底层流中了。就在此时,如果客户程序再调用flush()方法,那它实际上只是将底层流中的48个比特的数据写入了设备中了,但是至少还有3个比特的数据仍旧滞留在本地缓存中。

为了解决这个问题,DeflaterOutputStream定义了一个新方法,叫作finish()finish()方法有能力提交本地缓存的所有数据到底层流中,但是作为代价就是它会牺牲掉一些性能。

DataInputStreamDataOutputStream并没有对接收的数据做任何型式的修改。但是,由于它们都分别实现DataInputDataOutput接口,所以这一对中间层输入/输出流就兼具有了将其它类型的数据转化为字节序列的能力,以及反向处理的能力。例如,DataOutput接口中定义的writeFloat(float value)方法能够将IEEE 754 浮点数以(四字节)字节序列的形式写入底层流。

       如果数据是由DataOutputStream实例编码且写入底层流,那么此数据就只能被DataInputStream实例从底层流中读出和解码。于是,在这里就引入了流程序设计的一个重要原则:
             
所有具有数据处理功能的中间层输入/输出流必须被成对地使用。
具体地说,就是如果数据被压缩,那它就必须被解压缩;如果数据被加密,那它就必须被解密;如果写数据时用DataOuputStream实例编码,那在读数据时就必须用DataInputStream实例解码。

旁白虽然我们在这里并没有特别深入地讲解流的相关技术与概念。但是,以上所介绍的知识已经足以帮助我们理解RMI技术了。如果您想学习更多关于流技术的知识,那么请深度学习JDK及其文档 这总是首选的方法 或者阅读Java I/O by Elliotte Rusty Harold (O'Reilly)也是不错的选择。

1.4 ReaderWriter

本章涉及的最后一个主题是ReaderWriter抽象类。Reader/Writer很像InputStream/OuputStream。只是它们处理的数据类型不同:InputStream/OuputStream是面向“字节”的;而Reader/Writer是面向“字符与字符串”的。

Reader/Writer封装了对本地字符集的操控代码,以解决“国际化”的问题。此外,Reader/Writer还采用了类似于流的数据交换模式。在Reader/Writer抽象类中定义的方法集与在InputStream/OutputStream抽象类中定义的方法集极为类似。比如,在Reader抽象类中定义的方法有:
public void close()
public void mark(int readAheadLimit)
public boolean markSupported()
public int read()
public int read(char[] cbuf)
public int read(char[] cbuf, int off, int len)
public boolean ready( )
public void reset( )
public long skip(long n)

       其中,read()方法非常接近于InputStream抽象类中定义的read()方法。一方面,Readerread()方法也是返回一个整数且返回值为-1时也表示没有数据再可以读取了;另一方面,Readerread()方法返回值的取值范围却由从0255(一个字节)变成了从065535(两个字节宽度的一个字符)。

       另一个重要的变化在于Reader抽象类用ready()方法代替了InputStream定义的available()方法。ready()方法返回一个布尔值。如果它的返回值为true,表示read()方法在下次被调用时不会出现阻塞现象。测试Readerready()方法的返回值是否为真就等同于检查InputStreamavailable()方法的返回值是否大于零。

       Reader/Writer子类的数量远远少于InputStream/OutputStream子类的数量。但是,Reader/Writer通常是被作为流层次化结构中最外部的包装层 大多数Reader都有一个接受InputStream子类实例为输入参数的构造函数,而且大多数Writer也都有一个接受OutputStream子类实例为输入参数的构造函数。因此,为了在写文件过程中实现数据压缩与本地化功能,客户程序就需要先打开一个文件流,然后给它包装上一层具有数据压缩功能的中间层流实例,最后在最外层包装一个Writer子类的实例。具体的实现代码如下:
FileOutputStream destination = new FileOutputStream(fileName);
BufferedOutputStream bufferedDestination = new   BufferedOutputStream(destination);
GZIPOutputStream zippedDestination = new GZIPOutputStream(bufferedDestination);
OutputStreamWriter destinationWriter = new    OutputStreamWriter(zippedDestination);

1.4.1 重新审视ViewFile应用程序

JDK中有一组非常常用的Reader/Writer子类对:BufferedReaderBufferedWriter。与其它具备缓冲功能的中间层流不同,BufferedReaderBufferedWriter向超类的类定义中追加了用于字符串操作的新方法。比如说,在BufferedReader中添加一个能够从流中一次提取出一整行数据的readLine()方法;在BufferedWriter中添加了一个能够向输出流末尾追加行分隔符的newLine()方法

这些类在读写复杂的数据时可以大大简化编程工作。例如,一个换行符就通常被用来作为“当前记录的结束符”。为了举例说明这些类的用法,我用BufferedReader类重写了ViewFileFrame教学案例中的View按钮单击事件处理程序。代码如下:
private class ViewFileAction extends AbstractAction {
    public void actionPerformed(ActionEvent event) {
        FileReader fileReader = _fileTextField.getFileReader();
       if (null==fileReader) {
           _fileViewingArea.setText("Invalid file name");
       }else{
           try {
               copyReaderToViewingArea(fileReader);
               fileReader.close( );
           }catch (java.io.IOException ioException) {
               _fileViewingArea.setText("/n Error occured while reading file");
           }
       }
    }
    private void copyReaderToViewingArea(Reader reader) throws IOException {

        BufferedReader bufferedReader = new BufferedReader(reader);
        String nextLine;
        _fileViewingArea.setText("");
        while( null != (nextLine = bufferedReader.readLine( ))){
            _fileViewingArea.append(nextLine + "/n");
        }
    }

}

原创粉丝点击