[重學Java基礎][Java IO流][Part.12] [Part.12]緩衝字節輸入輸出流
===
文章目錄
BufferedInputStream
概述
BufferedInputStream繼承於FilterInputStream,
FilterInputStream 從名字上就可以看出 是個過濾流 類似於FilterReader
用來“封裝其它的輸入流,併爲它們提供額外的功能”。
BufferedInputStream的作用就是爲“輸入流提供緩衝功能,允許每次讀入一批數據 並且提供了按行讀取功能 通過包裝InputStream對象來發揮作用 很明顯 這是一個處理流 包裝流 一般包裝ByteArrayInputStream,system.in對象
源碼解析
成員函數
默認的緩衝大小 爲8192
private static int DEFAULT_BUFFER_SIZE = 8192;
最大的緩衝大小 爲Interger.MaxValue
private static int MAX_BUFFER_SIZE = 2147483639;
緩衝字節數組 是多線程內存可見的
protected volatile byte[] buf;
緩存數組的原子化更新器
這個是和緩衝字節數組byte[] buf配合使用的
以保證緩衝字節數組的原子化更新
也就是說在多線程環境下buf和bufUpdater都具有原子性
private static final AtomicReferenceFieldUpdater<BufferedInputStream, byte[]>
bufUpdater = AtomicReferenceFieldUpdater
.newUpdater(BufferedInputStream.class, byte[].class, "buf");
此流的緩衝區的有效字節數
protected int count;
此流的緩衝區讀入遊標位置
protected int pos;
此流的的緩衝區的標記位置
markpos和reset()配合使用纔有意義。操作步驟:
調用mark()方法,保存pos的值到markpos中。
通過reset()方法,會將pos的值重置爲markpos。
接着通過read()讀取數據時,就會從mark()保存的位置開始讀取。
protected int markpos;
標記的最大值
protected int marklimit;
成員方法
構造方法
輸入一個節點數據源輸入流 按默認緩衝區大小構造一個BufferedInputStream
public BufferedInputStream(InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
按指定的緩衝區大小構造BufferedInputStream
public BufferedInputStream(InputStream in, int size) {
super(in);
this.markpos = -1;
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
} else {
this.buf = new byte[size];
}
}
獲取輸入流 緩衝區數據
獲取被包裝的輸入流InputStream input
private InputStream getInIfOpen() throws IOException {
InputStream input = this.in;
if (input == null) {
throw new IOException("Stream closed");
} else {
return input;
}
}
直接獲取緩衝區數據 注意是引用傳遞而不是複製一個新的緩衝數組
private byte[] getBufIfOpen() throws IOException {
byte[] buffer = this.buf;
if (buffer == null) {
throw new IOException("Stream closed");
} else {
return buffer;
}
}
緩衝數組填充方法
這個方法比較複雜 是BufferedInputStream的核心方法
建立緩衝並從被包裝的數據源輸入流讀入就是通過此方法實現的
後面詳細解釋
private void fill() throws IOException {
…………
}
讀入方法
讀入下一個字節
public synchronized int read() throws IOException {
if (pos >= count) {
如果下一個讀入位置的遊標超過了此流的有效字節數
執行緩衝數組填充方法fill() 從被包裝的輸入流中讀入數據到緩衝區
fill();
緩衝區填充後 仍然無新數據 則說明已讀完 返回-1
if (pos >= count)
return -1;
}
return getBufIfOpen()[pos++] & 0xff;
}
讀入下一個字節並寫入到指定字節數組byte b[]
off爲字節數組b的寫入起始位置 len爲讀入的長度
public synchronized int read(byte b[], int off, int len)
throws IOException
{
檢測流是否關閉
getBufIfOpen();
檢測是否越界
if ((off | len | (off + len) | (b.length - (off + len))) < 0) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
int n = 0;
for (;;) {
可以看到這裏是用內部方法read1()進行讀取的
int nread = read1(b, off + n, len - n);
if (nread <= 0)
return (n == 0) ? nread : n;
n += nread;
讀入了指定的長度 返回
if (n >= len)
return n;
如果被包裝的輸入流未關閉但是無字節數據 則返回
InputStream input = in;
if (input != null && input.available() <= 0)
return n;
}
}
private int read1(byte[] b, int off, int len) throws IOException {
int avail = this.count - this.pos;
這裏使用了一個直接從被包裝的輸入流讀取的機制 如果緩衝區已經讀完 且沒有進行標記
則直接從被包裝的輸入流中讀入 避免了從被包裝的輸入流中讀入字節到緩衝區
再從緩衝區複製到字節數組b的性能損失過程
這是一個加速機制
if (avail <= 0) {
if (len >= this.getBufIfOpen().length && this.markpos < 0) {
return this.getInIfOpen().read(b, off, len);
}
刷新緩衝區
this.fill();
此時仍無數據 則返回-1 讀入結束
avail = this.count - this.pos;
if (avail <= 0) {
return -1;
}
}
如果緩衝區未讀完 則使用System.arraycopy從緩衝區複製數據到byte[] b
int cnt = avail < len ? avail : len;
System.arraycopy(this.getBufIfOpen(), this.pos, b, off, cnt);
this.pos += cnt;
return cnt;
}
跳過方法
InputStream的通用方法
public synchronized long skip(long n) throws IOException {
檢查流是否關閉
getBufIfOpen();
if (n <= 0) {
return 0;
}
long avail = count - pos;
if (avail <= 0) {
如果緩衝區已經讀完 且沒有進行標記
則直接讓被包裝的被包裝的輸入流跳過n個字節
if (markpos <0)
return getInIfOpen().skip(n);
刷新流緩衝體
fill();
avail = count - pos;
此時無數據則說明已讀完 直接返回0
if (avail <= 0)
return 0;
}
緩衝區未讀完 則直接跳過n個字節(實際是通過移動讀入遊標pos來實現的)
long skipped = (avail < n) ? avail : n;
pos += skipped;
return skipped;
}
下一個字節是否可以讀入方法
public synchronized int available() throws IOException {
int n = count - pos;
實際上調用的是被包裝的輸入流的available()方法
int avail = getInIfOpen().available();
return n > (Integer.MAX_VALUE - avail)
? Integer.MAX_VALUE
: n + avail;
}
標記與重置
標記方法 入參是讀入的限制 標記則直接標記爲當前遊標的位置
public synchronized void mark(int readlimit) {
this.marklimit = readlimit;
this.markpos = this.pos;
}
重置此流到標記的位置
public synchronized void reset() throws IOException {
this.getBufIfOpen();
if (this.markpos < 0) {
throw new IOException("Resetting to invalid mark");
} else {
實際就是移動讀入遊標到標記位置
this.pos = this.markpos;
}
}
是否支持標記 恆定爲支持
public boolean markSupported() {
return true;
}
關閉流
public void close() throws IOException {
while(true) {
byte[] buffer = this.buf;
如果緩衝區不爲空的話 則循環執行bufUpdater的CAS操作
直到buffer被置空(這塊就是檢查看是否有其他線程還在操作緩衝區)
if (this.buf != null) {
if (!bufUpdater.compareAndSet(this, buffer, (Object)null)) {
continue;
}
InputStream input = this.in;
this.in = null;
if (input != null) {
input.close();
}
return;
}
return;
}
}
緩衝數組刷新內容 fill()方法
創建BufferedInputStream時,調用構造函數並傳入一個來自數據源的輸入流參數,讀取數據時,BufferedInputStream會將該輸入流數據分批讀取,每次讀取一部分到緩衝中;操作完緩衝中的這部分數據之後,再從輸入流中讀取下一部分的數據。
因爲把數據源輸入流的數據讀入到BufferedInputStream的緩衝區中 所以此流叫做緩衝字節輸入流
使用緩衝區的原因是爲了提高讀入的效率 緩衝區的數據時存儲在內存中的 而被包裝的數據源輸入流的數據可能是在外存或者網絡接口中 相比內存 數據源輸入流讀入數據的速度可能較慢
至於爲什麼不一次性全部讀入到內存中?如果數據源數據較多 一次讀入可能會耗時很久 並且耗費大量內存空間
但如果數據源數據很少 則可以一次讀入到內存中 要根據情況決定
fill()方法源碼
private void fill() throws IOException {
byte[] buffer = this.getBufIfOpen();
int nsz;
if (this.markpos < 0) {
this.pos = 0;
} else if (this.pos >= buffer.length) {
if (this.markpos > 0) {
nsz = this.pos - this.markpos;
System.arraycopy(buffer, this.markpos, buffer, 0, nsz);
this.pos = nsz;
this.markpos = 0;
} else if (buffer.length >= this.marklimit) {
this.markpos = -1;
this.pos = 0;
} else {
if (buffer.length >= MAX_BUFFER_SIZE) {
throw new OutOfMemoryError("Required array size too large");
}
nsz = this.pos <= MAX_BUFFER_SIZE - this.pos ? this.pos * 2 : MAX_BUFFER_SIZE;
if (nsz > this.marklimit) {
nsz = this.marklimit;
}
byte[] nbuf = new byte[nsz];
System.arraycopy(buffer, 0, nbuf, 0, this.pos);
if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
throw new IOException("Stream closed");
}
buffer = nbuf;
}
}
this.count = this.pos;
nsz = this.getInIfOpen().read(buffer, this.pos, buffer.length - this.pos);
if (nsz > 0) {
this.count = nsz + this.pos;
}
}
-情景1 如果緩衝區中的數據已經被全部讀入 且沒有進行標記 此時 fill方法相當於
private void fill() throws IOException {
byte[] buffer = this.getBufIfOpen();
int nsz;
this.pos = 0;
this.count = this.pos;
nsz = this.getInIfOpen().read(buffer, this.pos, buffer.length - this.pos);
if (nsz > 0) {
this.count = nsz + this.pos;
}
此情景下 要讀入的數據被從數據源輸入流複製到流緩衝區 並更新有效數據字符數count
程序運行的情況是 數據源輸入流中有較長的數據,程序每次從中讀取一部分數據到緩衝區buffer中進行操作。
每次當我們讀取完buffer中的數據之後,並且此時輸入流沒有被標記;
那麼,就接着從輸入流中讀取下一部分的數據到buffer中。
其中,判斷是否讀完buffer中的數據,是通過 if (pos >= count) 來判斷的;
判斷輸入流有沒有被標記,是通過 if (markpos < 0) 來判斷的。
理解這個思想之後,我們再對這種情況下的fill()的代碼進行分析,就特別容易理解了。
private void fill() throws IOException {
byte[] buffer = this.getBufIfOpen();
int nsz;
判斷輸入流是否被標記
if (this.markpos < 0) {
未被標記 markpos =-1<0
this.pos = 0;
} else if (this.pos >= buffer.length) {
已經被標記了
……
}
this.count = this.pos;
獲取輸入流並複製到緩衝區中 讀入buffer.length - this.pos個字節 因爲此時未被標記
所以就是讀入buffer.length個字節
nsz = this.getInIfOpen().read(buffer, this.pos, buffer.length - this.pos);
if (nsz > 0) {
根據從輸入流中讀取的實際數據的多少,來更新buffer中數據的實際大小
this.count = nsz + this.pos;
}
}
-情景2 緩衝區數據已讀完 但是進行了標記
此時 fill方法相當於
private void fill() throws IOException {
byte[] buffer = this.getBufIfOpen();
if (markpos >= 0 && pos >= buffer.length) {
int sz = pos - markpos;
System.arraycopy(buffer, markpos, buffer, 0, sz);
pos = sz;
markpos = 0;
}
count = pos;
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
count = n + pos;
}
此時程序運行的情況是 — — 數據源輸入流中有較長的數據,程序每次從中讀取一部分數據到緩衝區buffer中進行操作。
當讀取完buffer中的數據之後,並且此時輸入流存在標記時;
那麼,就發生情景2。
此時,需要先將“被標記位置”到“buffer末尾”的數據保存起來,然後再從輸入流中讀取下一部分的數據到buffer中。
其中,判斷是否讀完buffer中的數據,是通過 if (pos >= count) 來判斷的;
判斷輸入流有沒有被標記,是通過 if (markpos < 0) 來判斷的。
判斷buffer是否已經被用完,是通過 if (pos >= buffer.length) 來判斷的。
理解這個思想之後,我們再對這種情況下的fill()代碼進行分析,就特別容易理解了。
注意:情況2進行fill緩衝區刷新後,markpos的值由“大於0”變成了“等於0”!
private void fill() throws IOException {
byte[] buffer = this.getBufIfOpen();
int nsz;
if (this.markpos < 0) {
……
}
下一個讀入遊標位置超過了buffer長度 說明buffer空間已經用完
else if (this.pos >= buffer.length) {
並且進行了標記
if (this.markpos > 0) {
標定當前讀入位置到標記位置的長度nsz
nsz = this.pos - this.markpos;
將buffer中標記位置開始的nsz長度的數據複製到buffer從0位置開始的部分
System.arraycopy(buffer, this.markpos, buffer, 0, nsz);
下一個讀入數據遊標移動到nsz位置
this.pos = nsz;
標記遊標移動到buffer頭部
this.markpos = 0;
}
有效數據長度被置爲pos的位置 也即使nsz長度的位置
this.count = this.pos;
繼續從數據源輸入流中讀入數據
讀入長度爲緩衝區減掉標記的長度剩餘的空間buffer.length - this.pos
然後複製到buffer中從pos開始的空間
nsz = this.getInIfOpen().read(buffer, this.pos, buffer.length - this.pos);
if (nsz > 0) {
更新count大小數據
this.count = nsz + this.pos;
}
}
- 情況3 讀取完buffer中的數據,進行了標記 標記位置爲0 但buffer長度超過了標記的限制
此時代碼相當於
private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
if (markpos >= 0 && pos >= buffer.length) {
if ( (markpos <= 0) && (buffer.length >= marklimit) ) {
緩衝區buffer長度過大 超過了標記限制marklimit
則標記無效 markpos被置爲未標記的-1
markpos = -1;
讀入遊標置回0
pos = 0;
}
}
count = pos;
從數據源輸入流中讀入數據並複製到緩衝區buffer中
複製長度爲buffer.length - pos 實際上就是buffer.length
int nsz= getInIfOpen().read(buffer, pos, buffer.length - pos);
if (nsz> 0){
count = n + pos;
}
}
- 情況4 讀取完buffer中的數據,進行了標記,標記位置爲0 並且buffer長度沒有超過標記的限制
此時代碼等於
private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
已經進行了標記 標記位置爲0 並且 緩衝區已經讀完
if (this.pos >= buffer.length) {
if (this.markpos > 0) {
……
}
else {
如果緩衝區長度超過了緩衝區允許的最大值 拋出內存耗盡異常
if (buffer.length >= MAX_BUFFER_SIZE) {
throw new OutOfMemoryError("Required array size too large");
}
下面要用的數組長度nsz 其的大小是“pos*2”和“marklimit”中較小的那個數
這其實是一個擴容操作 因爲標記的位置是0 所以要把緩衝區中所有有效數據都保存起來
所以緩衝區要進行擴容 既可以容納新讀入的數據 又要保存之前的數據以預備重置讀入遊標
int nsz = this.pos <= MAX_BUFFER_SIZE - this.pos ? this.pos * 2 : MAX_BUFFER_SIZE;
if (nsz > this.marklimit) {
nsz = this.marklimit;
}
新建新緩衝數組nbuf 長度爲nsz
byte[] nbuf = new byte[nsz];
將數據從舊的緩衝數組buffer中複製到新的數組nbuf 中
System.arraycopy(buffer, 0, nbuf, 0, this.pos);
CAS比較和替換 檢查buffer是否是期望值
作用是多線程下檢查流是否關閉
if (!bufUpdater.compareAndSet(this, buffer, nbuf)) {
throw new IOException("Stream closed");
}
buffer替換爲nbuf
buffer = nbuf;
}
}
count = pos;
從數據源輸入流讀入新數據
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
count = n + pos;
}
注意:在這裏,我們思考一個問題,“爲什麼需要marklimit,它的存在到底有什麼意義?”我們結合“情況2”、“情況3”、“情況4”的情況來分析。
假設,marklimit是無限大的,而且我們設置了markpos。當我們從輸入流中每讀完一部分數據並讀取下一部分數據時,都需要保存markpos所標記的數據;這就意味着,我們需要不斷執行情況4中的操作,要將buffer的容量擴大……隨着讀取次數的增多,buffer會越來越大;這會導致我們佔據的內存越來越大。所以,我們需要給出一個marklimit;當buffer>=marklimit時,就不再保存markpos的值了。
情況5:除了上面4種情況之外的情況
代碼等於
private void fill() throws IOException {
byte[] buffer = getBufIfOpen();
count = pos;
int n = getInIfOpen().read(buffer, pos, buffer.length - pos);
if (n > 0)
count = n + pos;
}
直接從輸入流讀取部分新數據到buffer中
BufferedOutputStream
概述
BufferedOutputStream繼承於FilterOutputStream,
FilterOutputStream 從名字上就可以看出 是個過濾流 類似於FilterWriter
用來“封裝其它的輸出流,併爲它們提供額外的功能”。
BufferedOutputStream的作用就是爲“輸出流提供緩衝功能,允許每次寫入一批數據到底層輸出流中
而不是每次都調用底層節點流操作每一個字節
源碼解析
成員函數
字節緩衝數組 byte[] buf
protected byte[] buf;
有效數據大小
protected int count;
成員方法
構造方法
傳入一個節點輸入流 創建一個默認緩衝大小的BufferedOutputStream
public BufferedOutputStream(OutputStream out) {
this(out, 8192);
}
創建指定緩衝大小的BufferedOutputStream
public BufferedOutputStream(OutputStream out, int size) {
super(out);
if (size <= 0) {
throw new IllegalArgumentException("Buffer size <= 0");
} else {
this.buf = new byte[size];
}
}
刷新並寫入底層流方法
private void flushBuffer() throws IOException {
if (this.count > 0) {
調用write方法 緩衝區數據全部寫入底層輸出流
this.out.write(this.buf, 0, this.count);
this.count = 0;
}
}
寫出方法
寫出方法 參數爲整型 寫出的時候被轉換
public synchronized void write(int b) throws IOException {
如果有效數據較多 則先寫出到底層輸出流
if (this.count >= this.buf.length) {
this.flushBuffer();
}
this.buf[this.count++] = (byte)b;
}
public synchronized void write(byte[] b, int off, int len) throws IOException {
若寫入長度大於緩衝區大小,則先將緩衝中的數據寫入到輸出流,然後直接將數組b寫入到底層輸出流中
if (len >= this.buf.length) {
this.flushBuffer();
this.out.write(b, off, len);
}
若剩餘的緩衝空間 不足以 存儲即將寫入的數據,則先將緩衝中的數據寫入到底層輸出流中
然後將寫入數據存儲到緩衝數組中
else {
if (len > this.buf.length - this.count) {
this.flushBuffer();
}
System.arraycopy(b, off, this.buf, this.count, len);
this.count += len;
}
}
刷新方法 執行寫入操作
public synchronized void flush() throws IOException {
this.flushBuffer();
this.out.flush();
}