字元編碼問題是 Java 開發者必經的坑。本文從 JVM 內部字串儲存機制出發,徹底講透 String.getBytes(charset) 與 new String(bytes, charset) 兩大核心方法原理,並給出 Servlet 參數亂碼、資料庫儲存亂碼等經典場景的標準解決方案。
查看 java.lang.String 原始碼可知:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[]; // JDK 8 及之前
// private final byte[] value; // JDK 9+ Compact Strings
// private final byte coder; // JDK 9+ LATIN1/UTF16
}
關鍵結論:
char[] 陣列,每個 char 佔 2 位元組,儲存 UTF-16 代碼單元byte[] + coder,採用 Compact Strings,單字元語言用 ISO-8859-1 (1 位元組),雙字元語言用 UTF-16 (2 位元組)驗證範例:
char c = '你';
System.out.println(Integer.toHexString(c)); // 4f60 (Unicode 碼位)
System.out.println(Integer.valueOf(c)); // 20320 (十進制)
System.out.println(c); // 你
功能:將記憶體中的 Unicode 字串,按指定字元集編碼為位元組陣列
過程:Unicode (記憶體) → 編碼器 → byte[] (目標編碼)
String str = "你";
// UTF-8 編碼:3 位元組
byte[] utf8Bytes = str.getBytes("UTF-8"); // [-28, -67, -96] 即 0xE4 0xBD 0xA0
// GBK 編碼:2 位元組
byte[] gbkBytes = str.getBytes("GBK"); // [-76, -88] 即 0xC4 0xE3
// ISO-8859-1 編碼:1 位元組 (無法表示中文,會變成 ? 或亂碼)
byte[] isoBytes = str.getBytes("ISO-8859-1"); // [63] 即 0x3F ('?')
功能:將位元組陣列按指定字元集解碼為 Unicode 字串
過程:byte[] (來源編碼) → 解碼器 → Unicode (記憶體)
byte[] utf8Bytes = { (byte)0xE4, (byte)0xBD, (byte)0xA0 };
String s1 = new String(utf8Bytes, "UTF-8"); // "你" ✓
String s2 = new String(utf8Bytes, "GBK"); // "涓" ✗ (錯誤解碼)
String s3 = new String(utf8Bytes, "ISO-8859-1"); // "ä½?" ✗
根因:
標準解法:
// 核心原理:逆向操作還原原始位元組,再正確解碼
String correctStr = new String(wrongStr.getBytes("ISO-8859-1"), "UTF-8");
最佳實踐 (配置而非代碼):
server.xml <Connector> 加 URIEncoding="UTF-8"server.tomcat.uri-encoding=UTF-8CharacterEncodingFilter 設定 forceEncoding=true檢查鏈路:Java 應用 ↔ JDBC 驅動 ↔ 資料庫連線 ↔ 資料表字元集
// MySQL 連線 URL 關鍵參數
jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull
// 確認資料庫/資料表/欄位字元集
SHOW VARIABLES LIKE 'character_set_%';
ALTER TABLE t CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
// 讀取:明確指定來源檔案編碼
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(file), "GBK"))) { ... }
// 寫入:明確指定目標檔案編碼 (推薦 UTF-8)
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) { ... }
所有編碼轉換本質都是:String → byte[] → String
// 錯誤認知:直接將 String 從編碼 A 轉編碼 B
// String gbkStr = new String(isoStr.getBytes("GBK")); // 錯誤!
// 正確流程:
// 1. 先用正確編碼還原原始位元組
byte[] originalBytes = wrongString.getBytes("錯誤編碼");
// 2. 再用目標編碼解碼
String correctString = new String(originalBytes, "目標編碼");
| 字元集 | 別名 | 中文支援 | 位元組數 | 主要用途 |
|---|---|---|---|---|
| UTF-8 | UTF8 | 完美 | 1-4 | Web 標準、資料庫、檔案、API 傳輸 |
| GBK | CP936 | 完美 | 1-2 | Windows 系統、舊版中文專案 |
| GB2312 | EUC_CN | 基本 | 1-2 | 極舊系統、簡體中文子集 |
| ISO-8859-1 | Latin-1 | 不支援 | 1 | HTTP 預設、單字節透傳 |
| UTF-16 | Unicode | 完美 | 2/4 | Java 內部、Windows API |
String.getBytes() 無參版本使用平台預設編碼,跨平台必亂碼<meta charset="UTF-8">str.getBytes(StandardCharsets.UTF_8) 避免拼寫錯誤且編譯期檢查核心心法:Java 字串記憶體中只有 Unicode。亂碼本質是「用錯編碼解碼了位元組」。解決之道:找到原始位元組,用正確編碼解碼。
徹底理解編碼原理,不再恐懼亂碼。建議專案建立編碼規範文檔,新成員入職必讀,從源頭杜絕編碼問題。