关于Unicode UTF-8 、UTF-16字符集编码的理解

最近学习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;
    }

  目录