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

[重學Java基礎][JavaIO流][Part.2]緩衝字符輸入輸出流

BufferedReader

概述

BufferedReader緩衝字符數組輸入流,繼承了所有字符輸入流的超類Reader類,自帶緩存區,可以一次讀入緩衝區大小的字符內容,並且提供了按行讀取功能
通過包裝Reader對象來發揮作用
很明顯 這是一個處理流 包裝流 一般包裝InputStreamReader,FileReader對象

官方註釋

從一個輸入的字符流中讀取文本,爲字符、數組、一行文本的高效讀取提供字符緩衝功能。

緩衝區的大小可能是特殊設定值,可能是使用默認的大小。默認的大小已經足夠解決大部分問題。

一般情況下,每一個read請求由一個Reader類發起,使底層進行對字節流或byte流執行相應的讀取請求。

對一些read()函數開銷較大的的Reader類,(例如:FileReaders和InputStreamReaders)使用BufferedReader類進行封裝是很明智的。

例:

 BufferedReader in  = new BufferedReader(new FileReader("file.in"));

如果沒由進行緩衝,對read()和readline()調用會直接從文件中讀取一次byte數據,轉換成字符格式並返回,這是十分不明智的做法。

程序對文本數據使用DataInputStream時,在合適的情況下可以局部替換DataInputStream爲BufferedReader

源碼分析

成員屬性

public class BufferedReader extends Reader {

讀取的數據源  
private Reader in;
緩衝字符數組 流的實際內容體
private char cb[];

nChars 緩衝區中字符總個數
nextChar 下一個要讀取的字符位置
private int nChars, nextChar;

“標記無效”的標誌 
private static final int INVALIDATED = -2;
未設置標記
private static final int UNMARKED = -1;
默認情況下無標記字符 所以markedChar是未設置標記
private int markedChar = UNMARKED;
可標記位置能標記的最大長度 只有先設置了markedChar後此變量才能生效
private int readAheadLimit = 0; 

是否跳過換行字符 就是是否忽略換行 默認不忽略
private boolean skipLF = false;

設置標記時 是否忽略換行
private boolean markedSkipLF = false;

默認緩衝字符數組大小
private static int defaultCharBufferSize = 8192;
默認每一行的字符個數
private static int defaultExpectedLineLength = 80;
……
}

成員方法

這裏寫圖片描述

  • 構造方法

接受一個輸入流對象 並按照入參sz的大小創建字符緩衝區

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

接受一個輸入流大小 並創建默認大小的字符緩衝區

    public BufferedReader(Reader in) {
            this(in, defaultCharBufferSize);
        }

填入緩衝區方法 真正將數據填入緩衝區的方法 其他讀取方法都是此方法的裝飾器
緩衝區沒有數據時,通過fill()法以向緩衝區填充數據 緩衝區數據被讀完,需更新時,通過fill()可以更新緩衝區的數據

private void fill() throws IOException {
    緩衝數組填充的開始位置
    int dst;
    if (markedChar <= UNMARKED) {
        無標記的情況下 默認初始位置爲緩衝區頭部位置0
        dst = 0;
    } else {
        如果有標記 則標記區間長度delta 等於下一個要讀取的位置與被標記的位置的距離
        int delta = nextChar - markedChar;

        if (delta >= readAheadLimit) {
             若標記區間長度delta大於讀取的限制(超過了標記上限,
             即流下次讀取的位置已經超過了標記位置) 則標記無效
            markedChar = INVALIDATED;
            readAheadLimit = 0;
            dst = 0;
        } else {
            if (readAheadLimit <= cb.length) {
                若未超過標記上限,則將下一個讀取位置到標記位置開始的流內容複製到cb
                System.arraycopy(cb, markedChar, cb, 0, delta);
                markedChar = 0;
                dst = delta;
            } else {
                如果標記上限大於緩衝流的總長度 則將下一個讀取位置到標記位置開始的                    
                流內容複製到新的字符數組ncb中 再將cb設置爲ncb
                char ncb[] = new char[readAheadLimit];
                System.arraycopy(cb, markedChar, ncb, 0, delta);
                cb = ncb;
                markedChar = 0;
                dst = delta;
            }
            nextChar = nChars = delta;
        }
    }

    int n;
    do {
    從輸入流in中讀取數據 並存儲到cb中 讀取長度爲cb.length-dst
    如果沒讀到就繼續讀取

        n = in.read(cb, dst, cb.length - dst);
    } while (n == 0);
    if (n > 0) {
     如果從輸入流in中讀到了數據,則設置nChars(cb中字符的數目)=dst+n,
     並且nextChar(下一個被讀取的字符的位置)=dst。
        nChars = dst + n;
        nextChar = dst;
    }
}

從數據源讀取數據方法 每次讀入一個字符

 public int read() throws IOException {
        線程加鎖 說明讀取時線程安全的
        synchronized (lock) {
            ensureOpen();
            for (;;) {
            若下一個讀取的數據位置大於緩衝區的大小(緩衝區數據全部讀取)
            則先用fill()按方法刷新緩衝區 再讀取
                if (nextChar >= nChars) {
                    fill();
                    刷新後緩衝區仍然沒有新數據 則返回-1 讀入結束
                    if (nextChar >= nChars)
                        return -1;
                }
                是否忽略換行符
                if (skipLF) {
                    skipLF = false;
                    if (cb[nextChar] == '\n') {
                        nextChar++;
                        continue;
                    }
                }
                return cb[nextChar++];
            }
        }
    }

從數據源讀取數據方法 每次讀入指定長度字符內容

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

從數據源讀取數據方法 每次讀入一行內容
readLine()方法實際上調用了一個包內部方法 readLine(boolean ignoreLF)

 public String readLine() throws IOException {
        return readLine(false);
    }


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) { 
                if (s != null && s.length() > 0)
                    return s.toString();
                else
                    return null;
            }
            boolean eol = false;
            char c = 0;
            int i;

            是否跳過一個換行符
            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);
        }
    }
}

代碼示例

例1 按行讀入文件中字符 注意文件必須是UTF-8格式的 否則會亂碼

    BufferedReader br = new 
         BufferedReader(new FileReader("d:/a.txt"));


    String str;
    while((str=br.readLine())!=null)
    {
        System.out.println(str);
    }

輸出

第一行 四月是你的謊言
第二行 比宇宙更遠的地方
第三行 不死者之王
第四行 關於我的女友是個一本正經的碧池這件事

例2 按字符讀入並轉換

 BufferedReader br = new BufferedReader(new FileReader("d:/a.txt"));
 int c;
        while((c=br.read())!=-1)
        {
            System.out.print((char)c);
        }

結果同上

例3 標記並重置

    BufferedReader br = new BufferedReader(new FileReader("d:/a.txt"));
注意這裏的讀入限制爲大於讀入字符數 讀入字符數爲55 小於等於這個值reset時會報錯
    br.mark(100);
    String str;
    while((str=br.readLine())!=null)
    {
        System.out.println(str);
    }

    br.reset();
    int c;
    while((c=br.read())!=-1)
    {
        System.out.print((char)c);
    }

輸出

第一行 四月是你的謊言
第二行 比宇宙更遠的地方
第三行 不死者之王
第四行 關於我的女友是個一本正經的碧池這件事
第一行 四月是你的謊言
第二行 比宇宙更遠的地方
第三行 不死者之王
第四行 關於我的女友是個一本正經的碧池這件事

典型錯誤

1.

        BufferedReader br = new BufferedReader(new FileReader("d:/a.txt"));

    String str;
    while((str=br.readLine())!=null)
    {
        System.out.println(br.readLine());
    }

輸出

    第二行 比宇宙更遠的地方
    第四行 關於我的女友是個一本正經的碧池這件事

隔一行丟失了一行數據 因爲流被讀取後遊標移動 不會重複讀入 所以只能按例1方式操作

BuffererWriter

這裏寫圖片描述

概述

BufferedWriter緩衝字符數組輸出流,繼承了所有字符輸出流的超類Writer類,自帶緩存區,可以一次寫出緩衝區大小的字符內容,並且提供了按行寫出功能
通過包裝Writer對象來發揮作用
很明顯 這是一個處理流 包裝流 一般包裝InputStreamWriter,FileWriter對象

官方註釋

向字符輸出流寫入文本時,對字符進行緩衝處理是一種高效的處理方式,尤其是逐個的字符寫入,數組結構寫入或者字符串結構寫入

緩衝區的大小可被設定,默認大小足以滿足一般情況的需求

提供了newLine()方法,提供了基於不同平臺的行分割方法,因爲不是所有平臺都是用’\n’作爲換行符,所以使用newLine方法是首選。

一般情況下,Writer類總是立即將數據輸出到底層的字符流或比特流中。除非程序需要即刻輸出,否則使用BufferedWriter封裝時最好的選擇。(對於write()方法開銷較大的Writer類十分必要)

例如:

  PrintWriter out  = new PrintWriter(new BufferedWriter(new FileWriter("foo.out")));

對PrintWriter類進行了緩衝。如果沒有緩衝,任意一個對print方法的調用將會使字符轉化爲byte並且立刻寫入到文件中,這是非常低效率的。

源碼分析

成員屬性

    輸出的數據匯
    private Writer out;
    緩衝字符數組
    private char cb[];
    下個讀入字符遊標位置nextChar 緩衝區最大長度nChars
    private int nChars, nextChar;
    默認的緩衝區大小
    private static int defaultCharBufferSize = 8192;
    文本換行符 流創建的時刻會自動賦值 
    private String lineSeparator;

成員方法

Alt text

構造方法 創建一個默認緩衝區大小的緩衝輸出流或者指定大小的緩衝輸出流

public BufferedWriter(Writer out) {
        this(out, defaultCharBufferSize);
    }


public BufferedWriter(Writer out, int sz) {
    super(out);
    if (sz <= 0)
        throw new IllegalArgumentException("Buffer size <= 0");
    this.out = out;
    cb = new char[sz];
    nChars = sz;
    nextChar = 0;
    可以看到lineSeparator 文本換行符是通過底層方法創建的 
    按當前系統的方式創建分本換行符
    lineSeparator = java.security.AccessController.doPrivileged(
        new sun.security.action.GetPropertyAction("line.separator"));
}

刷新輸出流 可以看到刷新方法flush()內部調用了包級方法flushBuffer()方法
flushBuffer()方法則真正實現了將緩衝區字符數組寫入到輸出流的操作

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

  void flushBuffer() throws IOException {
        synchronized (lock) {
            ensureOpen();
            if (nextChar == 0)
                return;
            out.write(cb, 0, nextChar);
            nextChar = 0;
        }
    }

換行方法 寫入一個換行符 避免顯示輸出換行符可能引起的一些問題

  public void newLine() throws IOException {
        write(lineSeparator);
    }

寫入方法 讀入字符數組緩衝內容 並寫入到底層流

public void write(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;
            }

        如果寫入的字符數組大於緩衝區大小 則刷新緩衝區 並直接寫入下層流
        可以看到是直接調用底層流out的寫入方法
        if (len >= nChars) {
            flushBuffer();
            out.write(cbuf, off, len);
            return;
        }

        int b = off, t = off + len;
        while (b < t) {
            int d = min(nChars - nextChar, t - b);
            System.arraycopy(cbuf, b, cb, nextChar, d);
            b += d;
            nextChar += d;
            if (nextChar >= nChars)
                flushBuffer();
        }
    }
}

代碼示例

向控制檯寫入字符

    BufferedReader br = new BufferedReader(new FileReader("d:/b.txt"));
    BufferedWriter bwo=new BufferedWriter(new PrintWriter(System.out));

    String str;
    while((str=br.readLine())!=null)
    {
        bwo.write(str);
        bwo.newLine();
    }
    bwo.flush();

輸出結果

1.紫羅蘭永恆花園
2.龍王的工作!
3.Fate/EXTRA Last Encore
4.citrus~柑橘味香氣~

寫入到文件

BufferedWriter bw=new BufferedWriter(new FileWriter("d://c.txt"));

List<String> list= Arrays.asList("比宇宙更遠的地方","魔卡少女櫻 CLEAR CARD篇","衛宮家今天的飯","博多豚骨拉麪團");

    for (String s : list) {
        bw.write(s,0,s.length());
        bw.newLine();
    }

    bw.flush();

輸出結果

比宇宙更遠的地方
魔卡少女櫻 CLEAR CARD篇
衛宮家今天的飯
博多豚骨拉麪團

注意 輸出到文件 重複執行會覆蓋上一次執行的內容

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章