Java中的代码点和代码单元

你真的懂Java中的char类型吗?你知道为什么会有人强烈建议不要使用char类型吗?

我们看一下《Java核心技术第一卷》中对char类型、代码点和代码单元的解读:

《Java核心技术》中关于字符和字符串的讲解:

3.3.3char类型

char类型用于表示单个字符。通常用来表示字符常量。例如:'A'是编码为65所对应的字符常量。与"A"不同,"A"是一个包含字符A的字符串。Unicode编码单元可以表示为十六进制值,其范围从\u0000到\uffff。例如:。\u2122表示注册符号,\u03C0表示希腊字母π。

除了可以采用转义序列符\u表示Unicode代码单元的编码之外,还有一些用于表示特殊字符的转义序列符,请参看表3-3。所有这些转义序列符都可以出现在字符常量或字符串的引号内。例如,'\u2122'或"Hello\n"。转义序列符\u还可以出现在字符常量或字符串的引号之外(而其他所有转义序列不可以)。例如:

public static voidmain(String\u005B\u005D args)

这种形式完全符合语法规则,\u005B和\u005D是[和]的编码。

要想弄清char类型,就必须了解Unicode编码表。Unicode打破了传统字符编码方法的限制。在Unicode出现之前,已经有许多种不同的标准:美国的ASCII、西欧语言中的ISO 8859-1、俄国的KOI-8、中国的GB118030和BIG-5等等,这样就产生了下面两个问题:一个是对于任意给定的代码值,在不同的编码方案下有可能对应不同的字母;二是采用大字符集的语言其编码长度有可能不同。例如,有些常用的字符采用单字节编码,而另一些字符则需要两个或更多个字节。

设计Unicode编码的目的就是要解决这些问题。在20世纪80年代开始启动设计工作时,人们认为两个字节的代码宽度足以能够对世界上各种语言的所有字符进行编码,并有足够的空间留给未来的扩展。在1991年发布了Unicode 1.0,当时仅占用65536个代码值中不到一半的部分。在设计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编码的(存关编码算法的描述请参看http://en.wikipe-dia.org/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=greeting.codePointCount(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=greeting.codePointAt(index);

注释:Java以独特的风格对字符串中的代码单元计数:字符串中的第一个代码单元位置为。这种习愤起源于C,这样处理主要出于技术上的原因。具体理由似乎已经淡忘,而麻烦却保留了下来。但是,许多程序员习惯于这种风格,因而Java设计者也就将其保留了下来。

为什么会对代码单元如此大惊小怪?请考虑下列语句:

Ƶis the set of integers

使用UTF-16编码表示Ƶ需要两个代码单元。调用charch =sentence.charAt(1);返回的不是空格,而是第二个代码单元Z。为了避免这种情况的发生,请不要使用char类型。这太低级了。

如果想要遍历一个字符串,并且依次査看每一个代码点,可以使用下列语句:

int cp=sentence.codePointAt(i);

if(Character.isSupplementaryCodePoint(cp))i += 2;

else i++;

非常幸运,codePointAt方法能够辨别一个代码单元是辅助字符的第一部分还是第二部分,并能够返回正确的结果。也就是说,可以使用下列语句实现回退操作:

i--;

int cp=sentence.codePointAt(i);

if(Character.isSupplementaryCodePoint(cp))i--;

继续说:

Java号称对Unicode提供天然的支持,这话在很久很久以前就已经是假的了(不过曾经是真的),实际上,到JDK5.0为止,Java才算刚刚跟上Unicode的脚步,开始提供对增补字符的支持。

现在的Unicode码空间为U+0000到U+10FFFF,一共1114112个码位,其中只有1,112,064个码位是合法的(我来替你做算术,有2048个码位不合法),但并不是说现在的Unicode就有这么多个字符了,实际上其中很多码位还是空闲的,到Unicode 4.0规范为止,只有96,382个码位被分配了字符(但无论如何,仍比很多人认为的65536个字符要多得多了)。其中U+0000到U+FFFF的部分被称为基本多语言面(Basic Multilingual Plane,BMP)。U+10000及以上的字符称为补充字符。在Java中(Java1.5之后),补充字符使用两个char型变量来表示,这两个char型变量就组成了所谓的surrogatepair(在底层实际上是使用一个int进行表示的)。第一个char型变量的范围称为“高代理部分”(high-surrogates range,从"uD800到"uDBFF,共1024个码位),第二个char型变量的范围称为low-surrogates range(从"uDC00到"uDFFF,共1024个码位),这样使用surrogate pair可以表示的字符数一共是1024的平方计1048576个,加上BMP的65536个码位,去掉2048个非法的码位,正好是1,112,064个码位。

关于Unicode的码空间实际上有一些稍不小心就会让人犯错的地方。比如我们都知道从U+0000到U+FFFF的部分被称为基本多语言面(Basic Multilingual Plane,BMP),这个范围内的字符在使用UTF-16编码时,只需要一个char型变量就可以保存。仔细看看这个范围,应该有65536这么大,因此你会说单字节的UTF-16编码能够表示65536个字符,你也会说Unicode的基本多语言面包含65536个字符,但是再想想刚才说过的surrogatepair,一个UTF-16表示的增补字符(再一次的,需要两个char型变量才能表示的字符)怎样才能被正确的识别为增补字符,而不是两个普通的字符呢?答案你也知道,就是通过看它的第一个char是不是在高代理范围内,第二个char是不是在低代理范围内来决定,这也意味着,高代理和低代理所占的共2048个码位(从0xD800到0xDFFF)是不能分配给其他字符的。

但这是对UTF-16这种编码方法而言,而对Unicode这样的字符集呢?在Unicode的编号中,U+D800到U+DFFF是否有字符分配?答案是也没有!这是典型的字符集为方便编码方法而做的安排(你问他们这么做的目的?当然是希望基本多语言面中的字符和一个char型的UTF-16编码的字符能够一一对应,少些麻烦,从中我们也能看出UTF-16与Unicode间很深的渊源与结合)。也就是说,无论Unicode还是UTF-16编码后的字符,在0x0000至0xFFFF这个范围内,只有63488个字符。这就好比最初的CPU被勉强拿来做多媒体应用,用得多了,CPU就不得不修正自己从硬件上对多媒体应用提供支持了。

尽管不情愿,但说到这里总还得扯扯相关的概念:代码点和代码单元。

代码点(Code Point)就是指Unicode中为字符分配的编号,一个字符只占一个代码点,例如我们说到字符“汉”,它的代码点是U+6C49。

代码单元(Code Unit)则是针对编码方法而言,它指的是编码方法中对一个字符编码以后所占的最小存储单元。例如UTF-8中,代码单元是一个字节,因为一个字符可以被编码为1个,2个或者3个4个字节;在UTF-16中,代码单元变成了两个字节(就是一个char),因为一个字符可以被编码为1个或2个char(你找不到比一个char还小的UTF-16编码的字符,嘿嘿)。说得再罗嗦一点,一个字符,仅仅对应一个代码点,但却可能有多个代码单元(即可能被编码为2个char)。

以上概念绝非学术化的绕口令,这意味着当你想以一种统一的方式指定自己使用什么字符的时候,使用代码点(即你告诉你的程序,你要用Unicode中的第几个字符)总是比使用代码单元更好(因为这样做的话你还得区分情况,有时候提供一个16进制数字,有时候要提供两个)。

例如我们有一个增补字符???(哈哈,你看到了三个问号对吧?因为我的系统显示不出这个字符),它在Unicode中的编号是U+2F81A,当在程序中需要使用这个字符的时候,就可以这样来写:

后面的for循环把这个字符的UTF-16编码打印了出来,结果是

d87edc1a

注意到了吗?这个字符变成了两个char型变量,其中0xd87e就是高代理部分的值,0xdc1a就是低代理的值。

再谈谈Java字符串:

Java字符串由char序列组成。在Java字符串处理时,在使用length和charAt方法时,应该格外小心,因为length返回的是UTF-16编码表示下的代码单元数量,而非我们所认为的字符的个数,charAt方法返回的是指定位置处的代码单元,而非我们所认为的字符。

如果要想获得字符串中的字符的个数(即字符串的实际长度),则应当使用String.codePointCount(0, String.length())。

最后,来点代码,并请仔细阅读备注,就不对代码做解释工作了:

  • 发表于:
  • 原文链接:http://kuaibao.qq.com/s/20180226G00Q6M00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券