Java 7之基础类型第4篇 - Java字符类型

来源:互联网 发布:php 在线拍卖系统源码 编辑:程序博客网 时间:2024/09/21 08:58

在Java基本类型对应的包装类型中,最为复杂的就是字符类型和字符串类型了。本篇在讲解字符类型之前,必须要讲解一下Unicode编码方面的知识,否则不好理解源代码。

1、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,要么属于增补字符

2、基于Unicode的具体编码格式


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表示这个字符由三个字节组成。

下面来看一个使用各种编码对字符进行编码的例子,如下:



3、源代码相关API

在使用源码相关的API时,需要注意(摘自Java 7 API说明文档):

  • The methods that only accept a 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.
  • The methods that accept an 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).
也就是说,在使用以char为参数的一类方法时,这类方法并不支持增补字符。参数为整数,也就是支持代码点值做为参数时是支持增补字符的。

4、字符的属性

我们在编写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 

那么Java是怎么判断这些字符的属性的呢?其实每一个Java字符都用一个32位,也就是4个字节来表示这个属性。具体的32位中的各个位所代表的涵义如下:

1 bit   mirrored property4 bits  directionality property9 bits  signed offset used for converting case(有符号偏移,用于转换)1 bit   if 1, adding the signed offset converts the character to lowercase1 bit   if 1, subtracting(减去) the signed offset converts the character to uppercase1 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 whitespace2 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 value5 bits  digit offset5 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。
 ISO-8859-1编码是单字节编码,向下兼容ASCII,其编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。
  ISO-8859-1收录的字符除ASCII收录的字符外,还包括西欧语言、希腊语、泰语、阿拉伯语、希伯来语对应的文字符号。欧元符号出现的比较晚,没有被收录在ISO-8859-1当中。
  因为ISO-8859-1编码范围使用了单字节内的所有空间,在支持ISO-8859-1的系统中传输和存储其他任何编码的字节流都不会被抛弃。换言之,把其他任何编码的字节流当作ISO-8859-1编码看待都没有问题。这是个很重要的特性,MySQL数据库默认编码是Latin1就是利用了这个特性。ASCII编码是一个7位的容器,ISO-8859-1编码是一个8位的容器。
       回到我们的源代码中,可以看到最终A[]中存储了256个整数,就是使用有4个字节,32bits来存储的数,但是不能将这256个数当作一个整数来看待,没有任何的意义,需要读取32个比特位中特定的位的值,因为他代表着字符的属性。举个例子:ASCII表中的49代表'0'字符,获取这个字符对应的属性值为A[49],转换后的二进制值如下:
  0- 0011-000   000000-0-0   0-011-01-10   000-01001
1位:0表示没有mirrored property,如果是'(','[',这些字符,这个位置的值为1
4位:3
9位:无偏移
1位:无小写
1位:无大写
1位:无首字母大写属性
3位:3 表示是一个合法的Unicode标识符或Java标识符
2位:1 有数字的属性
5位:数字移位为0
5位:字符类型代表的值为9

既然能够得到每个字符的代表属性的整数,接下来当然就是编写方法取出特定二进制位上的值了。如要查看一个字符的类型,而这个类型由二进制位的最后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    }
当然还有许多的方法,有兴趣的读者可以自己去看一下。






3 0
原创粉丝点击