【字符編碼】Java字符編碼詳細解答及問題探討
一、前言
繼上一篇寫完字節編碼內容後,現在分析在Java中各字符編碼的問題,並且由這個問題,也引出了一個更有意思的問題,筆者也還沒有找到這個問題的答案。也希望各位園友指點指點。
二、Java字符編碼
直接上代碼進行分析似乎更有感覺。
View Code運行結果:
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
【字符編碼】Java字符編碼詳細解答及問題探討