Chinaunix首页 | 论坛 | 博客
  • 博客访问: 48451
  • 博文数量: 7
  • 博客积分: 169
  • 博客等级: 入伍新兵
  • 技术积分: 85
  • 用 户 组: 普通用户
  • 注册时间: 2011-08-23 18:20
文章分类

全部博文(7)

文章存档

2012年(7)

我的朋友

分类: 服务器与存储

2012-10-31 21:28:54

可能大家都听说过 Unicode、UCS-2、UTF-8 等等词汇,但它们具体是什么意思,是什么原理,之间有什么关系,恐怕就很少有人明白了。下面就分别介绍一下它们。

基本知识

介绍Unicode之前,首先要讲解一些基础知识。虽然跟Unicode没有直接的关系,但想弄明白Unicode,没这些还真不行。

字节和字符的区别

咦,字节和字符能有什么区别啊?不都是一样的吗?完全正确,但只是在古老的DOS时代。当Unicode出现后,字节和字符就不一样了。

字节(octet)是一个八位的存储单元,取值范围一定是0~255。而字符(character,或者word)为语言意义上的符号,范围就不一定了。例如在UCS-2中定义的字符范围为0~65535,它的一个字符占用两个字节。

Big Endian和Little Endian

上面提到了一个字符可能占用多个字节,那么这多个字节在计算机中如何存储呢?比如字符0xabcd,它的存储格式到底是 AB CD,还是 CD AB 呢?

实际上两者都有可能,并分别有不同的名字。如果存储为 AB CD,则称为Big Endian;如果存储为 CD AB,则称为Little Endian

具体来说,以下这种存储格式为Big Endian,因为值(0xabcd)的高位(0xab)存储在前面:

地址
0x00000000 AB
0x00000001 CD

相反,以下这种存储格式为Little Endian:

地址
0x00000000 CD
0x00000001 AB
UCS-2和UCS-4

Unicode是为整合全世界的所有语言文字而诞生的。任何文字在Unicode中都对应一个值,这个值称为代码点(code point)。代码点的值通常写成 U+ABCD 的格式。而文字和代码点之间的对应关系就是UCS-2(Universal Character Set coded in 2 octets)。顾名思义,UCS-2是用两个字节来表示代码点,其取值范围为 U+0000~U+FFFF。

为了能表示更多的文字,人们又提出了UCS-4,即用四个字节表示代码点。它的范围为 U+00000000~U+7FFFFFFF,其中 U+00000000~U+0000FFFF和UCS-2是一样的。

要注意,UCS-2和UCS-4只规定了代码点和文字之间的对应关系,并没有规定代码点在计算机中如何存储。规定存储方式的称为UTF(Unicode Transformation Format),其中应用较多的就是UTF-16和UTF-8了。

UTF-16和UTF-32 UTF-16

UTF-16由规定,它使用两个字节来表示一个代码点。

不难猜到,UTF-16是完全对应于UCS-2的,即把UCS-2规定的代码点通过Big Endian或Little Endian方式直接保存下来。UTF-16包括三种:UTF-16,UTF-16BE(Big Endian),UTF-16LE(Little Endian)。

UTF-16BE和UTF-16LE不难理解,而UTF-16就需要通过在文件开头以名为BOM(Byte Order Mark)的字符来表明文件是Big Endian还是Little Endian。BOM为U+FEFF这个字符。

其实BOM是个小聪明的想法。由于UCS-2没有定义U+FFFE,因此只要出现 FF FE 或者 FE FF 这样的字节序列,就可以认为它是U+FEFF,并且可以判断出是Big Endian还是Little Endian。

举个例子。“ABC”这三个字符用各种方式编码后的结果如下:

UTF-16BE 00 41 00 42 00 43
UTF-16LE 41 00 42 00 43 00
UTF-16(Big Endian) FE FF 00 41 00 42 00 43
UTF-16(Little Endian) FF FE 41 00 42 00 43 00
UTF-16(不带BOM) 00 41 00 42 00 43

Windows平台下默认的Unicode编码为Little Endian的UTF-16(即上述的 FF FE 41 00 42 00 43 00)。你可以打开记事本,写上ABC,然后保存,再用二进制编辑器看看它的编码结果。


另外,UTF-16还能表示一部分的UCS-4代码点——U+10000~U+10FFFF。表示算法比较复杂,简单说明如下:

  1. 从代码点U中减去0x10000,得到U'。这样U+10000~U+10FFFF就变成了 0x00000~0xFFFFF。
  2. 用20位二进制数表示U'。 U'=yyyyyyyyyyxxxxxxxxxx
  3. 将前10位和后10位用W1和W2表示,W1=110110yyyyyyyyyy,W2=110111xxxxxxxxxx,则 W1 = D800~DBFF,W2 = DC00~DFFF。

例如,U+12345表示为 D8 08 DF 45(UTF-16BE),或者08 D8 45 DF(UTF-16LE)。

但是由于这种算法的存在,造成UCS-2中的 U+D800~U+DFFF 变成了无定义的字符。

UTF-32

UTF-32用四个字节表示代码点,这样就可以完全表示UCS-4的所有代码点,而无需像UTF-16那样使用复杂的算法。与UTF-16类似,UTF- 32也包括UTF-32、UTF-32BE、UTF-32LE三种编码,UTF-32也同样需要BOM字符。仅用'ABC'举例:

UTF-32BE 00 00 00 41 00 00 00 42 00 00 00 43
UTF-32LE 41 00 00 00 42 00 00 00 43 00 00 00
UTF-32(Big Endian) 00 00 FE FF 00 00 00 41 00 00 00 42 00 00 00 43
UTF-32(Little Endian) FF FE 00 00 41 00 00 00 42 00 00 00 43 00 00 00
UTF-32(不带BOM) 00 00 00 41 00 00 00 42 00 00 00 43
UTF-8

UTF-16和UTF-32的一个缺点就是它们固定使用两个或四个字节,这样在表示纯ASCII文件时会有很多00字节,造成浪费。而定义的UTF-8则解决了这个问题。

UTF-8用1~4个字节来表示代码点。表示方式如下:

UCS-2 (UCS-4) 位序列 第一字节 第二字节 第三字节 第四字节
U+0000 .. U+007F 00000000-0xxxxxxx 0xxxxxxx      
U+0080 .. U+07FF 00000xxx-xxyyyyyy 110xxxxx 10yyyyyy    
U+0800 .. U+FFFF xxxxyyyy-yyzzzzzz 1110xxxx 10yyyyyy 10zzzzzz  
U+10000..U+1FFFFF 00000000-000wwwxx-
xxxxyyyy-yyzzzzzzz
11110www 10xxxxxx 10yyyyyy 10zzzzzz

可见,ASCII字符(U+0000~U+007F)部分完全使用一个字节,避免了存储空间的浪费。而且UTF-8不再需要BOM字节。

另外,从上表中可以看出,单字节编码的第一字节为[00-7F],双字节编码的第一字节为[C2-DF],三字节编码的第一字节为[E0-EF]。这样只要看到第一个字节的范围就可以知道编码的字节数。这样也可以大大简化算法。

首先看一下下面的程序(测试英文和中文在Unicode、UTF-8、UTF-16这三种编码下,一个字符占几个字节)

点击(此处)折叠或打开

  1. System.out.println("a(Unicode) :" + "a".getBytes("Unicode").length);
  2. System.out.println("a(Unicode) :" + "aa".getBytes("Unicode").length);
  3. System.out.println("啊(Unicode) :" + "啊".getBytes("Unicode").length);
  4. System.out.println("啊啊(Unicode) :" + "啊啊".getBytes("Unicode").length);
  5. System.out.println("");
  6. System.out.println("a(UTF-8) :" + "a".getBytes("UTF-8").length);
  7. System.out.println("aa(UTF-8) :" + "aa".getBytes("UTF-8").length);
  8. System.out.println("啊(UTF-8) :" + "啊".getBytes("UTF-8").length);
  9. System.out.println("啊啊(UTF-8) :" + "啊啊".getBytes("UTF-8").length);
  10. System.out.println("");
  11. System.out.println("a(UTF-16) :" + "a".getBytes("UTF-16").length);
  12. System.out.println("aa(UTF-16) :" + "aa".getBytes("UTF-16").length);
  13. System.out.println("啊(UTF-16) :" + "啊".getBytes("UTF-16").length);
  14. System.out.println("啊啊(UTF-16) :" + "啊啊".getBytes("UTF-16").length);
运行结果如下:

a(Unicode)      :4
a(Unicode)      :6
啊(Unicode)     :4
啊啊(Unicode) :6

a(UTF-8)      :1
aa(UTF-8)    :2
啊(UTF-8)     :3
啊啊(UTF-8) :6

a(UTF-16)      :4
aa(UTF-16)    :6
啊(UTF-16)     :4
啊啊(UTF-16) :6

可以看到UTF-8的情况:一个英文字符占一个字节,一个汉字占三个字节

但是Unicode和UTF-16的情况比较奇怪,不管是英文还是汉字,看不出占几个字节。其实正确的答案是:Unicode和UTF-16的编码 下,不管是英文字符还是汉字字符,都占两个字节(至于上面结果中多出来的两个字节是用来表示字节顺序的默认字节)。至于为什么,继续往下看。


Unicode规范中推荐的标记字节顺序的方法是BOM。BOM不是“Bill Of Material”的BOM表,而是Byte Order Mark。

(Unicode是一种字符编码方法,不过它是由国际组织设计,可以容纳全世界所有语言文字的编码方案。Unicode的学名 是"Universal Multiple-Octet Coded Character Set",简称为UCS。UCS可以看作是"Unicode Character Set"的缩写。)

在UCS编码中有一个叫做"ZERO WIDTH NO-BREAK SPACE"的字符,它的编码是FEFF。而FFFE在UCS中是不存在的字符,所以不应该出现在实际传输中。UCS规范建议在传输字节流前,先传输字 符"ZERO WIDTH NO-BREAK SPACE"。

这样如果接收者收到FEFF,就表明这个字节流是Big-Endian的;如果收到FFFE,就表明这个字节流是Little-Endian的。因此字符"ZERO WIDTH NO-BREAK SPACE"又被称作BOM。

在 Java 中直接使用Unicode 转码时会按照UTF-16LE 的方式拆分,并加上 BOM。 如果采用 UTF-16 拆分,在 Java 中默认采用带有 BOM 的 UTF-16BE 拆分。

再来看一个程序:

点击(此处)折叠或打开

  1. public class Test {
  2.     private final static char[] HEX = "0123456789abcdef".toCharArray();
  3.  
  4.     public static void main(String[] args) throws UnsupportedEncodingException {
  5.          String str = "中国";
  6.          String[] encoding = { "Unicode", "UnicodeBig", "UnicodeLittle", "UnicodeBigUnmarked",
  7.          "UnicodeLittleUnmarked", "UTF-16", "UTF-16BE", "UTF-16LE" };
  8.          
  9.          for (int i = 0; i < encoding.length; i++) {
  10.          System.out
  11.          .printf("%-22s %s%n", encoding[i], bytes2HexString(str.getBytes(encoding[i])));
  12.          }
  13.     }
  14.  
  15.     public static String bytes2HexString(byte[] bys) { 
  16. //一个字节用两个char,再加上字节数-1个空格
  17. //offset:chs的偏移
  18.         char[] chs = new char[bys.length * 2 + bys.length - 1];
  19.         for (int i = 0, offset = 0; i < bys.length; i++) {
  20.             if (i > 0) {
  21.                 chs[offset++] = ' ';
  22.             } 
  23.             //取字节的低位
  24.             chs[offset++] = HEX[bys[i] >> 4 & 0xf]; 
  25.             //取字节的高位
  26.             chs[offset++] = HEX[bys[i] & 0xf];
  27.         }
  28.         return new String(chs);
  29.     }
  30. }
运行结果如下:

Unicode                fe ff 4e 2d 56 fd
UnicodeBig             fe ff 4e 2d 56 fd
UnicodeLittle          ff fe 2d 4e fd 56
UnicodeBigUnmarked     4e 2d 56 fd
UnicodeLittleUnmarked  2d 4e fd 56
UTF-16                 fe ff 4e 2d 56 fd
UTF-16BE               4e 2d 56 fd
UTF-16LE               2d 4e fd 56

 


可以看到几个不同的Unicode和UTF-16编码的字节顺序是不同的,有的是fe ff,有的是ff fe,有的没有。

 


总上所述:

Unicode和UTF-16:1个字符占2个字节(不管是哪国语言)

UTF-8:1个英文字符占1个字节,一个汉字(包括日文和韩文等)占3个字节

Java中的char默认采用Unicode编码,所以Java中char占2个字节


另外,顺便提一个知识点:1个字节(byte)占8位(bit)




阅读(1613) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~