1. UTF-8 — 現代編碼的王者
在程式設計的工作中,你會接觸到使用不同編碼的檔案、網路協定、資料庫與網路服務。如果你不了解 UTF-8 與 ISO-8859-1 有何不同,就很容易踩坑:你的程式可能在「我的電腦」上正常,卻會在法國同事的機器上或在雲端(預設系統不同)中壞掉。
今天我們要剖析在 Java 實務中最常見的三種編碼:
- UTF-8 — 現代且通用的標準。
- UTF-16 — Java 的內部字串格式,Windows 中也常見。
- ISO-8859-1(Latin-1)— 舊但仍可見的西歐語系單位元組編碼。
讓我們更深入地認識它們!
這是什麼?
UTF-8(Unicode Transformation Format, 8-bit)是一種能表示巨大 Unicode 字元集任意符號的編碼(包括西里爾字母、中文漢字、表情符號,甚至罕見的古文字)。而且它很聰明:對於最常用的符號(英文字母、數字、標點),只用 1 個位元組;其他符號則用 2、3,甚至 4 個位元組。
事實: 幾乎所有現代網站、多數 JSON 與 XML 檔案,甚至 Java 原始碼,預設都使用 UTF-8。
一個字元需要多少位元組?
- ASCII 字元(英文字母、數字、標點)— 1 個位元組。
- 西里爾字母、帶變音符的歐洲字元 — 2 個位元組。
- 中日韓漢字 — 3 個位元組。
- 超稀有字符、表情符號 — 4 個位元組。
範例:
| 符號 | Unicode 代碼 | UTF-8(位元組) |
|---|---|---|
| A | |
|
| Ya | |
|
| € | |
|
| 😀 | |
|
為什麼 UTF-8 這麼棒?
- 向後相容 ASCII:如果檔案只包含英文字元,它與 ASCII 完全一致。
- 精簡:對英文短文而言,UTF-8 可讓檔案大小最小。
- 國際化:支援所有語言,包括表情符號。
- Web 標準:HTML、CSS、JSON、XML、JavaScript——預設都是 UTF-8。
Java 中的使用範例
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
public class Utf8Demo {
public static void main(String[] args) throws Exception {
// 以 UTF-8 將字串寫入檔案
String text = "你好,世界! 😀";
Files.write(Paths.get("utf8.txt"), text.getBytes(StandardCharsets.UTF_8));
// 以 UTF-8 讀取檔案
String read = Files.readString(Paths.get("utf8.txt"), StandardCharsets.UTF_8);
System.out.println(read); // 輸出:你好,世界! 😀
}
}
這裡我們在寫入前明確以 UTF-8 編碼字串,讀取時也同樣明確指定編碼。如此即可確保文字不失真地保存與還原,即使其中包含表情符號或多語系字元。
何時使用 UTF-8?
永遠——除非你有非常充足的理由選擇其他方案。這是所有現代應用程式的通用選擇。
2. UTF-16 — Java 的內部格式
UTF-16 是 Unicode 家族中的另一種編碼。與 UTF-8 不同,它通常為每個字元使用 2 個位元組。Java 在 JVM 內部就是這樣存放字串(String)與字元(char)。但有個細節:某些字元(例如罕見漢字或表情符號)需要 4 個位元組(以「代理對」表示)。
UTF-16 在哪裡使用?
- 在 Java 內部:所有字串與字元都是 UTF-16。
- Windows:許多 Windows 系統檔案使用 UTF-16(例如 Notepad 預設以 UTF-16 LE 儲存)。
- 某些網路協定與格式:例如,XML 可以使用 UTF-16。
範例:UTF-16 中的字元如何表示
| 符號 | Unicode 代碼 | UTF-16(位元組) | 備註 |
|---|---|---|---|
| A | |
|
2 個位元組 |
| Ya | |
|
2 個位元組 |
| € | |
|
2 個位元組 |
| 😀 | |
|
4 個位元組(代理對) |
Java 中的使用範例
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
public class Utf16Demo {
public static void main(String[] args) throws Exception {
String text = "你好,世界! 😀";
// 使用 UTF-16 的編碼將字串寫入檔案
Files.write(Paths.get("utf16.txt"), text.getBytes(StandardCharsets.UTF_16));
// 指定 UTF-16 編碼讀取檔案內容
String read = Files.readString(Paths.get("utf16.txt"), StandardCharsets.UTF_16);
System.out.println(read); // 正確輸出原始字串
}
}
關鍵點:寫入與讀取必須指定相同的編碼。如果以 UTF-16 寫入,卻用預設(例如 UTF-8)來讀取,就會產生一團「亂碼」。
UTF-16 的特點
- 對多數字元為固定長度:2 個位元組。
- 代理對:部分字元(例如表情符號)需要 4 個位元組。
- Byte Order Mark (BOM):檔案開頭常加入指示位元組順序的標頭(LE — little endian,BE — big endian)。Java 通常能自動辨識 BOM。
- 與 ASCII 不相容:若你把 UTF-16 檔案用預期 ASCII 或 UTF-8 的編輯器開啟,會看到一堆 0 與奇怪的符號。
何時使用 UTF-16?
- 如果你對接的系統或檔案明確要求 UTF-16(例如與 Windows 程式整合)。
- 一般應用與檔案——優先使用 UTF-8 更佳。
4. ISO-8859-1 (Latin-1) — 來自過去的問候
ISO-8859-1,或稱 Latin-1,是一種誕生於上世紀 80 年代的單位元組編碼。它支援 256 個符號:英文字母、西歐語言的變音符字母(é、ü、ç 等)、標點與特殊符號。
事實: 此編碼不包含西里爾字母、希臘文、阿拉伯文、中文與其他非拉丁字元。
ISO-8859-1 在哪裡出現?
- 舊檔案與舊程式(特別在歐洲)。
- 部分資料庫與協定的「預設值」。
- 在 HTTP 標頭中(標準規定未指明編碼的文字資料視為 ISO-8859-1,但實務上較少見)。
範例:在 ISO-8859-1 中字元如何表示
| 符號 | Unicode 代碼 | ISO-8859-1(位元組) | 備註 |
|---|---|---|---|
| A | |
|
與 ASCII 相同 |
| é | |
|
法語 |
| ü | |
|
德語 |
| Ya | |
— | 沒有這個符號! |
Java 中的使用範例
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.charset.Charset;
public class Latin1Demo {
public static void main(String[] args) throws Exception {
String text = "Bonjour, ça va? Café!"; // 法語文字
Files.write(Paths.get("latin1.txt"), text.getBytes(StandardCharsets.ISO_8859_1));
// 以 ISO-8859-1 讀取檔案
String read = Files.readString(Paths.get("latin1.txt"), StandardCharsets.ISO_8859_1);
System.out.println(read); // 一切都能正確讀取!
// 試著寫入非 Latin-1 字元(例如中文)
String cyrillic = "你好,世界!";
try {
Files.write(Paths.get("badlatin1.txt"), cyrillic.getBytes(StandardCharsets.ISO_8859_1));
} catch (Exception e) {
System.out.println("錯誤:無法將非 Latin-1 字元寫入 ISO-8859-1!");
}
}
}
ISO-8859-1(Latin-1)只支援西歐語系的符號——例如法語、德語或西班牙語的重音字母。因此法語文字能無痛寫入與讀取。
然而此編碼缺少西里爾字母、中文等字元,嘗試寫入時會發生錯誤或字元遺失。因此處理多語系文字,最好使用 UTF-8 或 UTF-16,它們涵蓋的字元集要大得多。
ISO-8859-1 的特點
- 語言受限:僅西歐語系。
- 每字元 1 個位元組:體積小,但限制多。
- 寫入「外來」字元會扭曲:若嘗試保存西里爾字母或漢字,會變成問號或亂碼。
何時使用 ISO-8859-1?
- 僅當你要整合非常舊的系統或資料庫,且它明確要求此編碼時。
- 對於現代需求——不建議使用。
5. 實用細節
編碼比較表
| 編碼 | 每個符號的位元組數 | 涵蓋語言 | 與 ASCII 的相容性 | 使用場景 |
|---|---|---|---|---|
|
1-4 | 全部 | 完全 | Web、Java、JSON、XML、Linux |
|
2 或 4 | 全部 | 無 | Java 內部、Windows、XML |
|
1 | 西歐 | 完全 | 舊程式、資料庫、HTTP |
如何選擇編碼?
- UTF-8 —— 你在 99% 的情況下都該選它。相容 ASCII、支援所有語言、對英文最省空間,且是 web 與 Java 的標準。
- UTF-16 —— 僅當規格要求,或需與 Windows 程式整合時使用。
- ISO-8859-1 —— 僅用於與舊系統相容。切勿用來儲存西里爾字母、希臘文、阿拉伯文等。
如何判斷檔案的編碼?
- 用能顯示/切換編碼的編輯器(例如 Notepad++、VS Code)開啟檔案。
- 若看到「亂碼」,試著用其他編碼開啟。
- 在 Linux 終端可用指令 file 檔名——有時能推測出編碼。
如何在 Java 設定編碼?
使用 java.nio.charset 套件的類別:
- StandardCharsets.UTF_8
- StandardCharsets.UTF_16
- StandardCharsets.ISO_8859_1
或透過編碼名稱:
Charset windows1251 = Charset.forName("Windows-1251");
Charset utf8 = Charset.forName("UTF-8");
6. 實作:如果把編碼弄錯會怎樣?
我們來做個小實驗。把字串以 UTF-8 寫入,然後嘗試以 ISO-8859-1 讀取:
import java.nio.file.*;
import java.nio.charset.*;
public class EncodingMismatchDemo {
public static void main(String[] args) throws Exception {
String text = "你好,世界!"; // 中文
// 以 UTF-8 寫入
Files.write(Paths.get("utf8demo.txt"), text.getBytes(StandardCharsets.UTF_8));
// 以 ISO-8859-1 讀取(錯誤示範!)
String wrong = Files.readString(Paths.get("utf8demo.txt"), StandardCharsets.ISO_8859_1);
System.out.println("錯誤的讀取: " + wrong);
// 現在用正確的方式讀取
String correct = Files.readString(Paths.get("utf8demo.txt"), StandardCharsets.UTF_8);
System.out.println("正確的讀取: " + correct);
}
}
結果:
- 第一種情況你會看到「亂碼」——一串奇怪的符號。
- 第二種情況——正常的中文內容。
7. 處理編碼時的常見錯誤
錯誤 1:以錯誤的編碼讀取檔案。 如果檔案以 UTF-8 寫入,卻用 ISO-8859-1 或 Windows-1251 來讀取,所有非 ASCII 字元都會變成垃圾。這是經典情境:「我這邊都正常,為什麼同事那邊不行?」
錯誤 2:試圖把「外來」字元寫入單位元組編碼。 如果你嘗試把西里爾字母或漢字存成 ISO-8859-1,Java 會把它們變成問號或直接拋錯。資料將不可逆地遺失。
錯誤 3:使用系統「預設」編碼。 在 Java 的某些方法(例如 new FileReader("file.txt"))裡,若未明確指定編碼,就會使用系統預設編碼。在一台電腦上可能是 UTF-8,另一台是 Windows-1251,第三台甚至是更冷門的設定。因此請改用能明確指定編碼的方法。
錯誤 4:把 UTF-16 檔案當成 UTF-8 開啟。 以 UTF-16 寫入的檔案,如果用 UTF-8 開啟,會看到大量 0 與奇怪符號,因為位元組被錯誤解讀。
GO TO FULL VERSION