Java 的字節流文件讀取(二)


接着上篇文章,我們繼續來學習 Java 中的字節流操作。

裝飾者緩沖流 BufferedInput/OutputStream

裝飾者流其實是基於一種設計模式「裝飾者模式」而實現的一種文件 IO 流,而我們的緩沖流只是其中的一種,我們一起來看看。

在這之前,我們使用的文件讀寫流 FileInputStream 和 FileOutputStream 都是一個字節一個字節的從磁盤讀取或寫入,非常耗時。

而我們的緩沖流可以預先從磁盤一次性讀出指定容量的字節數到內存中,之后的讀取操作將直接從內存中讀取,提高效率。下面我們一起看看緩沖流的具體實現情況:

依然先以 BufferedInputStream 為例,我們簡單提一下它的幾個核心屬性:

  • private static int DEFAULT_BUFFER_SIZE = 8192;
  • protected volatile byte buf[];
  • private static int MAX_BUFFER_SIZE = Integer.MAX_VALUE - 8;
  • protected int count;
  • protected int pos;
  • protected int markpos = -1;
  • protected int marklimit;

buf 就是用於緩沖讀的字節數組,它的值將隨着流的讀取而不停的被填充,繼而后續的讀操作可以直接基於這個緩沖數組。

DEFAULT_BUFFER_SIZE 規定了默認緩沖區的大小,即 buf 的數組長度。MAX_BUFFER_SIZE 指明了緩沖區的上限。

count 指向緩沖數組中最后一個有效字節索引后一位。pos 指向下一個待讀取的字節索引位置。

markpos 和 marklimit 用於重復讀操作。

接着我們看看 BufferedInputStream 的幾個示例構造器:

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];
}

整體上來說,前者只需要傳入一個「被裝飾」的 InputStream 實例,並使用默認大小的緩沖區。后者則可以顯式指明緩沖區的大小。

除此之外,super(in) 會將這個 InputStream 實例保存進父類 FilterInputStream 的 in 屬性字段中,並且所有實際的磁盤讀操作都由這個 InputStream 實例發出。

下面我們來看最重要的讀操作以及緩沖區是如何被填充的。

public synchronized int read() throws IOException {
    if (pos >= count) {
        fill();
        if (pos >= count)
            return -1;
    }
    return getBufIfOpen()[pos++] & 0xff;
}

這個方法想必大家已經很熟悉了,從流中讀取下一個字節並返回,但細節上的實現還是稍稍有些不同。

count 指向了緩沖數組中有效字節索引后一位置處,pos 指向下一個待讀取的字節索引位置。理論上 pos 是不可能大於 count 的,最多等於。

如果 pos 等於 count,那說明緩沖數組中所有有效字節都已經被讀取過了,此時即需要丟棄緩沖區中那些「無用」的數據,從磁盤重新加載一批新數據填充緩沖區。

而事實上,fill 方法就是做的這個事情,它的代碼比較多,就不帶大家去解析了,你理解了它的作用,想必分析它的實現也是容易的。

如果 fill 方法調用之后,pos 依然 等於 count,那么說明 InputStream 實例並沒有從流中讀取出任何數據,也即文件流中無數據可讀。關於這一點,參見 fill 方法 246 行。

總的來說,如果成功填充了緩沖區,那么我們的 read 方法將直接從緩沖區取出一個字節返回給調用者。

public synchronized int read(byte b[], int off, int len){
    //.....
}

這個方法也是「熟人」了,不再多余的解釋了,實現是類似的。

skip 方法用於跳過指定長度的字節數進行文件流的繼續讀取:

public synchronized long skip(long n){
    //.....
}

注意一點的是,skip 方法盡量去跳過 n 個字節,但不保證一定跳過 n 個字節,方法返回的是實際跳過的字節數。如果緩沖數組中剩余可用字節數小於 n,那么最終將跳過緩沖數組中實際可跳過的字節數。

最后要說一說這個 close 方法:

public void close() throws IOException {
    byte[] buffer;
    while ( (buffer = buf) != null) {
        if (bufUpdater.compareAndSet(this, buffer, null)) {
            InputStream input = in;
            in = null;
            if (input != null)
                input.close();
            return;
        }
        // Else retry in case a new buf was CASed in fill()
    }
}

close 方法將賦空「被裝飾者」流,並調用它的 close 方法釋放相關資源,最終也會清空緩沖數組所占用的內存空間。

BufferedInputStream 提供了讀緩沖能力,而 BufferedOutputStream 則提供了寫緩沖能力,即內存的寫操作並不會立馬更新到磁盤,暫時保存在緩沖區,待緩沖區滿時一並寫入。

protected byte buf[];

protected int count;

buf 代表了內部緩沖區,count 表示緩沖區中實際數據容量,即 buf 中有效字節數,而不是 buf 數組長度。

public BufferedOutputStream(OutputStream out) {
    this(out, 8192);
}

public BufferedOutputStream(OutputStream out, int size) {
    super(out);
    if (size <= 0) {
        throw new IllegalArgumentException("Buffer size <= 0");
    }
    buf = new byte[size];
}

一樣的實現思路,必須提供的是一個 OutputStream 輸出流實例,也可以選擇性指明緩沖區大小。

public synchronized void write(int b) throws IOException {
    if (count >= buf.length) {
        flushBuffer();
    }
    buf[count++] = (byte)b;
}

寫方法將首先檢查緩沖區是否還能容納本次寫操作,如果不能將發起一次磁盤寫操作,將緩沖區數據全部寫入磁盤文件,否則將優先寫入緩沖區。

當然,BufferedOutputStream 也提供了 flush 方法向外提供接口,也即不一定非要等到緩沖區滿了才向磁盤寫數據,你也可以顯式的調用該方法讓它清空緩沖區並更新磁盤文件。

public synchronized void flush() throws IOException {
    flushBuffer();
    out.flush();
}

關於緩沖流,核心內容介紹如上,這是一種能夠顯著提升效率的流,通過它,能夠減少磁盤訪問次數,提升程序執行效率。

有關對象序列化流 ObjectInput/OutputStream 以及基於基本類型的裝飾者流 DataInput/OutputStream 我們這里暫時不做討論。待到我們學習序列化的時候,再回頭討論這兩個字節流。


文章中的所有代碼、圖片、文件都雲存儲在我的 GitHub 上:

(https://github.com/SingleYam/overview_java)

歡迎關注微信公眾號:撲在代碼上的高爾基,所有文章都將同步在公眾號上。

image


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM