Unicode,UTF-8,UTF-16傻傻分不清?一文看懂

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 17个平面分布 --来源:IBM

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编码方式的特点

  • 优点:变长,节省空间,自动纠错性能好,利于传输
  • 缺点:不利于程序内部处理,如正则表达式检索
UTF-8编码方式特点 --来源:IBM

说明:

  • 自同步:意思是在传输过程中,若存在字节丢失,或者存在错误的字节序列,也不会影响到其他字节的正常 读取,如读取了一个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种不同的组合。
UTF-16编码工作方式 --来源:IBM

如,汉字 “?” 的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);
}
3种编码方式比较
编码方式UTF-8UTF-16UTF-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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值