聊聊Java中codepoint和UTF-16相關的一些事

VinoZhu's Blog發表於2016-10-08

Unicode和UTF-8/UTF-16/UTF-32的關係

Unicode和UTF-8/UTF-16/UTF-32之間就是字符集和編碼的關係。字符集的概念實際上包含兩個方面,一個是字元的集合,一個是編碼方案。字符集定義了它所包含的所有符號,狹義上的字符集並不包含編碼方案,它僅僅是定義了屬於這個字符集的所有符號。但通常來說,一個字符集並不僅僅定義字符集合,它還為每個符號定義一個二進位制編碼。當我們提到GB2312或者ASCII的時候,它隱式地指明瞭編碼方案是GB2312或者ASCII,在這些情況下可以認為字符集與編碼方案互等。

但是Unicode具有多種編碼方案。Unicode字符集規定的標準編碼方案是UCS-2(UTF-16),用兩個位元組表示一個Unicode字元(UTF-16中兩個位元組的為基本多語言平面字元,4個位元組的為輔助平面字元)。而UCS-4(UTF-32)用4個位元組表示一個Unicode字元。另外一個常用的Unicode編碼方案–UTF-8用1到4個變長位元組來表示一個Unicode字元,並可以從一個簡單的轉換演算法從UTF-16直接得到。所以在使用Unicode字符集時有多種編碼方案,分別用於合適的場景。

再通俗一點地講,Unicode字符集就相當於是一本字典,裡面記載著所有字元(即影像)以及各自所對應的Unicode碼(與具體編碼方案無關),UTF-8/UTF-16/UTF-32碼就是Unicode碼經過相應的公式計算得到的並且實際儲存、傳輸的資料。

UTF-16

JVM規範中明確說明了java的char型別使用的編碼方案是UTF-16,所以先來了解下UTF-16。

Unicode的編碼空間從U+0000到U+10FFFF,共有1112064個碼位(code point)可用來對映字元,,碼位就是字元的數字形式。這部分編碼空間可以劃分為17個平面(plane),每個平面包含2^16(65536)個碼位。第一個平面稱為基本多語言平面(Basic Multilingual Plane, BMP),或稱第零平面(Plane 0)。其他平面稱為輔助平面(Supplementary Planes)。基本多語言平面內,從U+D800到U+DFFF之間的碼位區塊是永久保留不對映到Unicode字元。UTF-16就利用保留下來的0xD800-0xDFFF區段的碼位來對輔助平面的字元的碼位進行編碼。

最常用的字元都包含在BMP中,用2個位元組表示。輔助平面中的碼位,在UTF-16中被編碼為一對16位元長的碼元,稱作代理對(surrogate pair),具體方法是:

  • 將碼位減去0×10000,得到的值的範圍為20位元長的0~0xFFFFF。
  • 高位的10位元的值(值的範圍為0~0x3FF)被加上0xD800得到第一個碼元或稱作高位代理(high surrogate),值的範圍是0xD800~0xDBFF.由於高位代理比低位代理的值要小,所以為了避免混淆使用,Unicode標準現在稱高位代理為前導代理(lead surrogates)。
  • 低位的10位元的值(值的範圍也是0~0x3FF)被加上0xDC00得到第二個碼元或稱作低位代理(low surrogate),現在值的範圍是0xDC00~0xDFFF.由於低位代理比高位代理的值要大,所以為了避免混淆使用,Unicode標準現在稱低位代理為後尾代理(trail surrogates)。

例如U+10437編碼:

  • 0×10437減去0×10000,結果為0×00437,二進位制為0000 0000 0100 0011 0111。
  • 分割槽它的上10位值和下10位值(使用二進位制):0000000001 and 0000110111。
  • 新增0xD800到上值,以形成高位:0xD800 + 0×0001 = 0xD801。
  • 新增0xDC00到下值,以形成低位:0xDC00 + 0×0037 = 0xDC37。

由於前導代理、後尾代理、BMP中的有效字元的碼位,三者互不重疊,搜尋時一個字元編碼的一部分不可能與另一個字元編碼的不同部分相重疊。所以可以通過僅檢查一個碼元(構成碼位的基本單位,2個位元組)就可以判定給定字元的下一個字元的起始碼元。

java中的codepoint相關

對於一個字串物件,其內容是通過一個char陣列儲存的。char型別由2個位元組儲存,這2個位元組實際上儲存的就是UTF-16編碼下的碼元。我們使用charAt和length方法的時候,返回的實際上是一個碼元和碼元的數量,雖然一般情況下沒有問題,但是如果這個字元屬於輔助平面字元,以上2個方法便無法得到正確的結果。正確的處理方式如下:

int character = aString.codePointAt(i);
int length = aString.codePointCount(0, aString.length());

需要注意codePointAt的返回值,是int而非char,這個值就是Unicode碼。

codePointAt方法呼叫了codePointAtImpl:

static int codePointAtImpl(char[] a, int index, int limit) {
        char c1 = a[index];
        if (isHighSurrogate(c1) && ++index < limit) {
            char c2 = a[index];
            if (isLowSurrogate(c2)) {
                return toCodePoint(c1, c2);
            }
        }
        return c1;
    }

isHighSurrogate方法判斷下標字元的2個位元組是否為UTF-16中的前導代理(0xD800~0xDBFF):

public static boolean isHighSurrogate(char ch) {
        // Help VM constant-fold; MAX_HIGH_SURROGATE + 1 == MIN_LOW_SURROGATE
        return ch >= MIN_HIGH_SURROGATE && ch < (MAX_HIGH_SURROGATE + 1);
    }
public static final char MIN_HIGH_SURROGATE = '\uD800';
public static final char MAX_HIGH_SURROGATE = '\uDBFF';

然後++index,isLowSurrogate方法判斷下一個字元的2個位元組是否為後尾代理(0xDC00~0xDFFF):

public static boolean isLowSurrogate(char ch) {
        return ch >= MIN_LOW_SURROGATE && ch < (MAX_LOW_SURROGATE + 1);
    }
public static final char MIN_LOW_SURROGATE  = '\uDC00';
public static final char MAX_LOW_SURROGATE  = '\uDFFF';

toCodePoint方法將這2個碼元組裝成一個Unicode碼:

public static int toCodePoint(char high, char low) {
        // Optimized form of:
        // return ((high - MIN_HIGH_SURROGATE) << 10)
        //         + (low - MIN_LOW_SURROGATE)
        //         + MIN_SUPPLEMENTARY_CODE_POINT;
        return ((high << 10) + low) + (MIN_SUPPLEMENTARY_CODE_POINT
                                       - (MIN_HIGH_SURROGATE << 10)
                                       - MIN_LOW_SURROGATE);
    }

這個過程就是以上將一個輔助平面的Unicode碼位轉換成2個碼元的逆過程。

所以,列舉字串的正確方法:

for (int i = 0; i < aString.length();) {
	int character = aString.codePointAt(i);
	//如果是輔助平面字元,則i+2
	if (Character.isSupplementaryCodePoint(character)) i += 2;
	else ++i;
}

將codePoint轉換為char[]可呼叫Character.toChars方法,然後可進一步轉換為字串:

new String(Character.toChars(codePoint));

toChars方法所做的就是以上將Unicode碼位轉換為2個碼元的過程。

參考:

相關文章