Java號稱對Unicode提供天然的支持,這話在很久很久以前就已經是假的了(不過曾經是真的),實際上,到JDK5.0為止,Java才算剛剛跟上Unicode的腳步,開始提供對增補字符的支持。
現在的Unicode碼空間為U+0000到U+10FFFF,一共1114112個碼位,其中只有1,112,064 個碼位是合法的(我來替你做算術,有2048個碼位不合法),但並不是說現在的Unicode就有這麼多個字符了,實際上其中很多碼位還是空閒的,到Unicode 4.0 規范為止,只有96,382個碼位被分配了字符(但無論如何,仍比很多人認為的65536個字符要多得多了)。其中U+0000 到U+FFFF的部分被稱為基本多語言面(Basic Multilingual Plane,BMP)。U+10000及以上的字符稱為補充字符。在Java中(Java1.5之後),補充字符使用兩個char型變量來表示,這兩個char型變量就組成了所謂的surrogate pair(在底層實際上是使用一個int進行表示的)。第一個char型變量的范圍稱為“高代理部分”(high-surrogates range,從"uD800到"uDBFF,共1024個碼位), 第二個char型變量的范圍稱為low-surrogates range(從"uDC00到"uDFFF,共1024個碼位),這樣使用surrogate pair可以表示的字符數一共是1024的平方計1048576個,加上BMP的65536個碼位,去掉2048個非法的碼位,正好是1,112,064個碼位。
關於Unicode的碼空間實際上有一些稍不小心就會讓人犯錯的地方。比如我們都知道從U+0000到U+FFFF的部分被稱為基本多語言面(Basic Multilingual Plane,BMP),這個范圍內的字符在使用UTF-16編碼時,只需要一個char型變量就可以保存。仔細看看這個范圍,應該有65536這麼大,因此你會說單字節的UTF-16編碼能夠表示65536個字符,你也會說Unicode的基本多語言面包含65536個字符,但是再想想剛才說過的surrogate pair,一個UTF-16表示的增補字符(再一次的,需要兩個char型變量才能表示的字符)怎樣才能被正確的識別為增補字符,而不是兩個普通的字符呢?答案你也知道,就是通過看它的第一個char是不是在高代理范圍內,第二個char是不是在低代理范圍內來決定,這也意味著,高代理和低代理所占的共2048個碼位(從0xD800到0xDFFF)是不能分配給其他字符的。
但這是對UTF-16這種編碼方法而言,而對Unicode這樣的字符集呢?在Unicode的編號中,U+D800到U+DFFF是否有字符分配?答案是也沒有!這是典型的字符集為方便編碼方法而做的安排(你問他們這麼做的目的?當然是希望基本多語言面中的字符和一個char型的UTF-16編碼的字符能夠一一對應,少些麻煩,從中我們也能看出UTF-16與Unicode間很深的淵源與結合)。也就是說,無論Unicode還是UTF-16編碼後的字符,在0x0000至0xFFFF這個范圍內,只有63488個字符。這就好比最初的CPU被勉強拿來做多媒體應用,用得多了,CPU就不得不修正自己從硬件上對多媒體應用提供支持了。
盡管不情願,但說到這裡總還得扯扯相關的概念:代碼點和代碼單元。
代碼點(Code Point)就是指Unicode中為字符分配的編號,一個字符只占一個代碼點,例如我們說到字符“漢”,它的代碼點是U+6C49。
代碼單元(Code Unit)則是針對編碼方法而言,它指的是編碼方法中對一個字符編碼以後所占的最小存儲單元。例如UTF-8中,代碼單元是一個字節,因為一個字符可以被編碼為1個,2個或者3個4個字節;在UTF-16中,代碼單元變成了兩個字節(就是一個char),因為一個字符可以被編碼為1個或2個char(你找不到比一個char還小的UTF-16編碼的字符,嘿嘿)。說得再羅嗦一點,一個字符,僅僅對應一個代碼點,但卻可能有多個代碼單元(即可能被編碼為2個char)。
以上概念絕非學術化的繞口令,這意味著當你想以一種統一的方式指定自己使用什麼字符的時候,使用代碼點(即你告訴你的程序,你要用Unicode中的第幾個字符)總是比使用代碼單元更好(因為這樣做的話你還得區分情況,有時候提供一個16進制數字,有時候要提供兩個)。
例如我們有一個增補字符???(哈哈,你看到了三個問號對吧?因為我的系統顯示不出這個字符),它在Unicode中的編號是U+2F81A,當在程序中需要使用這個字符的時候,就可以這樣來寫:
String s=String.valueOf(Character.toChars(0x2F81A)); char[]chars=s.toCharArray(); for(char c:chars){ System.out.format("%x",(short)c); }
後面的for循環把這個字符的UTF-16編碼打印了出來,結果是
d87edc1a
注意到了嗎?這個字符變成了兩個char型變量,其中0xd87e就是高代理部分的值,0xdc1a就是低代理的值。