4.2 Unicode

不同的国家和地区通常会有自己特定的编码字符集,而且以包含某种语言相关的字符为主。这使得支持国际化的工作变得复杂,因为需要同时考虑非常多的编码字符集。这些编码字符集的字符的代码点各不相同,为了提高效率也需要使用不同的代码单元来进行映射,有可能不同的编码字符集用相同的编码来表示不同的字符,或是用不同的编码来表示相同的字符。对于平台和应用开发人员来说,理解这些编码字符集并在程序中正确地使用,所要花费的代价太高。

Unicode就是为了解决这个问题而出现的。Unicode的目标是能够包括世界上所有语言的文字。以目前最新的Unicode 6.0来说,它已经包含了来自93种语言的超过109 000个字符。现在Unicode的开发和维护工作由1991年成立的Unicode联盟负责。该联盟的成员包括了大部分重量级的信息技术公司。Unicode除了在各种主流操作系统和应用平台上得到应用之外,同时也与国际标准化组织(ISO)的ISO/IEC 10646标准保持完全同步。从这两个方面来说,Unicode应该是开发国际化应用程序时的编码字符集的最佳选择。随着Unicode本身的发展,越来越多的语言将被包括进来。除了语言中的字符之外,Unicode还包含了各种常见的符号。

4.2.1 Unicode编码格式

由于Unicode中包含的字符非常多,另外为了尽可能地保持与已有编码字符集的兼容性,Unicode中对字符的编码格式比较复杂。Unicode一共定义了1 114 112个代码点,值的范围从0到0x10FFFF。在下面对代码点的说明中,都是以16进制的方式来表示的,并在前面加上“U+”。这一百多万个代码点中目前使用的只有10%左右,大部分代码点目前还没有被使用,可以在以后的版本中被分配给新添加的语言的字符。Unicode中的代码点被分成17个区域,每个区域为一个平面(plane)。每个平面中包含65 536个字符。第1个平面的代码点的范围是0到U+FFFF,第2个平面的代码点的范围是U+10000到U+1FFFF,依次类推,最后一个平面的代码点的范围是U+F0000到U+10FFFF。也就是说代码点的后四位(65 536个值)总是从0变化到0xFFFF,而前两位则在0到0x10之间变化,即总计17个平面。代码点的前两位表明了代码点所在的平面的编号。在这17个平面中,目前只有前4个和后3个已经被使用了或被保留,中间的10个平面尚处于未分配的状态。

在Unicode的这17个平面中,最常见的是第1个平面,即代码点范围是0到U+FFFF。这个平面被称为基本多语言平面(Basic Multilingual Plane, BMP),其中包含了最常见的语言中的字符和大量的符号。第2个平面称为补充多语言平面(Supplementary Multilingual Plane, SMP),其中主要包含一些不常见的语言中的字符以及音乐与数学符号。第3个平面称为补充表意文字平面(Supplementary Ideographic Plane, SIP),其中包含的是Unicode所定义的统一的汉字的表意符号。第4个平面称为第三表意文字平面(Tertiary Ideographic Plane, TIP),被保留用来包含其他的表意文字,目前其中还没有定义任何字符。第5到第14个平面目前处于未分配的状态,也没有被保留使用。第15个平面称为补充特殊用途平面(Supplementary Special-purpose Plane, SSP),其中主要包含非图形化的字符。第16和第17个平面称为私有使用区域平面(Private Use Area planes, PUA)。这两个平面中的代码点所对应的字符不由Unicode联盟来规定,而允许第三方自行定义。相当于允许第三方在Unicode的框架之内开发自己的字符编码格式。

在说明了Unicode编码字符集中代码点的定义方式之后,下面来看看如何把这些代码点映射到计算机程序中。Unicode没有采用一一映射的方式,而是把字符的代码点分别映射到多个代码单元上。Unicode规范中定义的映射方式分成Unicode传输格式(Unicode Transformation Format, UTF)编码和统一字符集(Universal Character Set, UCS)编码两种。这两种映射方式根据代码单元的位数的不同,又有不同的变体,比如UTF-8、UTF-16和UTF-32就分别使用8位、16位和32位来表示单个代码单元;而UCS-2和UCS-4则分别使用2个字节和4个字节来表示单个代码单元。这些映射方式中最常见的是UTF编码格式的两种变体UTF-8和UTF-16,而UCS编码格式用得比较少。

1.UTF-8

UTF-8编码格式应该是最为开发人员所熟悉的Unicode编码格式。有很多图书和教程都告诉开发人员在编写网页的时候应该使用UTF-8编码,在创建数据库的时候也需要使用UTF-8编码。很多开发人员都认为使用UTF-8编码是解决程序中各种乱码问题的终极解决方案,不过经过本章的详细介绍之后,开发人员应该会对乱码问题有更加深刻的认识,而不是仅了解这样一个结论。UTF-8编码格式能够流行是有原因的。UTF-8是一种变长的编码格式,其中每个代码单元是8位。一个Unicode字符的代码点会被映射到1到6个代码单元。这种变长编码格式的一个重要的好处是可以减少所需要的存储空间。对于Unicode中的常见字符,仅用一个代码单元就可以表示。另外,UTF-8编码的前128个字符与ASCII编码是完全一致的。也就是说一段用ASCII编码的文本也是一段合法的UTF-8编码的文本。考虑到ASCII编码的流行程度,保持UTF-8编码与ASCII编码的这种兼容性,有利于UTF-8的推广。

由于UTF-8是变长编码的,它对不同范围内的Unicode代码点的编码方式是不同的,所以UTF-8的编码和解码方式有点复杂。不过UTF-8编码格式本身设计得非常精巧。[1]表4-1给出了具体的编码分配方式,其中的“x”表示的是可以用来编码的位。

4.2 Unicode - 图1

如表4-1所示,首先是用1个字节来表示的Unicode字符。为了保持与ASCII的兼容性,这个字节中的高位始终是0,仅用剩下的7位来表示字符,因此可以表示的Unicode中的字符的代码点范围是0~U+007F,对应的编码之后的代码值是0~0x007F。接着是用2个字节表示的Unicode字符。在这两个字节中,前一个字节的前3位固定为110,后一个字节的前2位固定为10,剩下的11位用来编码。这两个字节可以表示的代码点范围是U+0080~U+07FF。进行编码就是把代码点的11位按照从高到低的顺序分成5位和6位两个部分,分别放在第一个和第二个字节中的剩余部分,这样就得到了对应的UTF-8编码。对于由3个字节表示的Unicode代码点来说,第一个字节的前4位固定为1110,后面两个字节的前2位固定为10。这样3个字节总共剩下16位用来编码,可以表示的代码点范围是U+0800~U+FFFF。利用3个字节进行编码的方式与利用2个字节的做法相似,即把16位按照从高到低的顺序分配到各个字节的剩余位中。对于剩下的分别用4、5和6个字节来表示的Unicode代码点来说,基本的编码方式是相似的。从上面对不同字节数的编码方式的分析中可以看出编码时的规律。如果使用的字节数多于一个,那么第一个字节中的高位固定为多个1后面加上一个0,其中1的个数等于使用的字节数。而除了第一个字节之外的其他字节的高位都固定以二进制的“10”开头。利用这种编码方式可以很容易地对一个UTF-8编码的字节序列进行解码。只需要查看第一个字节的高位中第一个0之前的1的个数,就可以知道整个编码序列由几个字节组成。同样的,当遇到一个字节的时候,只需要查看其前2位就可以知道该字节在UTF-8编码之后的字节序列中的位置:如果第一位是0,说明这是一个与ASCII兼容的编码;如果前两位都是1,说明这是一个多字节编码的起始字节;如果前两位是10,说明这是一个多字节编码的后续字节,只需要往前查找就可以找到整个编码序列的起始字节。

2.UTF-16

除了UTF-8之外,另一个常用的Unicode编码格式是UTF-16。与UTF-8不同的是,UTF-16中的每个代码单元是16位。每个Unicode代码点会被映射到1或2个16位的代码单元上。在使用UTF-16的时候,最多只需要4字节就可以表示所有的字符;而在使用UTF-8的时候,则最多需要6字节。这是因为UTF-8中有很多位的值是固定的,不能用在编码中。就UTF-8和UTF-16的使用场景来说,UTF-8更多是作为字符传输时的编码格式,而UTF-16则更多是作为字符在系统中的内部表示方式。这是因为UTF-8编码格式可以减少传输时所需的字节数,而UTF-16使用起来相对简单。使用UTF-16对Unicode中的代码点进行编码的过程也比较复杂。

对于Unicode的BMP中的65 536个字符,这些字符是直接将代码点一一映射到16位整数值上的。BMP中的代码点只需要一个代码单元就可以表示。对于不在BMP中的代码点,由于其数值范围已经超过了16位所能表示的范围,所以需要两个16位的代码单元才能表示。这两个代码单元被称为一个代理项对(surrogate pair)。由于不在BMP中的代码点的范围是U+10000~U+10FFFF,对这些代码点的具体的映射规则是:用代码点的数值减去0x10000,这样就得到了0~0xFFFFF范围内的20位的数值。用这20位中的前10位加上0xD800之后作为代理项对的高位代理,得到的值的范围是0xD800~0xDBFF;再把这20位中剩下的10位加上0xDC00之后作为代理项对的低位代理,得到的值的范围是0xDC00~0xDFFF。把高位代理和低位代理拼接起来,就得到了由两个16位值组成的代码点的UTF-16的表示形式。代码清单4-1给出了一个UTF-16的示意编码过程。

代码清单4-1 UTF-16的示意编码过程


public char[]encode(int codePoint){

if((codePoint>=0&&codePoint<=0xD7FF)||(codePoint>=0xE000&&codePoint<=0xFFFF)){

return new char[]{(char)codePoint};

}else{

codePoint=codePoint-0x10000;

int high=(codePoint>>10)+0xD800;

int low=(codePoint&0x3FF)+0xDC00;

return new char[]{(char)high,(char)low};

}

}


值得一提的是,在Unicode编码字符集中,U+D800~U+DFFF中间的代码点是没有定义字符的。这么做就是为UTF-16编码考虑的。经过这样的设计,对于编码之后的字节序列,以16位为一个单元来看,仅使用一个16位的BMP中字符的编码和使用两个16位的字符编码中的高位和低位代理项,这三者的数值范围是互相不重叠的。这种不重叠的设计,使得在解码的时候可以很容易地判断一个字节序列中不同代码点所对应的字节范围。比如对一个16位值来说,如果其数值在0~0xD7FF或0xE000~0xFFFF之间,就说明这是一个BMP中的字符的编码,使用这16位来解码就足够了;如果数值范围在0xD800~0xDBFF之间,就说明这是一个代理项对的高位代理,应该再往后查看16位;如果数值范围在0xDC00~0xDFFF之间,就说明这是一个代理项对的低位代理,应该再往前查看16位。

UTF-16和UTF-8编码格式都具有“自我同步”的特性。这种特性的含义是,一个代码点对应的编码后的字节序列的起点和终点位置只需要查看当前代码点就可以得出来。也就是说,在一段文本被编码之后的字节序列中,任意指定一个字节位置,最多只需要查看一个代码点对应的字节序列长度的字节数,就可以找到这个字节所属的代码点所对应的完整字节序列。这种特性的优势使解码过程变得比较简单,可以很容易地把一个完整的字节序列切分成不同的小段,每个小段对应一个代码点。

由于UTF-16通过2个或4个字节来编码一个代码点,因此这些字节之间的顺序就变得有意义了。这就是第3章介绍过的字节顺序的含义。同样的UTF-16编码,按照大端表示(big-endian)和小端表示(little-endian)所解释出来的字符是不一样的。对于这种情况,UTF-16允许在一段编码之后的字节序列的最前面使用字节顺序标记(byte order mark, BOM)来说明解码时应该使用的字节顺序。字节顺序标记是一个特殊的Unicode代码点U+FEFF,所表示的字符含义是宽度为零的不断行的空格(ZERO-WIDTH NON-BREAKING SPACE)。当解码器进行解码时,它会根据底层平台的字节顺序去尝试解码。如果解码之后的结果是0xFEFF,则说明不需要改变字节顺序,直接使用底层平台的字节顺序即可;如果解码之后的结果是0xFFFE,即两个字节的顺序发生了调换,由于U+FFFE在Unicode规范中不对应任何字符,因此实际上产生了解码错误。出现这个错误就说明解码时的字节顺序不对,应该把字节调换之后再进行解码。解码器就会在每次读取两个字节的时候,先把顺序颠倒一下,再进行解码。除了使用BOM之外,另外一种指定字节顺序的做法是在编码格式上给出具体的声明:UTF-16BE说明采用的是大端表示的UTF-16编码,而UTF-16LE说明采用的是小端表示的UTF-16编码。

UTF-16、UTF-16BE和UTF-16LE在处理字节顺序标记时的行为是不同的。对于UTF-16BE和UTF-16LE来说,在编码的时候,不会输出字节顺序标记,因为编码格式的名称就指明了字节顺序;在解码的时候,会把字节顺序标记当成其对应的普通Unicode字符,即代码点为U+FEFF的字符。而对于UTF-16来说,在编码的时候总是使用大端表示并输出字节顺序标记;在解码的时候则根据字节顺序标记进行判断,如果没有就默认为大端表示。

3.UTF-32与UCS-2

另外一个使用得比较少的编码格式是UTF-32。UTF-32总是使用32位来编码一个Unicode。由于32位对于Unicode的代码点范围来说完全足够了,因此UTF-32是一种定长编码格式,不同于UTF-8和UTF-16的变长编码格式。Unicode代码点与UTF-32的32位之间是一一映射的关系。UTF-32的优势在于支持对代码点的随机查找,这也是其定长编码带来的好处。比如一段文本中包含了128个Unicode字符,那么在经UTF-32编码之后的字节序列中,只需要定位到第13个字节,就找到了第4个字符的起始位置。而对于UTF-8和UTF-16来说,则只能进行顺序查找,因为每个代码点所对应的字节数是不相同的。UTF-32的最大缺点在于对存储空间的浪费,这也是前面说过的一一映射的编码方式的共同缺点。因为这个缺点,在实际的开发中很难见到UTF-32的影子。一般的开发人员也不需要特别关注UTF-32编码格式。

还有一个可能会遇到的Unicode编码格式是UCS-2。UCS-2是在国际标准ISO/IEC 10646中定义的,是UTF-16的一个子集。UCS-2总是使用2字节来表示一个Unicode编码。因此它只能对BMP中的代码点进行编码。在UCS-2的基础上进行扩充,增加了对非BMP中代码点的支持之后,就得到了UTF-16编码。UCS-2编码会出现在一些规范和应用平台的早期版本中,现在基本都改为使用UTF-16。

[1]UTF-8格式的主要设计者之一是1973年图灵奖获得者之一的Ken Thompson,他因设计UNIX而闻名。