java字符编码-Unicode编码问题刨根究底

作者 Lei da 日期 2019-12-02
java字符编码-Unicode编码问题刨根究底

前段时间在读《java核心技术卷一》,遇到一些名词:码点、代码单元等,其实字面意思不难理解,解释如下

  • 码点(code point):Unicode编码表中某个字符对应的代码值
  • 代码单元(code unit):用于UTF-16编码的最小单元,16个bit

注意上述只是针对java中字符和字符串的Unicode+UTF-16机制的解释。若是其他编码方式就另说,如UTF-8的代码单元是用8个bit编码。

下面问题来了

书中建议,尽量不要使用char类型,最好将字符串转化为抽象数据类型来处理,即codepoints数组

1
2
//将String转化为码点数组
int[] codePoints = str.codePoints().toArray();

那么为什么要这样做呢,像c语言那样直接使用char数组不好吗?当然不行,因为在Unicode+UTF-16这种机制中,一个码点可由一个代码单元表示,但很多特殊字符也可能由两个代码单元表示。而char类型只能是一个代码单元。所以,若字符串中存在特殊字符,遍历char数组或者使用charAt等方法时就会出问题。但使用码点数组就OK,因为数组中每一个元素都代表一个码点,而不是一个代码单元。而c语言中采用ASCII字符集,每个码点都由一个8bit代码单元表示,使用char数组则不存在这个问题。

这样的描述对懂Unicode和UTF-16的人来说,很容易理解。但我想在我的博客中深究一下编码机制背后的原理。为了便于小白理解,先介绍一下编码规范的基本概念。

编码规范

制定编码规范为了将计算机能识别的二进制数,映射成人类能识别的字符。依据编码规范,计算机就可以将二进制数显示成字符。常见的编码规范有,ASCII码、GBK、ISO-8859-1、Unicode等。编码规范中有三个子概念,字库表、字符集、编码方式。

字库表

字库表中存储该编码规范能表示的所有字符。一套编码规范不一定能表示世界上所有的字符。例如GBK规范可以显示汉字,但不能显示法语、俄语等。

字符集

字库表中每一个字符都有一个二进制地址,字符集就是这些二进制数的集合。例如 00000000 - 01111111 为ASCII字符集的范围。

编码方式

某些编码规范包括大量的字符,例如Unicode中包含上百万个字符。若每个字符都采用同样长度的二进制数来编码,即定长编码,则要用三个字节来存储,甚至在将来会用到四个字节。这样很多本身只需要单字节存储的字符也会占用三四个字节,会导致极大的资源浪费。

如果能采用一些算法,使得部分字符采用单字节编码,部分采用双字节等(即变长编码),可节省不少资源,这些算法即为编码方式。常见的编码方式有UTF-8、UTF-16、UTF-32等。我前文所说的:Unicode+UTF-16机制,就容易理解了,即基于Unicode编码规范并采用UTF-16编码方式的机制。java中的字符串和字符正是采用这种机制进行编码的,下面详细介绍Unicode和UTF-16。

注意UTF的全称是Unicode Transformation Format,含义为将Unicode转换为某种格式。所以UTF-8、UTF-16等都是针对Unicode来说的。

Unicode

在Unicode出现之前,已经有了很多编码规范,如美国的ASCII码、中国的GBK、西欧的ISO-8859-1等,每一种规范都不能涵盖所有国家的语言。Unicode设计的初衷就是将所有语言中的字符进行统一编码。

Unicode最早的1.0版本中,字符集数量远远不到65536,因为当时的字符集不是那么庞大,使用2个字节编码足够使用,java也正是此时引进了16位的Unicode字符集。

然而,在Unicode增加了大量的汉语、日语、韩语字符之后,字符数量超过了65536,于是16位的char类型也就不能满足了。实际上,这些海量的Unicode字符可以被划分为17个代码级别(code plane)

  • 第一级别,基本多语言级别(basic multilingual plane),范围U+0000~U+FFFF,下文称其为基本平面
  • 其它16个级别,范围U+10000~U+10FFFF,存储辅助字符,下文把它称作增补平面

UTF-16编码则是对不同的代码级别做文章,采用不同长度的编码表示不同代码级别的码点

UTF-16

UTF-16使用16位作为一个代码单元。基本平面的码点采用一个代码单元进行编码,增补平面的码点使用两个代码单元。

可能你会问,使用一对代码单元表示一个增补平面的字符时,有没有可能把它判别成两个基本平面的码点?也就是说,有没有可能出现冲突的情况?

  • 当然不会,UTF-16中使用了代理机制,即使用基本平面中未映射字符的字符集区域,来作为增补平面中字符的代码单元的区域

这些码点区域称为“替代区域”(syrrogate area),即U+D800 ~ U+DFFF范围,该区域在基本平面中属于空闲区域,2048个值。如此一来,便避免了冲突。

该替代区域分割为两部分,U+D800 ~ U+DBFF用于第一个代码单元,U+DC00 ~ U+DFFF用于第二个代码单元。

代码单元1 代码单元2
1101 10pp ppxx xxxx 1101 11xx xxxx xxxx

pppp是指16个级别的级别编号,2^4=16。两个代码单元的变数部分(p和x)共20位,可表示2^20=1048576个码点。而增补平面范围U+10000~U+10FFFF恰好也是1048576个码点。所以这两个代码单元可完美表示出增补平面中的所有字符。

这种方式十分巧妙。

事实上,只有UTF-16使用了“替代区域”方法,像现在被广泛接纳的UTF-8编码,是通过首字节的比特位判断码点的代码单元数量。

最后简单介绍下UTF-16和UTF-8的区别,以后再找时间细究,把UTF-8的坑填上。

  • UTF-16使用2个或4个字节进行编码,大部分汉字采用两个字节编码,少量汉字采用四个字节
  • UTF-8使用1个到4个字节编码,大部分汉字采用三个字节