Java 的字串


任何一本 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 之後,字串的實作位元組呢?如果想取得字串的位元組資料,可以使用 StringgetBytes() 方法。例如:

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 的討論文章