任何一本 Java 入門的書都會談到,Java 的字串使用 Unicode,那麼是否想過,明明你 的文字編輯器是使用 MS950 編碼,為什麼會寫下的字串在 JVM 中會是 Unicode?如果在一個 Main.java 中寫下以下的程式碼並編譯:
public class Main {
public static void main(String[] args) {
System.out.println("Test");
System.out.println("測試");
}
}
如果作業系統預設編碼是 MS950,而文字編輯器是使用 MS950 編碼,那麼你如下執行編譯:
C:\workspace>javac Main.java
產生的 .class 檔案,使用任何的反組譯工具還原回來的程式碼中,你可能會看到以下的內容:
import java.io.PrintStream;
public class Main {
public Main(){}
public static void main(String args[]) {
System.out.println("Test");
System.out.println("\u6E2C\u8A66");
}
}
其中 "\u6E2C\u8A66"
就是 "測試"
的 Unicode 碼點表示,JVM 在載入 .class 之後,就是讀取 Unicode 編碼並產生對應的字串物件,而不是最初在原始碼中寫下的 "測試"
。
那麼編譯器怎麼知道要將中文字元轉為哪個 Unicode 編碼?正如〈你的原始碼是什麼編碼?〉談過的,當使用 javac
指令沒有指定 -encoding
選項時,會使用作業系統預設編碼,如果文字編譯器是使用 UTF-8 編碼,那麼編譯時就要指定 -encoding
為 UTF-8,如此編譯器才會知道用何種編譯讀取.java的內容。例如:
C:\workspace>javac -encoding UTF-8 Main.java
那麼啟動 JVM 之後,字串的實作位元組呢?如果想取得字串的位元組資料,可以使用 String
的 getBytes()
方法。例如:
package cc.openhome;
import static java.lang.System.out;
public class Main {
public static void main(String[] args) throws Exception {
print("UTF-16", "測試".getBytes("UTF-16"));
print("UTF-8", "測試".getBytes("UTF-8"));
print("Big5", "測試".getBytes("Big5"));
print("default", "測試".getBytes());
}
private static void print(String encoding, byte[] bytes) {
out.printf("%s\t", encoding);
for(byte b : bytes) {
out.printf("%-3h", b & 0x00FF);
}
out.println();
}
}
getBytes()
在使用時,可以指定用哪個編碼取得字串的位元組序列,上面這個程式,分別將「測試」以UTF-16、UTF-8、Big5 三種編碼取得位元組序列,結果如下,可以用十六進位編輯器來印證「測試」在這幾種編碼下,位元組序列是不是與執行結果相同(注意純文字中 UTF-16 開頭若是 fe、ff,那是 BOM):
UTF-16 fe ff 6e 2c 8a 66
UTF-8 e6 b8 ac e8 a9 a6
Big5 b4 fa b8 d5
default b4 fa b8 d5
getBytes()
可以不指定編碼呼叫,此時會用〈JVM 預設編碼〉,在啟動 JVM 沒有任何指定的情況下,就會與作業系統預設編碼一致,上面的執行結果是在正體中文 Windows 下執行的,所以 getBytes()
預設就會使用 Big5 來取得位元組序列。
如果有一個位元組陣列,可以用來建構字串,建構時可指定以何種編碼方式來解釋所提供的位元組陣列。例如:
package cc.openhome;
import static java.lang.System.out;
public class Main {
public static void main(String[] args) throws Exception {
out.println(new String(toBytes(0xfe, 0xff, 0x6e, 0x2c, 0x8a, 0x66), "UTF-16"));
out.println(new String(toBytes(0xe6, 0xb8, 0xac, 0xe8, 0xa9,0xa6), "UTF-8"));
out.println(new String(toBytes(0xb4, 0xfa, 0xb8, 0xd5), "Big5"));
out.println(new String(toBytes(0xb4, 0xfa, 0xb8, 0xd5)));
}
private static byte[] toBytes(int... ints) {
byte[] bytes = new byte[ints.length];
for(int i = 0; i < ints.length; i++) {
bytes[i] = (byte) ints[i];
}
return bytes;
}
}
在使用位元組陣列建構字串時,如果不指定編碼,則會使用 JVM 預設編碼,在啟動 JVM 沒有任何指定的情況下,就會與作業系統預設編碼一致,下面的執行結果是在正體中文 Windows 下執行的,所以最後一個字串建構就是使用 Big5:
測試
測試
測試
測試
現在有個簡單的問題,如果在 UTF-8 下檢視某個檔案內容有亂碼,例如看到「®õ¤s½Y¥Û±Ð·|」的亂碼,你知道這個亂碼本來應該是某段 Big5 正體中文,那要如何找回原來正確的中文字串?
可以將以下的原始碼存為 UTF-8:
import static java.lang.System.out;
import java.nio.charset.Charset;
public class Main {
public static void main(String[] args) {
Charset.availableCharsets().keySet().forEach(charset -> {
try {
// 當初是哪個位元組陣列被解釋為 UTF-8 的?逐一嘗試!
byte[] bytes = "®õ¤s½Y¥Û±Ð·|".getBytes(charset);
out.printf("%s %s%n", charset, new String(bytes, "Big5"));
} catch (Exception e) {
// nope
}
});
}
}
編譯與執行後,嘗試從結果中看到可辨識的文字:
C:\workspace>javac -encoding UTF-8 Main.java
C:\workspace>java Main
略...
ISO-8859-1 泰山磐石教會
ISO-8859-13 泰山磐???會
略...
windows-1252 泰山磐石教會
windows-1253 ?山磐??會
windows-1254 泰山磐石?會
略...
當使用 ISO-8859-1 或 windows-1252 嘗試取回的位元組陣列,可用 Big5 編碼解釋來建立可辨識的字串結果。如果這是資料庫的某個欄位,現在可以直接撰寫程式取得其它欄位字串結果,使用 ISO-8859-1(或 windows-1252,不過這比較不可能)取得位元組陣列,再用以建立 Big5 字串,看看結果是否都是可辨識的,如果確認是的話,就可以重新將這些欄位用正確的編碼寫回資料庫。
PS. 上面這個範例來自 JWorld@TW 的討論文章。