简介
我们常和io打交道,这个类族里面牵涉到的类很多,经常让人觉得比较迷茫。在使用的时候也不容易找到合适的类来完成自己的任务。这里结合前面一篇讨论java io和设计模式相关的文章针对java io里几个主要io类型的类做了一个总结。通过对类的结构和关系的理顺,我们可以更好的使用现有io相关的类,在以后有必要的时候也可以去定制自己所需要的功能。
总体说明
在详细讨论每个具体类别之前,我们可以先回想一下自己以前使用java io的时候常碰到的一些用法。比如说我们在要读取一个文件的时候,我们常使用如下的形式:
InputStream in = new BufferedInputStream( new FileInputStream("test.txt"));
我们通过FileInputStream可以直接打开文件,如果我们想增加一些新的特性,我们用BufferedInputStream来包装了它一把。比较有意思的是它居然也是一个InputStream类型,还可以把它当成一个基类来使。这种用法的核心根源在于它采用的是decorator的设计模式。在我的这篇文章里对它的由来做了一个详细的分析。在一个decorator模式描述的场景里我们有一系列的核心类,相当于提供了若干基本的功能。同时,我们也有一系列的包装类,它是对现有类功能的增强,但是它不是一个独立的类,需要将一个核心类作为参数给它包装。我们以java io里的InputStream类族为例,它的主要类图结构如下图:
我们可以发现真正要扩展功能的点是都继承自类FilterInputStream。它只是一个简单的类代理,在它内部有一个指向InputStream类型的成员变量:
protected volatile InputStream in;
它的构造函数也只是相当于将一个InputStream类型的对象引入进来:
protected FilterInputStream(InputStream in) { this.in = in; }
从这里只看到这个类其实什么都没干,它只是做一个把功能代理给另外一个类做的示范。具体添加扩展功能的都是它的子类,我们就以它的子类BufferedInputStream为例来看看它是怎么增强功能的。
前面那部分的代码将定义的成员变量以及方法声明为protected,这说明这些继承它的子类是可以访问到这个InputStream成员变量的。我们以read方法为例看看它具体的增强:
public synchronized int read() throws IOException { if (pos >= count) { fill(); if (pos >= count) return -1; } return getBufIfOpen()[pos++] & 0xff; }
这个方法只是读取下一个字节,但是它内部采用了一个字节串作为缓冲。在读取的时候fill()方法实际上在填充整体的缓冲区。fill方法的实现虽然比较繁琐:
private void fill() throws IOException { byte[] buffer = getBufIfOpen(); if (markpos < 0) pos = 0; /* no mark: throw away the buffer */ else if (pos >= buffer.length) /* no room left in buffer */ if (markpos > 0) { /* can throw away early part of the buffer */ int sz = pos - markpos; System.arraycopy(buffer, markpos, buffer, 0, sz); pos = sz; markpos = 0; } else if (buffer.length >= marklimit) { markpos = -1; /* buffer got too big, invalidate mark */ pos = 0; /* drop buffer contents */ } else { /* grow buffer */ int nsz = pos * 2; if (nsz > marklimit) nsz = marklimit; byte nbuf[] = new byte[nsz]; System.arraycopy(buffer, 0, nbuf, 0, pos); if (!bufUpdater.compareAndSet(this, buffer, nbuf)) { // Can't replace buf if there was an async close. // Note: This would need to be changed if fill() // is ever made accessible to multiple threads. // But for now, the only way CAS can fail is via close. // assert buf == null; throw new IOException("Stream closed"); } buffer = nbuf; } count = pos; int n = getInIfOpen().read(buffer, pos, buffer.length - pos); if (n > 0) count = n + pos; }
但是它本质上是一个填充缓冲区数组的操作,同时通过int n = getInIfOpen().read(buffer, pos, buffer.length - pos);语句使用InputStream做了一个预读取操作。
我们前面举这个示例并不是要对它所有的代码做一个分析,而是针对decorator具体功能的实现做一个跟踪。在扩展类里,无非做的就是一个调用被包装对象的方法,同时将自己附加的功能也添加上去。相信有了这么一个说明,我们对它的具体实现理解更加深刻,在后面部分的说明也更加容易理解。
在java io里,InputStream, OutputStream, Reader, Writer的类结构都采用了decorator模式。
Stream
InputStream和OutputStream面向的是通用的字节流访问方式,对于文件读取和数据传输都可以使用。
下图是InputStream的类结构:
FilterInputStream就是里面包装类的基类,所有需要增强InputStream效果的类实现可以继承它。这里已经包含的有BufferedInputStream, CheckedInputStream等。我们如果要增加新功能的话也可以按照BufferedInputStream等类的方式继承FilterInputStream并实现里面的方法。这种类似的convension对于OutputStream也类似。
OutputStream的类结构图如下:
和前面一样,大同小异的结构。
Reader/Writer
Reader/Writer和前面不一样的地方在于他们是面向具体字符串的。在一定程度上他们要关注传输访问的内容。比如说前面的IOStream只要把一串字节给读过来就可以了,而这里要考虑到编码和具体内容的问题。在很多时候我们读写一些文本内容就需要用到这些。
下面分别是Reader和Writer的类结构:
File
和前面两种io方式不一样的地方是,前面的两种方式提供了一个比较高层别的抽象。他们访问的是面向流或者字符的输入输出接口。可以是文件,但是也可以是其他的,比如说USB口,网络socket接口等。而这里对于纯文件的访问,有一个专门的类就是File。
我们使用File的时候更多的是考虑对于一个个的具体文件来说,要看他们的具体属性,比如说产生和修改时间,文件大小,文件目录结构和层级等。所以说在一些比较直接对文件操作的场景下,它们是最常用的。
在用File类遍历文件目录的时候会突然有一种想法,感觉就是这里目录和文件是一种树形的结构,目录包含文件或者目录。我们也可以将目录当作一种文件来看,它只是一个其他文件的容器而已。File类是一个相对比较独立的类。由于File相对比较简单,就不详细讨论它的具体使用和实现了。
总结
我们通常使用的java io主要包含面向流的输入输出(InputStream, OutputStream)以及面向字符的输入输出(Reader, Writer)。采用的这两种输入输出的方式有一个重要的好处就是他们提供了足够的抽象,我们读写的对象可以不仅仅只是单纯的文件,可也以是网络socket, url,也可以是磁盘的块设备或者其他字符串等输入。因为这种抽象涵盖的意义是这么广,它的设计思想有点像unix里面的设计哲学,将各种复杂的输入输出都归结到一种统一的形式,方便使用和扩展。除了以上提到的这两种,如果我们需要对文件操作的话,最常用的是使用File或者RandomAccessFile对象。比如遍历文件或者访问文件某些内容。
我们前面访问io是采用阻塞式的,也就是说如果我们有一个线程去访问文件或者网络socket等,因为在内存中执行的线程速度和其他IO设备之间访问速度有很大的差别。这就导致io要执行完毕的时候而系统内的线程已经等了好一阵了。于是为了提高线程运行的效率引入了nio。这是一种非阻塞式的io。它可以保证当一个线程执行io的时候,在io执行完毕之前的这段时间它可以继续做自己的事情。关于nio的详细介绍我们会在后面有专门的文章讨论。