1. 錯誤的症狀
在理想世界裡,程式設計師總是知道檔案使用哪種編碼儲存,並在讀取時正確指定。但現實是——檔案在 Windows、Linux、伺服器與各種編輯器之間流轉,每個環境都可能用自己的方式解讀位元組。結果就會出現以下症狀:
- 「亂碼」——原本應該是正常文字,卻看到奇怪符號、問號、方塊,或是看起來不像任何語言的字母組合。
- 字元遺失——部分文字消失或被替換成 ?。
- 例外——例如,MalformedInputException,當 Java 無法用所選編碼「消化」這些位元組時。
- 解析錯誤——由於文字被扭曲,關鍵字或結構受損,程式無法正確處理檔案。
以下是經典範例:在錯誤的編碼下讀取含西里爾字母的檔案時會看到的「亂碼」:
預期: Privet, mir!
實際: Привет, мир
這不是新語言,而是因為位元組被用錯誤的「字典」來詮釋。
2. 為何會出錯:問題根源
檔案用一種編碼寫入,卻用另一種編碼讀取
假設有人把檔案存成 Windows-1251,而你用 UTF-8 開啟。Java 會忠實地依照 UTF-8 的規則解碼位元組,但結果會變得毫無意義,因為位元組的數值與預期不符。
使用系統的「預設」編碼
若你未明確指定編碼,Java 會使用系統編碼——也就是你電腦上設定的那個。在採用俄文語系的 Windows 上可能是 Windows-1251,在 Linux 上是 UTF-8,在 Mac 上通常也是 UTF-8。在你這裡能正常開啟的檔案,到了使用不同作業系統的同事那裡可能就讀不出來。
使用過時的建構子
在舊版 Java(以及某些教材)中,常能看到使用 FileReader/FileWriter 的寫法;它們使用系統編碼,無法讓你加以控制——這是陷阱,也是產生「亂碼」的來源。
FileReader reader = new FileReader("file.txt");
FileWriter writer = new FileWriter("file.txt");
是否存在 BOM (Byte Order Mark)
有些編碼(例如帶 BOM 的 UTF-8 或 UTF-16)會在檔案開頭加入特殊位元組,以表明自身性質。若程式沒有預期會有 BOM,或相反地——期望有它卻沒找到,就可能出問題:不是檔案的前幾個字元被扭曲,就是整個檔案無法被辨識。
3. 錯誤如何表現:實務解析
範例 1:含西里爾字母的檔案以 Windows-1251 寫入,卻用 UTF-8 讀取
import java.nio.file.*;
import java.nio.charset.*;
public class EncodingDemo {
public static void main(String[] args) throws Exception {
Path path = Paths.get("russian.txt");
// 檔案以 Windows-1251 寫入,卻用 UTF-8 讀取 — 會出現亂碼!
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
System.out.println(reader.readLine());
}
}
}
結果你看到的不是「Privet, mir!」,而是一串怪異的符號。
範例 2:檔案以 UTF-8 寫入,卻用 ISO-8859-1 讀取
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.ISO_8859_1)) {
System.out.println(reader.readLine());
}
結果: 所有非 ASCII 字元都會變成垃圾或被替換為 ?。
範例 3:讀取檔案時拋出例外
如果位元組不符合所選編碼的規則,Java 可能會拋出例外:
Exception in thread "main" java.nio.charset.MalformedInputException: Input length = 1
at java.base/sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
...
這表示 Java 遇到了一個在該編碼下無法正確解讀的位元組。
4. 診斷:如何判斷編碼問題
檢查檔案的編碼
- 在 Notepad++、VS Code 或 Sublime Text 等編輯器中,通常可以查看或更改檔案的編碼(一般在下方狀態列)。
- 在 Linux 中可以用指令大致判斷檔案的編碼(不一定 100% 準確):
file filename.txt
檢查 Java 的系統編碼
在主控台列印屬性 file.encoding 的值:
System.out.println(System.getProperty("file.encoding"));
使用測試資料
建立一個包含多種字元(西里爾字母、拉丁字母、特殊符號、emoji)的小檔案,嘗試用不同編碼讀取,觀察何時結果與預期一致。
務必明確指定編碼
只要你看到在讀寫檔案時沒有指定編碼——就要提高警覺。例如,使用 Files.newBufferedReader(..., StandardCharsets.UTF_8) 來取代「預設值」。
5. 最佳實務:如何避免踩雷
規則 №1:
務必 在處理檔案時明確指定編碼,尤其當檔案會在不同電腦、不同作業系統之間使用,或透過網路傳輸時。
規則 №2:
使用現代且通用的編碼——首選 UTF-8(StandardCharsets.UTF_8)。僅在有特殊需求(例如與舊系統整合)時,才使用其他編碼。
規則 №3:
避免使用 FileReader 與 FileWriter(它們無法指定編碼);改用 InputStreamReader、OutputStreamWriter,或 Files 中可明確傳入 Charset 的方法。
規則 №4:
檢查結果!用支援多種編碼的編輯器開啟你寫出的檔案,確認文字顯示正確。
6. 細節與眉角:BOM, XML, JSON 與其他「有趣」情況
BOM (Byte Order Mark):有時檔案在 UTF-8 中會以「看不見」的位元組(EF BB BF)開頭。多數現代程式會忽略它們,但也有些會在行首顯示「亂碼」,或拒絕該檔案(例如舊版的剖析器 XML/JSON)。
XML/HTML:有時檔案開頭會有一行像是 <?xml version="1.0" encoding="UTF-8"?> 的宣告。它告訴程式應以哪種編碼解讀位元組。但若實際編碼與宣告不一致——仍會出現「亂碼」。
JSON:按照標準應為 UTF-8,但若檔案其實是 Windows-1251,剖析器會拋出錯誤或產生失真的資料。
GO TO FULL VERSION