【字元編碼】Java字元編碼詳細解答及問題探討

leesf發表於2016-03-26

一、前言

  繼上一篇寫完位元組編碼內容後,現在分析在Java中各字元編碼的問題,並且由這個問題,也引出了一個更有意思的問題,筆者也還沒有找到這個問題的答案。也希望各位園友指點指點。

二、Java字元編碼

  直接上程式碼進行分析似乎更有感覺。 

public class Test {    
    public static String stringInfo(String str, String code) throws Exception {
        byte[] bytes = null;
        if (code.equals("")) // 使用預設編碼格式
            bytes = str.getBytes();
        else // 使用指定編碼格式
            bytes = str.getBytes(code);
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < bytes.length; i++) { // 轉化為十六進位制
            sb.append(Integer.toHexString(bytes[i] & 0xff).toUpperCase() + " ");
        }
        // 對最後一個空格做處理(為了顯示美觀)
        String info = sb.toString().substring(0, sb.toString().length() - 1);
        // 組合返回
        StringBuffer result = new StringBuffer();
        result.append(bytes.length);
        result.append("[");
        result.append(info);
        result.append("]");
        return result.toString();
    } 
    
    public static void main(String[] args) throws Exception {
        String left = "(";
        String right = ") : ";
        String[] strs = {"L", "LD", "李", "李鄧"};
        String[] codes = {"ASCII", "ISO-8859-1", "GB2312", "GBK", "Unicode", "UTF-8", "UTF-16", "UTF-16BE", "UTF-16LE", ""};
        for (String code : codes) {
            for (String str : strs) {
                System.out.println(str + left + (!code.equals("") ? code : "default") + right + stringInfo(str, code));
            }
            System.out.println("---------------------------------------");
        }
    }    
}
View Code

  執行結果:   

L(ASCII) : 1[4C]
LD(ASCII) : 2[4C 44]
李(ASCII) : 1[3F]
李鄧(ASCII) : 2[3F 3F]
---------------------------------------
L(ISO-8859-1) : 1[4C]
LD(ISO-8859-1) : 2[4C 44]
李(ISO-8859-1) : 1[3F]
李鄧(ISO-8859-1) : 2[3F 3F]
---------------------------------------
L(GB2312) : 1[4C]
LD(GB2312) : 2[4C 44]
李(GB2312) : 2[C0 EE]
李鄧(GB2312) : 4[C0 EE B5 CB]
---------------------------------------
L(GBK) : 1[4C]
LD(GBK) : 2[4C 44]
李(GBK) : 2[C0 EE]
李鄧(GBK) : 4[C0 EE B5 CB]
---------------------------------------
L(Unicode) : 4[FE FF 0 4C]
LD(Unicode) : 6[FE FF 0 4C 0 44]
李(Unicode) : 4[FE FF 67 4E]
李鄧(Unicode) : 6[FE FF 67 4E 90 93]
---------------------------------------
L(UTF-8) : 1[4C]
LD(UTF-8) : 2[4C 44]
李(UTF-8) : 3[E6 9D 8E]
李鄧(UTF-8) : 6[E6 9D 8E E9 82 93]
---------------------------------------
L(UTF-16) : 4[FE FF 0 4C]
LD(UTF-16) : 6[FE FF 0 4C 0 44]
李(UTF-16) : 4[FE FF 67 4E]
李鄧(UTF-16) : 6[FE FF 67 4E 90 93]
---------------------------------------
L(UTF-16BE) : 2[0 4C]
LD(UTF-16BE) : 4[0 4C 0 44]
李(UTF-16BE) : 2[67 4E]
李鄧(UTF-16BE) : 4[67 4E 90 93]
---------------------------------------
L(UTF-16LE) : 2[4C 0]
LD(UTF-16LE) : 4[4C 0 44 0]
李(UTF-16LE) : 2[4E 67]
李鄧(UTF-16LE) : 4[4E 67 93 90]
---------------------------------------
L(default) : 1[4C]
LD(default) : 2[4C 44]
李(default) : 3[E6 9D 8E]
李鄧(default) : 6[E6 9D 8E E9 82 93]
---------------------------------------
View Code

  說明:通過結果我們知道如下資訊。

  1. 在Java中,中文在用ASCII碼錶示為3F,實際對應符號'?',用ISO-8859-1表示為3F,實際對應符號也是為'?',這意味著中文已經超出了ASCII和ISO-8859-1的表示範圍。

  2. UTF-16採用大端儲存,即在位元組陣列前新增了FE FF,並且FE FF也算在了字元陣列長度中。

  3. 指定UTF-16的大端(UTF-16BE)或者小端(UTF-16LE)模式後,則不會有FE FF 或 FF FE控制符,相應的位元組陣列大小也不會包含控制符所佔的大小。

  4. Unicode表示與UTF-16相同。

  5. getBytes()方法預設是採用UTF-8。

三、char表示問題

  我們知道,在Java中char型別為兩個位元組長度,我們來看下一個示例。 

public class Test {    
    public static void main(String[] args) throws Exception {
        char ch1 = 'a';    // 1
        char ch2 = '李'; // 2
        char ch3 = '\uFFFF'; // 3
        char ch4 = '\u10000'; // 4
    
    }
}

  問題:讀者覺得這樣的程式碼能夠編譯通過嗎?如不能編碼通過是為什麼,又具體是那一行程式碼出現了錯誤?

  分析:把這個示例拷貝到Eclipse中,定位到錯誤,發現是第四行程式碼出現了錯誤,有這樣的提示,Invalid character constant。

  解答:問題的關鍵就在於char型別為兩個位元組長度,Java字元采用UTF-16編碼。而'\u10000'顯然已經超過了兩個位元組所能表示的範圍了,一個char無法表示。說得更具體點,就是char表示的範圍為Unicode表中第零平面(BMP),從0000 - FFFF(十六進位制),而在輔助平面上的碼位,即010000 - 10FFFF(十六進位制),必須使用四個位元組進行表示。

  有了這個理解後,我們看下面的程式碼  

public class Test {    
    public static void main(String[] args) throws Exception {
        char ch1 = 'a';
        char ch2 = '李';
        char ch3 = '\uFFFF';
        String str = "\u10000";
        System.out.println(String.valueOf(ch1).length());
        System.out.println(String.valueOf(ch2).length());
        System.out.println(String.valueOf(ch3).length());
        System.out.println(str.length());    
    }
}

  執行結果:

1
1
1
2

  說明:從結果我們可以知道,所有在BMP上的碼點(包括'a'、'李'、'\uFFFF')的長度都是1,所有在輔助平面上的碼點的長度都是2。注意區分字串的length函式與位元組陣列的length欄位的差別。

四、問題的發現

  在寫Java小程式時,筆者一般不會開啟Eclispe,而是直接在NodePad++中編寫,然後通過javac、java命令執行程式,檢視結果。也正是由於這個習慣,發現瞭如下的問題,請聽筆者慢慢道來,來請園友們指點指點。

  有如下簡單程式,請忽略字串的含義。

public class Test {    
    public static void main(String[] args) throws Exception {
        String str = "我我我我我我我\uD843\uDC30";
        System.out.println(str.length());
    }
}

  說明:程式功能很簡單,就是列印字串長度。

  4.1 兩種編譯方法

  1. 筆者通過javac Test.java進行編譯,編譯通過。然後通過java Test執行程式,執行結果如下:

  

  說明:根據結果我們可以推測,字元'我'為長度1,\uD843\uDC30為長度10,其中\u為長度1。

  2. 筆者通過javac -encoding utf-8 Test.java進行編譯,編譯通過。然後通過java Test執行程式,執行結果如下:

  

  說明:這個結果很好理解,字元'我'、\uD843、\uDC30都在BMP,都為長度1,故總共為9。

  通過兩種編譯方法,得到的結果不相同,經過查閱資料知道javac Test.java預設的是採用GBK編碼,就像指定javac -encoding gbk Test.java進行編譯。

  4.2. 檢視class檔案

  1. 檢視java Test.java的class檔案,使用winhex開啟,結果如下:

  說明:圖中紅色標記給出了字串"我我我我我我我\uD843\uDC30"大致所在位置。因為前面我們分析過,class檔案的儲存使用UTF-8編碼,於是,先算E9 8E B4,得到Unicode碼點為94B4(十六進位制),查閱Unicode表,發現表示字元為'鎴',這完全和'我'沒有關係。並且E9 8E B4 後面的E6 88 9E,和E9 8E B4也不相等,照理說,相同的字元編碼應該相同。後來發現,紅色標記地方好像有點規則,就是E9 8E B4 E6 88 9E E5 9E 9C(九個位元組)表示'我我',重複迴圈了3次,表示字元'我我我我我我',之後的E9 8E B4 E6 85(五個位元組)表示'我',總共7個'我',很明顯又出現疑問了。

  猜測是因為使用javac Test.java進行編譯,採用的是GBK編碼,而class檔案儲存的格式為UTF-8編碼。這兩種操作中肯定含有某種轉化關係,並且最後的class檔案中也加入相應的資訊。

  2. 檢視java -encoding -utf-8 Test.java的class檔案,使用winhex開啟,結果如下:

  說明:紅色標記給出了字串的大體位置,E6 88 91,經過計算,確實對應字元'我'。這是沒有疑問的。

  4.3 針對疑問的探索

  1. 又改變了字串的值,使用如下程式碼:

public class Test {    
    public static void main(String[] args) throws Exception {
        String str = "我我coder";
        System.out.println(str.length());
    }
}

  同樣,使用javac Test.java、java Test命令。得到結果為:

  

  這就更加疑惑了。為什麼會得到8。

  2. 查閱資料結果

   在Javac時,若沒有指定-encoding引數指定Java源程式的編碼格式,則javac.exe首先獲得我們作業系統默認採用的編碼格式,也即在編譯java程式時,若我們不指定源程式檔案的編碼格式,JDK首先獲得作業系統的file.encoding引數(它儲存的就是作業系統預設的編碼格式,如WIN2k,它的值為GBK),然後JDK就把我們的java源程式從file.encoding編碼格式轉化為Java內部預設的UTF-16格式放入記憶體中。之後會輸出class檔案,我們知道class是以UTF-8方式編碼的,它內部包含我們源程式中的中文字串,只不過此時它己經由file.encoding格式轉化為UTF-8格式了。

五、問題提出

  1. 使用javac Test.java編譯後,為何會得到上述class檔案的格式(即GBK -> UTF16 -> UTF8具體是如何實現的)。

  2. 使用javac Test.java編譯後,為何得到的結果一個是17,而另外一個是8。

六、總結

  探索的過程有很意思,這個問題暫時還沒有解決,以後遇到該問題的答案會貼出來,也歡迎有想法的讀者進行交流探討。謝謝各位園友的觀看~

 

參考連結:

http://blog.csdn.net/xiunai78/article/details/8349129

  

  

 

相關文章