[重學Java基礎][Java IO流][Part.12]緩衝字節輸入輸出流

[重學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();
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章