1.5.6 码点和代码单元
Java第一次发布时,就非常自豪地采纳了同样是新生事物的Unicode标准。Unicode标准旨在解决字符编码这个非常棘手的问题。在Unicode之前,有许多互相不兼容的字符编码。以英语为例,有几乎可以作为通用标准的7位ASCII编码标准,该标准为所有英文字母、十进制数字和许多符号分配了介于0~127的编码。在西欧,ASCII还被扩展为8位代码,用来容纳类似ä和é等重音字符。在俄罗斯,ASCII也同样被扩展,俄罗斯使用128~255的位置表示一些斯拉夫字符。在日本,通常使用可变长度编码对英语和日语字符进行编码。此外,还有多种不兼容的中文字符编码也在被广泛使用。总之,在使用不同编码的情况下交换文件是一个很困难的问题。
Unicode通过介于0~65535的唯一的16位编码,来对所有书写系统的每个字符分配唯一的编码,来解决困扰大家已久的字符编码问题。1991年,Unicode 1.0发布,该标准使用了略少于半数的有效65536编码。Java从一开始就被设计成使用16位Unicode字符的系统,这一点对比其他使用传统8位字符编码的编程语言,是一个重大进步。但随后又发生了一些尴尬的事情,即汉字的数量远远超过了之前的预估值,这就迫使Unicode必须使用超过16位的编码方案。
如今,Unicode需要21位进行编码。每个有效的Unicode值称为码点(code point),其基本形式为U+与其后的4个或多个十六进制的数字。例如,字符A的码点是U+0041,而表示八元数集合的数学符号的码点是U+1D546 。
还有一种更加清楚的方式来表示码点,例如使用int值,但这显然是非常浪费的。Java使用一种变长的编码形式,称为UTF-16,它将所有“经典”的Unicode字符表示为单个16位的值,此外对于所有超过U+FFFF的字符编码,都需要通过一个16位的值组合配对表示,这个16位的值表示一个特殊的代码区域,通常被称为“代理字符”。在UTF-16编码中,字符A可以通过一个char值来表示,记作\u0041;而会被记作一对char值\ud835\udd46。
换句话说,char并不是Unicode字符或码点。它只是一个代码单元(code unit),是UTF-16编码中所使用的一个16位的量。
如果你并不使用中国的汉字,并且愿意把等特殊字符抛在脑后的话,那么字符串是一个Unicode字符序列的事情就对你没有太大影响,你当它是一个神话传说就行。在这种情况下,可以这样获得第 i个字符:
char ch = str.charAt(i);
也可以这样获取字符串的长度:
int length = str.length();
但是如果你想正确地处理字符串,那么必须工作得更加辛苦一些。例如,要获取Unicode的第i个码点,需要调用:
int codePoint = str.codePointAt(str.offsetByCodePoints(0, i));
码点总数为:
int length = str.codePointCount(0, str.length());
循环提取每一个码点:
int i = 0; while (i < s.length()) { int cp = sentence.codePointAt(i); i += Character.charCount(cp); ... // Do something with cp }
或者,也可以使用codePoints方法来生成一个int值的流(stream),这样每个int值都对应一个码点。我们将在第8章中讨论流。你也可以将流转换为一个数组,如:
int[] codePoints = str.codePoints().toArray();
注意:过去,字符串总是在内部采用UTF-16编码表示,以char值数组的形式来表示。现在,String对象会尽可能地以ISO-8859-1字符的byte数组的形式来表示。未来版本的Java内部可能会改用UTF-8。