1. 程式人生 > >深入學習Java中的字串,程式碼點和程式碼單元

深入學習Java中的字串,程式碼點和程式碼單元

在Java字串處理時,在使用length和charAt方法時,應該格外小心,因為length返回的是UTF-16編碼表示下的程式碼單元數量,而非我們所認為的字元的個數,charAt方法返回的是指定位置處的程式碼單元,而非我們所認為的字元。

至於為什麼都是“程式碼單元”而非字元,這和Unicode字符集的增補相關,具體的參看下面的附錄。

要想獲得字串中的字元的個數,應當使用aString.codePointCount(0, aString.length());要想獲得指定位置處的字元,使用aString.codePointAt(i);需要注意codePointAt的返回值,是int而非char。

列舉字串的正確方法:

 

for (int i = 0; i < aString.length();) {
int character = aString.codePointAt(i);
if (Character.isSupplementaryCodePoint(character)) i += 2;
else ++i;
}

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

String s(Character.toChars(codePoint));

 

附錄A:

《Java核心技術》中關於字元和字串的講解:

 

3.3.3 char型別

 

    char型別用於表示單個字元。通常用來表示字元常量。例如:'A'是編碼為65所對應的字元常量。與"A"不同,"A"是一個包含字元A的字串。Unicode編碼單元可以表示為十六進位制值,其範圍從\u0000到\uffff。例如:。\u2122表示註冊符號,\u03C0表示希臘字母π。

    除了可以採用轉義序列符\u表示Unicode程式碼單元的編碼之外,還有一些用於表示特殊字元的轉義序列符,請參看表3-3。所有這些轉義序列符都可以出現在字元常量或字串的引號內。例如,'\u2122'或"Hello\n"。轉義序列符\u還可以出現在字元常量或字串的引號之外(而其他所有轉義序列不可以)

。例如:

    public static void main(String\u005B\u005D args)

    這種形式完全符合語法規則,\u005B和\u005D是[和]的編碼。

表3-3 特殊字元的轉義序列符

轉義序列

名稱

Unicode值

\b

退格

\u0008

\t

製表

\u0009

\n

換行

\u000a

\r

回車

\u000d

\"

雙引號

\u0022

\'

單引號

\u0027

\\

反斜槓

\u005c

    要想弄清char型別,就必須瞭解Unicode編碼表。Unicode打破了傳統字元編碼方法的限制。在Unicode出現之前,已經有許多種不同的標準:美國的ASCII、西歐語言中的ISO 8859-1、俄國的KOI-8、中國的GB118030和BIG-5等等,這樣就產生了下面兩個問題:一個是對於任意給定的程式碼值,在不同的編碼方案下有可能對應不同的字母;二是採用大字符集的語言其編碼長度有可能不同。例如,有些常用的字符采用單位元組編碼,而另一些字元則需要兩個或更多個位元組。

    設計Unicode編碼的目的就是要解決這些問題。在20世紀80年代開始啟動設計工作時,人們認為兩個位元組的程式碼寬度足以能夠對世界上各種語言的所有字元進行編碼,並有足夠的空間留給未來的擴充套件。在1991年釋出了Unicode 1.0,當時僅佔用65 536個程式碼值中不到一半的部分。在設計Java時決定採用16位的Unicode字符集,這樣會比使用8位字符集的程式設計語言有很大的改進。

    十分遺憾,經過一段時間,不可避免的事情發生了。Unicode字元超過了65 536個,其主要原因是增加了大量的漢語、日語和韓國語言中的表意文字。現在,16位的char型別已經不能滿足描述所有Unicode字元的需要了。

    下面利用一些專用術語解釋一下Java語言解決這個問題的基本方法。從JDK 5.0開始,程式碼點(code point)是指與一個編碼表中的某個字元對應的程式碼值。在Unicode標準中,程式碼點採用十六進位制書寫,並加上字首U+,例如U+0041就是字母A的程式碼點。Unicode程式碼點可以分成17個程式碼級別(code plane)。第一個程式碼級別稱為基本的多語言組別(basic multilingual plane),程式碼點從U+0000到U+FFFF,其中包栝了經典的Unicode程式碼。其餘的16個附加級別,程式碼點從U+10000到U+10FFFF,其中包栝了一些輔助字元(supplementarycharacter)。

    UTF-16編碼採用不同長度的編碼表示所有Unicode程式碼點。在基本的多語言級別中,每個字元用16位表示,通常被稱為程式碼單元(code unit);而輔助字符采用對連續的程式碼單元進行編碼。這樣構成的編碼值一定落入基本的多語言級別中空閒的2048位元組內,通常被稱為替代區域(surrogate area)[U+D800~U+DBFF用於第一個程式碼單元,U+DC00〜U+DFFF用於第二個程式碼單元]。這樣設計十分巧妙,我們可以從中迅速地知道一個程式碼單元是一個字元的編碼,還是一個輔助字元的第一或第二部分。例如,對於整數集合的數學符號,它的程式碼點是U+1D568,並且是用兩個程式碼單元U+D835和U+DD68編碼的(存關編碼演算法的描述請參看http://en.wikipe-dia.org/wiki/UTF-16)。

    在Java中,char型別用UTF-16編碼描述一個程式碼單元。

    我們強烈建議不要在程式中使用char型別,除非確實需要對UTF-16程式碼單元進行操作。最好將需要處理的字串用抽象資料型別表示(有關這方面的內容將在稍後討論)。

 

3.6.6 程式碼點與程式碼單元

 

    Java字串由char序列組成。從前面已經看到,字元資料型別是一個採用UTF-16編碼表示Unicode程式碼點的程式碼單元。大多數的常用Unicode字元使用一個程式碼單元就可以表示,而輔助字元需要一對程式碼單元表示。

    length方法將返回採用UTF-16編碼表示的給定字串所需要的程式碼單元數量。例如:

        Stringgreeting = "Hello";

        int n = greeting.length();// is 5

    要想得到實際的長度,即程式碼點數量,可以呼叫:

        int cpCount =greeting.codePointCount(0, greeting.length());

    呼叫s.charAt(n)將返回位置n的程式碼單元,n介於0~s.length()-1之間。例如:

        char first =greeting.charAt(0); // first is 'H'

        char last =greeting.charAt(4); // last is 'o'

    要想得到第i個程式碼點,應該使用下列語句

        int index =greeting.offsetByCodePoints(0, i);

        int cp =greeting.codePointAt(index);

    註釋:Java以獨特的風格對字串中的程式碼單元計數:字串中的第一個程式碼單元位置為0。這種習憤起源於C,這樣處理主要出於技術上的原因。具體理由似乎已經淡忘,而麻煩卻保留了下來。但是,許多程式設計師習慣於這種風格,因而Java設計者也就將其保留了下來。

    為什麼會對程式碼單元如此大驚小怪?請考慮下列語句:

    Ƶis the set of integers

    使用UTF-16編碼表示Ƶ需要兩個程式碼單元。呼叫char ch =sentence.charAt(1);返回的不是空格,而是第二個程式碼單元Z。為了避免這種情況的發生,請不要使用char型別。這太低階了。

    如果想要遍歷一個字串,並且依次査看每一個程式碼點,可以使用下列語句:

        int cp =sentence.codePointAt(i);

        if (Character.isSupplementaryCodePoint(cp))i += 2;

        else i++;

    非常幸運,codePointAt方法能夠辨別一個程式碼單元是輔助字元的第一部分還是第二部分,並能夠返回正確的結果。也就是說,可以使用下列語句實現回退操作:

        i--;

        int cp =sentence.codePointAt(i);

        if (Character.isSupplementaryCodePoint(cp))i--;

背景

    Unicode最初設計是作為一種固定寬度的16位字元編碼。在Java程式語言中,基本資料型別char初衷是通過提供一種簡單的、能夠包含任何字元的資料型別來充分利用這種設計的優點。不過,現在看來,16位編碼的所有65 536個字元並不能完全表示全世界所有正在使用或曾經使用的字元。於是,Unicode標準已擴充套件到包含多達1 112 064個字元。那些超出原來的16位限制的字元被稱作增補字元。Unicode標準2.0版是第一個包含啟用增補字元設計的版本,但是,直到3.1版才收入第一批增補字符集。由於J2SE的5.0版必須支援Unicode標準4.0版,因此它必須支援增補字元。

    對增補字元的支援也可能會成為東亞市場的一個普遍商業要求。政府應用程式會需要這些增補字元,以正確表示一些包含罕見中文字元的姓名。出版應用程式可能會需要這些增補字元,以表示所有的古代字元和變體字元。中國政府要求支援GB18030(一種對整個Unicode字符集進行編碼的字元編碼標準),因此,如果是Unicode 3.1版或更新版本,則將包括增補字元。臺灣標準CNS-11643包含的許多字元在Unicode 3.1中列為增補字元。香港政府定義了一種針對粵語的字符集,其中的一些字元是Unicode中的增補字元。最後,日本的一些供應商正計劃利用增補字元空間中大量的專用空間收入50 000多個日文漢字字元變體,以便從其專有系統遷移至基於Java平臺的解決方案。

    因此,Java平臺不僅需要支援增補字元,而且必須使應用程式能夠方便地做到這一點。由於增補字元打破了Java程式語言的基礎設計構想,而且可能要求對程式設計模型進行根本性的修改,因此,Java Community Process召集了一個專家組,以期找到一個適當的解決方案。該小組被稱為JSR-204專家組,使用Unicode增補字元支援的Java技術規範請求的編號。從技術上來說,該專家組的決定僅適用於J2SE平臺,但是由於Java2平臺企業版(J2EE)處於J2SE平臺的最上層,因此它可以直接受益,我們期望Java2平臺袖珍版(J2ME)的配置也採用相同的設計方法。

    不過,在瞭解JSR-204專家組確定的解決方案之前,我們需要先理解一些術語。

程式碼點、字元編碼方案、UTF-16:這些是指什麼?

    不幸的是,引入增補字元使字元模型變得更加複雜了。在過去,我們可以簡單地說“字元”,在一個基於Unicode的環境(例如Java平臺)中,假定字元有16位,而現在我們需要更多的術語。我們會盡量介紹得相對簡單一些—如需瞭解所有詳細的討論資訊,您可以閱讀Unicode標準第2章或Unicode技術報告17“字元編碼模型”。Unicode專業人士可略過所有介紹直接參閱本部分中的最後定義。

    字元是抽象的最小文字單位。它沒有固定的形狀(可能是一個字形),而且沒有值。“A”是一個字元,“€”(德國、法國和許多其他歐洲國家通用貨幣的標誌)也是一個字元。

    字符集是字元的集合。例如,漢字字元是中國人最先發明的字元,在中文、日文、韓文和越南文的書寫中使用。

    編碼字符集是一個字符集,它為每一個字元分配一個唯一數字。Unicode標準的核心是一個編碼字符集,字母“A”的編碼為0041和字元“€”的編碼為20AC。Unicode標準始終使用十六進位制數字,而且在書寫時在前面加上字首“U+”,所以“A”的編碼書寫為“U+0041”。

    程式碼點是指可用於編碼字符集的數字。編碼字符集定義一個有效的程式碼點範圍,但是並不一定將字元分配給所有這些程式碼點。有效的Unicode程式碼點範圍是U+0000至U+10FFFF。Unicode 4.0將字元分配給一百多萬個程式碼點中的96 382程式碼點。

    增補字元是程式碼點在U+10000至U+10FFFF範圍之間的字元,也就是那些使用原始的Unicode的16位設計無法表示的字元。從U+0000至U+FFFF之間的字符集有時候被稱為基本多語言面(BMP)。因此,每一個Unicode字元要麼屬於BMP,要麼屬於增補字元。

    字元編碼方案是從一個或多個編碼字符集到一個或多個固定寬度程式碼單元序列的對映。最常用的程式碼單元是位元組,但是16位或32位整數也可用於內部處理。UTF-32、UTF-16和UTF-8是Unicode標準的編碼字符集的字元編碼方案。

    UTF-32即將每一個Unicode程式碼點表示為相同值的32位整數。很明顯,它是內部處理最方便的表達方式,但是,如果作為一般字串表達方式,則要消耗更多的記憶體。

    UTF-16使用一個或兩個未分配的16位程式碼單元的序列對Unicode程式碼點進行編碼。值U+0000至U+FFFF編碼為一個相同值的16位單元。增補字元編碼為兩個程式碼單元,第一個單元來自於高代理範圍(U+D800至U+DBFF),第二個單元來自於低代理範圍(U+DC00至U+DFFF)。這在概念上可能看起來類似於多位元組編碼,但是其中有一個重要區別:值U+D800至U+DFFF保留用於UTF-16;沒有這些值分配字元作為程式碼點。這意味著,對於一個字串中的每個單獨的程式碼單元,軟體可以識別是否該程式碼單元表示某個單單元字元,或者是否該程式碼單元是某個雙單元字元的第一個或第二單元。這相當於某些傳統的多位元組字元編碼來說是一個顯著的改進,在傳統的多位元組字元編碼中,位元組值0x41既可能表示字母“A”,也可能是一個雙位元組字元的第二個位元組。

    UTF-8使用一至四個位元組的序列對編碼Unicode程式碼點進行編碼。U+0000至U+007F使用一個位元組編碼,U+0080至U+07FF使用兩個位元組,U+0800至U+FFFF使用三個位元組,而U+10000至U+10FFFF使用四個位元組。UTF-8設計原理為:位元組值0x00至0x7F始終表示程式碼點U+0000至U+007F(Basic Latin字元子集,它對應ASCII字符集)。這些位元組值永遠不會表示其他程式碼點,這一特性使UTF-8可以很方便地在軟體中將特殊的含義賦予某些ASCII字元。