Java的字符类型采用的是UTF-16编码方式对Unicode编码表进行表示。其中一个char类型固定2Bytes(16bits)。首先先介绍一下Unicode编码表和UTF-16编码算法:
Unicode编码表的专业术语:
代码点 (code point): 指在Unicode编码表中一个字符所对应的代码值。如汉字“一”的代码点是U+4E00,英文字母“A”的代码点是U+0041。
代码单元( code unit): 规定16bits的存储容量就是一个代码单元。
Unicode编码表 分为17个代码级别 (code plane),其中代码点/-/?为第一级别 ——基本多语言级别 (basic multiling l plane),可以用一个代码单元存储一个代码点。其余16个附加级别 从0x10000-0x10FFFF(需要两个代码单元)。其中需要指出的是在多语言级别中,U+D800-U+DFFF这2048值没有表示任何字符,被称为Unicode的替代区域(surrogate area)。UTF-16正是的运用了这一区域,用2个代码单元(2*16bits)巧妙的表示出20bits代码点的Unicode附加级别。
UTF-16编码算法 假设U是一个代码点,也就是Unicode编码表中一个字符所对应的Unicode值。
1) 如果U<U+10000,也就是处于Unicode的基本多语言级别中。这样16bits(一个代码单元)就足够表示出字符的Unicode值。
2) 如果U+10FFFF>U>=U+10000,也就是处于附加级别中。UTF-16用2个16位来表示出了,并且正好将每个16位都控制在替代区域U+D800-U+DFFF 中了,具体操作如下:
分别初始化2个16位无符号的整数 —— W1和W2。其中W1=110110yyyyyyyyyy(0xD800-0xDBFF),W2 = 110111xxxxxxxxxx(0xDC00-OxDFFF)。然后,将U的高10位分配给W1的低10位,将U的低10位分配给W2的低10位。这样就可以将20bits的代码点U拆成两个16bits的代码单元。而且这两个代码点正好落在替代区域U+D800-U+DFFF中。
具体举个例子:代码点U+1D56B(一个整数集的算术符号Z)
0x1D56B= 0001 1101 0101 0110 1011
将0x1D56B的高10位0001 1101 01分配给W1的低10位组合成110110 0001 1101 01=0xD875
将0x1D56B的低10位01 0110 1011分配给W2的低10位组合成110111 01 0110 1011=0xDD6B
这样代码点U+1D56B采用UTF-16编码方式,用2个连续的代码单元U+D875和U+DD68表示出了。
Java的char类型是固定16bits的。代码点在U+0000 — U+FFFF之内到是可以用一个char完整的表示出一个字符。但代码点在U+FFFF之外的,一个char无论如何无法表示一个完整字符。这样用 char类型来获取字符串中的每一个字符就有问题了(针对那些含有代码点在U+FFFF之外的字符串)。我们还用U+1D56B举个例子:
假设程序中有一个字符串String str = new String(Character.toChars(0x1D56B)),str只有一个字符。注意我们不能用转义字符'/?'来表示,' /uXXXX'只能表示四位16进制数,也就是只能表示基本多语言级别的Unicode代码点。对于附加级别的代码点,我们只能用上面那种形式表示。
此时,char ch=str.charAt(0); 这个时候我们返回的结果是第一个代码单元,也就是代码点0xD875(上面已经讲过了U+1D56B采用UTF-16编码方式分成了U+D875和 U+DD6B,这样就需要两个char的容量来存储这个字符)。这种方法我们永远无法得到字符串中的这个附加级别字符。
我们可以这样来精确表示整个字符串中的每一个字符
int cp=str.codePointAt(0);
if(Charcter.isSupplementaryCodePoint(cp)) //判断Unicode 代码点是否在附加级别字符范围内。
i+=2;
else i++;
另外,String类中的length()方法也是对字符串进行char统计,也就是计算代码单元数量(代码单元有可能大于正真的字符数量)。如果要计算代码点数量,必须用str.codePointCount(0,str.length())方法。
由此可见,用char类型来处理字符串在有些情况下是不准确的,我们最好使用字符串类型中处理代码点的方法,不要使用处理char类型(一个代码单元)的方法。
我顺便提一下,虽然我们的很多系统支持Unicode编码表,但是这并不意味这我们能显示一些字符。比如要显示上面那个字符串 System.out.println(str);显示的就是一个'?',这主要是因为字符的显示需要有显示字库的支持,否则还是屏幕上什么都看不到。
附:有这样一个问题
char c='编'; // 我们都知道c占用2个字节,这毫无疑问。
String str=“遍”;
byte[] bytes=str.getBytes(); //这个打印出来,在不同的操作系统上可能有不同的长度。为什么呢?
我们可以去看看API, String 的getBytes()方法使用平台的默认字符集将此 String 编码为 byte 序列 。关于默认的字符集可以看看这篇文章《Java字符串与字符集的基本概念 》。
也就是说,你当前的OS默认支持的字符集使用的是UTF-8,则是3个字节。如果是gb2312/gbk,则2个字节。如果使用unicode/UTF- 16则4个字节(开头的两个字节是一个mark)。
千万不要认为char的占用字节数变了,注意Java标榜自己的一个重要性能就是平台无关性。固定长度的基本数据类型在Java中是永远占固定长度的,这一点和C有很大区别。