在Java基本类型对应的包装类型中,最为复杂的就是字符类型和字符串类型了。本篇在讲解字符类型之前,必须要讲解一下Unicode编码方面的知识,否则不好理解源代码。
(1)编码字符集
编码字符集是一个字符集,它为每一个字符分配一个唯一数字。Unicode 标准的核心是一个编码字符集,字母“A”的编码为0041和字符“€”的编码为20AC。Unicode标准始终使用十六进制数字,而且在书写时在前面加上前缀“U+”,所以“A”的编码书写为“U+0041”。
(2)代码点code point和代码单元
代码点是指可用于编码字符集的数字。编码字符集定义一个有效的代码点范围,但是并不一定将字符分配给所有这些代码点。有效的 Unicode代码点范围是 U+0000 至 U+10FFFF
代码单元可以理解为字符编码的一个基本单元,最常用的代码单元是字节(即8位),但是16位和32位整数也可以用于内部处理。
(3)增补字符
16 位编码的所有 65,536 个字符并不能完全表示全世界所有正在使用或曾经使用的字符。于是,Unicode 标准已扩展到包含多达 1,112,064 个字符。那些超出原来的16 位限制的字符被称作增补字符。
Java的char类型是固定16bits的。代码点在U+0000 — U+FFFF之内到是可以用一个char完整的表示出一个字符。但代码点在U+FFFF之外的,一个char无论如何无法表示一个完整字符。这样用char类型来获取字符串中的那些代码点在U+FFFF之外的字符就会出现问题。
因此,Java 平台不仅需要支持增补字符,而且必须使应用程序能够方便地做到这一点。Java Community Process 召集了一个专家组,以期找到一个适当的解决方案。该小组被称为JSR-204专家组,使用Unicode 增补字符支持的Java技术规范请求的编号。
增补字符是代码点在 U+10000 至 U+10FFFF 范围之间的字符,也就是那些使用原始的 Unicode 的 16 位设计无法表示的字符。从 U+0000 至 U+FFFF 之间的字符集有时候被称为基本多语言面 (BMP UBasic Multilingual Plane )。因此,每一个 Unicode 字符要么属于 BMP,要么属于增补字符。
UTF-32 即将每一个 Unicode 代码点表示为相同值的32位整数。很明显,它是内部处理最方便的表达方式,但是,如果作为一般字符串表达方式,则要消耗更多的内存。
UTF-16 使用一个或两个未分配的16位代码单元的序列对 Unicode 代码点进行编码。假设U是一个代码点,也就是Unicode编码表中一个字符所对应的Unicode值。
(1) 如果在BMP级别中,那么16bits(一个代码单元)就足够表示出字符的Unicode值。
(2) 如果U+10FFFF>U>=U+10000,也就是处于增补字符级别中。UTF-16用2个16位来表示出了,并且正好将每个16位都控制在替代区域U+D800-U+DFFF(其中\uD800-\uDBFF为高代理项 范围,\uDC00- \uDFFF为低代理项 范围) 中。
下面来看一下Java是如何处理这些增补字符的。
分别初始化2个16位无符号的整数 —— W1和W2。其中W1=110110yyyyyyyyyy(0xD800-0xDBFF),W2 = 110111xxxxxxxxxx(0xDC00-OxDFFF)。然后,将Unicode的高10位分配给W1的低10位,将Unicode 的低10位分配给W2的低10位。这样就可以将20bits的代码点U拆成两个16bits的代码单元。而且这两个代码点正好落在替代区域U+D800-U+DFFF中。
举个例子:代码点U+1D56B(使用4个字节表示的代码点)
0x1D56B= 0001 1101 01-01 0110 1011
将0x1D56B的高10位0001 1101 01分配给W1的低10位组合成110110 0001 1101 01=0xD875
将0x1D56B的低10位01 0110 1011分配给W2的低10位组合成110111 01 0110 1011=0xDD6B
这样代码点U+1D56B采用UTF-16编码方式,用2个连续的代码单元U+D875和U+DD68表示出了。
在Charachter类中定义相关的变量如下:
public static final char MIN_HIGH_SURROGATE = '\uD800'; public static final char MAX_HIGH_SURROGATE = '\uDBFF'; public static final char MIN_LOW_SURROGATE = '\uDC00'; public static final char MAX_LOW_SURROGATE = '\uDFFF'; public static final char MIN_SURROGATE = MIN_HIGH_SURROGATE; // min_surrogate '\uD800' public static final char MAX_SURROGATE = MAX_LOW_SURROGATE; // max_surrogate '\uDFFF' public static final int MIN_SUPPLEMENTARY_CODE_POINT = 0x010000; public static final int MIN_CODE_POINT = 0x000000; public static final int MAX_CODE_POINT = 0X10FFFF;
利用如上定义的一些常量,就可以对传入的代码点或字符进行判断了,如下:
// 最大的数为:10FFFF,也就是说以10开头的一定为Unicode编码 public static boolean isValidCodePoint(int codePoint) { // Optimized form of: // codePoint >= MIN_CODE_POINT && codePoint <= MAX_CODE_POINT int plane = codePoint >>> 16; return plane < ((MAX_CODE_POINT + 1) >>> 16); } public static boolean isBmpCodePoint(int codePoint) { // 是否为BMP代码点 return codePoint >>> 16 == 0;// // Optimized form of: // codePoint >= MIN_VALUE && codePoint <= MAX_VALUE // We consistently use logical shift (>>>) to facilitate(促进) // additional runtime optimizations. } public static boolean isSupplementaryCodePoint(int codePoint) { // 是否为增补字符代码点 return codePoint >= MIN_SUPPLEMENTARY_CODE_POINT && codePoint < MAX_CODE_POINT + 1; } 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 boolean isLowSurrogate(char ch) {// 是否为低代理项 return ch >= MIN_LOW_SURROGATE && ch < (MAX_LOW_SURROGATE + 1); } public static boolean isSurrogate(char ch) { // 是否为代理项 return ch >= MIN_SURROGATE && ch < (MAX_SURROGATE + 1); }
如上只是介绍了一些基本的方法,这种类似的方法还很多,如
public static boolean isSurrogatePair(char high, char low) {
return isHighSurrogate(high) && isLowSurrogate(low);
}
// 计算一个字符需要用几个单元块来表示
public static int charCount(int codePoint) {
return codePoint >= MIN_SUPPLEMENTARY_CODE_POINT ? 2 : 1;
}
// 根据高和低代理项计算增补字符代码点
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);//???
}
public static int codePointAt(CharSequence seq, int index) {
char c1 = seq.charAt(index++);
if (isHighSurrogate(c1)) {
if (index < seq.length()) {
char c2 = seq.charAt(index);
if (isLowSurrogate(c2)) {
return toCodePoint(c1, c2);
}
}
}
return c1;
}
然后使用字符常量对一些字符和代码点进行了判断,如判断是否为合法的高位和低位增补字符、Unicode代码点需要几个单元块来存储、从字符序列中取出增补字符的代码点等。
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 字符。以下是Unicode和UTF-8之间的转换关系表:
U-00000000 - U-0000007F: 0xxxxxxx U-00000080 - U-000007FF: 110xxxxx 10xxxxxx U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxx x 10xxxxxx U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx |
可以看到:
(1)如果一个字节以10开头,一定不是首字节,需要向前查找。
(2)在一个首字节中,如果以0开头,表示是一个ASCII字符,而开头的连续的1的个数也表示了这个字符的字节数。如1110xxxx表示这个字符由三个字节组成。
下面来看一个使用各种编码对字符进行编码的例子,如下:
在使用源码相关的API时,需要注意(摘自Java 7 API说明文档):
char
value cannot support supplementary characters. They treat char
values from the surrogate ranges as undefined characters. For example, Character.isLetter('\uD840')
returns false
, even though this specific value if followed by any low-surrogate value in a string would represent a letter.int
value support all Unicode characters, including supplementary characters. For example, Character.isLetter(0x2F81A)
returns true
because the code point value represents a letter (a CJK ideograph).我们在编写Java代码的过程中,如果要定义一个Java合法的标识符或者需要将一个字符串转换为数值,那么Java是怎么来确定标识符合法或者字符串合法的呢?这就要涉及到字符属性的概念了。先来看CharacterData类的源代码,如下:
abstract class CharacterData { abstract int getProperties(int ch); abstract int getType(int ch); abstract boolean isWhitespace(int ch); abstract boolean isMirrored(int ch); abstract boolean isJavaIdentifierStart(int ch); abstract boolean isJavaIdentifierPart(int ch); abstract boolean isUnicodeIdentifierStart(int ch); abstract boolean isUnicodeIdentifierPart(int ch); abstract boolean isIdentifierIgnorable(int ch); abstract int toLowerCase(int ch); abstract int toUpperCase(int ch); abstract int toTitleCase(int ch); abstract int digit(int ch, int radix); abstract int getNumericValue(int ch); abstract byte getDirectionality(int ch); //need to implement for JSR204 int toUpperCaseEx(int ch) { return toUpperCase(ch); } char[] toUpperCaseCharArray(int ch) { return null; } boolean isOtherLowercase(int ch) { return false; } boolean isOtherUppercase(int ch) { return false; } boolean isOtherAlphabetic(int ch) { return false; } boolean isIdeographic(int ch) { return false; } // Character <= 0xff (basic latin) is handled by internal fast-path // to avoid initializing large tables. // Note: performance of this "fast-path" code may be sub-optimal // in negative cases for some accessors due to complicated ranges. // Should revisit after optimization of table initialization. static final CharacterData of(int ch) { if (ch >>> 8 == 0) { // fast-path return CharacterDataLatin1.instance; // 使用Latin编码所能表示的字符 } else { switch(ch >>> 16) { //plane 00-16 case(0): return CharacterData00.instance;// 是由两个字节表示的字符,以下全部为增补字符 case(1): return CharacterData01.instance; case(2): return CharacterData02.instance; case(14): return CharacterData0E.instance; case(15): // Private Use case(16): // Private Use return CharacterDataPrivateUse.instance; default: return CharacterDataUndefined.instance; } } } }
如上是一个抽象类,这个抽象类中定义了许多判断字符属性的抽象方法,这些方法的具体实现都在Character0X类中,这些类都继承了这个抽象类。其实Character类中有许多对应的方法,并不是Character类继承了这个抽象类,而是通过调用抽象类的具体实现类方法来实现字符属性的判断。我们并不关心这个字符由哪个具体类中的方法来判断,如果以后还增加了一些增补字符,那么只需要实现抽象类并且稍加修改of()方法即可。这就是使用策略模式的好处。
由于抽象类中定义的抽象方法都不是公开的,所以我们只好利用Character类中提供的方法进行字符属性的判断,如下:
System.out.println(Character.getDirectionality('('));//13 System.out.println(Character.getDirectionality('{'));//13 System.out.println(Character.isMirrored('['));//true System.out.println(Character.isMirrored(']'));//true System.out.println(Character.isMirrored('c'));//false System.out.println(Character.isMirrored('&'));//false System.out.println(Character.getNumericValue('z'));//35 System.out.println(Character.getNumericValue('中'));//-1 System.out.println(Character.toUpperCase('A'));//A System.out.println(Character.digit('c', 16));//12 c在十六进制中代表12 System.out.println(Character.isJavaIdentifierStart('$'));//true 可以做为Java一个标识符的开头 System.out.println(Character.isJavaIdentifierStart('&'));//false System.out.println(Character.isJavaIdentifierStart('4'));//true 关键字不允许以数字开头 System.out.println(Character.isJavaIdentifierStart('解'));//true System.out.println(Character.isJavaIdentifierPart(' '));//false 可以看到一个Java标识符的定义是不允许有空格的 System.out.println(Character.isJavaIdentifierPart('4'));//true 关键字不允许以数字开头 System.out.println(Character.isJavaIdentifierPart('解'));//true
1 bit mirrored property 4 bits directionality property 9 bits signed offset used for converting case(有符号偏移,用于转换) 1 bit if 1, adding the signed offset converts the character to lowercase 1 bit if 1, subtracting(减去) the signed offset converts the character to uppercase 1 bit if 1, this character has a titlecase equivalent (possibly itself) 3 bits 0 may not be part of an identifier 1 ignorable control; may continue a Unicode identifier or Java identifier 2 may continue a Java identifier but not a Unicode identifier (unused) 3 may continue a Unicode identifier or Java identifier 4 is a Java whitespace character 5 may start or continue a Java identifier; may continue but not start a Unicode identifier (underscores) 6 may start or continue a Java identifier but not a Unicode identifier ($) 7 may start or continue a Unicode identifier or Java identifier Thus:(因此) 5, 6, 7 may start a Java identifier(Java标识符) 1, 2, 3, 5, 6, 7 may continue a Java identifier 7 may start a Unicode identifier 1, 3, 5, 7 may continue a Unicode identifier 1 is ignorable within an identifier 4 is Java whitespace 2 bits 0 this character has no numeric property 1 adding the digit offset to the character code and then masking with 0x1F will produce the desired numeric value 2 this character has a "strange" numeric value 3 a Java supradecimal digit: adding the digit offset to the character code, then masking with 0x1F, then adding 10 will produce the desired numeric value 5 bits digit offset 5 bits character type当我们传入一个'0'字符时,实际上通过
static final CharacterData of(int ch)
方法判断后,最终会调用CharacterDataLatin1类中对应的方法去处理。Latin1中的所有字符都会在CharacterDataLatin1类中进行处理,而对于其他的字符,可能会在不同的CharacterDataXX.java类中进行处理,如可以用两个字节表示的字符用CharacterData01类处理,而一些增补字符会用其他的一些类进行处理。
读者如果不是很理解的话,可以查看一下Unicode字符编码分布表,你就会明白CharacterData这个方法了。
具体来看一下ChatacterDataLation1的实现代码:
static final int A[] = new int[256];// 2^8=256 static final String A_DATA = "\u4800\u100F\u4800\u100F\u4800\u100F\u4800\u100F\u4800\u100F\u4800\u100F\u4800"+ "\u100F\u4800\u100F\u4800\u100F\u5800\u400F\u5000\u400F\u5800\u400F\u6000\u400F"+ // 省略.... "\u061D\u7002"; static { { // THIS CODE WAS AUTOMATICALLY CREATED BY GenerateCharacter: char[] data = A_DATA.toCharArray(); assert (data.length == (256 * 2)); int i = 0, j = 0; while (i < (256 * 2)) { int entry = data[i++] << 16; A[j++] = entry | data[i++]; } } }首先还需要说一下Latin1编码。Latin1是ISO-8859-1的别名,有些环境下写作Latin-1。
既然能够得到每个字符的代表属性的整数,接下来当然就是编写方法取出特定二进制位上的值了。如要查看一个字符的类型,而这个类型由二进制位的最后5位表示,取出后5位的方法如下:
int getPropertiesEx(int ch) { char offset = (char)ch; int props = B[offset]; return props; } int getType(int ch) { int props = getProperties(ch); return (props & 0x1F);// 0001 1111取后5bits代表了character type }当然还有许多的方法,有兴趣的读者可以自己去看一下。