檔案編碼不一致是中文亂碼首因。本文提供一個生產級 FileEncodingUtil,統一解決:指定編碼讀寫、自動建立父目錄、大檔案分行串流、編碼偵測整合、資源安全釋放,並附帶單元測試範本。
package com.example.io;
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.StandardOpenOption;
import java.util.List;
import java.util.function.Consumer;
public final class FileEncodingUtil {
private FileEncodingUtil() {}
/** 寫入字串 (覆寫模式),自動建立父目錄 */
public static void writeString(Path path, String content, Charset charset) throws IOException {
Files.createDirectories(path.getParent());
Files.writeString(path, content, charset, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
/** 寫入字串 (附加模式) */
public static void appendString(Path path, String content, Charset charset) throws IOException {
Files.createDirectories(path.getParent());
Files.writeString(path, content, charset, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
/** 寫入多行 (List<String>) */
public static void writeLines(Path path, List<String> lines, Charset charset) throws IOException {
Files.createDirectories(path.getParent());
Files.write(path, lines, charset, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
/** 讀取整個檔案為字串 (小檔案) */
public static String readString(Path path, Charset charset) throws IOException {
return Files.readString(path, charset);
}
/** 逐行讀取並處理 (大檔案,記憶體友善) */
public static void readLines(Path path, Charset charset, Consumer<String> lineProcessor) throws IOException {
try (BufferedReader br = Files.newBufferedReader(path, charset)) {
String line;
while ((line = br.readLine()) != null) {
lineProcessor.accept(line);
}
}
}
/** 讀取所有行至 List (中等檔案) */
public static List<String> readAllLines(Path path, Charset charset) throws IOException {
return Files.readAllLines(path, charset);
}
/** 二進位寫入 (圖片、壓縮檔等) */
public static void writeBytes(Path path, byte[] data) throws IOException {
Files.createDirectories(path.getParent());
Files.write(path, data, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
/** 二進位讀取 */
public static byte[] readBytes(Path path) throws IOException {
return Files.readAllBytes(path);
}
/** 複製檔案 (保留屬性、支援大檔零拷貝) */
public static void copyFile(Path src, Path dst, boolean preserveAttributes) throws IOException {
Files.createDirectories(dst.getParent());
if (preserveAttributes) {
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
} else {
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
}
}
/** 移動/重命名檔案 */
public static void moveFile(Path src, Path dst, boolean atomic) throws IOException {
Files.createDirectories(dst.getParent());
if (atomic) {
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} else {
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);
}
}
/** 刪除檔案 (不存在不拋例外) */
public static boolean deleteQuietly(Path path) {
try {
return Files.deleteIfExists(path);
} catch (IOException e) {
return false;
}
}
/** 取得檔案大小 */
public static long size(Path path) throws IOException {
return Files.size(path);
}
/** 判斷檔案是否存在且可讀 */
public static boolean isReadable(Path path) {
return Files.isRegularFile(path) && Files.isReadable(path);
}
// --- 編碼偵測整合 (需搭配前文 CharsetDetector) ---
public static String readStringAutoDetect(Path path) throws IOException {
String encoding = CharsetDetector.detect(path.toFile());
return readString(path, Charset.forName(encoding));
}
public static void readLinesAutoDetect(Path path, Consumer<String> processor) throws IOException {
String encoding = CharsetDetector.detect(path.toFile());
readLines(path, Charset.forName(encoding), processor);
}
}
Path log = Paths.get("logs/app.log");
// 寫入 UTF-8
FileEncodingUtil.appendString(log, "新日誌行\n", StandardCharsets.UTF_8);
// 讀取 GBK 編碼 CSV
FileEncodingUtil.readLines(Paths.get("data.csv"), Charset.forName("GBK"), line -> {
String[] cols = line.split(",");
// 處理欄位...
});
// 自動偵測編碼讀取
String content = FileEncodingUtil.readStringAutoDetect(Paths.get("unknown.txt"));
// 大檔案二進位複製 (零拷貝)
FileEncodingUtil.copyFile(Paths.get("big.iso"), Paths.get("backup/big.iso"), true);
@TempDir Path tempDir;
@Test void writeAndReadUtf8() throws IOException {
Path file = tempDir.resolve("test.txt");
String original = "測試中文內容\nSecond line";
FileEncodingUtil.writeString(file, original, StandardCharsets.UTF_8);
assertEquals(original, FileEncodingUtil.readString(file, StandardCharsets.UTF_8));
}
@Test void appendMode() throws IOException {
Path file = tempDir.resolve("append.txt");
FileEncodingUtil.writeString(file, "Line 1\n", StandardCharsets.UTF_8);
FileEncodingUtil.appendString(file, "Line 2\n", StandardCharsets.UTF_8);
assertEquals("Line 1\nLine 2\n", FileEncodingUtil.readString(file, StandardCharsets.UTF_8));
}
@Test void largeFileStreaming() throws IOException {
Path file = tempDir.resolve("big.txt");
// 產生 10MB 測資
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100_000; i++) sb.append("Line ").append(i).append('\n');
FileEncodingUtil.writeString(file, sb.toString(), StandardCharsets.UTF_8);
AtomicInteger count = new AtomicInteger();
FileEncodingUtil.readLines(file, StandardCharsets.UTF_8, line -> count.incrementAndGet());
assertEquals(100_000, count.get());
}
@Test void autoDetectGbk() throws IOException {
Path file = tempDir.resolve("gbk.txt");
// 手工寫入 GBK 位元組
Files.write(file, "中文測試".getBytes("GBK"));
String content = FileEncodingUtil.readStringAutoDetect(file);
assertEquals("中文測試", content);
}
| 舊寫法 | 新寫法 (NIO.2) | 優勢 |
|---|---|---|
| new FileWriter(file, "UTF-8") | Files.writeString(path, str, StandardCharsets.UTF_8, ...) | 自動建目錄、原子選項、編碼型別安全 |
| new BufferedReader(new InputStreamReader(new FileInputStream(file), "GBK")) | Files.newBufferedReader(path, Charset.forName("GBK")) | try-with-resources 自動關閉、路徑物件化 |
| file.mkdirs() | Files.createDirectories(path.getParent()) | 拋出具體例外、支援 Path |
| file.length() | Files.size(path) | 拋 IOException 而非回傳 0 |
readString/writeString 最簡潔。readAllLines/writeLines 平衡記憶體與速度。readLines(path, charset, consumer) 串流處理,或 Files.newInputStream 零拷貝複製。BufferedWriter 持有開啟狀態,批次 flush,避免重複開關檔案。