Java 字符编码(一)Unicode 字符编码

时间:2022-05-10 10:06:59

Java 字符编码(一)Unicode 字符编码

Unicode(http://www.unicode.org/versions/#TUS_Latest_Version) 是一个编码方案,说白了希望给世界上每一种文字系统的每一个字符,都分配一个唯一的整数,这样就不可能有任何冲突了。

一、字符编码规范

1.1 ASCII(American Standard Code for Information Interchange)

美国信息交换标准代码,这是计算机上最早使用的通用的编码方案。那个时候计算机还只是拉丁文字的专利,根本没有想到现在计算机的发展势头,如果想到了,可能一开始就会使用 unicode 了。当时绝大部分专家都认为,要用计算机,必须熟练掌握英文。这种编码占用 7 个 Bit,在计算机中占用一个字节,8 位,最高位没用,通讯的时候有时用作奇偶校验位。因此 ASCII 编码的取值范围实际上是:0x00-0x7f,只能表示 128 个字符。后来发现 128 个不太够用,做了扩展,叫做 ASCII 扩展编码,用足八位,取值范围变成:0x00-0xff,能表示 256 个字符。其实这种扩展意义不大,因为 256 个字符表示一些非拉丁文字远远不够,但是表示拉丁文字,又用不完。所以扩展的意义还是为了下面的 ANSI 编码服务。

1.2 ANSI(American National Standard Institite )

美国国家标准协会,也就是说,每个国家(非拉丁语系国家)自己制定自己的文字的编码规则,并得到了 ANSI 认可,符合 ANSI 的标准,全世界在表示对应国家文字的时候都通用这种编码就叫 ANSI 编码。换句话说,中国的 ANSI 编码和在日本的 ANSI 的意思是不一样的,因为都代表自己国家的文字编码标准。比如中国的 ANSI 对应就是 GB2312 标准,日本就是 JIT 标准,香港,*对应的是 BIG5 标准等等。当然这个问题也比较复杂,微软从 95 开始,用就是自己搞的一个标准 GBK。GB2312 里面只有 6763 个汉字,682 个符号,所以确实有时候不是很够用。GBK 一直能和 GB2312 相互混淆并且相安无事的一个重要原因是 GBK 全面兼容 GB2312,所以没有出现任何冲突,你用 GB2312 编码的文件通过 GBK 去解释一定能获得相同的显示效果,换句话说:GBK 对 GB2312 就是,你有的,我也有,你没得的,我还有!

好了,ANSI 的标准是什么呢,首先是 ASCII 的代码你不能用!也就是说 ASCII 码在任何 ANSI 中应该都是相同的。其他的,你们自己扩展。所以呢,中国人就把 ASCII 码变成 8 位,0x7f 之前我不动你的,我从 0xa0 开始编,0xa0 到 0xff 才 95 个码位,对于中国字那简直是杯水车薪,因此,就用两个字节吧,此编码范围就从 0xA1A1 - 0xFEFE,这个范围可以表示 23901 个汉字。基本够用了吧,GB2312 才 7000 多个呢!GBK 更猛,编码范围是从 0x8140 - 0xFEFE,可以表示 3 万多个汉字。可以看出,这两种方案,都能保证汉字头一个字节在 0x7f 以上,从而和 ASCII 不会发生冲突。能够实现英文和汉字同时显示。

BIG5,香港和*用的比较多,繁体,范围: 0xA140-0xF9FE,0xA1A1-0xF9FE,每个字由两个字节组成,其第一字节编码范围为 0xA1-0xF9,第二字节编码范围为 0x40-0x7E 与 0xA1-0xFE,总计收入 13868 个字 (包括 5401个 常用字、7652 个次常用字、7 个扩充字、以及 808 个各式符号)。

那么到底 ANSI 是多少位呢?这个不一定!比如在 GB2312 和 GBK,BIG5 中,是两位!但是其他标准或者其他语言如果不够用,就完全可能不止两位!

例如:GB18030: GB18030-2000(GBK2K)在 GBK 的基础上进一步扩展了汉字,增加了藏、蒙等少数民族的字形。GBK2K 从根本上解决了字位不够,字形不足的问题。它有几个特点:它并没有确定所有的字形,只是规定了编码范围,留待以后扩充。编码是变长的,其二字节部分与 GBK 兼容;四字节部分是扩充的字形、字位,其编码范围是首字节 0x81-0xfe、二字节 0x30-0x39、三字节 0x81-0xfe、四字节 0x30-0x39。它的推广是分阶段的,首先要求实现的是能够完全映射到 Unicode3.0 标准的所有字形。它是国家标准,是强制性的。

搞懂了 ANSI 的含义,我们发现 ANSI 有个致命的缺陷,就是每个标准是各自为阵的,不保证能兼容。换句话说,要同时显示中文和日本文或者阿拉伯文,就完全可能会出现一个编码两个字符集里面都有对应,不知道该显示哪一个的问题,也就是编码重叠的问题。显然这样的方案不好,所以 Unicode 才会出现!

1.3 MBCS(Multi-Byte Chactacter System(Set))

多字节字符系统或者字符集,基于 ANSI 编码的原理上,对一个字符的表示实际上无法确定他需要占用几个字节的,只能从编码本身来区分和解释。因此计算机在存储的时候,就是采用多字节存储的形式。也就是你需要几个字节我给你放几个字节,比如 A 我给你放一个字节,比如"中“,我就给你放两个字节,这样的字符表示形式就是 MBCS。

在基于 GBK 的 windows 中,不会超过 2 个字节,所以 windows 这种表示形式有叫做 DBCS(Double-Byte Chactacter System),其实算是 MBCS 的一个特例。C 语言默认存放字符串就是用的 MBCS 格式。从原理上来说,这样是非常经济的一种方式。

1.4 CodePage

代码页,最早来自 IBM,后来被微软,oracle,SAP 等广泛采用。因为 ANSI 编码每个国家都不统一,不兼容,可能导致冲突,所以一个系统在处理文字的时候,必须要告诉计算机你的 ANSI 是哪个国家和地区的标准,这种国家和标准的代号(其实就是字符编码格式的代号),微软称为 Codepage 代码页,其实这个代码页和字符集编码的意思是一样的。告诉你代码页,本质就是告诉了你编码格式。

但是不同厂家的代码页可能是完全不同,哪怕是同样的编码,比如, UTF-8 字符编码 在 IBM 对应的代码页是 1208,在微软对应的是 65001,在德国的 SAP 公司对应的是 4110 。所以啊,其实本来就是一个东西,大家各自为政,搞那么多新名词,实在没必要!所以标准还是很重要的!!!

比如 GBK 的在微软的代码页是 936,告诉你代码页是 936 其实和告诉你我编码格式是 GBK 效果完全相同。那么处理文本的时候就不会有问题,不会去考虑某个代码是显示的韩文还是中文,同样,日文和韩文的代码页就和中文不同,这样就可以避免编码冲突导致计算机不知如何处理的问题。当然用这个也可以很容易的切换语言版本。但是这都是治标不治本的方法,还是无法解决同时显示多种语言的问题,所以最后还是都用 unicode 吧,永远不会有冲突了。

1.5 Unicode(Universal Code)

这是一个编码方案,说白了就是一张包含全世界所有文字的一个编码表,只要这个世界上存在的文字符号,统统给你一个唯一的编码,这样就不可能有任何冲突了。不管你要同时显示任何文字,都没有问题。因此在这样的方案下,Unicode 出现了。Unicode 编码范围是:0-0x10FFFF,可以容纳 1114112 个字符,100 多万啊。全世界的字符根本用不完了,Unicode 5.0 版本中,才用了 238605 个码位。所以足够了。

因此从码位范围看,严格的 unicode 需要 3 个字节来存储。但是考虑到理解性和计算机处理的方便性,理论上还是用 4 个字节来描述。

Unicode 采用的汉字相关编码用的是《CJK 统一汉字编码字符集》— 国家标准 GB13000.1 是完全等同于国际标准《通用多八位编码字符集 (UCS)》 ISO 10646.1。《GB13000.1》中最重要的也经常被采用的是其双字节形式的基本多文种平面。在这 65536 个码位的空间中,定义了几乎所有国家或地区的语言文字和符号。其中从 0x4E00-0x9FA5 的连续区域包含了 20902 个来自中国(包括*)、日本、韩国的汉字,称为 CJK (Chinese Japanese Korean) 汉字。CJK 是 《GB2312-80》、《BIG5》 等字符集的超集。

CJK 包含了中国,日本,韩国,越南,香港,也就是 CJKVH。这个在 UNICODE 的 Charset chart 中可以明显看到。 unicode 的相关标准可以从 https://www.unicode.org/standard/standard.html 上面获得。

二、Unicode 中的基本概念

2.1 代码点

Unicode 标准的本意很简单:希望给世界上每一种文字系统的每一个字符,都分配一个唯一的整数,这些整数叫做 代码点(Code Points)。

2.2 代码空间

所有的代码点构成一个 代码空间(Code Space),根据 Unicode 定义,总共有 1,114,112 个代码点,编号从 0x0-0x10FFFF。 换句话说,如果每个代码点都能够代表一个有效字符的话,Unicode 标准最多能够编码 1,114,112,也就是大概 110 多万个字符。最新的 Unicode 标准(7.0)已经给超过 11 万个字符分配了代码点。

2.3 代码平面

Unicode 标准把代码点分成了 17 个代码平面(Code Plane),编号为 #0-#16。每个代码平面包含 65,536(2^16)个代码点(17*65,536=1,114,112)。 其中,Plane#0 叫做基本多语言平面(Basic Multilingual Plane,BMP),其余平面叫做补充平面(Supplementary Planes)。Unicode7.0 只使用了 17 个平面中的 6 个,并且给这 6 个平面起了名字,如下图所示:

Java 字符编码(一)Unicode 字符编码

下面是这些平面的名字和用途:

  1. Plane#0 BMP(Basic Multilingual Plane) 大部分常用的字符都坐落在这个平面内,比如 ASCII 字符,汉字等。
  2. Plane#1 SMP(Supplementary Multilingual Plane) 这个平面定义了一些古老的文字,不常用。
  3. Plane#2 SIP(Supplementary Ideographic Plane) 这个平面主要是一些BMP中没有包含汉字。
  4. Plane#14 SSP(Supplementary Special-purpose Plane) 这个平面定义了一些非图形字符。
  5. Plane#15 SPUA-A(Supplementary Private Use Area A)
  6. Plane#16 SPUA-B(Supplementary Private Use Area B)

2.4 BMP

BMP 是最重要的一个代码平面,大部分常用的字符都定义在这个平面内,如下图所示:

Java 字符编码(一)Unicode 字符编码

在 BMP 中定义的代码点包括:

  1. ASCII ASCII总共有128个字符,占据了BMP的前128个代码点(上图绿线)
  2. ISO-8859-1 共256个字符,占据了BMP的前256个代码点(上图绿线+蓝线)
  3. CJK Unified Ideographs 上图的红色区域(占据BMP大约1/3)定义了两万多个汉字,其中前 20,902 个汉字是按照《康熙字典》里笔画顺序排列的
  4. Surrogate Code Points 从 0xD800-0xDBFF 的 1024 个代码点是 High-surrogate 代码点,从 0xDC00-0xDFFF 的 1024 个代码点是 Low-surrogate 代码点。这 2048 个代码点并不是有效的字符代码点,它们是为 UTF 编码保留的。一个 High-surrogate 代码点和一个 Low-surrogate 代码点组成一个代理对(Surrogate Pair),可以在 UTF-16 里编码 BMP 之外的某个代码点(1024^2+65,536=1,114,112)。

三、Unicode 编码方案

之前提到,Unicode 没有规定字符对应的二进制码如何存储。以汉字“汉”为例,它的 Unicode 码点是 0x6c49,对应的二进制数是 110110001001001,二进制数有 15 位,这也就说明了它至少需要 2 个字节来表示。可以想象,在 Unicode 字典中往后的字符可能就需要 3 个字节或者 4 个字节,甚至更多字节来表示了。

这就导致了一些问题,计算机怎么知道你这个 2 个字节表示的是一个字符,而不是分别表示两个字符呢?这里我们可能会想到,那就取个最大的,假如 Unicode 中最大的字符用 4 字节就可以表示了,那么我们就将所有的字符都用 4 个字节来表示,不够的就往前面补 0。这样确实可以解决编码问题,但是却造成了空间的极大浪费,如果是一个英文文档,那文件大小就大出了 3 倍,这显然是无法接受的。

于是,为了较好的解决 Unicode 的编码问题, UTF-8 和 UTF-16 两种当前比较流行的编码方式诞生了。当然还有一个 UTF-32 的编码方式,也就是上述那种定长编码,字符统一使用 4 个字节,虽然看似方便,但是却不如另外两种编码方式使用广泛。

3.1 UTF-8

UTF-8 是一个非常惊艳的编码方式,漂亮的实现了对 ASCII 码的向后兼容,以保证 Unicode 可以被大众接受。

UTF-8 是目前互联网上使用最广泛的一种 Unicode 编码方式,它的最大特点就是可变长。它可以使用 1-4 个字节表示一个字符,根据字符的不同变换长度。编码规则如下:

对于单个字节的字符,第一位设为 0,后面的 7 位对应这个字符的 Unicode 码点。因此,对于英文中的 0 - 127 号字符,与 ASCII 码完全相同。这意味着 ASCII 码那个年代的文档用 UTF-8 编码打开完全没有问题。

对于需要使用 N 个字节来表示的字符(N > 1),第一个字节的前 N 位都设为 1,第 N + 1 位设为0,剩余的 N - 1 个字节的前两位都设位 10,剩下的二进制位则使用这个字符的 Unicode 码点来填充。

编码规则如下:

Unicode编码(十六进制) UTF-8 字节流(二进制)
000000-00007F 0xxxxxxx
000080-0007FF 110xxxxx 10xxxxxx
000800-00FFFF 1110xxxx 10xxxxxx 10xxxxxx
010000-10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-8 的特点是对不同范围的字符使用不同长度的编码。对于 0x00-0x7F 之间的字符,UTF-8 编码与 ASCII 编码完全相同。UTF-8 编码的最大长度是 4 个字节。从上表可以看出,4 字节模板有 21 个x,即可以容纳 21 位二进制数字。Unicode 的最大码位 0x10FFFF 也只有 21 位。

例1:“汉”字的 Unicode 编码是 0x6C49。0x6C49 在 0x0800-0xFFFF 之间,使用 3 字节模板:1110xxxx 10xxxxxx 10xxxxxx。将 0x6C49 写成二进制是:0110 1100 0100 1001, 用这个比特流依次代替模板中的 x,得到:11100110 10110001 10001001,即 E6 B1 89。

例2:Unicode 编码 0x20C30 在 0x010000-0x10FFFF 之间,使用 4 字节模板:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。将 0x20C30 写成 21 位二进制数字(不足 21 位就在前面补 0):0 0010 0000 1100 0011 0000,用这个比特流依次代替模板中的 x,得到:11110000 10100000 10110000 10110000,即 F0 A0 B0 B0。

解码的过程也十分简单:如果一个字节的第一位是 0 ,则说明这个字节对应一个字符;如果一个字节的第一位1,那么连续有多少个 1,就表示该字符占用多少个字节。

3.2 UTF-16

UTF-16 是 Unicode 的一种编码方式,它用两个字节来编码 BMP 里的代码点,用四个字节编码其余平面里的代码点(暂不考虑字节顺序)。由于 BMP 里只有 65535 个代码点,所以直接把代码点转换成 2 个字节就可以了。BMP 之外的平面稍微复杂一点,需要先将代码点转化为一个代理对,然后再转为 4 个字节。

我们把 Unicode 编码记作 U。编码规则如下:

  • 如果 U<0x10000,U的 UTF-16 编码就是 U 对应的 16 位无符号整数(为书写简便,下文将 16 位无符号整数记作 WORD)。
  • 如果 U≥0x10000,我们先计算 U'=U-0x10000,然后将 U 写成二进制形式:yyyy yyyy yyxx xxxx xxxx,U 的 UTF-16 编码(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。

为什么 U 可以被写成 20 个二进制位?Unicode 的最大码位是 0x10FFFF,减去 0x10000 后,U 的最大值是 0xFFFFF,所以肯定可以用 20 个二进制位表示。例如:Unicode 编码 0x20C30,减去 0x10000 后,得到 0x10C30,写成二进制是:0001 0000 1100 0011 0000。用前 10 位依次替代模板中的y,用后 10 位依次替代模板中的x,就 得到:1101100001000011 1101110000110000,即 0xD843 0xDC30。

按照上述规则,Unicode 编码 0x10000-0x10FFFF 的 UTF-16 编码有两个 WORD,第一个 WORD 的高 6 位是 110110,第二个 WORD 的高 6 位是 110111。可见,第一个 WORD 的取值范围(二进制)是 11011000 00000000-11011011 11111111,即 0xD800-0xDBFF。第二个 WORD 的取值范围(二进制)是 11011100 00000000-11011111 11111111,即 0xDC00-0xDFFF。

为了将一个 WORD 的 UTF-16 编码与两个 WORD 的 UTF-16 编码区分开来,Unicode 编码的设计者将 0xD800-0xDFFF 保留下来,并称为代理区(Surrogate):

范围 说明 备注
D800-DB7F High Surrogates 高位替代
DB80-DBFF High Private Use Surrogates 高位专用替代
DC00-DFFF Low Surrogates 低位替代

高位替代就是指这个范围的码位是两个 WORD 的 UTF-16 编码的第一个 WORD。低位替代就是指这个范围的码位是两个 WORD 的 UTF-16 编码的第二个 WORD。

UTF-16 计算规则

Java 字符编码(一)Unicode 字符编码

假设要编码的补充平面内的代码点为 X,具体的编码过程为:

  1. X 必定在 0x010000-0x10FFFF 之间
  2. 将 X 减去 0x010000,得到的数在 0x0-0xFFFFF 之间,正好可以用 20 个 bit 来表示
  3. 将高位的 10 个 bit 和 0xD800 相加,将地位的 10 个比特和 0xDC00 相加,得到的正好是一个代理对,也就是四个字节

Unicode3.0 中给出了辅助平面字符的转换公式:

High Surrogates:H = Math.floor((c-0x10000) / 0x400)+0xD800
Low Surrogates:L = (c - 0x10000) % 0x400 + 0xDC00

3.3 UTF-32

UTF-32 编码以 32 位无符号整数为单位。Unicode 的 UTF-32 编码就是其对应的 32 位无符号整数。

3.4 字节序

字节序有两种,分别是“大端”(Big Endian, BE)和“小端”(Little Endian, LE)。

根据字节序的不同,UTF-16可被实现为UTF-16LE或UTF-16BE,UTF-32可被实现为UTF-32LE或UTF-32BE。例如:

Unicode编码 UTF-16LE UTF-16BE UTF32-LE UTF32-BE
0x006C49 49 6C 6C 49 49 6C 00 00 00 00 6C 49
0x020C30 43 D8 30 DC D8 43 DC 30 30 0C 02 00 00 02 0C 30

Unicode 标准建议用 BOM(Byte Order Mark)来区分字节序,即在传输字节流前,先传输被作为 BOM 的字符“零宽无中断空格”。这个字符的编码是 FEFF,而反过来的 FFFE(UTF-16)和 FFFE0000(UTF-32)在 Unicode 中都是未定义的码位,不应该出现在实际传输中。

下表是各种 UTF 编码的 BOM:

UTF编码 Byte Order Mark (BOM)
UTF-8 without BOM
UTF-8 with BOM EF BB BF
UTF-16LE FF FE
UTF-16BE FE FF
UTF-32LE FF FE 00 00
UTF-32BE 00 00 FE FF

参考:

  1. 《Unicode.org》:http://www.unicode.org/versions/#TUS_Latest_Version
  2. 《Unicode》:https://blog.csdn.net/wm_1991/article/details/52230716
  3. 《Unicode的流言终结者和编码大揭秘》:https://blog.csdn.net/soonfly/article/details/51161771
  4. 《从字节理解Unicode(UTF8/UTF16)》:https://www.cnblogs.com/zizifn/p/4716712.html
  5. 《UTF-8、UTF-16、UTF-32 编码》:https://blog.csdn.net/guxiaonuan/article/details/78678043
  6. 《百度百科Unicode》:https://baike.baidu.com/item/Unicode/750500?fr=aladdin
  7. 《Unicode character table》:https://unicode-table.com/en/

每天用心记录一点点。内容也许不重要,但习惯很重要!