美国人制定的一套字符集,描述英语中的字符和8位二进制数(1字节)的对应关系,这被称为 ASCII 码。ASCII码共定义了128个字符,使用了8位二进制数中的7位,最高位统一规定为0。128个字符对英语来说足够了,但对于其他语言来说是不够的。大家在0~127号字符上达成了一致,但对于128~255号字符,不同国家有不同的定义。并且,亚洲的语言拥有更多的字符,1个字节已经满足不了需求。因此,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个字节连在一起为一个字符。于是出现了一些解决方案:
Unicode字符集可以有不同的编码方式,如UTF-8,UTF-16,UTF-32,这里UTF指的是Unicode Transformation Format,即Unicode转换格式,即将Unicode编码空间中每个字符对应的码点,与字节顺序进行一一映射。
一种变长编码方式,一般用1-4个字节来编码一个Unicode字符,是目前应用最广泛的一种编码方式。
首字节的前几位用以提示编码的字节数
那么在解码时
例子:
如中文的 “黄”,查得其Unicode码点为U+9EC4
,转化为二进制表示则为1001 1110 1100 0100
,对照上图可知,在UTF-8编码下,这个字符应占用3个字节,于是将二进制表示,按顺序从低位到高位插入到各个字节的有效位上(上图中的xxxx),得 “黄” 的 UTF-8编码为11101001 10111011 10000100
,转为16进制则为 0xE9BB84
UTF-8编码方式的特点
说明:
UTF-16源于UCS-2,UCS-2将字符码点直接映射为字符编码,中间无特别的编码算法。
UCS-2编码方式固定2字节编码,只覆盖了BMP的码点,对于SMP的码点,2字节的16位二进制数是不足以表示的。
而UTF-16扩展了原来的UCS-2编码,解决了SMP码点的字符无法表示的问题:
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
固定以4字节来编码,ISO 10646中称 UTF-32是UCS-4的一个子集。
若用UTF-32来编码,程序处理会比较简单,但是所有字符皆占4个字节,比较浪费空间。
一些术语:
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编码的文本,即会出现乱码。
参考链接1
参考链接2