1. 前言
回顧一下 FileReader 和 FileWriter 的工作方式。每次你呼叫它們的 read() 或 write() 方法,都會進行一次對檔案系統的存取——也就是電腦真的去磁碟上讀或寫一個字元。如果檔案很大,而你一次只讀或寫一個字元,這就會變成成千上萬甚至數百萬次的磁碟存取「馬拉松」。而磁碟,眾所周知,並不是程式設計師最快的朋友。
可以這樣想像:你要把水從水桶倒進瓶子,卻用茶匙來舀。形式上能做,但會非常久。更明智的做法是用杓子或杯子。對檔案也是一樣:一次只讀或寫一個字元,就像用茶匙搬水。
實際上會長這樣:
// 逐字元讀取檔案(用「茶匙」!)
try (FileReader reader = new FileReader("big.txt")) {
int c;
while ((c = reader.read()) != -1) {
// 處理字元(例如,單純計數)
}
}
如果檔案很大,你會注意到程式執行得非常緩慢。
緩衝:是什麼、為什麼需要
緩衝(buffer)是一塊特殊的記憶體區域(通常是陣列),用來一次載入或寫入較大的資料塊(例如 8 KB 或更多),而不是一個字元一個字元處理。簡單說,緩衝就是一塊記憶體,類似中間的水桶,資料會先被成批讀入或寫入。
運作方式是:在讀取時,程式只需向磁碟請求一次,就能把整個資料區塊載入緩衝,之後再從記憶體中一個字元一個字元地把資料「分發」給你。一旦該區塊用完,就再載入下一個。寫入時也一樣:資料會先累積到緩衝區,接著再一次性寫到磁碟(或若你呼叫了 flush(),就會立即寫出)。
為什麼更快?因為磁碟本身很慢,特別是被頻繁「零碎地」存取時。而若是減少存取次數、一次處理更大的資料量,效能就會明顯提升。更白話地說:緩衝能減少對磁碟的存取次數,並讓程式執行更快。
2. BufferedReader 與 BufferedWriter:語法與範例
在 Java 中,讀寫文字檔案的緩衝化由兩個類別負責:
- BufferedReader —— 用於讀取文字檔案。
- BufferedWriter —— 用於寫入文字檔案。
它們包裹在一般的 Reader/Writer(例如 FileReader/FileWriter)之上,提供緩衝功能。
使用 BufferedReader 逐行讀取檔案
最常見的情境是逐行讀取。readLine() 方法會傳回從目前位置到換行符號為止的一行("\n" 或 "\r\n")。
import java.io.*;
public class BufferedReaderExample {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line); // 將該行輸出到螢幕
}
} catch (IOException e) {
System.out.println("讀取檔案時發生錯誤:" + e.getMessage());
}
}
}
這裡發生了什麼?
我們建立能逐字元讀檔的 FileReader,並把它包在 BufferedReader 中。緩衝讀取器不是一次取一個字元,而是一次抓取較大的資料塊到記憶體,然後透過 readLine() 逐行提供給你。結果是你只要寫個 while 迴圈就能一行一行取得字串,不用擔心檔案多大:讀取依然快速且省資源。
使用 BufferedWriter 寫入檔案
寫入字串到檔案也可以很有效率:
import java.io.*;
public class BufferedWriterExample {
public static void main(String[] args) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))) {
writer.write("Hello, world!");
writer.newLine(); // 換行(取決於作業系統)
writer.write("這是第二行。");
} catch (IOException e) {
System.out.println("寫入檔案時發生錯誤:" + e.getMessage());
}
}
}
這裡發生了什麼?
先建立 FileWriter,再把它包在 BufferedWriter 中。當你呼叫 write() 或 newLine() 時,資料不會立刻寫到磁碟,而是先進入緩衝——一塊中介的記憶體區。等到緩衝區滿了,或你關閉串流(或顯式呼叫 flush())時,累積的文字才會一次性寫入檔案。這樣能顯著加速寫入並減少對磁碟的存取。
在一個完整應用程式中會是什麼樣子?
假設我們寫一個簡單的「日記」程式,把條目存到檔案並顯示在螢幕上。
import java.io.*;
import java.util.Scanner;
public class DiaryApp {
public static void main(String[] args) {
String fileName = "diary.txt";
Scanner scanner = new Scanner(System.in);
// 新增一則條目
System.out.print("請輸入新的日記內容:");
String entry = scanner.nextLine();
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName, true))) {
writer.write(entry);
writer.newLine();
System.out.println("記錄已儲存!");
} catch (IOException e) {
System.out.println("寫入時發生錯誤:" + e.getMessage());
}
// 讀取所有條目
System.out.println("\n您的日記:");
try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("讀取時發生錯誤:" + e.getMessage());
}
}
}
我們拿到檔名 "diary.txt",並請使用者輸入新的條目。為了儲存,我們用 FileWriter 的 append 模式(傳入 true),因此舊的內容不會被覆蓋——每一行都會被追加到檔案末尾。BufferedWriter 這層包裝讓寫入更快且省資源:資料先累積在記憶體,再一次寫入磁碟。
接著我們用 BufferedReader 開啟同一個檔案來讀取。它以較大的區塊讀取內容,再逐行提供給你。在迴圈中程式只負責把每行輸出到螢幕,最後你就能從頭到尾看到整本「日記」。
3. BufferedReader 與 BufferedWriter 的優點
顯著的效能提升
使用緩衝化的讀寫時,程式對磁碟的存取次數會少上好幾十倍、甚至上百倍。這在大檔案上特別明顯。
方便的方法
- BufferedReader.readLine() —— 允許逐行讀取,對處理文字檔(如日誌、CSV、設定檔)非常方便。
- BufferedWriter.newLine() —— 會依作業系統自動使用正確的換行符號。
易於使用
- 類別能輕鬆與其他串流組合(例如把 InputStreamReader 包在 BufferedReader 裡以支援不同編碼)。
- 搭配 try-with-resources 使用時會自動關閉所有資源。
彈性
- 可以明確指定緩衝區大小,如果預設值(通常 8 KB)不合適:
BufferedReader reader = new BufferedReader(new FileReader("big.txt"), 16384); // 16 KB 的緩衝區
4. 何時使用 BufferedReader 與 BufferedWriter
適用於:
- 處理文字檔案(如日誌、CSV、大量文字資料)。
- 需要逐行讀寫檔案的情況。
- 在意效能、需要加速處理大型檔案時。
- 需要處理來自網路或其他來源、且支援 Reader/Writer 的資料流。
不適用於:
- 處理二進位檔案(例如圖片、壓縮檔、影片)——請改用 InputStream/OutputStream。
- 檔案非常小,且只需一次性完整讀/寫——緩衝帶來的效益極小(但通常也不會有壞處)。
5. 實用細節
與不同編碼搭配使用
如果你需要用特定編碼讀/寫檔案(例如 "UTF-8"、"Windows-1251"),請把 InputStreamReader/OutputStreamWriter 與緩衝化串流搭配使用:
BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("input.txt"), "UTF-8")
);
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")
);
顯式沖刷緩衝區
有時候你需要確保資料真的已經寫到磁碟(例如寫日誌或發票)。此時請呼叫 writer.flush()。通常不需要這麼做,因為關閉串流會自動沖刷緩衝區。
緩衝區大小
預設緩衝區大小約為 8 KB。若你確定更改大小能帶來效能提升(例如處理巨型檔案),就可以自行調整。
比較:FileReader/FileWriter vs BufferedReader/BufferedWriter
| 類別 | 在大型檔案上的速度 | 逐行讀取的便利性 | 逐行寫入的便利性 | 與編碼相關的彈性 |
|---|---|---|---|---|
| FileReader/FileWriter | 慢 | 否(只能逐字元) | 否(只能逐字元) | 僅預設 |
| BufferedReader/Writer | 快 | 是(readLine()) | 是(newLine()) | 是(透過 InputStreamReader/OutputStreamWriter) |
6. 使用 BufferedReader 與 BufferedWriter 的常見錯誤
錯誤 №1:忘記關閉串流。 若不使用 try-with-resources 或沒呼叫 close(),檔案可能會保持鎖定,資料也可能不會寫入磁碟。請務必使用 try-with-resources!
錯誤 №2:混用文字與二進位檔案的處理方式。 嘗試用 BufferedReader 開啟二進位檔(".jpg"、".zip")會產生亂碼,甚至導致錯誤。處理二進位檔請使用 InputStream/OutputStream。
錯誤 №3:在大量資料處理時不使用緩衝。 若逐字元讀寫,程式會很慢。處理大檔案時請務必使用緩衝。
錯誤 №4:需要時沒有呼叫 flush()。 當你需要資料立刻寫到磁碟(例如記錄日誌),請呼叫 writer.flush()。但通常關閉串流就會自動處理。
錯誤 №5:忽略編碼。 若以錯誤的編碼開啟檔案,文字可能會顯示不正確(例如字母變成「?」、亂碼等)。當檔案編碼不同於系統預設時,請務必明確指定所需的編碼(例如 "UTF-8")。
GO TO FULL VERSION