Java编码方式再学
一直以来对编码方式对了解不是很深入。建议读下这几篇博文
学点编码知识又不会死:Unicode的流言终结者和编码大揭秘
这几篇博文上回答了内心存在的一些问题,这些问题可能也是大家经常遇到的。
1. Unicode字符和Unicode编码的区别
Unicode字符
Unicode背后的想法非常简单,然而却被普遍的误解了。Unicode就像一个电话本,标记着字符和数字之间的映射关系。Joel称之为「神奇数字」,因为它们可能是随机指定的,而且不会给出任何解释。官方术语是码位(Code Point),总是用U+开头。理论上每种语言中的每种字符都被Unicode协会指定了一个神奇数字。例如希伯来文中的第一个字母א,是U+2135,字母A是U+0061。
Unicode并不涉及字符是怎么在字节中表示的,它仅仅指定了字符对应的数字,仅此而已。
关于Unicode的其它误解包括:Unicode支持的字符上限是65536个,Unicode字符必须占两个字节。告诉你这些的人应该去换换脑子了。
记住,Unicode只是一个用来映射字符和数字的标准。它对支持字符的数量没有限制,也不要求字符必须占两个、三个或者其它任意数量的字节。
Unicode字符是怎样被编码成内存中的字节这是另外的话题,它是被UTF(Unicode Transformation Formats)定义的。
结论一:Unicode只是一种字符和数字的映射关系,并不涉及字符在计算机中的存储!
Unicode编码
两个最流行的Unicode编码方案是UTF-8和UTF-16。让我们看看它们的细节
UTF-8
UTF-8是一个非常惊艳的概念,它漂亮的实现了对ASCII码的向后兼容,以保证Unicode可以被大众接受。发明它的人至少应该得个诺贝尔和平奖。
在UTF-8中,0-127号的字符用1个字节来表示,使用和US-ASCII相同的编码。这意味着1980年代写的文档用UTF-8打开一点问题都没有。只有128号及以上的字符才用2个,3个或者4个字节来表示。因此,UTF-8被称作可变长度编码。
UTF-16
另一个流行的可变长度编码方案是UTF-16,它使用2个或者4个字节来存储字符。然而,人们逐渐意识到UTF-16可能会浪费存储空间,但那是另一个话题了。
低字节序(Little Endian)和高字节序(Big Endian)
Endian读作End-ian或者Indian。这个术语的起源可以追溯到格列佛游记。(小说中,小人国为水煮蛋应该从大的一端(Big-End)剥开还是小的一端(Little-End)剥开而争论,争论的双方分别被称为“大端派”和“小端派”。)
低字节序和高字节序只是一个关于在内存中存储和读取一段字节(被称作words)的约定。这意味着当你让计算机用UTF-16把字母A(占两个字节)存在内存中时,使用哪种字节序方案决定了你把第一个字节放在第二个字节的前面还是后面。这么说有点不太容易懂,让我们来看一个例子:当你使用UTF-16存下来自你朋友的附件时,在不同的系统中它的后半部分可能是这样的:
00 68 00 65 00 6C 00 6C 00 6F(高字节序,高位字节被存在前面)
68 00 65 00 6C 00 6C 00 6F 00(低字节序,低位字节被存在前面)
字节序方案只是一个微处理器架构设计者的偏好问题,例如,Intel使用低字节序,Motorola使用高字节序。
字节顺序标记(BOM)
如果你经常要在高低字节序的系统间转换文档,并且希望区分字节序,还有一种奇怪的约定,被称作BOM。BOM是一个设计得很巧妙的字符,用来放在文档的开头告诉阅读器该文档的字节序。在UTF-16中,它是通过在第一个字节放置FE FF来实现的。在不同字节序的文档中,它会被显示成FF FE或者FE FF,清楚的把这篇文档的字节序告诉了解释器。
BOM尽管很有用,但并不是很简洁,因为还有一个类似的概念,称作「魔术字」(Magic Byte),很多年来一直被用来表明文件的格式。BOM和魔术字间的关系一直没有被清楚的定义过,因此有的解释器会搞混它们。
结论二:Unicode的编码方式以及字节顺序决定了字符对应的Unicode值在计算机中的存储方式!
2. Java中的char和String
你只是大致知道平面0(「Basic Multilingual Plane」,即「BMP」)的 65536 个码点(即 0x0000 至 0xFFFF)如何编码,这不是 Unicode 的全部。
如果U<0x10000,U的UTF-16编码就是U对应的16位无符号整数(因此Java中char存储的值就是平面0字符对应的Unicode值)
如果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编码推导Unicode编码。
如果一个字符的UTF-16编码的第一个WORD在0xDB80到0xDBFF之间,那么它的Unicode编码在什么范围内?我们知道第二个WORD的取值范围是0xDC00-0xDFFF,所以这个字符的UTF-16编码范围应该是0xDB80 0xDC00到0xDBFF 0xDFFF。我们将这个范围写成二进制:
1101101110000000 11011100 00000000 - 1101101111111111 1101111111111111
按照编码的相反步骤,取出高低WORD的后10位,并拼在一起,得到
1110 0000 0000 0000 0000 - 1111 1111 1111 1111 1111 XML即0xe0000-0xfffff,按照编码的相反步骤再加上0x10000,得到0xf0000-0x10ffff。这就是UTF-16编码的第一个WORD在0xdb80到0xdbff之间的Unicode编码范围,即平面15和平面16。因为Unicode标准将平面15和平面16都作为专用区,所以0xDB80到0xDBFF之间的保留码位被称作高位专用替代。