前言
在上篇文章中我们深入的分析File Stream的读写文件的原理:通过 JNI 调用 C语言的文件操作接口,然后调用操作系统的接口去操作文件。流的操作的特性就是数据传输(一直前进,而不能往后退),在数据传输速度和处理的速度存在不平衡的,这种不平衡使得数据传输过程中进行缓存处理而释放数据处理器的资源是一种提高程序效率的机制。举个特别简单的例子,平时抽水马桶上方水槽的水是一点点积累的,如果水未积累到一定程度的话,冲马桶不干净的。我们也不能说一滴一滴的水进行冲洗的,那么这个水槽就是相当于我们的一个缓冲区,用来存放东西用的。Java也有一系列的概念Buffer Stream有缓冲的作用。
类结构图
###代码示例
public static void readFile() {
FileInputStream fis = null;
BufferedInputStream bis = null;
try {
//首先创建一个文件流
fis = new FileInputStream("C:\\install.log");
//然后再对文件流进行包装一下
bis = new BufferedInputStream(fis);
//创建一个字节数组用于存放 读取的内容
byte buf[] = new byte[1024];
int length = -1;
//然后再调用包装流的 read 方法将内容读取到 byte 数组中
while ((length = bis.read(buf)) != -1) {
String chunk = new String(buf, 0, length);
System.out.println(chunk);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭缓冲流
try {
bis.close();
} catch (IOException e1) {
e1.printStackTrace();
}
//然后再关闭文件流,这一步我们先持怀疑的态度。
try {
//System.out.print(fis.available());
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
代码分析
- 创建对象
首先我们需要创建FileInputStream 对象,并且作为参数传入到缓冲流中
public class BufferedInputStream extends FilterInputStream {
//内部定义默认的字节数组大小
public static final int DEFAULT_BUFFER_SIZE = 8192;
//内部默认定义的字节数组,用于存放读取的内容,相当于一个大水池。
protected volatile byte[] buf;
//数组中实际存放的大小
protected int count;
//这两个属性目前没有接触到
protected int marklimit;
protected int markpos = -1;
//内部字节数组被读取的位置
protected int pos;
//默认构造函数定义的是大小为 8KB的字节数组
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
//同时我们也可以自定义字节数组大小
public BufferedInputStream(InputStream in, int size) {
super(in);
if (size <= 0) {
throw new IllegalArgumentException("size <= 0");
}
buf = new byte[size];
}
}
从上面的代码中我们可以看到传入的InputStream起到了实际的作用,BufferedInputStream只是起到一个包装和封装代码的作用,最后还是调用了FileInputStream。
- 读取内容
BufferedInputStream由于继承了,然后重写了read()方法,当我们在跟踪 read(byte buf[])代码的时候,最开始的时候是调用InputStream的 read(byte[] buffer, int byteOffset, int byteCount)方法的,但是由于FilterInputStream重写了该方法,而且BufferedInputStream也重写了该方法,所以read(byte buf[])是调用自己的方法实现。注意这个地方刚开始看的时候非常不理解,很容易会被里面的代码给绕晕的,所以我们需要理解,多看几遍就知道了
//主意这里的 buffer 是我们外部定义的字节数组的。
public synchronized int read(byte[] buffer, int byteOffset, int byteCount) {
byte[] localBuf = buf;
if (localBuf == null) {
throw streamClosed();
}
//1. 首先检测我们传进来的参数是否合法
Arrays.checkOffsetAndCount(buffer.length, byteOffset, byteCount);
if (byteCount == 0) {
return 0;
}
InputStream localIn = in;
if (localIn == null) {
throw streamClosed();
}
int required;
//2. 首先判断内部 buf 是否还有内容没有读取完
if (pos < count) {//如果内部缓存buf还有内容,直接读取内部缓存内容到我们给定的字节数组中
//拷贝的数据长度
int copylength = count - pos >= byteCount ? byteCount : count - pos;
//将localBuf数组中的数据拷贝到我们外部定义的字节数组中
System.arraycopy(localBuf, pos, buffer, byteOffset, copylength);
//同时位置往前面移动 copylength 个位置
pos += copylength;
if (copylength == byteCount || localIn.available() == 0) {
return copylength;
}
byteOffset += copylength;
required = byteCount - copylength;
} else {
//表示内部 buf中没有内容,这个是则需要从 InputStream中读取内容
required = byteCount;
}
while (true) {
int read;
//3. 如果我们进行标志位置,而且我们定义的外部缓存大小大于内部缓存。
if (markpos == -1 && required >= localBuf.length) {
//直接将内容读取到外部缓存,不经过内部缓存了
read = localIn.read(buffer, byteOffset, required);
//如果读取的长度为 -1 表示已经到末尾了,同时返回读取到数据长度。
if (read == -1) {
return required == byteCount ? -1 : byteCount - required;
}
} else {
//将流中的数据写入到内部的缓存数组中 localBuf,如果写入的长度为 -1,表示写入数据失败。
if (fillbuf(localIn, localBuf) == -1) {
return required == byteCount ? -1 : byteCount - required;
}
.....
//这里需要计算 read的数目
read = count - pos >= required ? required : count - pos;
//将内部缓存数组中的数据拷贝一份数据到我们外部定义的缓存数组中。
System.arraycopy(localBuf, pos, buffer, byteOffset, read);
pos += read;
}
required -= read;
if (required == 0) {
return byteCount;
}
//如果 流的有效个数为0的话,表示已经读取到末尾了。
if (localIn.available() == 0) {
return byteCount - required;
}
//最后偏移位置 需要加入读取的数据实际长度。
byteOffset += read;
}
}
上面的代码解释我们估计也懵逼了,这都解释的啥玩意呢?下面我们就来画一个图来形象的解释一下BufferedInputStream的工作原理以及实现方式。其实BufferedInputStream无非就是一个包装类,它本生不涉及真正的数据读取的,只是作为一个缓存来使用的。就相当于我们生活中的蓄水池一样的。
模型
最后我们可以将所有的内容简化为上面的一张图就非常明白的知道其原理了,首先FileInputStream将内容读取存放到 BufferedInputStream的字节数组(8KB)中(我们也叫做为一级缓存吧),然后通过数组拷贝的形式,二级缓存的内容就直接在一级缓存中获取就可以了。这里我们就需要分两种情况来处理:
-
二级缓存大小大于一级缓存
如果我们外部定义的字节数组大小大于或者是等于 一级缓存中的字节数组大小(8KB),其实我们就没有必要将数据拷贝到一级缓存中了,而且直接拷贝到二级缓存中了,这个时候相当于BufferedInputStream根本也没有起到什么作用。其内部无非就是一个稍微比较大一点的字节数组而已。
-
二级缓存小于一级缓存
当二级缓存小于一级缓存的时候,首先会通过FileInputStream将内容读取到一级缓存(BufferedInputStream)中,然后在读取到 二级缓存(我们定义的字节数组中) -
关闭流
//如果我们调用了 close方法以后,就不需要再重新调用 FileInputStream的close方法了。
public void close() throws IOException {
buf = null;
InputStream localIn = in;
in = null;
//关闭通过构造方法传进来的 InputStream。
if (localIn != null) {
localIn.close();
}
}
小结
通过上面的代码和图的描述,非常的明白了BufferedInputStream内部其实就是定义了一个 8KB的字节数组用于存储数据而已,无非相当于了一个一级缓存的概念。首先会将数据读取到一级缓存中,然后我们再去一级缓存中拿数据,这个其实就好比蓄水池一样的道理,首先我们每天将井水抽到蓄水池中,如果我们需要用水了就直接去蓄水池中获取就行了。如果我们需要的水大于了蓄水池的话,那我们也没必要使用蓄水池了,就直接通过抽水机将水重井里面抽上来直接使用了。这里面的蓄水池就相当于我们的BufferedInputStream。
BufferedOutputStream
上面我们分析了BufferedInputStream的使用方法以及底层原理、实现方式,有读操作就有对应的写操作。BufferedOutputStream就是一个缓冲写流。本着好奇和格物致知的精神,我们就深入的看看内部的实现。
类结构图
代码分析
- 首先创建对象
public class BufferedOutputStream extends FilterOutputStream {
//该字节数组用于存储数据
protected byte[] buf;
//字节数组实际存储数据大小
protected int count;
public BufferedOutputStream(OutputStream out) {
this(out, 8192);
}
//创建对象的同时还创建一个大小为 8KB 的字节数组和传入一个真正的写入流
public BufferedOutputStream(OutputStream out, int size) {
super(out);
if (size <= 0) {
throw new IllegalArgumentException("size <= 0");
}
buf = new byte[size];
}
}
- 写入单个字节
public synchronized void write(int oneByte) throws IOException {
//首先检查是否调用 close 操作
checkNotClosed();
//如果字节数组的实际大小与数组长度相等,表明字节数组已经满了
if (count == buf.length) {
//调用我们之前传入的OutputStream来写入数据。该方法才是实际的写入数据
out.write(buf, 0, count);
//同时将 count 置为 0
count = 0;
}
//将 int 转换成字节,然后传入到字节数组中
buf[count++] = (byte) oneByte;
}
从这里我们可以看出写入单个字节的效率是非常低的,因为会将字节保存到字节数组中,当字节数组完全满了以后才会将字节数组中的内容真正的写入到文件中
- 写入字节数组
public synchronized void write(byte[] buffer, int offset, int length) throws IOException {
........
byte[] internalBuffer = buf;
//首先判断外部写入的数据大于内部数组的话,则直接将外部字节数组写入到文件中。
if (length >= internalBuffer.length) {
//同时将内部 字节数组(buf)剩余的数据写入到文件中,这个相当于强制刷新的功能
flushInternal();
//直接将外部字节数组写入到文件,不经过内部的字节数组(buf)了。
out.write(buffer, offset, length);
return;
}
//检查参数是否合法
Arrays.checkOffsetAndCount(buffer.length, offset, length);
//如果内部缓存空间存放不了 外部数据的话,先将内部缓存的数据写入到文件中
if (length > (internalBuffer.length - count)) {
flushInternal();
}
//将字节数组 buffer中的数据拷贝到 内部字节数组 buf 中
System.arraycopy(buffer, offset, internalBuffer, count, length);
//更新buf的实际总数 count。
count += length;
}
模型
上面的文字说明和代码可能会让你眼花缭乱的,估计你还是没有搞明白其中的原理,下面我将画一个图来对这几个关系来进行梳理和形象化。
BufferedOutputStream 相当于是一个协调的作用,其内部的字节数组相当于一个中转站,不断的将客户中的快递或者是包裹送到目的地(也就是 File),列车就是FileOutputStream,下面则会有两种情况出现:
-
如果快递量大于中转站空间
这种情况下因为快递量太大了,中转站已经放不下了,所以这个时候我们就不需要这个中转站了,因为通过中转站卸货(也就是数组拷贝) 耗时间,直接将快递(外部的字节数组)通过 FileOutputStream 运送到目的地 -
如果快递量小于中转站空间
快递量小于中转空间(内部的字节数组),那么我们首先会将快递放到中转站,当达到了一定的量以后我们在将中转站中的快递统一的通过 FileOutputStream(列车)送到目的地。
小结
通过上面对FileOutputStream的代码分析,我们发现其内部代码非常的简单,内部无非就是多了一个字节数组,我们要写入数据的时候,如果内容的长度大于内部数组的大小的时候,直接写入数据;如果数据长度小于内部数组大小的话,首先判断内部数组是否装满或者内部数组装不下外部数据,会将内容先写入到文件中,然后再将外部的数据放到内部数组中。这个跟我们生活中的快递的原理是一样的,我们总不能为了某一个人的快递去单独送到远方去,因为这样子非常浪费时间,效率低下。我们往往是将快递塞满一车子了然后在统一一起发出去的。
总结
通过上面对 BufferedOutputStream 和BufferedInputStream的代码分析,发现其内部都是有一个字节数组的。我们也可以把它们理解为一级缓存,如果外部的数据大于内部的数据空间的话,则直接绕过了缓冲流,直接操作文件。如果外部的数据小于内部的话,则会将内容保存到字节数组中,然后再统一的将数组中的内容一起写到文件中。这些东西其实跟我们生活中的一些常识和生活现象的原理是相通的,只不过歪果仁将这些东西应用到计算机编程中来了;通过对这些类的内部分析我们就清楚的知道了其内部的原理,以后用起来的时候也就是知道其效率和避免一些不必要的坑了。