Java I/O系统学习系列二:输入和输出
编程语言的I/O类库中常使用流这个抽象概念,它代表任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象。“流”屏蔽了实际的I/O设备中处理数据的细节。
在这个系列的第一篇文章:<<Java I/O系统学习系列一:File和RandomAccessFile>>中,我们讲到RandomAccessFile可以写入和读取文件,具备I/O功能,但是其只能针对文件,而I/O还涉及到很多其他场景比如网络、读取内存中的字符串等,所以Java类库中提供了一系列的类库来对其进行支持,也就是本文要总结学习的。
Java类库中的I/O类分成输入和输出两部分,可以在JDK文档里的类层次结构中查看到。通过继承,任何自Inputstream或Reader派生而来的类都含有名为read()的基本方法,用于读取单个字节或者字节数组。同样,任何自OutputStream或Writer派生而来的类都含有名为write()的基本方法,用于写单个字节或者字节数组。但是,我们通常不会用到这些方法,它们之所以存在是因为别的类可以使用它们,以便提供更有用的接口。因此,我们很少使用单一的类来创建流对象,而是通过叠合多个对象来提供所期望的功能(这是装饰器设计模式的应用,也有专门写文总结过:装饰器模式)。实际上,Java中“流”类库让人迷惑的主要原因就在于:创建单一的结果流,却需要创建多个对象。
I/O需要应对的场景往往是多样化的,Java类库的设计者则是通过创建大量的类来解决这个难题,区区一篇文章难以详述,本文也只是尽力对传统I/O类库所涉及到的类提供一个总览,在把握整个脉络的前提下才能更好的理解并应用I/O类库来解决实际编程问题。如需涉及到细节,还是需要参考JDK文档。
输入输出主要是字符流和字节流,本文主要从如下几个方面总结:
1. InputStream/OutputStream
Java 1.0中,类库的设计者首先限定与输入有关的所有类都应该从InputStream继承,而与输出有关的所有类都应该从OutputStream继承。
1.1 InputStream
InputStream的作用是用来表示那些从不同数据源产生输入的类。这些数据源包括:
- 字节数组;
- String对象;
- 文件;
- “管道”,工作方式与实际管道相似,即从一端输入,从另一端输出;
- 一个由其他种类的流组成的序列,以便我们可以将它们收集合并到一个流内;
- 其他数据源,如Internet连接等;
每一种数据源都有相应的InputStream子类,作为基础构件:
- ByteArrayInputStream,允许将内存的缓冲区当作InputStream使用;
- StringBufferInputStream,将String转换成InputStream;
- FileInputStream,用于从文件中读取信息;
- PipedInputStream,产生用于写入相关PipedOutputStream的数据。实现“管道化”概念;
- SequenceInputStream,将两个或多个InputStream对象转换成单一InputStream;
1.2 OutputStream
OutputStream的作用是表示那些可以输出到不同数据源的类,其具体的子类决定了输出所要去往的目标:字节数组、文件或管道,同样是作为基础构件:
- ByteArrayOutputStream,在内存中创建缓冲区。所有送往“流”的数据都要放置在此缓冲区;
- FileOutputStream,用于将信息写至文件;
- PipedOutputStream,任何写入其中的信息都会自动作为相关PipedInputStream的输出,实现“管道化”概念;
1.3 装饰器
除了有上面的基础构件,还有两个子类:FilterInputStream/FilterOutputStream,也是InputStream和OutputStream的子类,它们为“装饰器”(decorator)类提供基类,其中,“装饰器”类可以把属性或有用的接口与基础构件连接在一起。因为上面提到的InputStream/OutputStream是单字节为单位来操作的,而真实的I/O场景远不止于此,所以就通过“装饰”(其原理是类之间的组合)的方式来扩展其功能。
我自己梳理了一下InputStream/OutputStream流继承层次结构,结合下面的解释来看可以对字节流体系有一个更清晰的认识:
1.3.1 FilterInputStream
FilterInputStream类主要有如下子类,也就是具体装饰器:
- DataInputStream;
- BufferedInputStream;
- LineNumberInputStream;
其提供的装饰功能主要在两个方面:
-
读取不同的基本类型数据以及String对象,比如DataInputStream;
-
在内部修改InputStream的行为方式:是否缓冲、是否保留它所读过的行,如BuffereInputStream、LineNumberInputStream;
1.3.2 FilterOutputStream
与FilterInputStream类似,FilterOutputStream主要是完成写入的功能,主要有如下装饰器:
- DataOutputStream,与DataInputStream搭配使用,因此可以按照可移植方式向流中写入基本类型数据(int、char、long);
- PrintStream,用于产生格式化输出。其中DataOutputStream处理数据的存储,PrintStream处理显示;
- BufferedOutputStream,使用它以避免每次发送数据时都要进行实际的写操作。代表“使用缓冲区”。可以调用flush()清空缓冲区;
2. Writer/Reader
InputStream和OutputStream是提供面向字节形式的I/O,但是InputStream/OutputStream流继承层次结构仅支持8位字节流,并且不能很好地处理16位的Unicode字符。由于Unicode用于字符国际化(Java本身的char也是16位的Unicode),所以添加Reader/Writer继承层次结构就是为了在所有的I/O操作中都支持Unicode。
几乎所有原始的Java I/O流类都有相应的Reader和Writer类来提供天然的Unicode操作,我们可以对比一下:
我们发现大体上,这两个不同的继承层次结构中的接口即使不能完全相同,但是也是非常相似的。
对于InputStream和OutputStream来说,我们会使用FilterInputStream和FilterOutputStream的装饰器子类来修改“流”以满足特殊需要。Reader/Writer的类继承层次结构继续沿用相同的思想,但是又并不完全采用上面说到的装饰器模式。如下是自己梳理的Reader/Writer继承层次结构:
与前面的I/O继承层次结构图相对比可以发现,尽管BufferedOutputStream是FilterOutputStream的子类,但是BufferedWriter并不是FilterWriter的子类(FilterWriter是抽象类,但是没有任何子类,仅仅是作为一个占位符)。
2.1 适配器
有时我们必须把来自于“字节”层次结构中的类和“字符”层次结构中的类结合起来使用。为了实现这个目的,要用到“适配器”(adapter)类:InputStreamReader可以把InputStream转换为Reader,而OutputStreamWriter可以把OutputStream转换为Writer。
3. 总结
- I/O需要应对的场景往往是多样化的,Java类库的设计者通过创建大量的类来解决这个难题,在实际使用中,通过装饰器模式避免“类爆炸”,但类的数量还是不少,这也是Java中“流”类库让人迷惑的主要原因;
- InputStream和OutputStream是提供面向字节形式的I/O,而Reader和Writer则提供兼容Unicode与面向字符的IO功能;
- 如果需要把字节流和字符流结合起来使用,可以使用适配器进行转换,InputStreamReader可以把InputStream转换为Reader,而OutputStreamWriter可以把OutputStream转换为Writer;
本文主要是梳理了传统I/O流的类继承层次结构,包括字节流(InputStream/OutputStream)和字符流(Writer/Reader),并没有一开始就一头扎进I/O类库的海洋中,主要是希望通过这种方式能够对整个I/O体系有一个清晰的认识,这对于进一步的学习可以有更明确的指导作用,下文会针对一些I/O的的典型使用方式进行总结。