ASCII
美国人制定的一套字符集,描述英语中的字符和8位二进制数(1字节)的对应关系,这被称为 ASCII 码。ASCII码共定义了128个字符,使用了8位二进制数中的7位,最高位统一规定为0。128个字符对英语来说足够了,但对于其他语言来说是不够的。大家在0~127号字符上达成了一致,但对于128~255号字符,不同国家有不同的定义。并且,亚洲的语言拥有更多的字符,1个字节已经满足不了需求。因此,Unicode诞生了
Unicode
Unicode 编码字符集旨在收集全球所有的字符,为每个字符分配唯一的字符编号即代码点(Code Point),用 U+紧跟着十六进制数表示。所有字符按照使用上的频繁度划分为 17 个平面(编号为 0-16),即基本的多语言平面和增补平面。基本的多语言平面(英文为 Basic Multilingual Plane,简称 BMP)又称平面 0,收集了使用最广泛的字符,代码点从 U+0000 到 U+FFFF,每个平面有 2^16=65536 个码点;增补平面从平面 1~16,分为增补多语言平面(平面 1)、增补象形平面(平面 2)、保留平面(平面 3~13)、增补专用平面等,每个增补平面也有 2^16=65536 个码点。所以 17 个平面总计有 17 × 65,536 = 1,114,112 个码点。下图 是 Unicode 平面分布图,以及 Unicode 各个平面码点空间
--来源:IBM
注意:Unicode只是一个字符集,定义了字符与数字的映射关系,但对于计算机中如何存储,没有做任何规定。由于字符数量之大,码点的范围很宽,排在前面的码点,可能用1个字节就能表示,而码点较大的,可能需要2个字节,3个字节,4个字节才能表示。那么计算机如何确定是将1个字节解释为一个字符呢,还是将2个字节连在一起解释为一个字符呢?还是3个,4个字节连在一起为一个字符。于是出现了一些解决方案:
- 取最大的,将所有字符都存储为4个字节(假设4个字节已经足够囊括所有码点),这样计算机固定以4个字节为单位来解释字符编码。但这样会产生极大的空间浪费,对于一篇纯英文文档,会浪费3倍的空间。
- 存储表示字节数的信息,让计算机知道该字符是以几个字节来存储的,如UTF-8编码。
Unicode字符集可以有不同的编码方式,如UTF-8,UTF-16,UTF-32,这里UTF指的是Unicode Transformation Format,即Unicode转换格式,即将Unicode编码空间中每个字符对应的码点,与字节顺序进行一一映射。
UTF-8
一种变长编码方式,一般用1-4个字节来编码一个Unicode字符,是目前应用最广泛的一种编码方式。
-
首字节的前几位用以提示编码的字节数
- 首字节以0开头,表示单字节编码
- 首字节以110开头,表示双字节编码,后续字节以10开头
- 首字节以1110开头,表示三字节编码,后续字节以10开头
- 首字节以11110开头,表示四字节编码,后续字节以10开头
- 以此类推…
-
那么在解码时
- 当读取到一个字节的首位为 0,表示这是一个单字节编码的 ASCII 字符
- 当读取到一个字节的首位为 1,表示这是一个多字节编码的字符,如继续读到 1,则确定这是首字节,再继续读取直到遇到 0 为止,一共读取了几个 1,就表示该字符为几个字节的编码
- 当读取到一个字节的首位为 1,紧接着读取到一个 0,则该字节是多字节编码的后续字节
UTF-8编码方式 --来源 : IBM
例子:
如中文的 “黄”,查得其Unicode码点为U+9EC4
,转化为二进制表示则为1001 1110 1100 0100
,对照上图可知,在UTF-8编码下,这个字符应占用3个字节,于是将二进制表示,按顺序从低位到高位插入到各个字节的有效位上(上图中的xxxx),得 “黄” 的 UTF-8编码为11101001 10111011 10000100
,转为16进制则为 0xE9BB84
UTF-8编码方式的特点
- 优点:变长,节省空间,自动纠错性能好,利于传输
- 缺点:不利于程序内部处理,如正则表达式检索
说明:
- 自同步:意思是在传输过程中,若存在字节丢失,或者存在错误的字节序列,也不会影响到其他字节的正常 读取,如读取了一个10xxxx开头的字节,但是找不到首字节,就可以将该字节丢弃
- 无字节序:UTF-8的编码单元是1个字节,且多字节编码时,每个字节都有前缀,所以不存在字节序的问题,而像UTF-16,UTF-32,是双字节,四字节编码的,就需要考虑字节序的问题。
UTF-16
UTF-16源于UCS-2,UCS-2将字符码点直接映射为字符编码,中间无特别的编码算法。
UCS-2编码方式固定2字节编码,只覆盖了BMP的码点,对于SMP的码点,2字节的16位二进制数是不足以表示的。
而UTF-16扩展了原来的UCS-2编码,解决了SMP码点的字符无法表示的问题:
- BMP中的有效码点,用固定2字节16位来为其编码,数值等于对应的码点,同UCS-2
- SMP中的有效码点,使用代理对进行编码。在BMP中,有一个范围的码点是未定义的,被称为代理区,其码点范围是
0xD800~0xDFFF
,共211个码点,代理区又被分为高代理码点和低代理码点,其中高代理码点范围是0xD800~0XDBFF
,低代理码点范围是0xDC00~0XDFFF
,高代理码点和低代理码点结合在一起,就表示一个SMP中的字符。由于SMP中的字符共有220个(0x100000
~0x10FFFF
),高代理码点和低代理码点皆有210个取值,两者结合,恰好有220种不同的组合。
如,汉字 “?” 的Unicode码点为0x20BB7
,首先用0x20BB7 - 0x10000
得出超出BMP的部分,得0x10BB7
,转换为20位二进制,高位不足补0,得0001 0000 1011 1011 0111
,分为高10位和低10位,高10位加上0xD800
1101 1000 0000 0000
00 0100 0010
1101 1000 0100 0010 = 0xD842
低10位加上0xDC00
1101 1100 0000 0000
11 1011 0111
1101 1111 1011 0111 = 0xDFB7
所以汉字 “?” 的UTF-16编码为0xD842 0xDFB7
JAVA中对于SMP平面的字符,用2个char来表示
char[] cs = Character.toChars(Integer.parseInt("20BB7",16));
char high = cs[0];
char low = cs[1];
System.out.println(Integer.toHexString(high)); //d842
System.out.println(Integer.toHexString(low)); //dfb7
对于辅助平面字符UTF-16编码的转换公式:
public char[] toUTF16(int codePoint){
//注意前面用()括起来,因为+的优先级比>>高
//若不用括号的话,就会先计算10+0xD800 ,再做移位操作,得出不正确的结果
//而对int进行移位操作,编译器会对移动的位数自动做 mod 32 运算
int high = ((codePoint - 0x10000) >> 10) + 0xD800;
int low = (codePoint - 0x10000) % 0x400 + 0xDC00;
return new char[]{(char)high,(char)low};
}
对于汉字 “?” ,其UTF-8编码为
先将码点 0x20BB7 转换为二进制,得
0010 0000 1011 1011 0111
根据表,得UTF-8用4个字节来表示该字符
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
从低位开始,将码点二进制依次填入x
00 100000 101110 110111
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
11110000 10100000 10101110 10110111
F0 A0 AE B7
在JAVA中进行验证:
char[] cs = Character.toChars(Integer.parseInt("20BB7",16));
String s = new String(cs);
String utf8 = URLEncoder.encode(s,"UTF-8");
System.out.println(utf8); // %F0%A0%AE%B7
UTF-32
固定以4字节来编码,ISO 10646中称 UTF-32是UCS-4的一个子集。
若用UTF-32来编码,程序处理会比较简单,但是所有字符皆占4个字节,比较浪费空间。
一些术语:
- Code Unit:代码单元,指已编码的文本中,最短的比特组合单元。对于UTF-8来说,代码单元是8比特,对UTF-16来说,是16比特,UTF-32,32比特。即,UTF-8以1个字节为最小单位,UTF-16以2个字节为最小单位。
- BOM(Byte Order Mark):字节序,出现在文件头部,表示字节顺序。在UCS编码中,有一个叫“ZERO WIDTH NO-BREAK SPACE”字符,其码点为
0xFEFF
,UCS规范建议,在传输字节流时,先传输这个字符,这样,如果接受者收到FEFF
,表明字节流是Big-Endian(大端字节序),如果接受者收到FFFE
,表明字节流是Little-Endian(小端字节序)。对UTF-8来说,不需要BOM来表明字节序,但是可以用BOM来表明是UTF-8编码,因为0xFEFF
的UTF-8编码为EF BB BF
,若接收者收到EF BB BF
开头的字节流,就知道这是UTF-8编码的字节流了。(可以使用Windows自带的文本编辑器,点击另存为,选择UTF-8,或者unicode big-endian等编码方式,再用java inputstream读取文件,获取字节流,观察可以看得到首字节即是上面的码点为0xFEFF
的字符)
测试代码如下
@Test
public void test() throws IOException {
InputStream in = new FileInputStream(filePath);
byte[] bs = new byte[1024];
int n = in.read(bs);
for(int i = 0;i < n; i++){
System.out.print(byteToHex(bs[i])+" ");
}
}
public String byteToHex(byte b){
int i = b & 0xFF;
return Integer.toHexString(i);
}
编码方式 | UTF-8 | UTF-16 | UTF-32 |
---|---|---|---|
编码字节数 | 变长,1-4字节,代码单元为8位,1字节 | 2字节或4字节,代码单元为16位,2字节 | 4字节,代码单元为32位,4字节 |
优点 | 兼容ASCII码,节省空间,纠错能力强,利于网络传输 | 最早的编码方式,适合内存中的Unicode处理,很多编程语言中作为String类的编码方式 | 固定字节编码,简单,利于程序处理,Unicode码点和编码一一对应 |
缺点 | 变长编码方式不利于程序内部处理 | 不兼容ASCII,增补平面使用代理对,较为复杂,扩展性差 | 不兼容ASCII,浪费存储空间和网络带宽,扩展性差 |
BOM字节序 | 无字节序(可用BOM来表示UTF-8编码) | 有字节序(UTF-16LE 小端序(FFFE),UTF-16BE 大端序(FEFF)) | 有字节序(UTF-32LE 小端序,UTF-32BE 大端序) |
为什么说UTF-8兼容ASCII,而UTF-16和UTF-32不兼容ASCII呢?ASCII中的字符,用UTF-8,UTF-16,UTF-32不都能表示吗?这里说的兼容可以理解为,用不同的编码方式来读取ASCII编码的文件,是否可以正常读取。
试想一个ASCII编码的文件,用UTF-8编码方式来读取,是可以的,因为UTF-8可以是单字节,而UTF-16和UTF-32不行,因为它们至少都是2字节,即最少以2字节解释为一个字符,而ASCII码的代码单元是1字节,这样用UTF-16或UTF-32来解释ASCII编码的文本,即会出现乱码。