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

swiftma發表於2016-10-08

本系列文章經補充和完善,已修訂整理成書《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| |'\n' |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控制鍵'\t'。

更常用的檢查空格的方法

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

'\t','\n',全形空格' ',和半形空格' '的返回值都為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)相比,進行相反轉換,如果數字無效,返回'\0'。例如,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處理的基礎

相關文章