3.1 流

流是Java中最早提供的对I/O操作的抽象,从JDK 1.0就存在了。流把I/O操作抽象成数据的流动。流所代表的是流动中的数据。对于传输的数据来说,除了最底层的字节表示外,还支持不同的抽象表示方式。对一个计算机程序来说,其数据的最终表现形式都是0或1的比特值。程序一般不直接处理单个的比特值,而是处理由8个比特组成的字节。不管是内存中的数据、磁盘上的数据,还是通过网络传输的数据,其基本格式都是一系列的字节。所不同的是,不同的程序对这一系列的字节有不同的解释方式。将一组字节按照特定的方式进行解释,就形成了编程语言中的不同的基本类型。比如在Java语言中,4个字节可以表示一个整数(int)或是单精度浮点数(float),而8个字节可以表示一个长整数(long)或是双精度浮点数(double)。单纯的一个字节序列可以有多种不同的解释方式。比如一个16个字节的数组,既可以解释成4个整数,也可以解释成2个双精度浮点数。不同的解释所表示的语义是完全不同的。这种解释工作是由应用程序自己来完成的,编程语言的类库一般会提供相关的支持。

Java中最基本的流是在字节这个层次上进行操作的。也就是说基本的流只负责在来源和目的之间传输字节,并不负责对字节的含义进行解释。在基本的字节流基础上,Java也提供了一些过滤流(filter stream)的实现。这些过滤流实际上是基本字节流上的一个封装,在其上增加了不同的处理能力,如基本类型与字节序列之间的转换等。这些过滤流对开发人员的接口更加友好,可以自动完成很多转换工作。

3.1.1 基本输入流

最基本的I/O流是java.io包中的抽象类java.io.InputStream和java.io.OutputStream。由于流的相关API设计得比较早,因此并没有采用现在流行的面向接口编程的思路,而是采用了抽象类。新的I/O相关的API则大量使用了接口。如果流的实现只对使用者暴露字节这个层次的细节,则可以直接继承InputStream或OutputStream类,并提供自己额外的能力。

输入流InputStream类中包含两类功能:一类是与读取流中字节数据相关的功能,另一类则是流的控制功能。读取流中的字节通过read方法来完成。该方法有3种重载形式:第一种形式不带任何参数,每次读取一个字节并返回;第二种形式使用字节数组作为缓冲区,用读取到的字节数据填充缓冲区;最后一种形式需要提供作为缓冲区使用的字节数组和数组中的起始位置和长度,读取到的字节数据被填充到缓冲区的指定位置上。这3种形式中,第一种是声明为abstract的,必须由子类来实现。而对于另外两种,InputStream类有自己的默认实现,通过循环调用第一种形式的read方法来填充缓冲区。

最常见的读取InputStream类的对象中的数据的方式是创建一个字节数组作为缓冲区,然后循环读取,直到read方法返回-1或抛出java.io.IOException异常。read方法的返回值是每次调用中成功读取的字节数。在读取数据的过程中,对read方法的调用是阻塞的。当流中没有数据可用时,对read方法的调用需要等待。这种阻塞式的特性可能会成为应用中的性能瓶颈。如果不使用字节数组作为缓冲区,read方法一次只能读入一个字节。在提供缓冲区的情况下,虽然InputStream类也只是以循环的方式每次读取一个字节来填充缓冲区,但是InputStream类的子类一般会为接受缓冲区作为参数的read方法提供更加高效的实现。这也是为什么使用缓冲区的重要原因。

从流本身所代表的抽象层次出发,它表示的是一个流动的字节流,如流水一样。正因为如此,流中所包含的字节一旦流过去,就无法再重新使用。从这个角度出发,对一般的输入流所能做的操作就只是顺序地读取,直到流的末尾或中间出现读取错误。当然,不同的输入流可能也支持额外的控制操作,因此InputStream类中也包含了相应的方法来允许其子类进行选择性地覆写。这也是采用抽象类的设计方式所带来的弊端。所有可能会用到的方法都需要在抽象类中进行声明。

第一个最直接的功能是关闭流,通过close方法来完成。在Java 7中,应该尽量通过1.5节介绍的try-with-resources语句来使用流,可以避免显式调用close方法。

第二个流控制功能是跳过指定数目的字节,相当于把流中的当前读取位置往后移动若干个字节。这个功能是通过skip方法来实现的。由于跳过若干个字节后,可能就已经到达了流的末尾,因此skip方法并不总能正确跳过指定数目的字节。调用者应该检查skip方法的返回值来获取实际跳过的字节数。并不是所有InputStream类的子类都支持skip方法。

第三个流控制功能是流的标记(mark)与重置(reset)。标记与重置配合起来使用,可以实现流中部分内容的重复读取,而不会像一般的读取操作那样,数据流过去之后就无法再次读取。简单来说,标记操作负责在流的当前读取位置做一个记号。当进行重置操作时,流的当前读取位置会被移动到上次标记的位置,这样就可以从上次标记位置开始再次进行读取操作。不是所有的流都支持标记功能,因此在使用mark方法来标记当前位置之前,需要通过markSupported方法来判断当前流的实现是否支持标记功能。在使用mark方法进行标记时,需要指定一个整数来表示允许重复读取的字节数。例如,标记时使用的是“mark(1024)”,那么在调用了reset之后,就只能从之前标记的位置开始再次重复读取最多1024字节。一般的内部实现方式是在标记之后把读取到的字节先保存起来。当重置之后,再调用read方法读取的就是之前保存的数据。

除了上面介绍的流控制方法之外,InputStream类的最后一个方法是available。这个方法与前面提到的InputStream类的对象进行读取操作时的阻塞特性相关。当read方法被调用,且当前流中并没有立即可用的数据时,这个调用操作会被阻塞,直到当前流成功地完成数据的准备为止。而available方法的作用在于告诉流的使用者,在不产生阻塞的情况下,当前流中还有多少字节可供读取。如果每次只读取调用available方法获取到的字节数,那么读取操作肯定不会被阻塞。这种非阻塞的特性在某些场合可能是很有作用的,比如在读取一个大文件的同时对文件的内容进行处理,如果每次读取时都不发生阻塞,就可以比较好地平衡数据读取和处理的时间。