最近学习NIO文件读写的时候,就生成了一个疑问,程序怎么知道文件使用了什么编码,因为底层程序看到的都是二进制的字节码如:
中文 | utf-8二进制编码
人 | 11100100 10111010 10111010
程序怎么知道通过这是一个通过3个字节编码的人
字呢,于是查询了相关资料:
要解释这个问题,我们先来了解下ASCII码、GB2312、GBK、Unicode编码的关系和定义
1. ASSCII码,见ASCII码对照表
是一种使用一个字节(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编码规则
在计算机只有英文的时代,可能使用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编码是11100110
10111100
10110010
,转换成十六进制就是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;
}