UNICODE与UTF-8的转换详解

UNICODE与UTF-8的转换详解

 

1 编码
在计算机中,各种信息都是以二进制编码的形式存在的,也就是说,不管是文字、图形、声音、动画,还是电影等各种信息,在计算机中都是以0和1组成的二进制代码表示的。为了区分这些信息,人们就为计算机设计了一套识别准则,这也就是计算机编码。例如:英文字母与汉字的的区别,就是英文字母用的是单字节的ASCII码,汉字采用的是双字节的汉字内码。
1.1 基本概念
* 字符:字符是抽象的最小文本单位。它没有固定的形状(可能是一个字形),而且没有值。“A”是一个字符,“€”(德国、法国和许多其他欧洲国家通用货币的标志)也是一个字符。
* 字符集:字符集是字符的集合。例如,汉字字符是中国人最先发明的字符,在中文、日文、韩文和越南文的书写中使用。
* 编码字符集:编码字符集是一个字符集,它为每一个字符分配一个唯一数字。Unicode 标准的核心是一个编码字符集,字母“A”的编码为 004116 和字符“€”的编码为 20AC16。Unicode 标准始终使用十六进制数字,而且在书写时在前面加上前缀“U+”,所以“A”的编码书写为“U+0041”。
* 代码点:代码点是指可用于编码字符集的数字。编码字符集定义一个有效的代码点范围,但是并不一定将字符分配给所有这些代码点。有效的Unicode代码点范围是 U+0000 至U+10FFFF。Unicode4.0将字符分配给一百多万个代码点中的96,382代码点。
* 增补字符:增补字符是代码点在 U+10000 至 U+10FFFF 范围之间的字符,也就是那些使用原始的Unicode的16 位设计无法表示的字符。从U+0000至 U+FFFF之间的字符集有时候被称为基本多语言面 (BMP)。因此,每一个Unicode 字符要么属于BMP,要么属于增补字符。
* 字符编码方案:字符编码方案是从一个或多个编码字符集到一个或多个固定宽度代码单元序列的映射。最常用的代码单元是字节,但是 16 位或 32 位整数也可用于内部处理。UTF-32、UTF-16 和 UTF-8 是 Unicode 标准的编码字符集的字符编码方案。
* UTF-32:即将每一个 Unicode 代码点表示为相同值的 32 位整数。很明显,它是内部处理最方便的表达方式,但是,如果作为一般字符串表达方式,则要消耗更多的内存。
* UTF-16:使用一个或两个未分配的 16 位代码单元的序列对Unicode代码点进行编码。值U+0000至U+FFFF编码为一个相同值的16位单元。增补字符编码为两个代码单元,第一个单元来自于高代理范围(U+D800 至 U+DBFF),第二个单元来自于低代理范围(U+DC00至U+DFFF)。这在概念上可能看起来类似于多字节编码,但是其中有一个重要区别:值U+D800至U+DFFF 保留用于UTF-16;没有这些值分配字符作为代码点。这意味着,对于一个字符串中的每个单独的代码单元,软件可以识别是否该代码单元表示某个单单元字符,或者是否该代码单元是某个双单元字符的第一个或第二单元。这相当于某些传统的多字节字符编码来说是一个显著的改进,在传统的多字节字符编码中,字节值0x41既可能表示字母“A”,也可能是一个双字节字符的第二个字节。
* 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字符。
(表1-1 编码的一些基本概念)
1.2 字符编码
字符编码即英文编码,包括字母、数字、标点、运算符等。
    字符的编码采用国际通用的ASCII码(American Standard Code for Information Interchange,美国信息交换标准代码),每个ASCII码以1个字节(Byte)储存,从0到数字127代表不同的常用符号,例如大写A的 ASCII码是65,小写a则是97。由于ASCII码只用了字节的七个位,最高位并不使用,所以后来又将最高的一个位也编入这套编码码中,成为八个位的延伸ASCII(ExtendedASCII)码,这套内码加上了许多外文和表格等特殊符号,成为目前常用的编码。基本的ASCII字符集共有128个字符,其中有96个可打印字符,包括常用的字母、数字、标点符号等,另外还有32个控制字符。标准ASCII码使用7个二进位对字符进行编码,对应的ISO标准为ISO646标准。
    字母和数字的ASCII码的记忆是非常简单的。我们只要记住了一个字母或数字的ASCII码(例如记住A为65,0的ASCII码为48),知道相应的大小写字母之间差32,就可以推算出其余字母、数字的ASCII码。
    虽然标准ASCII码是7位编码,但由于计算机基本处理单位为字节(1byte = 8bit),所以一般仍以一个字节来存放一个ASCII字符。每一个字节中多余出来的一位(最高位)在计算机内部通常保持为0(在数据传输时可用作奇偶校验位)。由于标准ASCII字符集字符数目有限,在实际应用中往往无法满足要求。为此,国际标准化组织又制定了ISO2022标准,它规定了在保持与 ISO646兼容的前提下将ASCII字符集扩充为8位代码的统一方法。ISO陆续制定了一批适用于不同地区的扩充ASCII字符集,每种扩充ASCII 字符集分别可以扩充128个字符,这些扩充字符的编码均为高位为1的8位代码(即十进制数128~255),称为扩展ASCII码。
1.3 汉字编码
1.3.1 汉字内码
    汉字信息在计算机内部也是以二进制方式存放。由于汉字数量多,用一个字节的128种状态不能全部表示出来,因此在1980年我国颁布的《信息交换用汉字编码字符集——基本集》,即国家标准GB2312-80方案中规定用两个字节的十六位二进制表示一个汉字,每个字节都只使用低7位(与ASCII码相同),即有128×128=16384种状态。由于ASCII码的34个控制代码在汉字系统中也要使用,为不致发生冲突,不能作为汉字编码,128除去34只剩 94种,所以汉字编码表的大小是94×94=8836,用以表示国标码规定的7445个汉字和图形符号。
    每个汉字或图形符号分别用两位的十进制区码(行码)和两位的十进制位码(列码)表示,不足的地方补0,组合起来就是区位码。把区位码按一定的规则转换成的二进制代码叫做信息交换码(简称国标码)。国标码共有汉字6763个(一级汉字,是最常用的汉字,按汉语拼音字母顺序排列,共3755个;二级汉字,属于次常用汉字,按偏旁部首的笔划顺序排列,共3008个),数字、字母、符号等682个,共7445个。
    由于国标码不能直接存储在计算机内,为方便计算机内部处理和存储汉字,又区别于ASCII码,将国标码中的每个字节在最高位改设为1,这样就形成了在计算机内部用来进行汉字的存储、运算的编码叫机内码(或汉字内码,或内码)。内码既与国标码有简单的对应关系,易于转换,又与ASCII码有明显的区别,且有统一的标准(内码是惟一的)。
1.3.2 汉字外码
    无论是区位码或国标码都不利于输入汉字,为方便汉字的输入而制定的汉字编码,称为汉字输入码,即汉字外码。不同的输入方法,形成了不同的汉字外码。常见的输入法有以下几类:
* 按汉字的排列顺序形成的编码(流水码):如区位码;
* 按汉字的读音形成的编码(音码):如全拼、简拼、双拼等;
* 按汉字的字形形成的编码(形码):如五笔字型、郑码等;
* 按汉字的音、形结合形成的编码(音形码):如自然码、智能ABC。
* 输入码在计算机中必须转换成机内码,才能进行存储和处理。
(表1-2 常见的输入法)
1.3.3 汉字字形码
    为了将汉字在显示器或打印机上输出,把汉字按图形符号设计成点阵图,就得到了相应的点阵代码(字形码)。
    全部汉字字码的集合叫汉字字库。汉字库可分为软字库和硬字库。软字库以文件的形式存放在硬盘上,现多用这种方式,硬字库则将字库固化在一个单独的存储芯片中,再和其它必要的器件组成接口卡,插接在计算机上,通常称为汉卡。
    用于显示的字库叫显示字库。显示一个汉字一般采用16×16点阵或24×24点阵或48×48点阵。已知汉字点阵的大小,可以计算出存储一个汉字所需占用的字节空间。例:用16×16点阵表示一个汉字,就是将每个汉字用16行,每行16个点表示,一个点需要1位二进制代码,16个点需用16位二进制代码(即2个字节),共16行,所以需要16行×2字节/行=32字节,即16×16点阵表示一个汉字,字形码需用32字节。即:字节数 = 点阵行数 * 点阵列数 / 8。
    用于打印的字库叫打印字库,其中的汉字比显示字库多,而且工作时也不像显示字库需调入内存。可以这样理解,为在计算机内表示汉字而统一的编码方式形成汉字编码叫内码(如国标码),内码是惟一的。为方便汉字输入而形成的汉字编码为输入码,属于汉字的外码,输入码因编码方式不同而不同,是多种多样的。为显示和打印输出汉字而形成的汉字编码为字形码,计算机通过汉字内码在字模库中找出汉字的字形码,实现其转换。例如:
1:已知汉字"春"的国标码为343AH,求其机内码?
答:机内码 = 国标码 + 8080H = 343AH + 8080H = B4BAH
 
2:用24×24点阵来表示一个汉字(一点为一个二进制位),则2000个汉字需要多少KB容量?
答: 容量 = (24 * 24/8)* 2000 / 1024 = 140.7KB ≈ 141KB
(表1-3 汉字内码与字形码的转换实例)
2 UNICODE编码
Unicode 是基于通用字符集(Universal Character Set)的标准来发展,并且同时也以书本的形式(The Unicode Standard,目前第五版由Addison-Wesley Professional出版,ISBN-10: 0321480910)对外发表。Unicode包含了超过十万个字符(在2005年,Unicode的第十万个字符被采纳且认可成为标准之一)、一组可用以作为视觉参考的代码图表、一套编码方法与一组标准字符编码、一套包含了上标字、下标字等字符特性的列举等。
在计算机科学领域中,Unicode(统一码、万国码、单一码、标准万国码)是业界的一种标准,它可以使电脑得以呈现世界上数十种文字的系统。Unicode组织(The Unicode Consortium)是由一个非营利性的机构所运作,并主导Unicode的后续发展,其目标在于:将既有的字符编码方案,以Unicode编码方案来加以取代,特别是既有的方案在多语环境下,皆仅有有限的空间以及不相容的问题。
Unicode在字符集认可的成功,使其得以在电脑软件的国际化与本地化领域中,广泛且具优势的被采用。这标准已在近年来的多种新科技当中被加以采用,包含了可扩展置标语言(XML)、Java编程语言、以及最新的操作系统中。
2.2 UNICODE的起源与发展
Unicode是由于传统的字符编码方式的局限性而产生的,例如ISO 8859所定义的字符虽然在不同的国家中广泛地使用,可是在不同国家间却经常出现不相容的情况。很多传统的编码方式都具有一个共通的问题,即其容许电脑进行双语环境式的处理(通常使用拉丁字母以及其本地语言),但却无法同时支援多语言环境式的处理(指可同时处理混合多种语言的情况)。
Unicode试图将字位(字素,graphemes)与类字位字符加以认定与编码,而非以不同的字形(glyphs)来加以区分。然而在汉字的个案来看,这样方式有时会引起一字多形的认定争议(详见中日韩统一表意文字主题)。
在文字处理方面,Unicode的功用是为每一个字符提供一个唯一的代码(即一组数字),而不是一种字形。换句话说,Unicode是将字符以一种抽象的方式来呈现,而将视觉上的演绎工作(例如字体大小、外观形状、字体形态、文体等)留给其他软件来处理,例如网页浏览器或是文字处理器。
    为了使Unicode与已存在和广泛使用的旧有编码互相兼容,尤其是差不多所有电脑系统都支援的基本拉丁字母部分,所以Unicode的首256字符仍旧保留给ISO 8859-1所定义的字符,使既有的西欧语系文字的转换不需特别考量;另方面因相同的原因,Unicode 把大量相同的字符重复编到不同的字符码中去,使得旧有纷杂的编码方式得以和 Unicode 编码间互相直接转换,而不会遗失任何资讯。举例来说,全角格式区段包含了主要的拉丁字母的全角格式,在中文、日文、以及韩文字形当中,这些字符以全角的方式来呈现,而不以常见的半角形式显示,这对竖排文字和等宽排列文字有重要作用。
    Unicode组织位于美国加州,组织允许任何愿意支付会员费用的公司或是个人加入,其成员包含了主要的电脑软硬件厂商,例如奥多比系统(Adobe Systems)、苹果公司(Apple)、惠普(HP)、IBM、微软(Microsoft)、全录(Xerox)等。Unicode 组织在 1991 年首次发布了 The Unicode Standard(ISBN 0-321-18578-1)。Unicode的开发结合了国际标准化组织(International Organization for Standardization,简称 ISO)所制定的ISO/IEC 10646,即通用字符集(Universal Character Set,简称 UCS)。Unicode 与 ISO/IEC 10646 在编码的运作原理相同,但 The Unicode Standard 包含了更详尽的实现资讯、涵盖了更细节的主题,诸如字符编码(bitwise encoding)、校对以及呈现等。 The Unicode Standard 也列举了诸多的字符特性,包含了那些必须支援双方向呈现的文字(由左至右或由右至左的文字呈现方向,例如阿拉伯文是由右至左)。Unicode与ISO/IEC 10646两个标准在术语上的使用有些微的不同。
Unicode截至目前为止历次的版次与发布时间如下:
Unicode 1.0:1991年10月;
Unicode 1.0.1:1992年6月;
Unicode 1.1:1993年6月;
Unicode 2.0:1997年7月;
Unicode 2.1:1998年5月;
Unicode 2.1.2:1998年5月;
Unicode 3.0:1999年9月;涵盖了来自ISO 10646-1的十六位元通用字符集(UCS)基本多文种平面(Basic Multilingual Plane);
Unicode 3.1:2001年3月;新增从ISO 10646-2定义的辅助平面(Supplementary Planes);
Unicode 3.2:2002年3月;
Unicode 4.0:2003年4月;
Unicode 4.0.1:2004年3月;
Unicode 4.1:2005年3月;
Unicode 5.0:2006年7月;
Unicode 5.1:2008年4月。
(表2-1 unicode编码的版次与发布时间)
2.3 Unicode 的编码和实现
2.3.1 编码方式
Unicode 的编码方式与 ISO 10646 的通用字符集(Universal Character Set,UCS)概念相对应,目前实际应用的 Unicode 版本对应于 UCS-2,使用16位的编码空间。也就是每个字符占用2个字节。这样理论上一共最多可以表示 216 即 65536 个字符。基本满足各种语言的使用。实际上目前版本的 Unicode 尚未填充满这16位编码,保留了大量空间作为特殊使用或将来扩展。
上述16位Unicode字符构成基本多文种平面(Basic Multilingual Plane,简称BMP)。最新(但未实际广泛使用)的Unicode 版本定义了16个辅助平面,两者合起来至少需要占据21位的编码空间,比3字节略少。但事实上辅助平面字符仍然占用4字节编码空间,与UCS-4保持一致。未来版本会扩充到 ISO 10646-1 实现级别3,即涵盖UCS-4的所有字符。UCS-4 是一个更大的尚未填充完全的31位字符集,加上恒为0的首位,共需占据32位,即4字节。理论上最多能表示231个字符,完全可以涵盖一切语言所用的符号。
BMP字符的Unicode编码表示为 U+hhhh,其中每个 h 代表一个十六进制数位。与UCS-2编码完全相同。对应的4字节UCS-4编码后两个字节一致,前两个字节的所有位均为0。
2.3.2 实现方式
Unicode的实现方式不同于编码方式。一个字符的Unicode编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Translation Format,简称为 UTF)。
       例如,如果一个仅包含基本7位ASCII字符的Unicode文件,如果每个字符都使用2字节的原Unicode 编码传输,其第一字节的8位始终为0。这就造成了比较大的浪费。对于这种情况,可以使用 UTF-8 编码,这是一种变长编码,它将基本7位ASCII字符仍用7位编码表示,占用一个字节(首位补0)。而遇到与其他 Unicode 字符混合的情况,将按一定算法转换,每个字符使用1-3个字节编码,并利用首位为0或1进行识别。这样对以7位ASCII字符为主的西文文档就大大节省了编码长度。类似的,对未来会出现的需要4个字节的辅助平面字符和其他 UCS-4 扩充字符,2字节编码的UTF-16也需要通过一定的算法进行转换。
再如,如果直接使用与Unicode编码一致(仅限于 BMP 字符)的UTF-16编码,由于每个字符占用了两个字节,在Macintosh机和PC机上,对字节顺序的理解是不一致的。这时同一字节流可能会被解释为不同内容,如编码为U+594E的字符“奎”同编码为 U+4E59 的“乙”就可能发生混淆。于是在UTF-16编码实现方式中使用了大尾序(big-endian)、小尾序(little-endian)的概念,以及BOM(Byte Order Mark)解决方案。
       此外 Unicode 的实现方式还包括UTF-7、Punycode、CESU-8、SCSU、UTF-32等,这些实现方式有些仅在一定的国家和地区使用,有些则属于未来的规划方式。目前通用的实现方式是 UTF-16小尾序(BOM)、UTF-16大尾序(BOM)和 UTF-8。在微软公司Windows XP操作系统附带的记事本中,“另存为”对话框可以选择的四种编码方式除去非 Unicode 编码的 ANSI 外,其余三种“Unicode”、“Unicode big endian”和“UTF-8”即分别对应这三种实现方式。
 
3 UTF-8
UTF是UCS / Unicode Transformation Format(Unicode转换格式)的缩写,UTF-8(8位元Universal Character Set/Unicode Transformation Format)是一种针对 Unicode 的可变长度字符编码。它可以用来表示 Unicode 标准中的任何字符,且其编码中的第一个字节仍与ASCII相容,这使得原来处理ASCII字符的软件无须或只须做少部份修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他储存或传送文字的应用中,优先采用的编码。
3.1 UTF-8的历史
1992年初,为建立良好的字节串编码系统(byte-stream encoding)以供多字节字符集(multi-byte character sets)使用,开始了一个正式的研究。ISO/IEC 10646的初稿中有一个非必须的附录,名为UTF。当中包含了一个供32位元的字符使用的字节串编码系统。这个编码方式的性能并不令人满意,但它提出了将0-127的范围保留给ASCII以相容旧系统的概念。
1992年7月,X/Open委员会XoJIG开始寻求一个较佳的编码系统。Unix系统实验室(UNIX System Laboratories, USL)的Dave Prosser为此提出了一个编码系统的建议。它具备可更快速实作的特性,并引入一项新的改进。其中,7位元的ASCII符号只代表原来的意思,所有多字节序列则会包含第8位元的符号,也就是所谓的最高有效位元(Most significant bit)。
1992年8月,这个建议由IBMX/Open的代表流传到一些感兴趣的团体。与此同时,贝尔实验室Plan 9操作系统工作小组的肯·汤普逊对这编码系统作出重大的修改,让编码可以自我同步(self-synchronizing),使得不必从字串的开首读取,也能找出字符间的分界。1992年9月2日,肯·汤普逊和Rob Pike一起在美国新泽西州一架餐车的餐桌垫上描绘出此设计的要点。接下来的日子,Pike及汤普逊将它实现,并将这编码系统完全应用在Plan 9当中,及后他将有关成果回馈X/Open。
1993年1月25-29日的在圣地牙哥举行的USENIX会议首次正式介绍UTF-8。
自1996年起,微软的CAB(MS Cabinet)规格在UTF-8标准正式落实前就明确容许在任何地方使用UTF-8编码系统。但有关的编码器实际上从来没有实作这方面的规格。
3.2 UTF-8的优缺点
3.2.1 UTF-8的特质
UTF-8的设计有以下的多字符组序列的特质:
?单字节字符的最高有效位元永远为0;
?多字节序列中的首个字符组的几个最高有效位元决定了序列的长度。最高有效位为110的,是2字节序列,而1110的是三字节序列,如此类推;
?多字节序列中其余的字节中的首两个最高有效位元为10。
(表3-1 UTF-8的特质)
UTF-8的这些特质,保证了一个字符的字节序列不会包含在另一个字符的字节序列中。这确保了以字节为基础的部份字串比对(sub-string match)方法可以适用于在文字中搜寻字或词。有些比较旧的可变长度8位元编码(如Shift JIS)没有这个特质,故字串比对的算法变得相当复杂。虽然这增加了UTF-8编码的字串的信息冗余,但是利多于弊。另外,资料压缩并非Unicode的目的,所以不可混为一谈。即使在传送过程中有部份字节因错误或干扰而完全遗失,还是有可能在下一个字符的起点重新同步,令受损范围受到限制。
       另一方面,由于其字节序列设计,如果一个疑似为字符串的序列被验证为UTF-8编码,那么我们可以有把握地说它是UTF-8字符串。一段两字节随机序列碰巧为合法的UTF-8而非ASCII 的机率为32分1。对于三字节序列的机率为256分3,对更长的序列的机率就更低了。
3.2.2 UTF-8的优点
UTF-8编码可以通过屏蔽位和移位操作快速读写。字符串比较时strcmp()和wcscmp()的返回结果相同,因此使排序变得更加容易。字节FF和FE在UTF-8编码中永远不会出现,因此他们可以用来表明UTF-16或UTF-32文本(见BOM) UTF-8 是字节顺序无关的。它的字节顺序在所有系统中都是一样的,因此它实际上并不需要BOM。
UTF-8是ASCII的一个超集。因为一个纯ASCII字符串也是一个合法的UTF-8字符串,所以现存的ASCII文本不需要转换。为传统的扩展ASCII字符集设计的软件通常可以不经修改或很少修改就能与UTF-8一起使用;
使用标准的面向字节的排序例程对UTF-8排序将产生与基于Unicode代码点排序相同的结果。(尽管这只有有限的有用性,因为在任何特定语言或文化下都不太可能有仍可接受的文字排列顺序);
UTF-8和UTF-16都是可扩展标记语言文档的标准编码,所有其它编码都必须通过显式或文本声明来指定;
任何面向字节的字符串搜索算法都可以用于UTF-8的数据(只要输入仅由完整的UTF-8字符组成)。但是,对于包含字符记数的正则表达式或其它结构必须小心。
UTF-8字符串可以由一个简单的算法可靠地识别出来。就是:一个字符串在任何其它编码中表现为合法的UTF-8的可能性很低,并随字符串长度增长而减小。举例说,字符值C0,C1,F5至FF从来没有出现。为了更好的可靠性,可以使用正则表达式来统计非法过长和替代值(可以查看W3 FAQ: Multilingual Forms上的验证UTF-8字符串的正则表达式)。
3.2.3 UTF-8的缺点
A 不利于正则表达式检索,正则表达式可以进行很多英文高级的模糊检索。例如,[a-h]表示a到h间所有字母。同样GBK编码的中文也可以这样利用正则表达式,比如在只知道一个字的读音而不知道怎么写的情况下,也可用正则表达式检索,因为GBK编码是按读音排序的。只是UTF-8不是按读音排序的,所以会对正则表达式检索造成不利影响。但是这种使用方式并未考虑中文中的破音字,因此影响不大。Unicode是按部首排序的,因此在只知道一个字的部首而不知道如何发音的情况下,UTF-8 可用正则表达式检索而GBK不行。
B 你无法从UNICODE字符数判断出UTF-8文本的字节数,因为UTF-8是一种变长编码它需要用2个字节编码那些用扩展ASCII字符集只需1个字节的字符 ISO Latin-1 是UNICODE的子集,但不是UTF-8的子集 8位字符的UTF-8编码会被email网关过滤,因为internet信息最初设计为7为ASCII码。因此产生了UTF-7编码。UTF-8 在它的表示中使用值100xxxxx的几率超过50%,而现存的实现如ISO 2022,4873,6429,和8859系统,会把它错认为是C1 控制码。因此产生了UTF-7.5编码。
一份写得很差(并且与当前标准的版本不兼容)的UTF-8解析器可能会接受一些不同的伪UTF-8表示并将它们转换到相同的Unicode输出上。这为设计用于处理八位表示的校验例程提供了一种遗漏信息的方式。
C 占据空间大,与其他 Unicode 编码相比,特别是UTF-16,在 UTF-8 中 ASCII 字符占用的空间只有一半,可是在一些字符的 UTF-8 编码占用的空间就要多出,特别是中文、日文和韩文(CJK)这样的象形文字,所以具体因素因文档而异,但不论哪种情况,差别都不可能很明显。
3.3 UTF-8的编码方式
UTF-8是UNICODE的一种变长度的编码表达方式(一般UNICODE为双字节[指UCS2]),UTF-8就是以8位为单元对UCS进行编码,而UTF-8不使用大尾序和小尾序的形式,每个使用UTF-8储存的字符,除了第一个字节外,其余字节的头两个位元都是以"10"开始,使文字处理器能够较快地找出每个字符的开始位置。
为了与以前的ASCII码相容(ASCII为一个字节),因此 UTF-8 选择了使用可变长度字节来储存 Unicode,具体转换关系如下表:
UCS-4(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 10xxxxxx 10xxxxxx
U-04000000 – U-7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
(表3-2 Unicode和UTF-8之间的转换关系表)
在ASCII码的范围,用一个字节表示,超出ASCII码的范围就用字节表示,这就形成了我们上面看到的UTF-8的表示方法,这?的好处是当UNICODE文件中只有ASCII码时,储存的文件都为一个字节,所以就是普通的ASCII文件无异,读取的时候也是如此,所以能与以前的ASCII文件相容。
    大于ASCII码的,就会由上面的第一字节的前几位表示该unicode字符的长度,比如110xxxxxx前三位的二进制表示告诉我们这是个 2BYTE的UNICODE字符;1110xxxx是个三位的UNICODE字符,依此类推;xxx 的位置由字符编码数的二进制表示的位填入。越靠右的 x 具有越少的特殊意义。只用最短的那个足够表达一个字符编码数的多字节串。注意在多字节串中,第一个字节的开头"1"的数目就是整个串中字节的数目。
ASCII字母继续使用1字节储存,重音文字、希腊字母或西里尔字母等使用2字节来储存,而常用的汉字就要使用3字节。辅助平面字符则使用4字节。
在UTF-8文件的开首,很多时都放置一个U+FEFF字符(UTF-8以EF,BB,BF代表),以显示这个文字档案是以UTF-8编码。
 
4 UNICODE与UTF-8的转换
4.1 UNICODE转换为UTF-8
UTF-8的特点是对不同范围的字符使用不同长度的编码。对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同。UTF-8编码的最大长度是4个字节。从表3-2可以看出,4字节模板有21个x,即可以容纳21位二进制数字。Unicode的最大码位0x10FFFF也只有21位。
    如:“汉”字的Unicode编码是0x6C49。0x6C49在0x0800-0xFFFF之间,使用用3字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将0x6C49写成二进制是:0110 1100 0100 1001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,即E6 B1 89。
    又如:Unicode编码0x20C30在0x010000-0x10FFFF之间,使用4字节模板了:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。将0x20C30写成21位二进制数字(不足21位就在前面补0):0 0010 0000 1100 0011 0000,用这个比特流依次代替模板中的x,得到:11110000 10100000 10110000 10110000,即F0 A0 B0 B0。
4.2 UTF-8转换为UNICODE
4.2.1 java中文乱码出现的原因
乱码指的是计算机系统不能显示正确的字符,而显示其他无意义的字符或空白,如一堆ASCII代码。这样所显示出来的文本统称为乱码。
乱码是因为“所使用的字符的源码在本地计算机上使用了错误的显示字库”,或在本地计算机的字库中找不到相应于源码所指代的字符所致。不同国家和地区的文本字库采用了相同的一段源码,或是源文件中因为文件受到破坏,致使计算机默认提取的源码错误,或是计算机没有安装相应字库,都有可能产生乱码。
java 采用unicode 编码来处理字符。Java 程序无论是从向文件系统以字符流读、写文件,还是向URL连接写HTML信息,或从URL连接读取参数值,都会有字符编码的转换。下图编码——解码的示意图:
(图4-1 编码/解码示意图)
java乱码产生的根源是由于编码和解码采用的不是同一种码(GBK、UTF-8、iso8859-1、GB2312等),如将UNICODE编码按照UTF-8进行了编码,而解码的时候却用的是iso8859-1,此时在java程序中便会出现与原UNICODE编码不一致的情况,即出现乱码。下面以字符串“中国”为例,具体说明中文乱码出现的原因。
    字符(String或char[])"中国" 经过java 编码后的字节流(unicode 字节流)为:4E 2D  56 FD,如果你用new String("中国".getBytes("UTF-8"), "GB2312") 就会产生乱码,如图:
(图4-2 乱码出现原因示意图)
因为getBytes("UTF-8") 取得的是"中国" 经过UTF-8编码后的字节流E4 B8 AD E5 9B BD(UTF-8字节流),而在用new String(bytes, "GB2312") 构造字符串时java 则将UTF-8字节流(E4 B8 AD E5 9B BD)当作是unicode 字节流(因为java是采用unicode 来处理字符的,所以它把字节流统统当作是unicode 字节流),因此它把E4 B8 AD E5 9B BD也看成是unicode字节流。而unicode 字节流(E4 B8 AD E5 9B BD)经过GB2312 编码后就成了“涓??”。于是,乱码产生了。
解决乱码的一般方法是进行正确的解码操作,如编码使用的是utf-8的方式,那么解码是也须用utf-8进行解码。
正确的解码流程应该是:
(图4-3 UTF-8正确的解码示意图)
4.2.2 将UTF-8转换为UNICODE
    图3-3勾画了将UTF-8转换为UNICODE的基本流程,根据表3-2(Unicode和UTF-8之间的转换关系)可以看出,将UTF-8转换为UNICODE的过程,实际上是将按UTF-8编码规则填充至UNICODE的编码提取出来的过程,即将8位的字节“xxxx xxxx”提取出来的过程,如某字符以“1110xxxx 10xxxxxx 10xxxxxx”的编码方式进行UTF-8编码,还原后UNICODE编码为:“xxxx xxxx”。
    例如字符“中”,二进制序列(双字节)为:0100 1110 0010 1101,进行UTF-8编码后的字节二进制序列(三字节)为:1110 0100  10 111000  10 101101,进行UTF-8转码即去掉第一个字节的“1110”,第二个字节的“10”,第三个字节的“10”,然后再将剩余的二进制序列组合成一个双字节的二进制序列,即还原为:0100 1110 0010 1101,如此则完成转码。
下面的代码是将以“1110xxxx 10xxxxxx 10xxxxxx”的编码方式进行UTF-8编码,还原为UNICODE编码的操作:
/**
* 将utf-8编码转化为unicode编码
 * @param aByte byte[] -原utf-8编码字节数组
 * return sByte byte[] -转化后的unicode编码字节数组
 */
public static String changeUtf8ToUnicode(byte[] aByte) {
   int sLength = aByte.length; //原字节数组长度
   //存储转化为unicode编码后的StringBuffer字符串
   StringBuffer sUnicodeStringBuffer = new StringBuffer();
   char sChar; //用于临时存放每个从utf-8中解析出来的unicode编码
   //以下操作是判断字节是否以"1110 xxxx 10xxxxxx 10xxxxxx"的形式出现
   for (int i = 0; i < sLength; i++) { //循环每一个字节
       if (i + 2 < sLength) {
          /**
           * aByte[i] & 0xF0 == 0xE0       ---> 判断当前字节是否以“1110”的形式开始;
           * aByte[i + 1] & 0xC0 == 0x80   ---> 判断下一个字节是否以“10”的形式开始;
           * aByte[i + 2] & 0xC0 == 0x80   ---> 判断下下一个字节是否以“10”的形式开始。
           * 假如条件都满足,则表示此断字节进行了utf-8编码,则将对其进行解码操作(即转
* 化为unicode编码)
           */
          if ((aByte[i] & 0xF0) == 0xE0 && (aByte[i + 1] & 0xC0) == 0x80 &&
              (aByte[i + 2] & 0xC0) == 0x80) {
              /**
               * 将当前字节 1110 xxxx 转化为 xxxx 000000 000000 的形式,具体步骤为:
               * 1110 xxxx << 12 = xxxx 000000 000000
               * 1110 0100 << 12 = 0100 000000 000000
               */
              sChar = (char) (aByte[i] << 12);
              /**
               * 将 前两个字节 转化为 xxxx xxxxxx 000000 的形式,具体步骤为:
               * 10 xxxxxx & 0x003F = 0000 000000 xxxxxx
               * 10 111000 & 0x003F = 0000 000000 111000
               *
               * 0000 000000 xxxxxx << 6 = 0000 xxxxxx 000000
              * 0000 000000 111000 << 6 = 0000 111000 000000
               *
               * xxxx 000000 000000 | 0000 xxxxxx 000000 = xxxx xxxxxx 000000
               * 0100 000000 000000 | 0000 111000 000000 = 0100 111000 000000
               */
              sChar = (char) ((((aByte[i + 1] & 0x003F) << 6) | sChar));
              /**
               * 将此三个字节转化为 xxxx xxxxxx xxxxxx 的形式,具体步骤为:
               * 10 xxxxxx & 0x003F = 0000 0000 00 xxxxxx
               * 10 101101 & 0x003F = 0000 0000 00 101101
               *
               * xxxx xxxxxx 000000 | 0000 000000 xxxxxx = xxxx xxxxxx xxxxxx
               * 0100 111000 000000 | 0000 000000 101101 = 0100 111000 101101
               */
              sChar = (char) ((aByte[i + 2] & 0x003F) | sChar);
               i = i + 2;
               sUnicodeStringBuffer.append(sChar);
           } else {
               sUnicodeStringBuffer.append((char) aByte[i]);
           }
       }
     }
   return sUnicodeStringBuffer.toString();
}
(代码4-1 将UTF-8的一种编码方式转换为UNICODE)
    该代码是仅转换以“1110xxxx 10xxxxxx 10xxxxxx”形式进行UTF-8编码过的字节流,而要彻底的将UTF-8编码转换为UNICODE编码,则需要对表3-2(Unicode和UTF-8之间的转换关系)中的六种编码方式进行处理。在java中,Unicode代码点的作用范围是U+0000至U+10FFFF之间的字符值,所以表3-2中的前四种编码方式在java中有效,前三种编码方式(BOM)可以直接将编码前的两个字节(1个单位的char)提取出来,组成一个char,以达到解码的目的,具体实现代码为:
/**
 * 将UTF-8编码解码
 * @param aByte byte[]
 * @return String
 */
 public static String changeUtf8ToUnicode(byte[] aByte) {
     StringBuffer sUnicodeStringBuffer = new StringBuffer();
     int sLength = aByte.length;
     int sInt_1, sInt_2, sInt_3, sInt_4, sInt_5, sInt_6;
     for (int i = 0; i < sLength; i++) {
         sInt_1 = (int) aByte[i] & 0xff;
         switch (sInt_1 >> 4) {
         case 0:
         case 1:
         case 2:
         case 3:
         case 4:
         case 5:
         case 6:
         case 7:
              /* 0xxxxxxx*/
             sUnicodeStringBuffer.append((char) aByte[i]);
             break;
         case 12:
         case 13:
              /* 110x xxxx   10xx xxxx*/
             if (i + 1 < sLength) {
                 sInt_2 = (char) aByte[i + 1];
                 if ((sInt_2 & 0xC0) == 0x80) {
                     sUnicodeStringBuffer.append((char)(((sInt_1 & 0x1F) << 6)
 | (sInt_2 & 0x3F)));
                     i ++;
                 }
             }
             break;
         case 14:
             /* 1110 xxxx 10xx xxxx 10xx xxxx */
             if (i + 2 < sLength) {
                 sInt_2 = (int) aByte[i + 1];
                 sInt_3 = (int) aByte[i + 2];
                 if (((sInt_2 & 0xC0) == 0x80) || ((sInt_3 & 0xC0) == 0x80)) {
                     sUnicodeStringBuffer.append((char)(((sInt_1 & 0x0F) << 12) | ((sInt_2 & 0x3F) << 6) | ((sInt_3 & 0x3F) << 0)));
                     i = i + 2;
                 }
             }
             break;
         }
        }
        return sUnicodeStringBuffer.toString();
    }
(代码4-2 将UTF-8的前三种编码方式[BOM]还原为UNICODE)
前三种编码方式,即从U-00000000 - U-0000FFFF区域,包含了99.9%的日常使用字符,一般进行UTF-8与UNICODE的转换时,仅考虑前三中编码方式便可。
第四个编码方式(U-00010000 – U-001FFFFF)则需要使用增补字符进行处理。下面将具体讲解java平台中的增补字符。
4.2.3 Java平台中的增补字符
增补字符是Unicode标准中代码点超出U+FFFF的字符,而java中的增补字符则是指Unicode代码点的作用范围在U+FFFF至U+10FFFF之间的字符值。
4.2.3.1 Java平台中增补字符出现背景
Unicode最初设计是作为一种固定宽度的16位字符编码。在Java编程语言中,基本数据类型char初衷是通过提供一种简单的、能够包含任何字符的数据类型来充分利用这种设计的优点。不过,现在看来,16位编码的所有65,536 个字符并不能完全表示全世界所有正在使用或曾经使用的字符。于是,Unicode 标准已扩展到包含多达 1,112,064 个字符。那些超出原来的 16 位限制的字符被称作增补字符。Unicode 标准 2.0 版是第一个包含启用增补字符设计的版本,但是,直到 3.1 版才收入第一批增补字符集。由于 J2SE 的 5.0 版必须支持 Unicode 标准 4.0 版,因此它必须支持增补字符。
    对增补字符的支持也可能会成为东亚市场的一个普遍商业要求。政府应用程序会需要这些增补字符,以正确表示一些包含罕见中文字符的姓名。出版应用程序可能会需要这些增补字符,以表示所有的古代字符和变体字符。中国政府要求支持GB18030(一种对整个 Unicode字符集进行编码的字符编码标准),因此,如果是Unicode 3.1版或更新版本,则将包括增补字符。台湾标准 CNS-11643包含的许多字符在 Unicode 3.1 中列为增补字符。香港政府定义了一种针对粤语的字符集,其中的一些字符是Unicode中的增补字符。最后,日本的一些供应商正计划利用增补字符空间中大量的专用空间收入50,000多个日文汉字字符变体,以便从其专有系统迁移至基于Java平台的解决方案。
    因此,Java平台不仅需要支持增补字符,而且必须使应用程序能够方便地做到这一点。由于增补字符打破了Java编程语言的基础设计构想,而且可能要求对编程模型进行根本性的修改,因此,Java Community Process召集了一个专家组,以期找到一个适当的解决方案。该小组被称为JSR-204专家组,使用 Unicode增补字符支持的Java 技术规范请求的编号。从技术上来说,该专家组的决定仅适用于J2SE平台,但是由于 Java 2平台企业版 (J2EE)处于 J2SE 平台的最上层,因此它可以直接受益,我们期望Java 2平台袖珍版 (J2ME)的配置也采用相同的设计方法。
4.2.3.2 Java平台中增补字符的设计方法
JSR-204 专家组必须作出的主要决定是如何在Java API中表示增补字符,包括单个字符和所有形式的字符序列。专家组考虑并排除了多种方法:
* 重新定义基本类型char,使其具有32位,这样也会使所有形式的 char序列成为UTF-32序列;
* 在现有的16位类型char的基础上,为字符引入一种新的32位基本类型(例如,char32)。所有形式的Char序列均基于 UTF-16;
* 在现有的16位类型char的基础上,为字符引入一种新的32位基本类型(例如,char32)。String和StringBuffer 接受并行API,并将它们解释为UTF-16序列或UTF-32序列;其他char序列继续基于UTF-16;
* 使用int表示增补的代码点。String和StringBuffer接受并行API,并将它们解释为UTF-16序列或UTF-32序列;其他char序列继续基于UTF-16;
* 使用代理char对,表示增补代码点。所有形式的char序列基于UTF-16;
* 引入一种封装字符的类。String和StringBuffer接受新的API,并将它们解释为此类字符的序列;
* 使用一个CharSequence实例和一个索引的组合表示代码点。
(表4-1 专家组考虑的增补字符设计方法)
在这些方法中,一些在早期就被排除了。例如,重新定义基本类型 char,使其具有32位,这对于全新的平台可能会非常有吸引力,但是,对于J2SE来说,它会与现有的Java虚拟机、序列化和其他接口不兼容,更不用说基于 UTF-32的字符串要使用两倍于基于UTF-16的字符串的内存了。添加一种新类型的 char32可能会简单一些,但是仍然会出现虚拟机和序列化方面的问题。而且,语言更改通常需要比API更改有更长的提前期,因此,前面两种方法会对增补字符支持带来无法接受的延迟。为了在余下的方法中筛选出最优方案,实现小组使用四种不同的方法,在大量进行低层字符处理的代码(java.util.regex包)中实现了对增补字符支持,并对这四种方法的难易程度和运行表现进行了比较。最终,专家组确定了一种分层的方法:
* 使用基本类型int在低层API中表示代码点,例如Character类的静态方法。
* 将所有形式的char序列均解释为UTF-16序列,并促进其在更高层级API中的使用。
* 提供API,以方便在各种char和基于代码点的表示法之间的转换。
(表4-2 专家组最终确定的增补字符设计方法)
在需要时,此方法既能够提供一种概念简明且高效的单个字符表示法,又能够充分利用通过改进可支持增补字符的现有API。同时,还能够促进字符序列在单个字符上的应用,这一点一般对于国际化的软件很有好处。
4.2.3.2 开放的增补字符——基于代码点的API
    新增的低层API分为两大类:用于各种char和基于代码点的表示法之间转换的方法和用于分析和映射代码点的方法。
    最基本的转换方法是Character.toCodePoint(char high, char low)(用于将两个UTF-16代码单元转换为一个代码点)和 Character.toChars(int codePoint)(用于将指定的代码点转换为一个或两个 UTF-16 代码单元,然后封装到一个char[]内。不过,由于大多数情况下文本以字符序列的形式出现,因此,另外提供codePointAt和codePointBefore方法,用于将代码点从各种字符序列表示法中提取出来:Character.codePointAt(char[] a, int index)和String.codePointBefore(int index)是两种典型的例子。在将代码点插入字符序列时,大多数情况下均有一些针对StringBuffer和StringBuilder类的appendCodePoint(int codePoint)方法,以及一个用于提取表示代码点的int[]的String构建器。
    几种用于分析代码单元和代码点的方法有助于转换过程:Character类中的 isHighSurrogate和isLowSurrogate方法可以识别用于表示增补字符的char 值;charCount(int codePoint)方法可以确定是否需要将某个代码点转换为一个或两个char。
    但是,大多数基于代码点的方法均能够对所有Unicode字符实现基于char的旧方法对BMP字符所实现的功能。以下是一些典型例子:
* Character.isLetter(int codePoint)可根据Unicode标准识别字母;
* Character.isJavaIdentifierStart(int codePoint)可根据Java语言规范确定代码点是否可以启动标识符;
* Character.UnicodeBlock.of(int codePoint)可搜索代码点所属的Unicode字符子集;
* Character.toUpperCase(int codePoint)可将给定的代码点转换为其大写等值字符。尽管此方法能够支持增补字符,但是它仍然不能解决根本的问题,即在某些情况下,逐个字符的转换无法正确完成。例如,德文字符“ß”应该转换为“SS”,这需要使用String.toUpperCase方法;
(表4-3 代码点方法对BOM字符的处理实例)
注意:大多数接受代码点的方法并不检查给定的int值是否处于有效的Unicode代码点范围之内(如上所述,只有0x0至0x10FFFF之间的范围是有效的)。在大多数情况下,该值是以确保其有效的方法产生的,在这些低层API中反复检查其有效性可能会对系统性能造成负面的影响。在无法确保有效性的情况下,应用程序必须使用Character.isValidCodePoint方法确保代码点有效。大多数方法对于无效的代码点采取的行为没有特别加以指定,不同的实现可能会有所不同。
    API包含许多简便的方法,这些方法可使用其他低层的API实现,但是专家组觉得,这些方法很常用,将它们添加到J2SE平台上很有意义。不过,专家组也排除了一些建议的简便方法,这给我们提供了一次展示自己实现此类方法能力的机会。例如,专家组经过讨论,排除了一种针对String类的新构建器(该构建器可以创建一个保持单个代码点的String)。以下是使应用程序使用现有的API提供功能的一种简便方法:
/**
 * 创建仅含有指定代码点的新 String。
 */
String newString(int codePoint) {
    return new String(Character.toChars(codePoint));
}
(代码4-3 使应用程序使用现有的API提供功能的一种简便方法)
您会注意到,在这个简单的实现中,toChars方法始终创建一个中间数列,该数列仅使用一次即立即丢弃。如果该方法在您的性能评估中出现,您可能会希望将其优化为针对最为普通的情况,即该代码点为BMP字符:
/**
 * 创建仅含有指定代码点的新String。
 * 针对BMP字符优化的版本。
 */
String newString(int codePoint) {
    if (Character.charCount(codePoint) == 1) {
        return String.valueOf((char) codePoint);
    } else {
        return new String(Character.toChars(codePoint));
    }
}
(代码4-4 使应用程序使用现有的API提供功能的最为普通的情况)
或者,如果您需要创建许多个这样的string,则可能希望编写一个重复使用toChars方法所使用的数列的通用版本:
/**
 * 创建每一个均含有一个指定
 * 代码点的新 String。
 * 针对 BMP 字符优化的版本。
 */
String[] newStrings(int[] codePoints) {
    String[] result = new String[codePoints.length];
    char[] codeUnits = new char[2];
    for (int i = 0; i < codePoints.length; i++) {
         int count = Character.toChars(codePoints[i], codeUnits, 0);
         result[i] = new String(codeUnits, 0, count);
    }
    return result;
}
(代码4-5 toChars方法所使用的数列的通用版本)
不过,最终您可能会发现,您需要的是一个完全不同的解决方案。新的构建器String(intcodePoint)实际上建议作为String.valueOf(char)的一个基于代码点的备选方案。在很多情况下,此方法用于消息生成的环境,例如:
System.out.println("Character " + String.valueOf(char) + " is invalid.");
(代码4-6 消息生成的环境)
新的格式化API支持增补文字,提供一种更加简单的备选方案:
System.out.printf("Character %c is invalid.%n", codePoint);
(代码4-7 增补文字的备选方案)
使用此高层API不仅简捷,而它有很多特殊的优点:它可以避免串联(串联会使消息很难本地化),并将需要移进资源包(resourcebundle)的字符串数量从两个减少到一个。
    在Java编程语言源文件中,如果使用可以直接表示增补字符的字符编码,则使用增补字符最为方便。UTF-8是最佳的选择。在所使用的字符编码无法直接表示字符的情况下,Java编程语言提供一种Unicode转义符语法。此语法没有经过增强,无法直接表示增补字符。而是使用两个连续的Unicode转义符将其表示为UTF-16字符表示法中的两个编码单元。例如,字符U+20000写作“/uD840/uDC00”。最好是写入支持所需增补字符的编码,然后使用一种工具(如native2ascii)将其转换为转义序列。
    遗憾的是,由于其编码问题,属性文件仍局限于ISO8859-1(除非您的应用程序使用新的XML格式)。这意味着您始终必须对增补字符使用转义序列,而且可能要使用不同的编码进行编写,然后使用诸如native2ascii的工具进行转换。
4.2.3.3 经修订的UTF-8
    Java平台对经修订的UTF-8已经很熟悉,但是,问题是应用程序开发人员在可能包含增补字符的文本和UTF-8之间进行转换时需要更加留神。需要特别注意的是,某些J2SE接口使用的编码与UTF-8相似但与其并不兼容。以前,此编码有时被称为“JavamodifiedUTF-8”(经Java修订的UTF-8)或(错误地)直接称为“UTF-8”。对于J2SE5.0,其说明文档正在更新,此编码将统称为“modifiedUTF-8”(经修订的UTF-8)。
    经修订的UTF-8和标准UTF-8之间之所以不兼容,其原因有两点。其一,经修订的UTF-8将字符U+0000表示为双字节序列0xC00x80,而标准UTF-8使用单字节值0x0。其二,经修订的UTF-8通过对其UTF-16表示法的两个代理代码单元单独进行编码表示增补字符。每个代理代码单元由三个字节来表示,共有六个字节。而标准UTF-8使用单个四字节序列表示整个字符。
    Java虚拟机及其附带的接口(如Java本机接口、多种工具接口或Java类文件)在java.io.DataInput和DataOutput接口和类中使用经修订的UTF-8实现或使用这些接口和类,并进行序列化。Java本机接口提供与经修订的UTF-8之间进行转换的例程。而标准UTF-8由String类、java.io.InputStreamReader和OutputStreamWriter类、java.nio.charset设施(facility)以及许多其上层的API提供支持。
    由于经修订的UTF-8与标准的UTF-8不兼容,因此切勿同时使用这两种版本的编码。经修订的UTF-8只能与上述的Java接口配合使用。在任何其他情况下,尤其对于可能来自非基于Java平台的软件的或可能通过其编译的数据流,必须使用标准的UTF-8。需要使用标准的UTF-8时,则不能使用Java本机接口例程与经修订的UTF-8进行转换。
4.2.3.4 在应用程序内支持增补字符
    对于仅以各种形式char序列([char[]、java.lang.CharSequence实现、java.text.CharacterIterator实现)处理文本和仅使用接受和退回序列(如char序列)的JavaAPI的应用程序,可能根本不需要进行任何更改。Java平台API的实现应该能够处理增补字符。
    对于本身解释单个字符、将单个字符传送给Java平台API或调用能够返回单个字符的方法的应用程序,则需要考虑这些字符的有效值。在很多情况下,往往不要求支持增补字符。例如,如果某应用程序搜索char序列中的HTML标记,并逐一检查每个char,它会知道这些标记仅使用BasicLatin字符子集中的字符。如果所搜索的文本含有增补字符,则这些字符不会与标记字符混淆,因为UTF-16使用代码单元表示增补字符,而代码单元的值不会用于BMP字符。
    只有在某应用程序本身解释单个字符、将单个字符传送给Java平台API或调用能够返回单个字符的方法且这些字符可能为增补字符时,才必须更改该应用程序。在提供使用char序列的并行API时,最好转而使用此类API。在其他情况下,有必要使用新的API在char和基于代码点的表示法之间进行转换,并调用基于代码点的API。当然,如果您发现在J2SE5.0中有更新、更方便的API,使您能够支持增补字符并同时简化代码(如上格式化范例中所述),则没有必要这样做。
    您可能会犹豫,是将所有文本转换为代码点表示法(即int[])然后在该表示法中处理,还是在大多数情况下仍采用char序列,仅在需要时转换为代码点,两者之间孰优孰劣很难确定。当然,总体来说,Java平台API相对于char序列肯定具有一定的优势,而且采用Java平台API可以节省内存空间。
    对于需要与UTF-8之间进行转换的应用程序,还需要认真考虑是需要标准的UTF-8还是经修订的UTF-8,并针对每种UTF-8采用适当的Java平台。“经修订的UTF-8”部分介绍进行正确选择所需的信息。
 
参考文献:
[1] 维基百科.UTF-8[EB/OL]. http://zh.wikipedia.org/w/index.php?title=UTF-8&vari
ant=zh-cn,2007-06-04.
[2] 维基百科.UNICODE[EB/OL]. http://zh.wikipedia.org/w/index.php?title=Unicode&
variant=zh-cn,2007-07-08.
[3] 百度.UNICODE百科[EB/OL]. http://baike.baidu.com/view/40801.htm,2005-07-18.
[4] Norbert Lindenberg.java中的增补字符[EB/OL]. http://gceclub.sun.com.cn/devel
oper/technicalArticles/Intl/Supplementary/index_zh_CN.html,2004-05-16.

你可能感兴趣的:(字符编码,UTF-8)