BufferedInputStream与BufferedOutputStream的缓存底层实现
-
首先观察BufferedInputStream 的继承体系,可以看出他是继承自一个FilterInputStream,而这个又是继承自InputStream
-
我们在之前的装饰器模式就讲过,这个BufferedInputStream 是很典型的装饰器模式的案例,那么在其内部应该有封装一个被包装对象
BufferedInputStream 成员变量
class BufferedInputStream extends FilterInputStream {
private static int DEFAULT_BUFFER_SIZE = 8192;
private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
protected volatile byte buf[];
private static final
AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
AtomicReferenceFieldUpdater.newUpdater
(BufferedInputStream.class, byte[].class, "buf");
protected int count;
protected int pos;
protected int markpos = -1;
protected int marklimit;
- 1.但是我们观察其成员变量,我们发现它的内部并没有维护一个被包装对象,这时候我们可以再其父类FilterInputStream 中找到如下代码,在父类中封装了待包装的对象
public class FilterInputStream extends InputStream {
//此处封装了被包装对象
protected volatile InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
- 2.在BufferedInputStream 初始化的时候:可以看到调用super(in)父类的构造器,传入一个被包装对象,实现了装饰器模式
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
}
buf = new byte[size];
}
- 接下来对所有的成员变量进行分析
//根据名字我们可以得知,这个为默认_缓冲区_大小:为8kb
private static int DEFAULT_BUFFER_SIZE = 8192;
//此处为最大的缓冲区数组的大小,也就是Integer的最大值,但是数组也要保存数组本身的信息,比如数组长度等信息,也是保存在数组内部,所以减去8个位置
private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
//这里就是我们最重要的缓冲数组,默认每次从文件中读取8kb,如果文件剩余不足8kb,则count等于读入的长度
protected volatile byte buf[];
//这里不知道,所以不讨论
private static final
AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater =
AtomicReferenceFieldUpdater.newUpdater
(BufferedInputStream.class, byte[].class, "buf");
//这里是当前数组中可以读取的容量大小,如果此时buf数组中是满的,则count等于buf.length,如果此时不是满的,则count等于数组中总的可以读取的长度
protected int count;
//这个是当前缓冲区中已经读到的位置,例:缓冲区有8192个字节,此时读取了8个字节,则pos等于9,也就是下一个要读取的字节
protected int pos;
//标记位置,默认为-1
protected int markpos = -1;
protected int marklimit;
- 我们接下来对构造函数进行分析:有两个,一个单参,一个双参,我们可以看到单参是会通过this去调用双参的构造器,并且第二个参数是默认的缓冲区大小
在双参构造其中,super调用父类的构造器,传入被包装流,并且new了一个size长度的数组,如果没有传入缓冲区大小,则使用8192(aka 8kb)来创建缓冲区,否则按照传入的size来创建缓冲区
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];
}
- 接下来就是BufferedInputStream流中最重要的read方法,首先是read()的无参方法,也就是默认先读取8192字节到缓存中,然后再一个一个从数组中取。
首先是基本使用read()
public static void main(String[] args) throws Exception {
String path = "src/main/java/com/xj/dayio/demo.txt";
FileInputStream fis = new FileInputStream(path);
BufferedInputStream bis = new BufferedInputStream(fis);
int len = -1;
while((len = bis.read()) != -1){
System.out.print((char)len);
}
}
然后是read()的源码:可以看出很简短,因为大部分代码都在fill()里面
public synchronized int read() throws IOException {
//pos代表当前缓冲数组中已经读到的位置,count是缓冲数组所有可以读的字节数
//如果当前位置大于或者等于所有可以读的字节数,那么表示缓冲数组中已经没有可以读的字节了,所以需要再次从调用fill填充缓存数组
if (pos >= count) {
//填充满后
fill();
//如果此时pos位置还是大于或者等于count说明文件中已经没有字节可以读了,于是返回
if (pos >= count)
return -1;
}
//之后调用getBufIfOpen返回缓冲数组,并且返回当前pos位置的值
//getBufIfOpen我们看名字就可以得知,如果当前流没有关闭,就获取到缓冲数组
//此处与0xff具体原因未知,目前以我的理解是将两个字节的byte值提升为四个字节的int值
return getBufIfOpen()[pos++] & 0xff;
}
接下来我们具体看填充方法fill()的实现:我们可以看到这个是很长的,8着急,慢慢看
private void fill() throws IOException {
//首先拿到缓冲数组
byte[] buffer = getBufIfOpen();
//判断标记位置,默认为-1(关于标记先不讲,一会再说)
if (markpos < 0) //默认为-1,所以将pos当前位置,置为数组的开头
pos = 0;
else if (pos >= buffer.length) //由于默认值为-1,所以下面这一大段!else if!都不用看啦!暂时。。。
if (markpos > 0) {
int sz = pos - markpos;
System.arraycopy(buffer, markpos, buffer, 0, sz);
pos = sz;
markpos = 0;
} else if (buffer.length >= marklimit) {
markpos = -1;
pos = 0;
} else if (buffer.length >= MAX_BUFFER_SIZE) {
throw new OutOfMemoryError("Required array size too large");
} else {
int nsz = (pos <= MAX_BUFFER_SIZE - pos) ?
pos * 2 : MAX_BUFFER_SIZE;
if (nsz > marklimit)
nsz = marklimit;
byte nbuf[] = new byte[nsz];
System.arraycopy(buffer, 0, nbuf, 0, pos);
if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
throw new IOException("Stream closed");
}
buffer = nbuf;
}
//跳到这里啦!
//首先让count(也就是可以最多可读字节数量)等于我们的pos(aka 0)
count = pos;
//然后我们看这个方法的名字getInIfOpen,就是如果当前流没有被关闭,就获取到真正被包装的输入流对象
//然后调用真正的输入流对象的read方法(缓冲数组,当前位置(0),缓冲数组的长度(默认8192)-当前位置(0))
//默认来说的话,就是从文件中读取一个buffer数组长度的字节,并读取到buffer中,那么这里为什么会这么复杂呢,因为是为了标记用的(也就是我们上面跳过的那一大段else if)
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
//如果读取到返回的字节数量大于0的话,就说明读取到了字节,之后让count等于当前位置(pos)加上读取到的字节数量(n)
//否则n小于0或者等于0的话就说明文件中已经没有字节可读了
if (n > 0)
count = n + pos;
//如果没有字节了的话此时pos和count都为0,然后我们回到我们上一个read的源码,就可以判断出pos >= count,就返回-1,读取结束
}
- 那接下来就是read(byte[] buf)的无参方法,也就是默认先读取8192字节到缓存中,但是与无参的方法区别是:
- 我们使用无参方法的话是一个一个从缓冲数组中取;
- 而使用传入缓冲数组的方法,我们每次可以每次从BufferedInputStream 的缓冲数组中每次取一个buf数组长度(4096)的字节;这样子效率更高,不需要一个一个访问bis的缓冲数组
public static void main(String[] args) throws Exception {
String path = "src/main/java/com/xj/dayio/demo.txt";
FileInputStream fis = new FileInputStream(path);
BufferedInputStream bis = new BufferedInputStream(fis);
//这里是自定义缓冲数组的大小,(注意:这个不是BufferedInputStream 内部的缓冲数组,是我们自己的)
//这里仿佛是使用4096时效率更高,并没有具体的考量,只是分别测试了一下
byte[] buf = new byte[4096];
int len = -1;
while((len = bis.read(buf)) != -1){
String str = new String(buf,0,len);
System.out.print(str);
}
}
啊,先更新到这里,好累呀
本博客文章主要供博主学习交流用,所有描述、代码无法保证准确性,如有问题可以留言共同讨论。