深⼊学习Java中的字符串,代码点和代码单元
在Java字符串处理时,在使⽤length和charAt⽅法时,应该格外⼩⼼,因为length返回的是UTF-16编码表⽰下的代码单元数量,⽽⾮我们所认为的字符的个数,charAt⽅法返回的是指定位置处的代码单元,⽽⾮我们所认为的字符。
⾄于为什么都是“代码单元”⽽⾮字符,这和Unicode字符集的增补相关,具体的参看下⾯的附录。
要想获得字符串中的字符的个数,应当使⽤dePointCount(0, aString.length());要想获得指定位置处的字符,使⽤
枚举字符串的正确⽅法:
for (int i = 0; i < aString.length();) {
int character = dePointAt(i);
if (Character.isSupplementaryCodePoint(character)) i += 2;
else ++i;
}
将codePoint转换为char[]可调⽤Chars⽅法,然后可进⼀步转换为字符串:
String Chars(codePoint));
附录A:
《Java核⼼技术》中关于字符和字符串的讲解:
3.3.3 char类型
char类型⽤于表⽰单个字符。通常⽤来表⽰字符常量。例如:'A'是编码为65所对应的字符常量。与"A"不同,"A"是⼀个包含字符A的字符串。Unicode编码单元可以表⽰为⼗六进制值,其范围从\u0000到\uffff。例如:。\u2122表⽰注册符号,\u03C0表⽰希腊字母π。
除了可以采⽤转义序列符\u表⽰Unicode代码单元的编码之外,还有⼀些⽤于表⽰特殊字符的转义序列符,请参看表3-3。所有这些转义序列符都可以出现在字符常量或字符串的引号内。例如,'\u2122'或"
Hello\n"。转义序列符\u还可以出现在字符常量或字符串的引号之外(⽽其他所有转义序列不可以)。例如:
public static void main(String\u005B\u005D args)
这种形式完全符合语法规则,\u005B和\u005D是[和]的编码。
表3-3 特殊字符的转义序列符
转义序列名称Unicode值
\b退格\u0008
\t制表\u0009
\n换⾏\u000a
\r回车\u000d
\"双引号\u0022
\'单引号\u0027
\\反斜杠\u005c
要想弄清char类型,就必须了解Unicode编码表。Unicode打破了传统字符编码⽅法的限制。在Unicode出现之前,已经有许多种不同的标准:美国的ASCII、西欧语⾔中的ISO 8859-1、俄国的KOI-8、中国的GB118030和BIG-5等等,这样就产⽣了下⾯两个问题:⼀个是对于任意给定的代码值,在不同的编码⽅案下有可能对应不同的字母;⼆是采⽤⼤字符集的语⾔其编码长度有可能不同。例如,有些常⽤的字符采⽤单字节编码,⽽另⼀些字符则需要两个或更多个字节。
设计Unicode编码的⽬的就是要解决这些问题。在20世纪80年代开始启动设计⼯作时,⼈们认为两个字节的代码宽度⾜以能够对世界上各种语⾔的所有字符进⾏编码,并有⾜够的空间留给未来的扩展。在1991年发布了Unicode 1.0,当时仅占⽤65 536个代码值中不到⼀半的部分。在设计Java时决定采⽤16位的Unicode字符集,这样会⽐使⽤8位字符集的程序设计语⾔有很⼤的改进。
⼗分遗憾,经过⼀段时间,不可避免的事情发⽣了。Unicode字符超过了65 536个,其主要原因是增加了⼤量的汉语、⽇语和韩国语⾔中的表意⽂字。现在,16位的char类型已经不能满⾜描述所有Unicode字符的需要了。
下⾯利⽤⼀些专⽤术语解释⼀下Java语⾔解决这个问题的基本⽅法。从JDK 5.0开始,代码点(code point)是指与⼀个编码表中的某个
字符对应的代码值。在Unicode标准中,代码点采⽤⼗六进制书写,并加上前缀U+,例如U+0041就是字母A的代码点。Unicode代码点可以分成17个代码级别(code plane)。第⼀个代码级别称为基本的多语⾔组别(basic multilingual plane),代码点从U+0000到U+FFFF,其中包栝了经典的Unicode代码。其余的16个附加级别,代码点从U+10000到U+10FFFF,其中包栝了⼀些辅助字符(supplementarycharacter)。
UTF-16编码采⽤不同长度的编码表⽰所有Unicode代码点。在基本的多语⾔级别中,每个字符⽤16位表⽰,通常被称为代码单元(code unit);⽽辅助字符采⽤对连续的代码单元进⾏编码。这样构成的编码值⼀定落⼊基本的多语⾔级别中空闲的2048字节内,通常被称为替代区域(surrogate area)[U+D800~U+DBFF⽤于第⼀个代码单元,U+DC00〜U+DFFF⽤于第⼆个代码单元]。这样设计⼗分巧妙,我们可以从中迅速地知道⼀个代码单元是⼀个字符的编码,还是⼀个辅助字符的第⼀或第⼆部分。例如,对于整数集合的数学符号,它的代码点是U+1D568,并且是⽤两个代码单元U+D835和U+DD68编码的(存关编码算法的描述请参看/wiki/UTF-16)。
在Java中,char类型⽤UTF-16编码描述⼀个代码单元。
我们强烈建议不要在程序中使⽤char类型,除⾮确实需要对UTF-16代码单元进⾏操作。最好将需要处理的字符串⽤抽象数据类型表⽰(有关这⽅⾯的内容将在稍后讨论)。
3.6.6 代码点与代码单元
Java字符串由char序列组成。从前⾯已经看到,字符数据类型是⼀个采⽤UTF-16编码表⽰Unicode代码点的代码单元。⼤多数的常⽤Unicode字符使⽤⼀个代码单元就可以表⽰,⽽辅助字符需要⼀对代码单元表⽰。
length⽅法将返回采⽤UTF-16编码表⽰的给定字符串所需要的代码单元数量。例如:
Stringgreeting = "Hello";
int n = greeting.length();// is 5
要想得到实际的长度,即代码点数量,可以调⽤:
int cpCount =dePointCount(0, greeting.length());
调⽤s.charAt(n)将返回位置n的代码单元,n介于0~s.length()-1之间。例如:
char first =greeting.charAt(0); // first is 'H'
char last =greeting.charAt(4); // last is 'o'
要想得到第i个代码点,应该使⽤下列语句
int index =greeting.offsetByCodePoints(0, i);
int cp =dePointAt(index);
注释:Java以独特的风格对字符串中的代码单元计数:字符串中的第⼀个代码单元位置为0。这种习愤起源于C,这样处理主要出于技术上的原因。具体理由似乎已经淡忘,⽽⿇烦却保留了下来。但是,许多程序员习惯于这种风格,因⽽Java设计者也就将其保留了下来。
为什么会对代码单元如此⼤惊⼩怪?请考虑下列语句:
Ƶis the set of integers
使⽤UTF-16编码表⽰Ƶ需要两个代码单元。调⽤char ch =sentence.charAt(1);返回的不是空格,⽽是第⼆个代码单元Z。为了避免这种情况的发⽣,请不要使⽤char类型。这太低级了。
如果想要遍历⼀个字符串,并且依次査看每⼀个代码点,可以使⽤下列语句:
int cp =dePointAt(i);
if (Character.isSupplementaryCodePoint(cp))i += 2;
else i++;
⾮常幸运,codePointAt⽅法能够辨别⼀个代码单元是辅助字符的第⼀部分还是第⼆部分,并能够返回正确的结果。也就是说,可以使⽤下列语句实现回退操作:
i--;
int cp =dePointAt(i);
if (Character.isSupplementaryCodePoint(cp))i--;
背景
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平台,但是由于Java2平台企业版(J2EE)处于J2SE平台的最上层,因此它可以直接受益,我们期望Java2平台袖珍版(J2ME)的配置也采⽤相同的设计⽅法。
不过,在了解JSR-204专家组确定的解决⽅案之前,我们需要先理解⼀些术语。
java技术专家代码点、字符编码⽅案、UTF-16:这些是指什么?
不幸的是,引⼊增补字符使字符模型变得更加复杂了。在过去,我们可以简单地说“字符”,在⼀个基于Unicode的环境(例如Java平台)中,假定字符有16位,⽽现在我们需要更多的术语。我们会尽量介绍得相对简单⼀些—如需了解所有详细的讨论信息,您可以阅读Unicode 标准第2章或Unicode技术报告17“字符编码模型”。Unicode专业⼈⼠可略过所有介绍直接参阅本部分中的最后定义。
字符是抽象的最⼩⽂本单位。它没有固定的形状(可能是⼀个字形),⽽且没有值。“A”是⼀个字符,“€”(德国、法国和许多其他欧洲国家通⽤货币的标志)也是⼀个字符。
字符集是字符的集合。例如,汉字字符是中国⼈最先发明的字符,在中⽂、⽇⽂、韩⽂和越南⽂的书写中使⽤。
编码字符集是⼀个字符集,它为每⼀个字符分配⼀个唯⼀数字。Unicode标准的核⼼是⼀个编码字符集,字母“A”的编码为0041和字
符“€”的编码为20AC。Unicode标准始终使⽤⼗六进制数字,⽽且在书写时在前⾯加上前缀“U+”,所以“A”的编码书写为“U+0041”。
代码点是指可⽤于编码字符集的数字。编码字符集定义⼀个有效的代码点范围,但是并不⼀定将字符分配给所有这些代码点。有效的Unicode代码点范围是U+0000⾄U+10FFFF。Unicode 4.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字符。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论