1、read()
2、skip()
3、available()
浅复制
BufferedInputStream内部维护一个缓冲字节数组,
当调用read1(byte[] b)方法时,核心原理是将内部缓冲字节数组的内容,浅复制到方法参数的目标字节数组中。
这样做的好处是,避免了大量的磁盘IO,因为原始的InputStream类实现的read是即时读取的,即每一次读取都会是一次磁盘IO操作(哪怕只读取了1个字节的数据),可想而知,如果数据量巨大,这样的磁盘消耗非常可怕。而通过缓冲区的实现,读取可以读取缓冲区中的内容,当读取超过缓冲区的内容后再进行一次磁盘IO,载入一段数据填充缓冲,那么下一次读取一般情况下就直接可以从缓冲区读取,减少了磁盘IO
read1(byte[] b, int off, int len)
方法是非贪婪读取,最多只从底层字节流读取一次
处理方式简称为:缓冲区没剩先填充,有剩直接读,读取有偏差,不一定按目标。
处理方式如下:
如果内部缓冲字节数组的剩余字节数为0:
额外判断:如果目标读取字节数大于内部缓冲字节数组的长度 ,直接从底层字节流读取目标读取字节数,给目标字节数组。这样做的好处是,不用经过填充内部缓冲字节数组的中间过程,实现流的平滑降级;
调用fill()方法填充内部缓冲字节数组。并再次检查填充后缓冲区的剩余字节数是否大于0,从而判断是否达到文件尾。
如果内部缓冲字节数组的剩余字节数大于0:
2、如果内部缓冲字节数组的剩余字节数,小于用户的目标读取字节数,按照剩余字节数的大小浅复制给目标字节数组;
/**
* Read characters into a portion of an array, reading from the underlying
* stream at most once if necessary.
*/
private int read1(byte[] b, int off, int len) throws IOException {
int avail = count - pos;
if (avail <= 0) {
/* If the requested length is at least as large as the buffer, and
if there is no mark/reset activity, do not bother to copy the
bytes into the local buffer. In this way buffered streams will
cascade harmlessly. */
if (len >= getBufIfOpen().length && markpos < 0) {
return getInIfOpen().read(b, off, len);
}
fill();
avail = count - pos;
if (avail <= 0) return -1;
}
int cnt = (avail < len) ? avail : len;
System.arraycopy(getBufIfOpen(), pos, b, off, cnt);
pos += cnt;
return cnt;
}
read(byte b[], int off, int len)
方法是贪婪读取,通过重复调用底层字节流的read()方法,尽可能的读取最多的数据。
实现逻辑是通过死循环调用read1(byte[] b)方法,直到遇到以下条件退出:
1、已经完成用户要求的目标读取字节数;2、底层字节流的read()方法返回-1,表示到达文件末尾,或者因为底层字节流的available()方法返回0。
public synchronized int read(byte b[], int off, int len)
throws IOException
{
getBufIfOpen(); // Check for closed stream
if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int n = 0;
for (;;) {
int nread = read1(b, off + n, len - n);
if (nread <= 0)
return (n == 0) ? nread : n;
n += nread;
if (n >= len)
return n;
// if not closed but no bytes available, return
InputStream input = in;
if (input != null && input.available() <= 0)
return n;
}
}
fill方法
需求:
如果有mark标记,markpos之前的数据才可以丢弃;
实现:
a. 如果没有markpos,直接重置缓冲区,即将pos设为0。这种方式下,缓冲区永远不会有达到极限,抛出内存溢出异常的时候。然后从底层输入流中读取数据,填满缓冲区。
b. 如果存在markpos,当pos未达到缓冲区的末端,说明缓冲区还有剩余空间:从底层输入流中读取数据,填满缓冲区。
c. 如果存在markpos,当pos已经达到缓冲区的末端,说明缓冲区没有剩余空间,它的处理方式有以下几种:1、如果markPos大于0,对mark位置到pos位置的数据予以保留,而将mark位置之前的数据丢弃,通过浅复制将缓冲区中markpos至pos部分的移至缓冲区头部,并将markPos重置为0;
2、由第一步可见,marrkPos会等于0。如果markPos等于0,进行缓冲区扩容。创建新的缓冲区,长度为当前pos的2倍长(可以预估新的缓冲区长度>=旧的缓冲区的长度的2倍),将旧的缓冲区的内容复制给新的缓冲区。
3、由第二步可见,缓冲区不断扩容,总会有达到极限的时候,当缓冲区的长度大于规定的最大缓冲区大小(MAX_BUFFER_SIZE)时,抛出内存溢出异常。另外,当缓冲区的长度大于用户规定的markLimit时,则认定该mark无效,重置缓冲区。
最后,才从底层输入流中读取数据,填满缓冲区。
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 if (buffer.length >= MAX_BUFFER_SIZE) {
throw new OutOfMemoryError("Required array size too large");
} else { /* grow buffer */
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)) {
// 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;
}
skip(long n)方法
表示跳过多少字节。通过调用底层字节流的skip()方法完成。该方法的实现为尽量原则,不保证一定跳过目标字节数。
处理方式简称为:缓冲区没剩先填充,有剩就skip,skip有偏差,不一定按目标。
处理方式如下:1、如果缓冲字节数组没有剩余字节数,处理方式如下:
如果没有mark标记,则直接从底层字节流中skip,结束。
如果有mark标记,调用fill()方法填充缓冲区。并再次检查填充后缓冲区的剩余字节数是否大于0,从而判断是否达到文件尾。剩余字节数大于0,就可以走第2步。
2、缓冲字节数组有剩余字节数,将pos前移用户的目标跳过字节数,或者前移缓冲区的剩余字节数。public synchronized long skip(long n) throws IOException {
getBufIfOpen(); // Check for closed stream
if (n <= 0) {
return 0;
}
long avail = count - pos;
if (avail <= 0) {
// If no mark position set then don't keep in buffer
if (markpos <0)
return getInIfOpen().skip(n);
// Fill in buffer to save bytes for reset
fill();
avail = count - pos;
if (avail <= 0)
return 0;
}
long skipped = (avail < n) ? avail : n;
pos += skipped;
return skipped;
}
available()方法
返回目前可用的字节数。等于底层字节流中可用的字节数+缓冲区中的剩余字节数。
public synchronized int available() throws IOException {
int n = count - pos;
int avail = getInIfOpen().available();
return n > (Integer.MAX_VALUE - avail)
? Integer.MAX_VALUE
: n + avail;
}
ps:在上述文字定义中,缓冲区没有剩余字节数和缓冲区没有剩余空间,两者是不一样的。
剩余字节数的计算公式为:count - pos
剩余空间的计算公式为:buffer.length - pos