java——char類型、碼點和代碼單元詳解

最近在看《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();
    }
}

上述代碼,執行結果如下:
圖片名稱


四、參考資料

  1. 《java核心技術 卷1》
  2. 淺談unicode編碼和utf-8編碼的關係
  3. UCS 和 UTF-8 編碼
  4. 細說:Unicode, UTF-8, UTF-16, UTF-32, UCS-2, UCS-4
  5. 爲什麼 Java 內部使用 UTF-16 表示字符串?
  6. 字符編碼筆記:ASCII,Unicode 和 UTF-8
  7. Unicode 及編碼方式概述
  8. [JAVA]isSurrogate(char) 與 isSupplementaryCodePoint(int)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章