关闭
Hit
enter
to search or
ESC
to close
May I Suggest ?
#leanote #leanote blog #code #hello world
Okeeper's Blog
Home
Archives
Tags
DevOps
软件笔记
Spring
学习
JVM系列
关于我
关于Unicode / UTF-8 、UTF-16字符集编码的理解
无
639
0
0
zhangyue
# 最近学习NIO文件读写的时候,就生成了一个疑问,程序怎么知道文件使用了什么编码,因为底层程序看到的都是二进制的字节码如: ``` 中文 | utf-8二进制编码 人 | 11100100 10111010 10111010 ``` 程序怎么知道通过这是一个通过3个字节编码的`人`字呢,于是查询了相关资料: 要解释这个问题,我们先来了解下ASCII码、GB2312、GBK、Unicode编码的关系和定义 # 1. ASSCII码,见[ASCII码对照表](http://tool.oschina.net/commons?type=4) 是一种使用一个字节(8位二进制)表示字母、数字和字符的一种编码,如字母`A`表示为使用十进制值为`65`的表示,转换为二进制就是`1000001`,我们知道,在计算机内部,所有的信息最终都表示为一个二进制的字符串。每一个二进制位(bit)有0和1两种状态,因此八个二进制位就可以组合出256种状态,这被称为一个字节(byte)。也就是说,一个字节一共可以用来表示256种不同的状态,每一个状态对应一个符号,就是256个符号,从0000000到11111111。 上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为ASCII码,一直沿用至今。 ASCII码一共规定了128个字符的编码,比如空格"SPACE"是32(二进制00100000),大写的字母A是65(二进制01000001)。这128个符号(包括32个不能打印出来的控制符号),只占用了一个字节的后面7位,最前面的1位统一规定为0。 # 2. `GB2312` ,全称是GB2312-80《信息交换用汉字编码字符集 基本集》,是中国标准化组织发布的,见[GB2312编码规则](http://www.qqxiuzi.cn/zh/hanzi-gb2312-bianma.php) 在计算机只有英文的时代,可能使用`ASCII`码就已经够了,但是随着计算机的普及和全球化,别国的语言肯定也是要计算机编码化的,所以就出现了`GB2312`编码规则。`GB2312`是使用固定两个字节来表示简体中文和英文、数字、中文符号、英文符号的一种编码规则。所以如果使用`GB2312`编码时,英文也是使用两个字节来编码的,这无疑是一种浪费 而`GBK`则是在`GB23312`的基础上添加了繁体中文的扩展 # 3. `Unicode` 正如上一节所说,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。 可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失。这就是Unicode,就像它的名字都表示的,这是一种所有符号的编码。 再次强调一下,Unicode只是为全世界的每一个文字符号都定义了一个数值对照 # 4. `UTF-8` 为什么会有`UTF-8`编码,我们来看下`Unicode`编码的定义和存在的问题: 需要注意的是,Unicode只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。 比如,汉字"严"的unicode是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。 那么第一个问题:怎么样使用二进制的表示来存储我们的编码呢,是和GBK一样使用固定的字节来存?如果这样的话就必须以最长的字节表示为单位,那么应为字母、数字都得使用3个字节或者4个字节来存储,这显然是不能够接受的,这对存储空间是极大的浪费。 根据第一个问题,我们是否能够使用变长的存储方式来存unicode编码呢,如果可以,怎么在读取的时候区分一个字符是使用一个字节表示(比如字母、数字),还是使用3个字节表示的中文呢? 所以最初期,由于存在Unicode存在这些问题的存在 它们造成的结果是:1)出现了Unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示Unicode。 2)Unicode在很长一段时间内无法推广,直到互联网的出现。 我们来看下`UTF-8`是怎么实现unicode编码的。 UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。 UTF-8的编码规则很简单,只有二条: 1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。 2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。 ``` Unicode符号范围 | UTF-8编码方式 (十六进制) | (二进制) --------------------+--------------------------------------------- 0000 0000 ~ 0000 007F | 0xxxxxxx 0000 0080 ~ 0000 07FF | 110xxxxx 10xxxxxx 0000 0800 ~ 0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx 0001 0000 ~ 0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx ``` 跟据上表,解读UTF-8编码非常简单。如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。 下面,还是以汉字"严"为例,演示如何实现UTF-8编码。 已知"漲"的unicode是`\u6f32`,6f32二进制为`110111100110010`,根据上表,可以发现4E25处在第三行的范围内(0000 0800 ~ 0000 FFFF),因此"漲"的UTF-8编码需要三个字节,即格式是`1110xxxx 10xxxxxx 10xxxxxx`。然后,从`漲`的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,"漲"的UTF-8编码是1110`0110` 101`11100` 10`110010`,转换成十六进制就是`e6bcb2`。 所以理论上讲UTF-8 可以表示2^31个字符,所以还有很多符号表情可以开发编入unicode中。 *其他实现方式还包括UTF-16(字符用两个字节或四个字节表示)和UTF-32(字符用四个字节表示),不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。* #5. 使用java打印出所有中文代码 ``` 下标 中文 unicode unicode十进制值 unicode二进制 utf-8编码二进制 utf-8十六进制 长度字节数 0 葦 \u6f32 28466 110111100110010 11101000 10010001 10100110 e891a6 3 1 葧 \u6f33 28467 110111100110011 11101000 10010001 10100111 e891a7 3 2 葨 \u6f34 28468 110111100110100 11101000 10010001 10101000 e891a8 3 3 葩 \u6f35 28469 110111100110101 11101000 10010001 10101001 e891a9 3 4 葰 \u6f36 28470 110111100110110 11101000 10010001 10110000 e891b0 3 5 葱 \u6f37 28471 110111100110111 11101000 10010001 10110001 e891b1 3 6 葲 \u6f38 28472 110111100111000 11101000 10010001 10110010 e891b2 3 7 葳 \u6f39 28473 110111100111001 11101000 10010001 10110011 e891b3 3 8 葴 \u6f3a 28474 110111100111010 11101000 10010001 10110100 e891b4 3 9 葵 \u6f3b 28475 110111100111011 11101000 10010001 10110101 e891b5 3 ``` java代码如下: ``` @Test public void writeAllChinese() { int start = 0x6f32; int index = 0; System.out.printf("%5s %10s %10s %20s %20s %15s %20s %10s\n","下标","中文" ,"unicode" ,"unicode十进制值" ,"unicode二进制" , "utf-8编码二进制", "utf-8十六进制" ,"长度字节数"); while (start < 0x6f32 + 10) { String unicode = "\\u" + Integer.toHexString(start); char c = (char) Integer.parseInt((start+""),16); String chinese = String.valueOf(c); byte[] bytes = chinese.getBytes(); System.out.printf("%5s %10s %10s %20s %30s %30s %15s %10s\n",index,chinese ,unicode ,start ,Integer.toBinaryString(start) , getBinaryString(bytes), Integer.toHexString(Integer.valueOf(getBinaryString(bytes).replace(" ",""),2)) ,bytes.length); start++; index++; } } public static String getBinaryString(byte bytes[]) { String s = ""; for(byte b : bytes) { /** * 由于java 虚拟机为了方便整数的加减,使用了补码(反码+1)来表示,方便数值的符号直接参与二进制的加减,这样省去了很多计算步骤 * 所以在java中使用String#getBytes()返回的字节数值是反码的表示方法; * 又由于Int 在java中表示是4个字节32位的,在控制台进行输出的时候,jvm把11001111之前进行了补补全然后再取补码,等到的就是11111111111111111111111110111110 * 所以需要在使用 与 运算将取 反码 0xff = 11111111 */ s = s + Integer.toBinaryString(b & 0xff) + " "; //s = s + Integer.toBinaryString(b) + " "; } return s; } ```
觉得不错,点个赞?
Please enable JavaScript to view the
comments powered by Disqus.
comments powered by
Disqus
文章目录