首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java IO 精通:BufferedReader 字元緩衝輸入流完整指南

     編輯:Java 相關知識

BufferedReader 核心定位

BufferedReader 是字元導向的緩衝輸入流,繼承自 Reader。它透過內部字元緩衝區(預設 8192 字元)批次從底層流讀取資料,再逐字元或逐行供應用程式消費,大幅減少系統呼叫開銷。其獨特的 readLine() 與 mark()/reset() 機制,使其成為文字檔案處理、配置解析、日誌分析的首選工具。

核心 API 分類速查

// 建構子
BufferedReader(Reader in)                    // 預設 8KB 緩衝
BufferedReader(Reader in, int sz)            // 自訂緩衝區大小

// 基礎讀取
int read()                                   // 單字元,-1 表 EOF
int read(char[] cbuf, int off, int len)      // 讀入字元陣列
String readLine()                            // 讀取一行(不含行結尾符)
boolean ready()                              // 是否可立即讀取而不阻塞
long skip(long n)                            // 跳過 n 字元

// 標記與重置(支援 markSupported() == true)
void mark(int readAheadLimit)                // 標記目前位置,限制往前讀取上限
void reset()                                 // 重置至標記位置

// 生命週期
void close()

原始碼核心機制深度剖析

緩衝區與指標設計

public class BufferedReader extends Reader {
    private Reader in;              // 底層字元流
    private char[] cb;              // 字元緩衝陣列
    private int nChars;             // 緩衝區有效字元數
    private int nextChar;           // 下一個讀取位置
    
    // 標記相關
    private int markedChar = UNMARKED;  // 標記位置,-1 表無標記
    private int readAheadLimit;         // 標記允許的最大前讀長度
    private boolean skipLF = false;     // 是否跳過 \n(處理 \r\n)
    private boolean markedSkipLF;       // 標記時的 skipLF 狀態
    
    private static final int UNMARKED = -1;
    private static final int INVALIDATED = -2;
    private static final int defaultCharBufferSize = 8192;
}

fill() 核心填充邏輯:四種情境完整解析

fill() 是緩衝區補充的核心,根據標記狀態與前讀限制分為四種情況:

情況 1:無標記,單純補充


// markedChar <= UNMARKED
dst = 0;
// 直接從底層流讀滿緩衝區
n = in.read(cb, 0, cb.length);
nChars = n; nextChar = 0;

最常見情境:循序讀取,緩衝區用盡後整區替換。

情況 2:標記失效(超過 readAheadLimit)


// markedChar > UNMARKED 且 delta >= readAheadLimit
markedChar = INVALIDATED;
readAheadLimit = 0;
dst = 0;
// 同情況 1,捨棄標記重新填充

標記保護範圍被超越,強制失效標記,避免記憶體無限增長。

情況 3:保留標記資料,緩衝區足夠


// delta < readAheadLimit 且 readAheadLimit <= cb.length
System.arraycopy(cb, markedChar, cb, 0, delta);
markedChar = 0;
dst = delta;
nextChar = nChars = delta;
// 續讀新資料至緩衝區剩餘空間

將標記後未讀資料移至緩衝區頭部,續讀新資料接在後面,標記位置歸零。

情況 4:保留標記資料,需擴充緩衝區


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

配置更大緩衝區以容納標記保護範圍,代價是記憶體分配與複製開銷。

readLine() 實現細節

掃描緩衝區尋找 \n、\r 或 \r\n,處理跨緩衝區邊界的行結尾符,遇到 \r 時透過 skipLF 標記決定是否吞噬後續 \n。這比手工解析更健壯。

實戰範例:標記重置與逐行處理

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

public class BufferedReaderDemo {
    public static void main(String[] args) {
        Path path = Path.of("sample.txt");
        // 建立測試檔案
        try {
            Files.writeString(path, 
                "abcdefghijklmnopqrstuvwxyz\n" +
                "0123456789\n" +
                "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n", 
                StandardCharsets.UTF_8);
        } catch (IOException e) { e.printStackTrace(); }

        demoMarkReset(path);
        demoLineProcessing(path);
    }

    // 展示 mark/reset 機制
    private static void demoMarkReset(Path path) {
        try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
            char[] buf = new char[5];
            
            // 讀前 5 字元:a b c d e
            for (int i = 0; i < 5; i++) {
                if (br.ready()) System.out.printf("%d : %c%n", i, (char) br.read());
            }
            
            // 標記目前位置(f),前讀限制 1024
            br.mark(1024);
            
            // 跳過 22 字元,讀 5 字元 -> 01234
            br.skip(22);
            br.read(buf, 0, 5);
            System.out.println("buf=" + new String(buf)); // 01234
            
            // 讀取剩餘行 -> 56789
            System.out.println("readLine=" + br.readLine()); // 56789
            
            // 重置至標記位置(f)
            br.reset();
            
            // 重新讀取 5 字元 -> fghij
            br.read(buf, 0, 5);
            System.out.println("buf=" + new String(buf)); // fghij
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 現代逐行處理範式
    private static void demoLineProcessing(Path path) {
        try (BufferedReader br = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
            String line;
            int lineNum = 0;
            while ((line = br.readLine()) != null) {
                System.out.printf("Line %d: %s%n", ++lineNum, line);
                // 這裡可進行解析、過濾、轉換等業務邏輯
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

效能與記憶體考量

參數預設值調優建議
緩衝區大小8192 字元 (16 KB)大檔案可增至 64-256 KB;小檔案或記憶體受限環境可降至 1-4 KB
readAheadLimit (mark)呼叫端指定設定為預期需回溯的最大字元數,過大觸發緩衝區擴充
預期行長80 字元內部優化參數,極長行(如壓縮 JSON)可考慮自訂實現

現代替代方案:Files.lines() 與 Stream API


// Java 8+ 函數式逐行處理,自動資源管理
try (var lines = Files.lines(Path.of("big.log"), StandardCharsets.UTF_8)) {
    lines.filter(l -> l.contains("ERROR"))
         .map(l -> l.substring(l.indexOf("ERROR")))
         .limit(100)
         .forEach(System.out::println);
}

Files.lines() 內部使用 BufferedReader,回傳惰性 Stream,適合大檔案過濾轉換,但需注意流關閉時機與檔案鎖定。

常見陷阱提醒

  1. mark() 後未 reset() 就 close():標記資源未釋放,雖 Java GC 會處理但屬不良習慣。
  2. readAheadLimit 設太小:導致標記頻繁失效,reset() 拋出 IOException。
  3. 混用 read() 與 readLine():read() 消耗字元會影響後續 readLine() 的行邊界判斷。
  4. ready() 判斷 EOF:ready() 僅表示「不阻塞」,非 EOF 指標。正確判斷 EOF 應檢查 read() == -1 或 readLine() == null。

掌握 BufferedReader 的緩衝策略、標記重置機制與 readLine() 實現細節,能讓你在文字處理、日誌分析、配置解析等場景中遊刃有餘,寫出高效且健壯的 IO 代碼。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved