計算機程式的思維邏輯 (28) – 剖析包裝類 (下) – 理解Java Unicode處理的基礎

swiftma發表於2019-01-03

本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結

計算機程式的思維邏輯 (28) – 剖析包裝類 (下) – 理解Java Unicode處理的基礎

本節探討Character類,它的基本用法我們在包裝類第一節已經介紹了,本節不再贅述。Character類除了封裝了一個char外,還有什麼可介紹的呢?它有很多靜態方法,封裝了Unicode字元級別的各種操作,是Java文字處理的基礎,注意不是char級別,Unicode字元並不等同於char,本節詳細介紹這些方法以及相關的Unicode知識。

在介紹這些方法之前,我們需要回顧一下字元在Java中的表示方法,我們在第六節第七節第八節介紹過編碼、Unicode、char等知識,我們先簡要回顧一下。

Unicode基礎

Unicode給世界上每個字元分配了一個編號,編號範圍從0x000000到0x10FFFF。編號範圍在0x0000到0xFFFF之間的字元,為常用字符集,稱BMP(Basic Multilingual Plane)字元。編號範圍在0x10000到0x10FFFF之間的字元叫做增補字元(supplementary character)

Unicode主要規定了編號,但沒有規定如何把編號對映為二進位制,UTF-16是一種編碼方式,或者叫對映方式,它將編號對映為兩個或四個位元組,對BMP字元,它直接用兩個位元組表示,對於增補字元,使用四個位元組,前兩個位元組叫高代理項(high surrogate),範圍從0xD800到0xDBFF,後兩個位元組叫低代理項(low surrogate),範圍從0xDC00到0xDFFF,UTF-16定義了一個公式,可以將編號與四位元組表示進行相互轉換。

Java內部採用UTF-16編碼,char表示一個字元,但只能表示BMP中的字元,對於增補字元,需要使用兩個char表示,一個表示高代理項,一個表示低代理項。

使用int可以表示任意一個Unicode字元,低21位表示Unicode編號,高11位設為0。整數編號在Unicode中一般稱為程式碼點(Code Point),表示一個Unicode字元,與之相對,還有一個詞程式碼單元(Code Unit)表示一個char。

Character類中有很多相關靜態方法,讓我們來看一下。

檢查code point和char

判斷一個int是不是一個有效的程式碼單元:

public static boolean isValidCodePoint(int codePoint) 
複製程式碼

小於等於0x10FFFF的為有效,大於的為無效。

判斷一個int是不是BMP字元:

public static boolean isBmpCodePoint(int codePoint) 
複製程式碼

小於等於0xFFFF的為BMP字元,大於的不是。

判斷一個int是不是增補字元:

public static boolean isSupplementaryCodePoint(int codePoint)
複製程式碼

0x010000和0X10FFFF之間的為增補字元。

判斷char是否是高代理項:

public static boolean isHighSurrogate(char ch) 
複製程式碼

0xD800到0xDBFF為高代理項。

判斷char是否為低代理項:

public static boolean isLowSurrogate(char ch) 
複製程式碼

0xDC00到0xDFFF為低代理項。

判斷char是否為代理項:

public static boolean isSurrogate(char ch) 
複製程式碼

char為低代理項或高代理項,則返回true。

判斷兩個字元high和low是否分別為高代理項和低代理項:

public static boolean isSurrogatePair(char high, char low) 
複製程式碼

判斷一個程式碼單元由幾個char組成:

public static int charCount(int codePoint) 
複製程式碼

增補字元返回2,BMP字元返回1。

code point與char的轉換

除了簡單的檢查外,Character類中還有很多方法,進行code point與char的相互轉換。

根據高代理項high和低代理項low生成程式碼單元:

public static int toCodePoint(char high, char low)
複製程式碼

這個轉換有個公式,這個方法封裝了這個公式。

根據程式碼單元生成char陣列,即UTF-16表示:

public static char[] toChars(int codePoint) 
複製程式碼

如果code point為BMP字元,則返回的char陣列長度為1,如果為增補字元,長度為2,char[0]為高代理項,char[1]為低代理項。

將程式碼單元轉換為char陣列:

public static int toChars(int codePoint, char[] dst, int dstIndex) 
複製程式碼

與上面方法類似,只是結果存入指定陣列dst的指定位置index。

對增補字元code point,生成高代理項和低代理項:

public static char lowSurrogate(int codePoint)
public static char highSurrogate(int codePoint) 
複製程式碼

按code point處理char陣列或序列

Character包含若干方法,以方便按照code point來處理char陣列或序列。

返回char陣列a中從offset開始count個char包含的code point個數:

public static int codePointCount(char[] a, int offset, int count) 
複製程式碼

比如說,如下程式碼輸出為2,char個數為3,但code point為2。

char[] chs = new char[3];
chs[0] = `馬`;
Character.toChars(0x1FFFF, chs, 1);
System.out.println(Character.codePointCount(chs, 0, 3));
複製程式碼

除了接受char陣列,還有一個過載的方法接受字元序列CharSequence:

public static int codePointCount(CharSequence seq, int beginIndex, int endIndex)
複製程式碼

CharSequence是一個介面,它的定義如下所示:

public interface CharSequence {
    int length();
    char charAt(int index);
    CharSequence subSequence(int start, int end);
    public String toString();
}
複製程式碼

它與一個char陣列是類似的,有length方法,有charAt方法根據索引獲取字元,String類就實現了該介面。

返回char陣列或序列中指定索引位置的code point

public static int codePointAt(char[] a, int index)
public static int codePointAt(char[] a, int index, int limit)
public static int codePointAt(CharSequence seq, int index) 
複製程式碼

如果指定索引位置為高代理項,下一個位置為低代理項,則返回兩項組成的code point,檢查下一個位置時,下一個位置要小於limit,沒傳limit時,預設為a.length。

返回char陣列或序列中指定索引位置之前的code point:

public static int codePointBefore(char[] a, int index)
public static int codePointBefore(char[] a, int index, int start)
public static int codePointBefore(CharSequence seq, int index)
複製程式碼

與codePointAt不同,codePoint是往後找,codePointBefore是往前找,如果指定位置為低代理項,且前一個位置為高代理項,則返回兩項組成的code point,檢查前一個位置時,前一個位置要大於等於start,沒傳start時,預設為0。

根據code point偏移數計算char索引:

public static int offsetByCodePoints(char[] a, int start, int count,
                                         int index, int codePointOffset)
public static int offsetByCodePoints(CharSequence seq, int index,
                                         int codePointOffset)
複製程式碼

如果字元陣列或序列中沒有增補字元,返回值為index+codePointOffset,如果有增補字元,則會將codePointOffset看做code point偏移,轉換為字元偏移,start和count取字元陣列的子陣列。

比如,我們看如下程式碼:

char[] chs = new char[3];
Character.toChars(0x1FFFF, chs, 1);
System.out.println(Character.offsetByCodePoints(chs, 0, 3, 1, 1));
複製程式碼

輸出結果為3,index和codePointOffset都為1,但第二個字元為增補字元,一個code point偏移是兩個char偏移,所以結果為3。

字元屬性

我們之前說,Unicode主要是給每個字元分配了一個編號,其實,除了分配編號之外,還分配了一些屬性,Character類封裝了對Unicode字元屬性的檢查和操作,我們來看一些主要的屬性。

獲取字元型別(general category):

public static int getType(int codePoint)
public static int getType(char ch)
複製程式碼

Unicode給每個字元分配了一個型別,這個型別是非常重要的,很多其他檢查和操作都是基於這個型別的。

getType方法的引數可以是int型別的code point,也可以是char型別,char只能處理BMP字元,而int可以處理所有字元,Character類中很多方法都是既可以接受int,也可以接受char,後續只列出int型別的方法。

返回值是int,表示型別,Character類中定義了很多靜態常量表示這些型別,下表列出了一些字元,type值,以及Character類中常量的名稱:

|字元 |type值 | 常量名稱
| ————- |:————-:|
|`A` |1 |UPPERCASE_LETTER|
|`a` |2 |LOWERCASE_LETTER|
|`馬` |5 |OTHER_LETTER|
|`1` |9 |DECIMAL_DIGIT_NUMBER|
|` ` |12 |SPACE_SEPARATOR|
|`
` |15 |CONTROL|
|`-` |20 |DASH_PUNCTUATION|
|`{` |21 |START_PUNCTUATION|
|`_` |23 |CONNECTOR_PUNCTUATION|
|`&` |24 |OTHER_PUNCTUATION|
|`<` |25 |MATH_SYMBOL|
|`$` |26 |CURRENCY_SYMBOL|

檢查字元是否在Unicode中被定義:

public static boolean isDefined(int codePoint) 
複製程式碼

每個被定義的字元,其getType()返回值都不為0,如果返回值為0,表示無定義。注意與isValidCodePoint的區別,後者只要數字不大於0x10FFFF都返回true。

檢查字元是否為數字:

public static boolean isDigit(int codePoint)
複製程式碼

getType()返回值為DECIMAL_DIGIT_NUMBER的字元為數字,需要注意的是,不光字元`0`,`1`,…`9`是數字,中文全形字元的0到9,即`0`,`1`,`9`也是數字。比如說:

char ch = `9`; //中文全形數字
System.out.println((int)ch+","+Character.isDigit(ch));
複製程式碼

輸出為:

65305,true
複製程式碼

全形字元的9,Unicode編號為65305,它也是數字。

檢查是否為字母(Letter):

public static boolean isLetter(int codePoint)
複製程式碼

如果getType()的返回值為下列之一,則為Letter:

UPPERCASE_LETTER
LOWERCASE_LETTER
TITLECASE_LETTER
MODIFIER_LETTER
OTHER_LETTER
複製程式碼

除了TITLECASE_LETTER和MODIFIER_LETTER,其他我們上面已經看到過了,而這兩個平時碰到的也比較少,就不介紹了。

檢查是否為字母或數字

public static boolean isLetterOrDigit(int codePoint)
複製程式碼

只要其中之一返回true就返回true。

檢查是否為字母(Alphabetic)

public static boolean isAlphabetic(int codePoint)
複製程式碼

這也是檢查是否為字母,與isLetter的區別是,isLetter返回true時,isAlphabetic也必然返回true,此外,getType()值為LETTER_NUMBER時,isAlphabetic也返回true,而isLetter返回false。Letter_NUMBER中常見的字元有羅馬數字字元,如:`Ⅰ`,`Ⅱ`,`Ⅲ`,`Ⅳ`。

檢查是否為空格字元

public static boolean isSpaceChar(int codePoint)
複製程式碼

getType()值為SPACE_SEPARATOR,LINE_SEPARATOR和PARAGRAPH_SEPARATOR時,返回true。這個方法其實並不常用,因為它只能嚴格匹配空格字元本身,不能匹配實際產生空格效果的字元,如tab控制鍵` `。

更常用的檢查空格的方法

public static boolean isWhitespace(int codePoint) 
複製程式碼

` `,`
`,全形空格` `,和半形空格` `的返回值都為true。

檢查是否為小寫字元

public static boolean isLowerCase(int codePoint) 
複製程式碼

常見的主要就是小寫英文字母a到z。

檢查是否為大寫字元

public static boolean isUpperCase(int codePoint)
複製程式碼

常見的主要就是大寫英文字母A到Z。

檢查是否為表意象形文字

public static boolean isIdeographic(int codePoint) 
複製程式碼

大部分中文都返回為true。

檢查是否為ISO 8859-1編碼中的控制字元

public static boolean isISOControl(int codePoint) 
複製程式碼

我們在第6節介紹過,0到31,127到159表示控制字元。

檢查是否可作為Java標示符的第一個字元

public static boolean isJavaIdentifierStart(int codePoint) 
複製程式碼

Java標示符是Java中的變數名、函式名、類名等,字母(Alphabetic),美元符號($),下劃線(_)可作為Java標示符的第一個字元,但數字字元不可以。

檢查是否可作為Java標示符的中間字元

public static boolean isJavaIdentifierPart(int codePoint) 
複製程式碼

相比isJavaIdentifierStart,主要多了數字字元,中間可以有數字。

檢查是否為映象(mirrowed)字元

public static boolean isMirrored(int codePoint)
複製程式碼

常見映象字元有( ) { } < > [ ],都有對應的映象。

字元轉換

Unicode除了規定字元屬性外,對有大小寫對應的字元,還規定了其對應的大小寫,對有數值含義的字元,也規定了其數值。

我們先來看大小寫,Character有兩個靜態方法,對字元進行大小寫轉換

public static int toLowerCase(int codePoint)
public static int toUpperCase(int codePoint)
複製程式碼

這兩個方法主要針對英文字元a-z和A-Z, 例如:toLowerCase(`A`)返回`a`,toUpperCase(`z`)返回`Z`。

返回一個字元表示的數值

public static int getNumericValue(int codePoint)  
複製程式碼

字元`0`到`9`返回數值0到9,對於字元a到z,無論是小寫字元還是大寫字元,無論是普通英文還是中文全形,數值結果都是10到35,例如,如下程式碼的輸出結果是一樣的,都是10。

System.out.println(Character.getNumericValue(`A`)); //全形大寫A
System.out.println(Character.getNumericValue(`A`));
System.out.println(Character.getNumericValue(`a`)); //全形小寫a
System.out.println(Character.getNumericValue(`a`));
複製程式碼

返回按給定進製表示的數值:

public static int digit(int codePoint, int radix) 
複製程式碼

radix表示進位制,常見的有2/8/10/16進位制,計算方式與getNumericValue類似,只是會檢查有效性,數值需要小於radix,如果無效,返回-1,例如:

digit(`F`,16)返回15,是有效的,但digit(`G`,16)就無效,返回-1。

返回給定數值的字元形式

public static char forDigit(int digit, int radix) 
複製程式碼

與digit(int codePoint, int radix)相比,進行相反轉換,如果數字無效,返回` `。例如,Character.forDigit(15, 16)返回`F`。

與Integer類似,Character也有按位元組翻轉

public static char reverseBytes(char ch)
複製程式碼

例如,翻轉字元0x1234:

System.out.println(Integer.toHexString(
                Character.reverseBytes((char)0x1234)));
複製程式碼

輸出為3412。

小結

本節詳細介紹了Characer類以及相關的Unicode知識,Character類在Unicode字元級別,而非char級別,封裝了字元的各種操作,通過將字元處理的細節交給Character類,其他類就可以在更高的層次上處理文字了。

至此,關於包裝類我們就介紹完了。下一節,讓我們在Character的基礎上,進一步探索字串類String。


未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。

計算機程式的思維邏輯 (28) – 剖析包裝類 (下) – 理解Java Unicode處理的基礎

相關文章