最近系统学习了下unicode,总结一下。
早期的对Unicode的介绍中,很多定义Unicode是16位,以弥补ASCII只能表示8位的不足。但事实上,Unicode最多可以支持32位。其最高位一定是0。然后接下来的7位共有128个数字,可以区分128个group。然后下面的8位,可以区分256个plane(平面)。剩下的16位,用来标示一个平面内的字符,最多有2的16次方即65536个。
最开始,Unicode只有一个平面,即BMP(Basic Multilingual Plane,基本多文种平面)。2001年3月的unicode3.1,才开始增加其他平面,即辅助平面(Supplementary Planes)。所以如果是上世纪末或本世纪早期已经开始编程工作的人,接触到的资料会说Unicode是一种16位的编码方式。但后来发现65536不足以表示所有字符(连汉字都涵盖不了),所以又搞了多个辅助平面。现在一共有16个辅助平面,加上BMP,总共有17个平面,涵盖了从0x00000到0x10FFFF的所有字符。这些数字,就是对应字符的Unicode编码。因为包括4个Byte,所以称为UCS-4(Universal Character Set,通用字符集)。除此之外,又有一个UCS-2编码。因为UCS-2只有两个bytes,所以只能表示BMP里面的字符,所以这种编码方式已经是obsolete了。
上面说的UCS-4和UCS-2是Unicode的编码方式,即不考虑计算机如何存储,是字符的一种数字表示。但同一个编码,在计算机中可以有不同的存储方式,这称为实现方式。
最自然的方式,就是让实现方式跟编码方式一样。我们可以在计算机中使用也32位来保存所有的字符。这就是UTF-32(UCS Transformation Format)实现方式。这里32表示使用32个bit来表示。优点是实现简单,也易读;缺点是浪费空间。绝大多数时间只是用BMP中的字符,使用16位就可以表达。而对于英语用户而言,8位的传统ASCII就基本可以满足。如果都要使用32位,则浪费很多空间。
为了解决这个问题,又有UTF-16和UTF-8两种实现方式。
先解释UTF-16。按照名字,UTF-16是使用16位bit来表示字符。这个理解是错的。正确的应该说最少使用16位bit来表示。事实上,对于BMP里的字符,是使用16位bit来表示;对于其他16个辅助plain,则使用32位来表示。因为BMP是最常用的,所以UTF-16基本上可以减少一半的内存使用。现在问题来了,如何判断一个16位bit(下面简称short)是一个独立的字符还是一个字符的前半段或者后半段呢?
Unicode的设计者在设计编码的时候已经考虑到16位不够用这一点。于是它们在BMP中预留了一段代理区(surrogate),0xD800-0xDFFF。该区间的编码不对应实际字符。如果发现一个short编码落在其中,说明该short编码需要与另外一个short编码共同组成一个真正的unicode字符。具体而言,如果一个short落在0xD800-0xDBFF之间,表示这是高端的16个bit;如果在0xDC00到0xDFFF之间,表示它是低端的16个bit。在表示辅助平面的32个bit中,高端的0xD800-0xDBFF(二进制是0b1101100000000000到0b1101101111111111)的前6个bit必须是0b110110,对于低端的0xDC00到0xDFFF(二进制是0b1101110000000000到0b1101111111111111),其前6个bit必须是0b110111。这12个bit是固定的,而剩下的32-12=20个bit是可以变化的,用来区分不同的编码。这20个bits,其前4位用来表示plain分区,后16位表示一个plain中的字符(因为一个plain可以有2的16次方个字符)。所以正好,UTF-16可以表示16个辅助平面的所有字符。其实并非“正好”,这是unicode设计的结果。
不知道大家对这种在编码中预留区间以扩展的设计是什么意见?可能大多数人会认为unicode高瞻远瞩,已经预料到16位不够用,所以这么设计。但在我个人看来,这种实际事实上把编码方式和实现方式混淆了。编码方式应该是不考虑具体的实现,而只应该为每个字符定义唯一的编码的时候。但事实上,Unicode的编码却有这么一段不对应任何字符,而完全是出于实现上的考虑,保留了这么一段空间以供扩展。编码时考虑实现方式,编码就不纯粹了。
另外一个问题是现在的这种编码方式,只是考虑了扩展到16个辅助平面的情况。如果16个辅助plain仍然不够用,那该怎么办呢?我想到的解决办法是仍然采用这种在编码中挖洞的方式。我们可以把最后一个辅助空间保留下来(虽然现在的定义中,它是作为自定义字符的空间)。如果发现32位编码落在这个空间,那说明这个32位是一个完整64位UTF-16编码实现的前一半或者后一半。
最后一个问题是对于英文为主的文档,传统的8位ASCII就基本够用。但现在对于英文字母也要使用16位来表示,在英文文档中仍然会浪费一半的空间。
UTF-8可以解决这些问题。不清楚UTF-8和UTF-16哪个在前,但从设计上而言,UTF-8在内存使用,扩展性和设计的纯粹性方面,我觉得都占优。
类似于UTF-16,UTF-8也是一种变长的编码方式。
总结下UTF-8的优缺点:
现实中utf-8和utf-16都很常用。例如java,java的class默认使用UTF-8存储(linux的文件默认都是UTF-8);但运行时,java却采用utf-16编码,这样存储省空间,读入之后,运行也很快。这是我个人的理解,不知道是否真的是设计的初衷。
但问题是,java诞生之时,unicode只有bmp,所以它直接把char定义为两个字节,String.charAt和String.length都返回的是其第N个char和char的个数。但有了辅助平面,可以两个char才表示一个真正的字符。所以String.charAt可能只返回一个真正字符的高字节或低字节,String.length也仅仅是char的个数,而不是真正的字符数。
所以Java为String又设计了另一套api,例如String.codePointCount可以获取真正字符数,String.codePointAt可以获取某个位置的真正字符。很多情况下,这套api才是我们真正应该使用的。