文件讀寫指定編碼是國際化、遷移舊系統、處理異構資料源的基礎需求。本文提供生產就緒的工具類,涵蓋緩衝流、try-with-resources、大文件分塊讀寫、編碼驗證與單元測試。
核心工具類 (Java 7+)
package com.example.util;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public final class ReadWriteFileWithEncode {
private ReadWriteFileWithEncode() {} // 工具類禁止實例化
/** 寫入文件:覆蓋模式,自動建立父目錄 */
public static void write(String path, String content, String encoding) throws IOException {
write(path, content, encoding, false);
}
/** 寫入文件:支援追加模式 */
public static void write(String path, String content, String encoding, boolean append) throws IOException {
Path filePath = Paths.get(path);
Files.createDirectories(filePath.getParent()); // 自動建立目錄
Charset cs = resolveCharset(encoding);
try (BufferedWriter writer = Files.newBufferedWriter(filePath, cs,
StandardOpenOption.CREATE, append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING)) {
writer.write(content);
}
}
/** 讀取整個文件為字串 (適合中小文件) */
public static String read(String path, String encoding) throws IOException {
Charset cs = resolveCharset(encoding);
return Files.readString(Paths.get(path), cs); // Java 11+ Files.readString
}
/** 大文件逐行讀取處理 (避免 OOM) */
public static void readLines(String path, String encoding, LineProcessor processor) throws IOException {
Charset cs = resolveCharset(encoding);
try (BufferedReader reader = Files.newBufferedReader(Paths.get(path), cs)) {
String line;
while ((line = reader.readLine()) != null) {
processor.process(line);
}
}
}
/** 函數式介面:逐行處理 */
@FunctionalInterface
public interface LineProcessor {
void process(String line) throws IOException;
}
/** 解析編碼名稱,不支援則拋異常 */
private static Charset resolveCharset(String encoding) {
if (encoding == null || encoding.isBlank()) {
return StandardCharsets.UTF_8; // 預設 UTF-8
}
try {
return Charset.forName(encoding.trim());
} catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
throw new IllegalArgumentException("不支援的編碼: " + encoding, e);
}
}
/** 測試主程式 */
public static void main(String[] args) throws IOException {
String content = "中文測試內容\nSecond line 英文";
String path = "target/test_utf8.txt";
String encoding = "UTF-8";
write(path, content, encoding);
System.out.println("寫入完成");
String readBack = read(path, encoding);
System.out.println("讀取結果:\n" + readBack);
// 大文件逐行示範
readLines(path, encoding, line -> System.out.println("[LINE] " + line));
}
}
關鍵 API 對比 (Java 7 vs 11+)
| 操作 | Java 7 (經典) | Java 11+ (現代) |
| 讀取全文 | BufferedReader + StringBuilder 迴圈 | Files.readString(path, cs) |
| 寫入全文 | BufferedWriter + write() | Files.writeString(path, content, cs, options) |
| 逐行讀取 | BufferedReader.readLine() | Files.lines(path, cs) 返回 Stream |
| 自動關閉 | try-with-resources | try-with-resources (同) |
| 目錄建立 | file.getParentFile().mkdirs() | Files.createDirectories(path.getParent()) |
編碼未知時的完整流程
- 呼叫 cpDetector 或
juniversalchardet 偵測編碼。
- 使用偵測到的
Charset 讀取內容。
- 若需統一存儲,轉寫為 UTF-8:
write(targetPath, content, "UTF-8")。
// 整合範例
public String convertToUtf8(String sourcePath) throws IOException {
Charset detected = EncodingDetector.detect(new File(sourcePath)); // 參考上篇
String content = read(sourcePath, detected.name());
String targetPath = sourcePath + ".utf8";
write(targetPath, content, StandardCharsets.UTF_8.name());
return targetPath;
}
單元測試 (JUnit 5 + TemporaryDirectory)
@Test
void testReadWriteUtf8(@TempDir Path tempDir) throws IOException {
Path file = tempDir.resolve("test.txt");
String original = "測試內容\nLine 2";
ReadWriteFileWithEncode.write(file.toString(), original, "UTF-8");
assertEquals(original, ReadWriteFileWithEncode.read(file.toString(), "UTF-8"));
}
@Test
void testReadWriteGbk(@TempDir Path tempDir) throws IOException {
Path file = tempDir.resolve("test_gbk.txt");
String original = "中文測試";
ReadWriteFileWithEncode.write(file.toString(), original, "GBK");
assertEquals(original, ReadWriteFileWithEncode.read(file.toString(), "GBK"));
}
@Test
void testLargeFileStreaming(@TempDir Path tempDir) throws IOException {
Path file = tempDir.resolve("big.txt");
// 寫入 100 萬行
try (BufferedWriter w = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
for (int i = 0; i < 1_000_000; i++) w.write("Line " + i + "\n");
}
// 逐行驗證不載入記憶體
AtomicInteger count = new AtomicInteger();
ReadWriteFileWithEncode.readLines(file.toString(), "UTF-8", line -> {
assertTrue(line.startsWith("Line "));
count.incrementAndGet();
});
assertEquals(1_000_000, count.get());
}
常見坑點避坑指南
| 坑點 | 後果 | 解決 |
| 未指定編碼直接 new FileReader() | 使用平台預設編碼,跨平台亂碼 | 強制指定 Charset/StandardCharsets |
| 大文件 readString() 導致 OOM | 堆積耗盡 | 改用 readLines() 流式處理 |
| Windows \r\n 換行符讀取後變雙換行 | 邏輯判斷錯誤 | readLine() 自動去除 \r\n,或手動 .replace("\r", "") |
| 追加寫入忘記 StandardOpenOption.APPEND | 覆蓋原內容 | 明確傳入 APPEND 選項 |
| 編碼名稱大小寫/別名不匹配 | UnsupportedCharsetException | 使用 Charset.forName() 統一解析,支援別名 |