最近在看《Java核心技術 卷Ⅰ》,遇到了這個生僻的知識點。想要徹底理解這個知識點需要了解不少東西,整理如下!
一、故事
我們知道,在計算機內部,所有信息最終都是用二進制值表示的。由二進制的數學特性可知,每一個二進制位(bit)有0和1兩種狀態,一個字節(byte)就可以組合出256種狀態,每一個狀態可以表示一個字符。爲了表示英語字符與二進制位之間的關係,在上個世紀60年代,美國製定了ASCII 碼。ASCII 碼一共規定了128個字符的編碼,比如大寫的字母A是65(二進制01000001)。
英語用128個符號編碼就夠了,但是用來表示其他語言,128個符號是不夠的。於是,一些歐洲國家就決定,利用字節中閒置的最高位編入新的符號。比如,法語中的é的編碼爲130(二進制10000010)。這樣一來,這些歐洲國家使用的編碼體系,可以表示最多256個符號,從128到255的範圍表示的字符集被稱"擴展字符集"。但是,等到其他國家(如中國、日本、韓國)使用計算機時,已經沒有可以利用的字節狀態來表示相關的字符。爲此,各個國家爲了適應本國語言的需要,紛紛設計本國自己的文字編碼集對ASCII 碼字符集進行擴充,如簡體中文的 GB 系列編碼、日文的 SHIFT-JIS 編碼等。這些不同的字符集互不兼容,相同的編碼可能代表不同的字符,同一個字符在不同的字符集中的編碼也往往不同。當信息從一臺計算機移植到另一臺計算機時,接收方若以錯誤的編碼方式解讀,就會出現亂碼。不同編碼文件之間的交流成爲一個亟待解決的問題。
令人欣慰的是,“統一碼聯盟”和ISO兩個國際組織爲了解決這一問題,雙方開始協同工作。他們採用的方法很簡單:廢了所有的地區性編碼方案,重新搞一個包括了地球上所有文化、所有字母和符號的編碼,這種編碼被命名爲"Universal Character Set",簡稱“UCS”,俗稱 “Unicode”。
Unicode開始制訂時,計算機的存儲器容量已經有了極大地發展,空間已不再是重點考慮的問題。由於,設計者天真地以爲 16 個比特就可以表示地球上所有仍具活力的文字,於是就直接規定用兩個字節來統一表示所有的字符。其實,16個比特僅僅可以表示65536個字符。UCS-2就是Unicode字符集的一種定長的2字節編碼(現在已廢棄)。
需要注意的是,Unicode只是一個字符集,它只規定了怎麼用多個字節的二進制值表示各種文字符號。這些編碼值如何在網絡中傳輸是UTF(UCS Transformation Format)規範規定的。當文本只包含英文和數字,如果用Unicode編碼就顯得特別浪費存儲空間(用 ASCII 編碼更合適),同樣,在用 Unicode編碼在網絡傳輸時也比ASCII 編碼多一倍的傳輸。當傳輸文件比較小的時候,內存資源和網絡帶寬尚能承受,當文件傳輸達到上TB的時候,如果 “硬”傳,則需要消耗的資源就不可小覷了。爲了節約空間,UTF爲Unicode字符集提供了多種實現方式。比如常用的UTF-8編碼就是Unicode的一種實現方式,顧名思義,UTF-8就是每次8個位傳輸數據,它是可變長編碼。當你的文本是 ASCII 編碼的字符時,UTF-8編碼用 1 個字節存放;而當文本中包含其它Unicode 字符時,它將按一定算法轉換,每個字符使用 1~3 個字節存放。這樣便實現了有效節省空間的目的。這也是UTF-8編碼被廣泛採用的原因。
不過正是因爲utf-8編碼可變長,一會兒一個字符是佔用一個字節,一會兒一個字符佔用兩個字節,還有的佔用三個字節,導致在內存中的格式不統一,運算效率低。而unicode編碼雖然佔用更多的內存空間,但是在編程過程中或者在內存處理的時候會比utf-8編碼更爲簡單,因爲它始終保持一樣的長度,處理就會變得更加簡單。所以,通常將utf-8編碼作爲“外碼”,用在網絡傳輸和文件保存時;而將unicode編碼作爲內碼,用在文件內容讀取到內存中時。
Java語言設計之初就認識到統一字符集(Unicode)的重要性,並積極擁抱了問世不久的Unicode標準。比如,java基本數據類型中char最初描述的就是UCS-2編碼中的代碼單元。與其他使用8比特字符的語言相比,這是java的主要優勢。
但尷尬的是,隨着更多字符的引入,尤其是漢語、韓語和日文中的表意文字的引入,使得Unicode遠遠超出16比特編碼的範圍。現在,Unicode需要21個比特,表示範圍從0x0~0x10FFFF。當單元固定長度16位的UCS-2到達容量上限不能支持更多的 Unicode 字符的時候,Unicode協會放棄了UCS-2。取而代之的是16位的變長編碼UTF-16。
在Unicode從16比特向21比特過渡時期,Java語言深受其苦。其中,Java 5.0 版本既要支持Unicode 4.0同時又要保證向後兼容性,所以java開始使用UTF-16作爲其內部編碼方式。引入碼點和編碼單元兩個概念。
碼點(code point)是一個編碼表中某個字符對應的二進制代碼值,即一個有效的Unicode字符的二進制代碼值被稱作一個碼點。碼點一般用十六進制書寫,並加上前綴U+,如此一來,21個比特的Unicode,碼點範圍從U+0000至U+10FFFF。其中碼點在U+0000 ~ U+FFFF範圍內的字符稱爲“經典”Unicode字符,超出U+FFFF範圍的字符被稱作“代理字符”。經典Unicode字符都可以用16個比特表示,通常被稱作編碼單元(code unit)。代理字符的編碼需要兩個連續的編碼單元。
二、碼點和代碼單元操作演示
java字符串是由char值序列組成。char數據類型是一個採用UTF-16編碼表示的Unicode碼點的代碼單元,即char表示一個代碼單元。
知識點1:String類中length方法將返回給定字符串採用UTF-16編碼表示時所需要的代碼單元的數量。如下:
String greeting = "Hello";
int n = greeting.length(); //n is 5
String str1 = "hi𝕆";
int m = str1.length(); // m is 4
知識點2:得到實際的字符長度,即碼點的數量,如下:
int cpCount_1 = greeting.codePointCount(0, greeting.length()); // cpCount_1 is 5
int cpCount_2 = str1.codePointCount(0, str1.length()); // cpCount_2 is 3
知識點3:調用String類中charAt(n)方法,將返回位置n的代碼代碼單元,n介於 0~greeting.length() -1之間;
char first = greeting.charAt(0); // first is 'H'
char last = greeting.charAt(1); // last is 'o'
注意,代理(輔助)字符需要2個代碼單元纔可以表示,所以通過charAt獲得的可能不是某個字符,而是輔助字符的一個代碼單元。爲了避免這個問題,不要使用char類型,它太底層了。
知識點4:獲取字符串的第i個碼點(),如下:
String greeting = "Hello";
int index = greeting.offsetByCodePoints(0, i);
int cp = greeting.codePointAt(index);
知識點5:Character類中的isSupplementaryCodePoint(int codePoint)方法是一個boolean型方法,用來確定指定字符(Unicode 代碼點)codePoint是否在代理字符範圍內;
知識點6:Character類中的isSurrogate(char)方法是一個boolean型方法,char對應代碼單元是用於表示輔助字符的話就返回true;
三、char和String兩種類型互相轉換
下面這段程序,是對上述知識點的總結。
public class CharCodePointDemo {
private static String str1 = "hi𝕆";
public static void main(String[] args) {
positiveOrderTraversalAllCodePoint(str1);
negativeOrderTraversalAllCodePoint(str1);
intStreamTraversalAllCodePoint(str1);
}
// 正序遍歷字符串的所有碼點
public static void positiveOrderTraversalAllCodePoint(String sentence) {
System.out.print(sentence + " length is " + sentence.length() + ". ");
for (int index = 0; index < sentence.length(); ) {
int cp = sentence.codePointAt(index);
System.out.print(cp + " ");
if (Character.isSupplementaryCodePoint(cp)) {
index += 2;
} else {
index++;
}
}
System.out.println();
}
// 逆序遍歷字符串的所有碼點
public static void negativeOrderTraversalAllCodePoint(String sentence) {
System.out.print(sentence + " length is " + sentence.length() + ". ");
int index = sentence.length();
while (--index >= 0) {
if (Character.isSurrogate(sentence.charAt(index))) {
index --;
}
System.out.print(sentence.codePointAt(index) + " ");
}
System.out.println();
}
// int流-遍歷字符串的所有碼點
public static void intStreamTraversalAllCodePoint(String sentence) {
System.out.print(sentence + " length is " + sentence.length() + ". ");
int[] codePoints = sentence.codePoints().toArray();
for (int ele : codePoints) {
System.out.print(ele + " ");
}
System.out.println();
}
}
上述代碼,執行結果如下: