接着上篇文章,咱们继续来学习 Java 中的字节流操做。java
装饰者流实际上是基于一种设计模式「装饰者模式」而实现的一种文件 IO 流,而咱们的缓冲流只是其中的一种,咱们一块儿来看看。git
在这以前,咱们使用的文件读写流 FileInputStream 和 FileOutputStream 都是一个字节一个字节的从磁盘读取或写入,很是耗时。github
而咱们的缓冲流能够预先从磁盘一次性读出指定容量的字节数到内存中,以后的读取操做将直接从内存中读取,提升效率。下面咱们一块儿看看缓冲流的具体实现状况:设计模式
依然先以 BufferedInputStream 为例,咱们简单提一下它的几个核心属性:数组
buf 就是用于缓冲读的字节数组,它的值将随着流的读取而不停的被填充,继然后续的读操做能够直接基于这个缓冲数组。bash
DEFAULT_BUFFER_SIZE 规定了默认缓冲区的大小,即 buf 的数组长度。MAX_BUFFER_SIZE 指明了缓冲区的上限。微信
count 指向缓冲数组中最后一个有效字节索引后一位。pos 指向下一个待读取的字节索引位置。学习
markpos 和 marklimit 用于重复读操做。ui
接着咱们看看 BufferedInputStream 的几个示例构造器:this
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
复制代码
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
复制代码
总体上来讲,前者只须要传入一个「被装饰」的 InputStream 实例,并使用默认大小的缓冲区。后者则能够显式指明缓冲区的大小。
除此以外,super(in) 会将这个 InputStream 实例保存进父类 FilterInputStream 的 in 属性字段中,而且全部实际的磁盘读操做都由这个 InputStream 实例发出。
下面咱们来看最重要的读操做以及缓冲区是如何被填充的。
public synchronized int read() throws IOException {
if (pos >= count) {
fill();
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
复制代码
这个方法想必你们已经很熟悉了,从流中读取下一个字节并返回,但细节上的实现仍是稍稍有些不一样。
count 指向了缓冲数组中有效字节索引后一位置处,pos 指向下一个待读取的字节索引位置。理论上 pos 是不可能大于 count 的,最多等于。
若是 pos 等于 count,那说明缓冲数组中全部有效字节都已经被读取过了,此时即须要丢弃缓冲区中那些「无用」的数据,从磁盘从新加载一批新数据填充缓冲区。
而事实上,fill 方法就是作的这个事情,它的代码比较多,就不带你们去解析了,你理解了它的做用,想必分析它的实现也是容易的。
若是 fill 方法调用以后,pos 依然 等于 count,那么说明 InputStream 实例并无从流中读取出任何数据,也即文件流中无数据可读。关于这一点,参见 fill 方法 246 行。
总的来讲,若是成功填充了缓冲区,那么咱们的 read 方法将直接从缓冲区取出一个字节返回给调用者。
public synchronized int read(byte b[], int off, int len){
//.....
}
复制代码
这个方法也是「熟人」了,再也不多余的解释了,实现是相似的。
skip 方法用于跳过指定长度的字节数进行文件流的继续读取:
public synchronized long skip(long n){
//.....
}
复制代码
注意一点的是,skip 方法尽可能去跳过 n 个字节,但不保证必定跳过 n 个字节,方法返回的是实际跳过的字节数。若是缓冲数组中剩余可用字节数小于 n,那么最终将跳过缓冲数组中实际可跳过的字节数。
最后要说一说这个 close 方法:
public void close() throws IOException {
byte[] buffer;
while ( (buffer = buf) != null) {
if (bufUpdater.compareAndSet(this, buffer, null)) {
InputStream input = in;
in = null;
if (input != null)
input.close();
return;
}
// Else retry in case a new buf was CASed in fill()
}
}
复制代码
close 方法将赋空「被装饰者」流,并调用它的 close 方法释放相关资源,最终也会清空缓冲数组所占用的内存空间。
BufferedInputStream 提供了读缓冲能力,而 BufferedOutputStream 则提供了写缓冲能力,即内存的写操做并不会立马更新到磁盘,暂时保存在缓冲区,待缓冲区满时一并写入。
protected byte buf[];
protected int count;
复制代码
buf 表明了内部缓冲区,count 表示缓冲区中实际数据容量,即 buf 中有效字节数,而不是 buf 数组长度。
public BufferedOutputStream(OutputStream out) {
this(out, 8192);
}
public BufferedOutputStream(OutputStream out, int size) {
super(out);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
复制代码
同样的实现思路,必须提供的是一个 OutputStream 输出流实例,也能够选择性指明缓冲区大小。
public synchronized void write(int b) throws IOException {
if (count >= buf.length) {
flushBuffer();
}
buf[count++] = (byte)b;
}
复制代码
写方法将首先检查缓冲区是否还能容纳本次写操做,若是不能将发起一次磁盘写操做,将缓冲区数据所有写入磁盘文件,不然将优先写入缓冲区。
固然,BufferedOutputStream 也提供了 flush 方法向外提供接口,也即不必定非要等到缓冲区满了才向磁盘写数据,你也能够显式的调用该方法让它清空缓冲区并更新磁盘文件。
public synchronized void flush() throws IOException {
flushBuffer();
out.flush();
}
复制代码
关于缓冲流,核心内容介绍如上,这是一种可以显著提高效率的流,经过它,可以减小磁盘访问次数,提高程序执行效率。
有关对象序列化流 ObjectInput/OutputStream 以及基于基本类型的装饰者流 DataInput/OutputStream 咱们这里暂时不作讨论。待到咱们学习序列化的时候,再回头讨论这两个字节流。
文章中的全部代码、图片、文件都云存储在个人 GitHub 上:
(https://github.com/SingleYam/overview_java)
欢迎关注微信公众号:扑在代码上的高尔基,全部文章都将同步在公众号上。