1. 程式人生 > >thinking in java (三十) ----- IO之 BufferedReader

thinking in java (三十) ----- IO之 BufferedReader

BufferedReader介紹

BufferedReader是緩衝字元流,繼承於Reader

作用是為其他的流新增緩衝功能

原始碼分析

package java.io;

public class BufferedReader extends Reader {

    private Reader in;

    // 字元緩衝區
    private char cb[];
    // nChars 是cb緩衝區中字元的總的個數
    // nextChar 是下一個要讀取的字元在cb緩衝區中的位置
    private int nChars, nextChar;

    // 表示“標記無效”。它與UNMARKED的區別是:
    // (01) UNMARKED 是壓根就沒有設定過標記。
    // (02) 而INVALIDATED是設定了標記,但是被標記位置太長,導致標記無效!
    private static final int INVALIDATED = -2;
    // 表示沒有設定“標記”
    private static final int UNMARKED = -1;
    // “標記”
    private int markedChar = UNMARKED;
    // “標記”能標記位置的最大長度
    private int readAheadLimit = 0; /* Valid only when markedChar > 0 */

    // skipLF(即skip Line Feed)是“是否忽略換行符”標記
    private boolean skipLF = false;

    // 設定“標記”時,儲存的skipLF的值
    private boolean markedSkipLF = false;

    // 預設字元緩衝區大小
    private static int defaultCharBufferSize = 8192;
    // 預設每一行的字元個數
    private static int defaultExpectedLineLength = 80;

    // 建立“Reader”對應的BufferedReader物件,sz是BufferedReader的緩衝區大小
    public BufferedReader(Reader in, int sz) {
        super(in);
        if (sz <= 0)
            throw new IllegalArgumentException("Buffer size <= 0");
        this.in = in;
        cb = new char[sz];
        nextChar = nChars = 0;
    }

    // 建立“Reader”對應的BufferedReader物件,預設的BufferedReader緩衝區大小是8k
    public BufferedReader(Reader in) {
        this(in, defaultCharBufferSize);
    }

    // 確保“BufferedReader”是開啟狀態
    private void ensureOpen() throws IOException {
        if (in == null)
            throw new IOException("Stream closed");
    }

    // 填充緩衝區函式。有以下兩種情況被呼叫:
    // (01) 緩衝區沒有資料時,通過fill()可以向緩衝區填充資料。
    // (02) 緩衝區資料被讀完,需更新時,通過fill()可以更新緩衝區的資料。
    private void fill() throws IOException {
        // dst表示“cb中填充資料的起始位置”。
        int dst;
        if (markedChar <= UNMARKED) {
            // 沒有標記的情況,則設dst=0。
            dst = 0;
        } else {
            // delta表示“當前標記的長度”,它等於“下一個被讀取字元的位置”減去“標記的位置”的差值;
            int delta = nextChar - markedChar;
            if (delta >= readAheadLimit) {
                // 若“當前標記的長度”超過了“標記上限(readAheadLimit)”,
                // 則丟棄標記!
                markedChar = INVALIDATED;
                readAheadLimit = 0;
                dst = 0;
            } else {
                if (readAheadLimit <= cb.length) {
                    // 若“當前標記的長度”沒有超過了“標記上限(readAheadLimit)”,
                    // 並且“標記上限(readAheadLimit)”小於/等於“緩衝的長度”;
                    // 則先將“下一個要被讀取的位置,距離我們標記的置符的距離”間的字元儲存到cb中。
                    System.arraycopy(cb, markedChar, cb, 0, delta);
                    markedChar = 0;
                    dst = delta;
                } else {
                    // 若“當前標記的長度”沒有超過了“標記上限(readAheadLimit)”,
                    // 並且“標記上限(readAheadLimit)”大於“緩衝的長度”;
                    // 則重新設定緩衝區大小,並將“下一個要被讀取的位置,距離我們標記的置符的距離”間的字元儲存到cb中。
                    char ncb[] = new char[readAheadLimit];
                    System.arraycopy(cb, markedChar, ncb, 0, delta);
                    cb = ncb;
                    markedChar = 0;
                    dst = delta;
                }
                // 更新nextChar和nChars
                nextChar = nChars = delta;
            }
        }

        int n;
        do {
            // 從“in”中讀取資料,並存儲到字元陣列cb中;
            // 從cb的dst位置開始儲存,讀取的字元個數是cb.length - dst
            // n是實際讀取的字元個數;若n==0(即一個也沒讀到),則繼續讀取!
            n = in.read(cb, dst, cb.length - dst);
        } while (n == 0);

        // 如果從“in”中讀到了資料,則設定nChars(cb中字元的數目)=dst+n,
        // 並且nextChar(下一個被讀取的字元的位置)=dst。
        if (n > 0) {
            nChars = dst + n;
            nextChar = dst;
        }
    }

    // 從BufferedReader中讀取一個字元,該字元以int的方式返回
    public int read() throws IOException {
        synchronized (lock) {
            ensureOpen();
            for (;;) {
                // 若“緩衝區的資料已經被讀完”,
                // 則先通過fill()更新緩衝區資料
                if (nextChar >= nChars) {
                    fill();
                    if (nextChar >= nChars)
                        return -1;
                }
                // 若要“忽略換行符”,
                // 則對下一個字元是否是換行符進行處理。
                if (skipLF) {
                    skipLF = false;
                    if (cb[nextChar] == '\n') {
                        nextChar++;
                        continue;
                    }
                }
                // 返回下一個字元
                return cb[nextChar++];
            }
        }
    }

    // 將緩衝區中的資料寫入到陣列cbuf中。off是陣列cbuf中的寫入起始位置,len是寫入長度
    private int read1(char[] cbuf, int off, int len) throws IOException {
        // 若“緩衝區的資料已經被讀完”,則更新緩衝區資料。
        if (nextChar >= nChars) {
            if (len >= cb.length && markedChar <= UNMARKED && !skipLF) {
                return in.read(cbuf, off, len);
            }
            fill();
        }
        // 若更新資料之後,沒有任何變化;則退出。
        if (nextChar >= nChars) return -1;
        // 若要“忽略換行符”,則進行相應處理
        if (skipLF) {
            skipLF = false;
            if (cb[nextChar] == '\n') {
                nextChar++;
                if (nextChar >= nChars)
                    fill();
                if (nextChar >= nChars)
                    return -1;
            }
        }
        // 拷貝字元操作
        int n = Math.min(len, nChars - nextChar);
        System.arraycopy(cb, nextChar, cbuf, off, n);
        nextChar += n;
        return n;
    }

    // 對read1()的封裝,添加了“同步處理”和“阻塞式讀取”等功能
    public int read(char cbuf[], int off, int len) throws IOException {
        synchronized (lock) {
            ensureOpen();
            if ((off < 0) || (off > cbuf.length) || (len < 0) ||
                ((off + len) > cbuf.length) || ((off + len) < 0)) {
                throw new IndexOutOfBoundsException();
            } else if (len == 0) {
                return 0;
            }

            int n = read1(cbuf, off, len);
            if (n <= 0) return n;
            while ((n < len) && in.ready()) {
                int n1 = read1(cbuf, off + n, len - n);
                if (n1 <= 0) break;
                n += n1;
            }
            return n;
        }
    }

    // 讀取一行資料。ignoreLF是“是否忽略換行符”
    String readLine(boolean ignoreLF) throws IOException {
        StringBuffer s = null;
        int startChar;

        synchronized (lock) {
            ensureOpen();
            boolean omitLF = ignoreLF || skipLF;

            bufferLoop:
            for (;;) {

                if (nextChar >= nChars)
                    fill();
                if (nextChar >= nChars) { /* EOF */
                    if (s != null && s.length() > 0)
                        return s.toString();
                    else
                        return null;
                }
                boolean eol = false;
                char c = 0;
                int i;

                /* Skip a leftover '\n', if necessary */
                if (omitLF && (cb[nextChar] == '\n'))
                    nextChar++;
                skipLF = false;
                omitLF = false;

            charLoop:
                for (i = nextChar; i < nChars; i++) {
                    c = cb[i];
                    if ((c == '\n') || (c == '\r')) {
                        eol = true;
                        break charLoop;
                    }
                }

                startChar = nextChar;
                nextChar = i;

                if (eol) {
                    String str;
                    if (s == null) {
                        str = new String(cb, startChar, i - startChar);
                    } else {
                        s.append(cb, startChar, i - startChar);
                        str = s.toString();
                    }
                    nextChar++;
                    if (c == '\r') {
                        skipLF = true;
                    }
                    return str;
                }

                if (s == null)
                    s = new StringBuffer(defaultExpectedLineLength);
                s.append(cb, startChar, i - startChar);
            }
        }
    }

    // 讀取一行資料。不忽略換行符
    public String readLine() throws IOException {
        return readLine(false);
    }

    // 跳過n個字元
    public long skip(long n) throws IOException {
        if (n < 0L) {
            throw new IllegalArgumentException("skip value is negative");
        }
        synchronized (lock) {
            ensureOpen();
            long r = n;
            while (r > 0) {
                if (nextChar >= nChars)
                    fill();
                if (nextChar >= nChars) /* EOF */
                    break;
                if (skipLF) {
                    skipLF = false;
                    if (cb[nextChar] == '\n') {
                        nextChar++;
                    }
                }
                long d = nChars - nextChar;
                if (r <= d) {
                    nextChar += r;
                    r = 0;
                    break;
                }
                else {
                    r -= d;
                    nextChar = nChars;
                }
            }
            return n - r;
        }
    }

    // “下一個字元”是否可讀
    public boolean ready() throws IOException {
        synchronized (lock) {
            ensureOpen();

            // 若忽略換行符為true;
            // 則判斷下一個符號是否是換行符,若是的話,則忽略
            if (skipLF) {
                if (nextChar >= nChars && in.ready()) {
                    fill();
                }
                if (nextChar < nChars) {
                    if (cb[nextChar] == '\n')
                        nextChar++;
                    skipLF = false;
                }
            }
            return (nextChar < nChars) || in.ready();
        }
    }

    // 始終返回true。因為BufferedReader支援mark(), reset()
    public boolean markSupported() {
        return true;
    }

    // 標記當前BufferedReader的下一個要讀取位置。關於readAheadLimit的作用,參考後面的說明。
    public void mark(int readAheadLimit) throws IOException {
        if (readAheadLimit < 0) {
            throw new IllegalArgumentException("Read-ahead limit < 0");
        }
        synchronized (lock) {
            ensureOpen();
            // 設定readAheadLimit
            this.readAheadLimit = readAheadLimit;
            // 儲存下一個要讀取的位置
            markedChar = nextChar;
            // 儲存“是否忽略換行符”標記
            markedSkipLF = skipLF;
        }
    }

    // 重置BufferedReader的下一個要讀取位置,
    // 將其還原到mark()中所儲存的位置。
    public void reset() throws IOException {
        synchronized (lock) {
            ensureOpen();
            if (markedChar < 0)
                throw new IOException((markedChar == INVALIDATED)
                                      ? "Mark invalid"
                                      : "Stream not marked");
            nextChar = markedChar;
            skipLF = markedSkipLF;
        }
    }

    public void close() throws IOException {
        synchronized (lock) {
            if (in == null)
                return;
            in.close();
            in = null;
            cb = null;
        }
    }
}

首先我們瞭解BufferedReader的思想。他是為其他的Reader提供緩衝功能,建立BufferedReader的時候,使用建構函式,其中建構函式以Reader為引數,每次讀取Reader中的一部分資料到快取中,分批次地讀取,這樣每次讀取一部分到快取,然後操作完這一部分資料以後,再接著讀取下一部分的資料。

為什麼需要緩衝呢,前面的緩衝位元組流也說過了,緩衝是存在記憶體中的,而原始資料可能是在U盤等硬體中,兩邊的讀取資料相差幾十倍。那麼為什麼不直接一次性就讀取完畢呢,因為記憶體貴啊,而且一次讀取所有資料可能需要的時間會比較長。並且記憶體的 大小和硬體相比一般會小很多。

之前的位元組流中,我們也學習了fill方法 是緩衝中最重要的辦法,在這裡我們依然對fill方法進行比較詳細的探討

fill原始碼

private void fill() throws IOException {
    int dst;
    if (markedChar <= UNMARKED) {
        /* No mark */
        dst = 0;
    } else {
        /* Marked */
        int delta = nextChar - markedChar;
        if (delta >= readAheadLimit) {
            /* Gone past read-ahead limit: Invalidate mark */
            markedChar = INVALIDATED;
            readAheadLimit = 0;
            dst = 0;
        } else {
            if (readAheadLimit <= cb.length) {
                /* Shuffle in the current buffer */
                System.arraycopy(cb, markedChar, cb, 0, delta);
                markedChar = 0;
                dst = delta;
            } else {
                /* Reallocate buffer to accommodate read-ahead limit */
                char ncb[] = new char[readAheadLimit];
                System.arraycopy(cb, markedChar, ncb, 0, delta);
                cb = ncb;
                markedChar = 0;
                dst = delta;
            }
            nextChar = nChars = delta;
        }
    }

    int n;
    do {
        n = in.read(cb, dst, cb.length - dst);
    } while (n == 0);
    if (n > 0) {
        nChars = dst + n;
        nextChar = dst;
    }
}

根據if  else情況,我們把fill分為四種情況

1,讀完緩衝區的資料,並且緩衝區沒有被標記

執行流程如下,
(01) 其它函式呼叫 fill(),來更新緩衝區的資料
(02) fill() 執行程式碼 if (markedChar <= UNMARKED) { ... }
為了方便分析,我們將這種情況下fill()執行的操作等價於以下程式碼:

private void fill() throws IOException {
    int dst;
    if (markedChar <= UNMARKED) {
        /* No mark */
        dst = 0;
    } 

    int n;
    do {
        n = in.read(cb, dst, cb.length - dst);
    } while (n == 0);

    if (n > 0) {
        nChars = dst + n;
        nextChar = dst;
    }
}

這種情況就是,原始資料有很長一段,先讀取一部分到快取中進行操作,然後操作完了以後,並且此時

緩衝沒有被標記,那麼就接著從Reader中讀取下一部分資料到緩衝中。其中判斷是否讀取完緩衝資料,是通過nextChar和nChars,nChars是快取區的總的個數,而nextChar是快取區中下一個要讀取的字元的位置。判斷有沒有被標記,是根據markedChar來判斷。接下來我們分析程式碼:

1),if (markedChar <= UNMARKED) 它的作用是判斷“BufferedReader是否被標記”。若被標記,則dst=0。

2),in.read(cb, dst, cb.length - dst) 等價於 in.read(cb, 0, cb.length),意思是從Reader物件in中讀取cb.length個數據,並快取到快取區cb中,而且是從快取區的位置0開始儲存。該函方法的返回值是n,n是實際讀取字元的個數。

3),nChars=dst+n 等價於 nChars=n;意味著,更新緩衝區資料cb之後,設定nChars(緩衝區的資料個數)為n。

4) nextChar=dst 等價於 nextChar=0;意味著,更新緩衝區資料cb之後,設定nextChar(緩衝區中下一個會被讀取的字元的索引值)為0。

 

2,讀取玩緩衝區資料,緩衝區的標記位置大於0,並且“當前標記的長度”超過“標記上限(readAheadLimit)”

執行流程如下,
(01) 其它函式呼叫 fill(),來更新緩衝區的資料
(02) fill() 執行程式碼 if (delta >= readAheadLimit) { ... }
為了方便分析,我們將這種情況下fill()執行的操作等價於以下程式碼:

private void fill() throws IOException {
    int dst;
    if (markedChar > UNMARKED) {
        int delta = nextChar - markedChar;
        if (delta >= readAheadLimit) {
            markedChar = INVALIDATED;
            readAheadLimit = 0;
            dst = 0;
        } 
    }

    int n;
    do {
        n = in.read(cb, dst, cb.length - dst);
    } while (n == 0);
    if (n > 0) {
        nChars = dst + n;
        nextChar = dst;
    }
}

說明:這種情況是我們每次讀取一部分資料到快取中,操作完快取中的資料以後,並且此時,BufferedReader中存在標記,同時,“當前標記的長度”大於“標記上限”;那麼,就發生情況2。此時我們會丟棄標記,並且更新緩衝區

1)delta = nextChar - markedChar;其中,delta就是“當前標記的長度”,他是“下一個被讀取的字元位置 “”減去“”被標記的位置“”的差值

2) if (delta >= readAheadLimit);其中,當delta >= readAheadLimit,就意味著被標記的長度大於標記上限,為什麼要有標記上限,即readAheadLimit的值到底有何意義呢?

我們標記一個位置之後,更新緩衝區的時候,被標記的位置會被儲存;當我們不停的更新緩衝區的時候,被標記的位置會被不停的放大。然後記憶體的容量是有效的,我們不可能不限制長度的儲存標記。所以,需要readAheadLimit來限制標記長度!

3)in.read(cb, dst, cb.length - dst) 等價於 in.read(cb, 0, cb.length),意思是從Reader物件in中讀取cb.length個數據,並存儲到緩衝區cb中,而且從緩衝區cb的位置0開始儲存。該函式返回值等於n,也就是n表示實際讀取的字元個數。若n=0(即沒有讀取到資料),則繼續讀取,直到讀到資料為止。

4) nChars=dst+n 等價於 nChars=n;意味著,更新緩衝區資料cb之後,設定nChars(緩衝區的資料個數)為n。
5)   nextChar=dst 等價於 nextChar=0;意味著,更新緩衝區資料cb之後,設定nextChar(緩衝區中下一個會被讀取的字元的索引值)為0。

 

3,情況3讀取完緩衝區的資料,緩衝區的標記位置>0,“當前標記的長度”沒超過“標記上限(readAheadLimit)”,並且“標記上限(readAheadLimit)”小於/等於“緩衝的長度”;
執行流程如下,
(01) 其它函式呼叫 fill(),來更新緩衝區的資料
(02) fill() 執行程式碼 if (readAheadLimit <= cb.length) { ... }
為了方便分析,我們將這種情況下fill()執行的操作等價於以下程式碼:

private void fill() throws IOException {
    int dst;
    if (markedChar > UNMARKED) {
        int delta = nextChar - markedChar;
        if ((delta < readAheadLimit) &&  (readAheadLimit <= cb.length) ) {
            System.arraycopy(cb, markedChar, cb, 0, delta);
            markedChar = 0;
            dst = delta;

            nextChar = nChars = delta;
        }
    }

    int n;
    do {
        n = in.read(cb, dst, cb.length - dst);
    } while (n == 0);
    if (n > 0) {
        nChars = dst + n;
        nextChar = dst;
    }
}

BufferedReader中有很長的資料,我們每次從中讀取一部分資料到緩衝區中進行操作。當我們讀取完緩衝區中的資料之後,並且此時,BufferedReader存在標記時,同時,“當前標記的長度”小於“標記上限”,並且“標記上限”小於/等於“緩衝區長度”;那麼,就發生情況3。此時,我們保留“被標記的位置”(即,保留被標記位置開始的資料),並更新緩衝區(將新增的資料,追加到保留的資料之後)。

 

情況4:讀取完緩衝區的資料,緩衝區的標記位置>0,“當前標記的長度”沒超過“標記上限(readAheadLimit)”,並且“標記上限(readAheadLimit)”大於“緩衝的長度”;
執行流程如下,
(01) 其它函式呼叫 fill(),來更新緩衝區的資料
(02) fill() 執行程式碼 else { char ncb[] = new char[readAheadLimit]; ... }
為了方便分析,我們將這種情況下fill()執行的操作等價於以下程式碼:

private void fill() throws IOException {
    int dst;
    if (markedChar > UNMARKED) {
        int delta = nextChar - markedChar;
        if ((delta < readAheadLimit) &&  (readAheadLimit > cb.length) ) {
            char ncb[] = new char[readAheadLimit];
            System.arraycopy(cb, markedChar, ncb, 0, delta);
            cb = ncb;
            markedChar = 0;
            dst = delta;
            
            nextChar = nChars = delta;
        }
    }

    int n;
    do {
        n = in.read(cb, dst, cb.length - dst);
    } while (n == 0);
    if (n > 0) {
        nChars = dst + n;
        nextChar = dst;
    }
}

這種情況發生的情況是 — — BufferedReader中有很長的資料,我們每次從中讀取一部分資料到緩衝區中進行操作。當我們讀取完緩衝區中的資料之後,並且此時,BufferedReader存在標記時,同時,“當前標記的長度”小於“標記上限”,並且“標記上限”大於“緩衝區長度”;那麼,就發生情況4。此時,我們要先更新緩衝區的大小,然後再保留“被標記的位置”(即,保留被標記位置開始的資料),並更新緩衝區資料(將新增的資料,追加到保留的資料之後)。

示例程式碼

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.lang.SecurityException;

/**
 * BufferedReader 測試程式
 *
 * @author skywang
 */
public class BufferedReaderTest {

    private static final int LEN = 5;

    public static void main(String[] args) {
        testBufferedReader() ;
    }

    /**
     * BufferedReader的API測試函式
     */
    private static void testBufferedReader() {

        // 建立BufferedReader字元流,內容是ArrayLetters陣列
        try {
            File file = new File("bufferedreader.txt");
            BufferedReader in =
                  new BufferedReader(
                      new FileReader(file));

            // 從字元流中讀取5個字元。“abcde”
            for (int i=0; i<LEN; i++) {
                // 若能繼續讀取下一個字元,則讀取下一個字元
                if (in.ready()) {
                    // 讀取“字元流的下一個字元”
                    int tmp = in.read();
                    System.out.printf("%d : %c\n", i, tmp);
                }
            }

            // 若“該字元流”不支援標記功能,則直接退出
            if (!in.markSupported()) {
                System.out.println("make not supported!");
                return ;
            }
              
            // 標記“當前索引位置”,即標記第6個位置的元素--“f”
            // 1024對應marklimit
            in.mark(1024);

            // 跳過22個字元。
            in.skip(22);

            // 讀取5個字元
            char[] buf = new char[LEN];
            in.read(buf, 0, LEN);
            System.out.printf("buf=%s\n", String.valueOf(buf));
            // 讀取該行剩餘的資料
            System.out.printf("readLine=%s\n", in.readLine());

            // 重置“輸入流的索引”為mark()所標記的位置,即重置到“f”處。
            in.reset();
            // 從“重置後的字元流”中讀取5個字元到buf中。即讀取“fghij”
            in.read(buf, 0, LEN);
            System.out.printf("buf=%s\n", String.valueOf(buf));

            in.close();
       } catch (FileNotFoundException e) {
           e.printStackTrace();
       } catch (SecurityException e) {
           e.printStackTrace();
       } catch (IOException e) {
           e.printStackTrace();
       }
    }
}

程式中讀取的bufferedreader.txt的內容如下:

abcdefghijklmnopqrstuvwxyz
0123456789
ABCDEFGHIJKLMNOPQRSTUVWXYZ

執行結果
0 : a
1 : b
2 : c
3 : d
4 : e
buf=01234
readLine=56789
buf=fghij

原文:http://www.cnblogs.com/skywang12345/p/io_23.html