3.1.3 输入流的复用
输入流的复用其实有些自我矛盾的应用场景。一方面,在实际应用中,很多需要提供输入数据的API都使用InputStream类作为其参数的类型,比如XML文档的解析API就是一个典型的例子。同时很多数据的提供者允许使用者通过InputStream类的对象的方式来读取其数据,比如通过java.net.HttpURLConnection类的对象打开一个网络连接之后,可以得到用来读取其中数据的InputStream类的对象。如果每个这样的数据源仅有一个接收者,处理起来比较简单;如果有多个接收者,那么就有些复杂。主要的原因在于,从另一个方面出发,按照流本身所代表的抽象含义,数据一旦流过去,就无法被再次使用。如果直接把一个InputStream类的对象传递给一个接收者使用之后再传递给另外一个接收者,后者不能读取到流中的任何数据,因为流的当前读取位置已经到了末尾。这其实可以理解成数据的使用方式和数据本身的区别。InputStream类表示的是数据的使用方式,而并不是数据本身。两者的根本区别在于每个InputStream类的对象作为Java虚拟机中的对象,是有其内部状态的,无法被简单地复用;而纯粹的数据本身是无状态的。在实际开发中,需要复用一个输入流的场景是比较多的。比如,通过HTTP连接获取到的XML文档的输入流,可能既要进行合法性检验,又要解析文档内容,还有可能要保存到磁盘中。这些操作都需要直接接收同一个输入流。
对于现实应用中存在的对输入流复用的需求,基本上来说有两种方式可以解决:第一种是利用输入流提供的标记和重置的控制能力,第二种则是把输入流转换成数据来使用。
对于第一种解决方案来说,需要用到java.io包中的InputStream类的子类java.io.BufferedInputStream。正如这个类名的含义一样,BufferedInputStream类在InputStream类的基础上使用内部的缓冲区来提升性能,同时提供了对标记和重置的支持。BufferedInputStream类属于过滤流的一种,在创建时需要传入一个已有的InputStream类的对象作为参数。BufferedInputStream类的对象则在这个已有的InputStream类的对象的基础上提供额外的增强能力。
使用BufferedInputStream类之后,对流进行复用的过程就变得简单清楚。只需要在流开始的地方进行标记,当一个接收者读取完流中的内容之后,再进行重置即可。重置完成之后,流的当前读取位置又回到了流的开始,就可以再次使用。代码清单3-1中给出了一个示例。对于一个InputStream类的子类的对象来说,如果它本来就支持标记,那么不再需要用BufferedInputStream类进行包装。在使用流之前,首先调用mark方法来进行标记。这里设置的标记在重置之后允许读取的字节数是整数的最大值,即Integer.MAX_VALUE,这是为了能够复用整个流的全部内容。当流的接收者使用完流之后,需要显式地调用markUsed方法来发出通知,以完成对流的重置。
代码清单3-1 使用BufferedInputStream类进行流复用的示例
- public class StreamReuse {
- private InputStream input;
- public StreamReuse(InputStream input) {
- if (!input.markSupported()) {
-
this.input = new BufferedInputStream(input);
- } else {
-
this.input = input;
- }
- }
-
- public InputStream getInputStream() {
- input.mark(Integer.MAX_VALUE);
- return input;
- }
-
- public void markUsed() throws IOException {
- input.reset();
- }
- }
第二种复用流的方案是直接把流中的全部数据读取到一个字节数组中。在不同的流的接收者之间的数据传递都是通过这个字节数组来完成的,而不再使用原始的InputStream类的对象。从一个字节数组得到一个InputStream类的对象是很容易的事情,只需要从该字节数组上创建一个java.io.ByteArrayInputStream类的对象就可以了。完整的实现如代码清单3-2所示。在创建SavedStream类的对象时,作为参数传递的InputStream类的对象中的数据首先被写入到一个java.io.ByteArrayOutputStream类的对象中,再把得到的字节数组保存下来。
代码清单3-2 通过保存流的数据进行流复用的示例
- public class SavedStream {
- private InputStream input;
-
private byte[] data = new byte[0];
-
- public SavedStream(InputStream input) throws IOException {
-
this.input = input;
- save();
- }
-
- private void save() throws IOException {
-
ByteArrayOutputStream output = new ByteArrayOutputStream();
-
byte[] buffer = new byte[1024];
-
int len = -1;
-
while ((len = input.read(buffer)) != -1) {
- output.write(buffer, 0, len);
- }
-
data = output.toByteArray();
- }
-
- public InputStream getInputStream() {
- return new ByteArrayInputStream(data);
- }
- }
实际上,这两种复用流的做法在实现上的思路是一样的,都是预先把要复用的数据保存起来。BufferedInputStream类在内部有一个自己的字节数组来维护标记位置之后可供读取的内容,与第二种做法中的字节数组的作用是一样的。