最近注意到 Java 中char类型占用2个字节(16位长),而 Unicode 的编码范围是U+000000-U+10FFFF(21位长),两者之间有怎样的关系呢?

Unicode简介

Unicode是一个字符集,它对世界上大部分的文字系统进行了整理、编码,为每一个字符而非字形定义唯一的整数,这个整数称为码点(Code Point)。

20世纪80年代,为了给全世界所有的文字字符统一编码,出现了两个标准化组织:

后来他们发现对方的存在,大家为着相同的目的工作。于是在 1991 年,他们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。虽然实际上两者的字集编码相同,但实质上两者确实为两个不同的标准。Unicode 1.1 对应于 ISO 10646-1:1993,Unicode 3.0 对应于 ISO 10646-1:2000,Unicode 3.2 对应于 ISO 10646-2:2001,Unicode 4.0 对应于 ISO 10646:2003,Unicode 5.0 对应于 ISO 10646:2003 及附录 1–3。

Unicode 平面

当前的 Unicode 字符分为17组编排,每组称为一个平面(Plane),而每一个平面拥有65536(即$2^{16}$)个码点。然而当前只用了少数平面。在表示一个 Unicode 的字符时,通常会用“U+”然后紧接着一组十六进制的数字来表示这一个字符。

平面 始末字符值 中文名称 英文名称
0号平面 U+0000 - U+FFFF 基本多文种平面 Basic Multilingual Plane,简称BMP
1号平面 U+10000 - U+1FFFF 多文种补充平面 Supplementary Multilingual Plane,简称SMP
2号平面 U+20000 - U+2FFFF 表意文字补充平面 Supplementary Ideographic Plane,简称SIP
3号平面 U+30000 - U+3FFFF 表意文字第三平面 Tertiary Ideographic Plane,简称TIP
4号平面 至 13号平面 U+40000 - U+DFFFF (尚未使用)
14号平面 U+E0000 - U+EFFFF 特别用途补充平面 Supplementary Special-purpose Plane,简称SSP
15号平面 U+F0000 - U+FFFFF 保留作为私人使用区(A区) Private Use Area-A,简称PUA-A
16号平面 U+100000 - U+10FFFF 保留作为私人使用区(B区) Private Use Area-B,简称PUA-B

UTF

UTF是 Unicode/UCS Transformation Format 的缩写,是将 Unicode 码点映射到唯一字节序列的算法,根据映射方法的的不同,有 UTF-8、UTF-16 和 UTF-32 等具体的编码格式。

UTF-16

UTF-16把 Unicode 码点映射为16位长的整数(即 Code Unit,码元)的序列。UTF-16使用单个16位码元对最常见的63K字符进行编码,并使用一对16位码元(称为代理)对 Unicode 中不常用的1M字符进行编码。

Unicode 的第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0),其他平面称为辅助平面(Supplementary Planes)。基本平面内,从U+D800U+DFFF之间的码点区段是永久保留不映射到Unicode字符。UTF-16就利用保留下来的0xD800-0xDFFF区块的码点来对辅助平面的字符的码点进行编码。

Unicode 编码范围 UTF-16 编码形式(二进制)
U+000000 - U+00FFFF xxxx xxxx xxxx xxxx
U+010000 - U+10FFFF 1101 10yy yyyy yyyy 1101 11xx xxxx xxxx

可以看到,UTF-16 用二个字节来表示基本平面,用四个字节来表示辅助平面。

基本平面中的码点,UTF-16编码为16位的单个码元,数值等价于对应的码点。

辅助平面中的码点,在UTF-16中被编码为一对16位长的码元(即32位,4字节),称作代理对(Surrogate Pair),具体方法是:

  1. Unicode 码点减去 0x10000,得到的值的范围为20位长的 0...0xFFFFF
  2. 将高位的10位的值(值的范围为 0...0x3FF)加上 0xD80011011000 00000000) 得到第一个码元或称作高位代理(high surrogate),值的范围是 0xD800...0xDBFF。由于高位代理比低位代理的值要小,所以为了避免混淆使用,Unicode标准现在称高位代理为前导代理(lead surrogates)。
  3. 将低位的10位的值(值的范围也是 0...0x3FF)加上 0xDC0011011100 00000000) 得到第二个码元或称作低位代理(low surrogate),现在值的范围是 0xDC00...0xDFFF。由于低位代理比高位代理的值要大,所以为了避免混淆使用,Unicode标准现在称低位代理为后尾代理(trail surrogates)。

由于前导代理、后尾代理、BMP中的有效字符的码点,三者互不重叠,搜索是简单的:一个字符编码的一部分不可能与另一个字符编码的不同部分相重叠。这意味着UTF-16是自同步(self-synchronizing)的:可以通过仅检查一个码元来判定给定字符的下一个字符的起始码元。

UTF-8

UTF-8是一种可变长度字符编码格式,它的码元是8位的,即 UTF-8 把 Unicode 码点映射为字节序列。其编码形式如下表

码点的位数 码点范围 字节序列 Byte 1 Byte 2 Byte 3 Byte 4
7 U+0000 - U+007F 1 0xxxxxxx
11 U+0080 - U+07FF 2 110xxxxx 10xxxxxx
16 U+0800 - U+FFFF 3 1110xxxx 10xxxxxx 10xxxxxx
21 U+10000 - U+10FFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

每个使用 UTF-8 存储的字符,除了第一个字节外,其余字节的最高两位都是以"10"开始。

在ASCII码的范围,用一个字节表示,超出 ASCII 码的范围就用多个字节表示,这就形成了我们上面看到的UTF-8的表示方法。这样的好处是当文本文件中只有 ASCII 字符(ASCII 编码与其在 Unicode 中的编码相同)时,存储的文件每个字符都为一个字节,与旧的普通 ASCII 文件无异,读取的时候也是如此,所以能与以前的 ASCII 文件兼容。

大于 ASCII 码的,就会由第一字节的前几位表示该字符编码字节序列的长度,比如110xxxxx前三位的二进制表示告诉我们这是个2字节长的序列;1110xxxx是个3字节长的的序列,依此类推;xxx的位置由字符编码的二进制表示的位填入。越靠右的x具有越少的特殊意义。只用最短的那个足够表达一个字符编码数的多字节串。在多字节串中,第一个字节的开头"1"的数目就是该字符编码字节的数目。

Java中的char类型

Java 中的 char 类型的长度为2字节,其存放的是 Unicode 码点,范围为U+0000 - U+FFFF ,即 Java 中的 char 类型只能表示 Unicode 标准中的基本平面(BMP)的字符。由于早期的 Unicode 标准(1991年到1995年,Unicode 2.0以前)使用了固定16位长的编码形式,char类型长度设计为16位也就不难理解了。

另外 Java 中的字符串使用了UTF-16 的编码(The Java programming language represents text in sequences of 16-bit code units, using the UTF-16 encoding.),此处指的是 Java 语言的内码是UTF-16 编码。所谓内码,指的是程序内部使用的字符编码,特别是某种语言的字符和字符串在内存里用的编码;相对地,外码是程序与外部交互时外部使用的字符编码。

参考资料

https://zh.wikipedia.org/wiki/Unicode

https://home.unicode.org/

https://zh.wikipedia.org/wiki/UTF-16

https://zh.wikipedia.org/wiki/UTF-8

https://docs.oracle.com/javase/specs/jls/se13/html/jls-3.html