前言
字元編碼是 Java IO 處理的核心難點。UTF-8 作為國際通用編碼,支援全字元集、無 BOM 干擾、ASCII 相容,是現代應用的預設首選。本文系統性整理 Java 寫入/讀取 UTF-8 檔案的四代 API 演進:OutputStreamWriter、Files (NIO.2)、try-with-resources 模式、Stream API,並解決BOM 問題、大檔案處理、編碼檢測實戰場景。
API 演進對比
| 世代 | API | 特點 | 適用場景 |
| 1.0 | FileOutputStream + OutputStreamWriter | 橋接位元組/字元流、手動關閉 | 舊系統維護、JDK 1.1 相容 |
| 1.4 | FileWriter (預設平台編碼,不可指定 UTF-8) | 簡化但不可指定編碼,不推薦 | 無 |
| 1.7 | Files.newBufferedWriter/Reader (NIO.2) | 路徑導向、自動資源管理、支援 OpenOption | 現代標準寫法 |
| 1.8 | Files.lines() / BufferedReader.lines() + Stream | 函數式、惰性流、記憶體友善 | 大檔案逐行處理、轉換管道 |
標準寫入範例 (NIO.2 推薦)
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;
public class Utf8FileWriter {
/** 寫入字串列表,每行一項 */
public static void writeLines(Path path, List lines, OpenOption... options) throws IOException {
// 預設 CREATE, WRITE, TRUNCATE_EXISTING
try (BufferedWriter writer = Files.newBufferedWriter(
path, StandardCharsets.UTF_8, options)) {
for (String line : lines) {
writer.write(line);
writer.newLine(); // 平台無關換行符
}
}
}
/** 寫入單一字串 */
public static void writeString(Path path, String content, OpenOption... options) throws IOException {
Files.writeString(path, content, StandardCharsets.UTF_8, options); // JDK 11+
// JDK 8 相容:
// Files.write(path, content.getBytes(StandardCharsets.UTF_8), options);
}
/** 附加模式寫入 */
public static void appendLine(Path path, String line) throws IOException {
writeLines(path, List.of(line), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
// 測試
public static void main(String[] args) throws IOException {
Path path = Paths.get("output/utf8_demo.txt");
Files.createDirectories(path.getParent());
writeString(path, "第一行:UTF-8 測試\n第二行:中文內容");
appendLine(path, "第三行:附加模式");
System.out.println("寫入完成:" + path.toAbsolutePath());
}
}
標準讀取範例
public class Utf8FileReader {
/** 一次性讀取全文 (小檔案) */
public static String readAll(Path path) throws IOException {
return Files.readString(path, StandardCharsets.UTF_8); // JDK 11+
// JDK 8: new String(Files.readAllBytes(path), StandardCharsets.UTF_8)
}
/** 逐行讀取並處理 (大檔案、記憶體安全) */
public static void processLines(Path path, LineProcessor processor) throws IOException {
try (Stream lines = Files.lines(path, StandardCharsets.UTF_8)) {
lines.forEach(processor::process);
}
}
/** 讀取所有行到 List (中等檔案) */
public static List readAllLines(Path path) throws IOException {
return Files.readAllLines(path, StandardCharsets.UTF_8);
}
// 函數式介面
@FunctionalInterface
public interface LineProcessor {
void process(String line);
}
// 測試
public static void main(String[] args) throws IOException {
Path path = Paths.get("output/utf8_demo.txt");
System.out.println("=== 全文讀取 ===");
System.out.println(readAll(path));
System.out.println("=== 逐行處理 ===");
processLines(path, line -> System.out.println("[處理] " + line));
}
}
關鍵技術細節
1. BOM (Byte Order Mark) 處理
- UTF-8 BOM:
EF BB BF (3 位元組),Windows 記事本儲存常附帶
- Java 不會自動識別/移除 BOM,讀取時會殘留
\uFEFF 字元
/** 移除 UTF-8 BOM */
public static String removeBom(String content) {
if (content != null && content.startsWith("\uFEFF")) {
return content.substring(1);
}
return content;
}
/** 寫入帶 BOM 的 UTF-8 (相容舊系統) */
public static void writeWithBom(Path path, String content) throws IOException {
try (OutputStream os = Files.newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
os.write(new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF}); // 寫入 BOM
os.write(content.getBytes(StandardCharsets.UTF_8));
}
}
2. 大檔案處理策略
| 檔案大小 | 策略 | 記憶體佔用 |
| < 10MB | Files.readAllLines / readString | 整檔載入 |
| 10MB - 1GB | Files.lines() + Stream 流水線 | 單行緩衝 |
| > 1GB | BufferedReader + 手動循環 / MappedByteBuffer | 固定緩衝區 |
// 大檔案逐行處理模板 (JDK 8+)
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
// 處理單行
process(line);
}
}
3. 編碼檢測與容錯
// 簡易編碼嘗試順序
public static String readWithFallback(Path path) throws IOException {
String[] encodings = {"UTF-8", "UTF-16LE", "UTF-16BE", "Big5", "GBK", "ISO-8859-1"};
for (String encoding : encodings) {
try {
return Files.readString(path, Charset.forName(encoding));
} catch (CharacterCodingException | UnmappableCharacterException e) {
// 嘗試下一個編碼
}
}
throw new IOException("無法識別檔案編碼: " + path);
}
4. 臨時檔案原子性寫入 (防止寫入中斷損壞原檔)
public static void atomicWrite(Path targetPath, String content) throws IOException {
Path tempPath = Files.createTempFile(targetPath.getParent(), "tmp_", ".utf8");
try {
Files.writeString(tempPath, content, StandardCharsets.UTF_8);
// 原子性移動替換 (同檔案系統)
Files.move(tempPath, targetPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (Exception e) {
Files.deleteIfExists(tempPath); // 清理臨時檔
throw e;
}
}
常見錯誤與避坑
| 錯誤 | 後果 | 正確做法 |
使用 FileWriter / FileReader | 依賴平台預設編碼 (Windows=Big5/GBK, Linux=UTF-8),跨平台亂碼 | 顯式指定 StandardCharsets.UTF_8 |
new String(bytes) 無編碼 | 平台預設編碼解碼,UTF-8 位元組在 Big5 系統變亂碼 | new String(bytes, StandardCharsets.UTF_8) |
忘記 flush() / close() | 資料停留在緩衝區,檔案不完整或為空 | try-with-resources 自動關閉 |
大檔案 readAllLines | OOM (OutOfMemoryError) | Files.lines() 流式處理 |
路徑硬編碼 C:\\data\\file.txt | 跨平台失效 | Paths.get("data", "file.txt") 或配置屬性 |
總結:現代 Java UTF-8 檔案處理清單
- 寫入:
Files.writeString(path, content, StandardCharsets.UTF_8) (JDK 11+) 或 Files.newBufferedWriter(path, StandardCharsets.UTF_8)
- 讀取:
Files.readString(path, StandardCharsets.UTF_8) 或 Files.lines(path, StandardCharsets.UTF_8)
- 資源管理:永遠使用
try-with-resources
- 編碼:永遠顯式指定
StandardCharsets.UTF_8
- 路徑:使用
Path/Paths 而非 File/字串
- 大檔案:Stream API 或 BufferedReader 逐行處理
- 原子性:臨時檔 +
Files.move(ATOMIC_MOVE)
- 上一篇文章: Tomcat Session 管理核心原理深度剖析:從 StandardManager 到集群方案的完整架構
- 下一篇文章: Java 呼叫預存程序獲取自增主鍵完整實戰:JDBC CallableStatement 與 GeneratedKeys 兩大方案
Copyright ©
程式碼之家 All Rights Reserved