「亂碼」是 Java 文件 IO 中最令開發者頭疼的問題。根因在於:**文件字節流與字符流轉換時,編碼不一致**。本文提供系統性解決方案:編碼自動檢測、指定編碼讀寫、二進制文件正確處理,附生產級工具類代碼。
InputStream/OutputStream 直接搬運字節。
import java.io.*;
import java.nio.charset.Charset;
public class CharsetDetector {
// 常見 BOM 頭
private static final byte[] BOM_UTF8 = {(byte)0xEF, (byte)0xBB, (byte)0xBF};
private static final byte[] BOM_UTF16_LE = {(byte)0xFF, (byte)0xFE};
private static final byte[] BOM_UTF16_BE = {(byte)0xFE, (byte)0xFF};
// UTF-32 BOM 罕見,略
public static String detectCharset(File file) throws IOException {
try (FileInputStream fis = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(fis)) {
bis.mark(4);
byte[] head = new byte[4];
int read = bis.read(head);
if (read < 2) return "ASCII"; // 太短視為 ASCII
// 1. 檢查 BOM
if (startsWith(head, BOM_UTF8)) return "UTF-8";
if (startsWith(head, BOM_UTF16_LE)) return "UTF-16LE";
if (startsWith(head, BOM_UTF16_BE)) return "UTF-16BE";
bis.reset();
// 2. 無 BOM:啟發式檢測 UTF-8 / GBK / ASCII
// 簡單版:嘗試按 UTF-8 解碼全文,若無錯誤則視為 UTF-8,否則回退 GBK (中文環境)
// 生產建議用 Apache Tika / ICU4j / juniversalchardet 庫,準確率極高
return heuristicDetect(bis);
}
}
private static boolean startsWith(byte[] data, byte[] prefix) {
if (data.length < prefix.length) return false;
for (int i = 0; i < prefix.length; i++)
if (data[i] != prefix[i]) return false;
return true;
}
// 啟發式:掃描前幾 KB 判斷
private static String heuristicDetect(BufferedInputStream bis) throws IOException {
// 這裡演示簡化邏輯:實際項目請引入 juniversalchardet (Mozilla 通用檢測庫)
// Maven: com.googlecode.juniversalchardet:juniversalchardet:1.0.3
return "GBK"; // 中文 Windows 默認回退
}
}
強烈建議:生產環境**直接引入 juniversalchardet 或 Apache Tika**,幾行代碼實現工業級檢測,勿自造輪子。
public static String readTextFile(File file, String charsetName) throws IOException {
if (charsetName == null) charsetName = detectCharset(file); // 自動檢測或默認
// 統一處理別名
if ("ASCII".equalsIgnoreCase(charsetName)) charsetName = "GBK"; // 中文環境常見誤判
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream(file), Charset.forName(charsetName));
BufferedReader br = new BufferedReader(isr)) {
StringBuilder sb = new StringBuilder();
char[] buf = new char[8192];
int len;
while ((len = br.read(buf)) != -1) {
sb.append(buf, 0, len);
}
return sb.toString();
}
}
public static void writeTextFile(File file, String content, String charsetName) throws IOException {
if (charsetName == null) charsetName = "UTF-8"; // 默認寫 UTF-8 (跨平台最佳)
// 確保父目錄存在
File parent = file.getParentFile();
if (parent != null && !parent.exists()) parent.mkdirs();
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream(file), Charset.forName(charsetName));
BufferedWriter bw = new BufferedWriter(osw)) {
bw.write(content);
}
}
適用:圖片、PDF、Word、Zip、視頻、序列化對象流等。
// 讀取二進制文件到 byte[]
public static byte[] readBytes(File file) throws IOException {
try (FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buf = new byte[8192];
int len;
while ((len = fis.read(buf)) != -1) {
baos.write(buf, 0, len);
}
return baos.toByteArray();
}
}
// 寫入 byte[] 到文件
public static void writeBytes(File file, byte[] data) throws IOException {
File parent = file.getParentFile();
if (parent != null && !parent.exists()) parent.mkdirs();
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(data);
}
}
// 大文件複製 (零拷貝/流式,不佔堆內存)
public static void copyBinaryFile(File src, File dest) throws IOException {
try (FileChannel in = FileChannel.open(src.toPath(), StandardOpenOption.READ);
FileChannel out = FileChannel.open(dest.toPath(),
StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
out.transferFrom(in, 0, in.size());
}
}
| 現象 | 原因 | 解決 |
|---|---|---|
| 中文變 ??? 或 □□□ | 讀取編碼與文件實際編碼不符 (如 UTF-8 文件用 GBK 讀) | 檢測真實編碼,指定正確 Charset 讀取 |
| 中文變亂碼字符 (如 鏂伴椈) | 雙重編碼轉換 (GBK→ISO-8859-1→UTF-8 等) | 追蹤轉換鏈路,統一編碼,避免多次轉碼 |
| 文件開頭有雜質字符 | BOM 頭 (EF BB BF) 被作為內容讀取 | 檢測並跳過 BOM,或用支持 BOM 的庫 (Tika) |
| 屬性文件 .properties 亂碼 | Java 規範要求 ISO-8859-1,中文需 unicode 轉義 | 用 native2ascii 或 IDE 自動轉義 / 選用 XML/YAML 配置 |
| 控制台輸出亂碼 | 控制台編碼 (如 GBK) 與 JVM file.encoding (如 UTF-8) 不一致 | 啟動參數 -Dfile.encoding=UTF-8 或配置 IDE 運行編碼 |
InputStream/OutputStream/byte[]/FileChannel。FileUtils.readText(file), FileUtils.writeText(file, content), FileUtils.copyBinary(src, dest) 統一入口,禁止業務代碼裸寫 IO。亂碼本質是**編碼協議不匹配**。解決之道在於:**明確分離文本/二進制、文本指定編碼讀寫、自動檢測未知編碼、全鏈路統一 UTF-8**。將上述工具類納入項目公共庫,強制團隊使用封裝方法,即可從根源杜絕亂碼困擾,讓文件 IO 變得可靠、無感。