一、名词解释

在聊编码集之前,我们先来了解一些名词解释:

字符集:所谓字符编码就是一个系统支持的所有抽象字符的集合,也就是说我们平常使用的文字,标点符号,图形符号等都是字符集。

我们知道,计算机无法识别我们平时说的文字,只能识别二进制的数字系统,那么就需要一套规则,将我们所说的字符转换为数字系统,那么这种操作,就是字符编码。官方解释如下:

字符编码:将符号转换为计算机可以接受的数字系统的规则。

理解了以上两个概念,接下来的两个概念就很容易理解了:

编码:按照某种字符编码将字符存储在计算机中

解码:将存储在计算机中的二进制数解析出来

二、编码集的发展史
ASCII编码

ASCII使用8个bit表示一个字节,共有128个字符,编码范围是0-127

众所周知,计算机最先使用是在美国,ASCII编码作为美国“元老级编码”,被称作“美国信息交换标准代码”。

ASCII字符集主要包括两部分,分别是:控制字符可显示字符

控制字符:主要包括回车键、退格键、换行键等

可显示字符:主要包括英文大小写、阿拉伯数字和西文符号等

ISO-8859-1

在ASCII推出后,计算机也在迅猛发展,当西欧等国家也开始使用计算机时,发现,ASCII虽然在显示英文方面做的很好,但是西欧国家的一些语言是无法显示的,所以,他们就对ASCII进行拓展。他们将新的符号填入128-255,将编码范围从0-127拓展成为0-255,共256个字符。相较于ASCII,ISO-8859-1加入了西欧语言,同时还加入了横线、竖线、交叉等符号。

GB系列的诞生

随着计算机的普及,我们国家也开始使用计算机,但是如何显示中文就成了一个大难题。

使用ASCII对中文进行编码和解码(以JAVA代码为例):

String chineseStr = "哈哈";
//编码
byte[] ascii = chineseStr.getBytes("ASCII");
//解码
String asciiStr = new String(ascii,"ASCII");
System.out.println("使用ASCII编码显示中文"+asciiStr);

运行结果不出所料,中文变成了“??”,可见,ASCII无法满足我们对中文的需要,可是已经没有可用的字节给我们用了,这可难不倒我们中国的劳动人民。那我们是怎么做的呢?

我们把127号之后的所有符号(即ASCIIM拓展码)取消掉,并且规定:一个小于127的字符,意义与原来相同,两个大于127的字符连在一起表示一个汉字

就这样,我们组合出7000多个常用简体汉字,还包括数字符号、罗马希腊字母、日文假名们。这就是我们常说的GB2312编码

但是,中国的汉字实在是太多了,还有繁体字也没有编进GB2312中,同时,在GB2312推出后又增加了许多简体字,这些汉字还是无法显示。

还好,GB2312并没有把所有的码位都用完。但是当我们把剩下的码位填满之后,发现还是有很多汉字无法编入,我们天朝的专家又说了,不再要求第二个字节也是127号以后的字符,只要第一个字节大于127就表示一个汉字的开始

这次的编码,增加了进20000个汉字(包括繁体字)和符号,我们把这中编码成为GBK编码集。GBK编码包括了GB2312的所有内容。

那么问题来了,之前使用ASCII编码的软件可以在GB系列环境下继续运行吗?

使用ASCII编码,使用GBK解码:

String englishStr = "hello world";
//编码
byte[] ascii = englishStr.getBytes("ASCII");
//解码
String gbkStr = new String(ascii,"GBK");
System.out.println("使用ASCII编码,使用GBK解码:"+gbkStr);

运行结果:没有发生乱码

使用ASCII编码,使用GBK解码:hello world

可见,GB系列解决了显示中文的问题,同时还保证了ASCII遗留软件还可以继续运行

unicode编码

在中国推出自己的GB系列编码的时候,其他国家也都推出了属于自己语言的编码集,各个国家的软件无法做到互通,因为当编码集不同时,乱码问题就会出现。

终于有一个叫ISO的国际组织实在是看不下去了,他们推出了一个新的编码:unicode编码

unicode编码是一个很大的编码,它废除了所有地区性的编码方案,重新规定了编码方式,包括了地球上所有文化、所有字母和符号。

它要求:

ASCII里的字符保持原编码不变,只是将其长度由原来的8位拓展成了16位;

其他文化语言的字符则全部统一重新编码,同样也是16位

我们来看一下unicode显示各国语言的效果如何:

String testStr = "abc哈哈하하あはは";
//编码
byte[] unicode = testStr.getBytes("unicode");
//解码
String unicodeStr = new String(unicode,"unicode");
System.out.println("使用UNICODE编码和解码:"+unicodeStr);

运行结果:未发生乱码

使用UNICODE编码和解码:abc哈哈하하あはは

我们刚才说到,unicode在处理英文时,将其长度拓展为16位,那么也就是说,在处理同等长度的纯英文字符串时,Unicode要使用两倍的空间来存储,为了能更直观地表达意思,使用代码来向大家说明:

String englishStr = "helloworld" ;
//使用ASCII编码
byte[] ascii = englishStr.getBytes("ASCII");
//使用UNICODE编码
byte[] unicode = englishStr.getBytes("unicode");
System.out.println("使用ASCII编码后字节数组长度:"+ascii.length);
System.out.println("使用UNICODE编码后字节数组长度:"+unicode.length);

运行结果:

使用ASCII编码后字节数组长度:10
使用UNICODE编码后字节数组长度:22

果然UNICODE的字节数组长度确实是ASCII的两倍。

在这边稍作解释:为什么UNICODE的字节数组的长度是22而不是20.

这是因为,UNICODE属于多字节编码,在存储多字节时,cpu有两种存储方式,分别是大端模式和小端模式。例如,“汉”字的UNICODE编码为6c49,在存储6c49时,CPU要判断是要将6c存在前面还是把49存在前面,如果是大端模式,那么将先存6c,再存49,如果是小端模式,则先存49再存6c。unicode多出的两位其实就是指定使用哪种存储模式,但是在编码过程中是毫无意义的,也就是说Unicode确实要比ASCII花费两倍的空间来存储纯英文文本。

现在我们来总结一下unicode的优缺点:

首先unicode在避免乱码上面功不可没,java底层的编码集就是使用的UNICODE,但同时,它在存储纯英文文本时要比ASCII花费两倍的存储空间,而且,它不与任何一种编码集兼容

UTF-8

在很长一段时间内,unicode一直没有得到广泛应用,直到互联网的出现,为了解决Unicode的传输问题,出现了一系列的新的编码:UTF系列

其中utf-8编码是在互联网上使用最广的一种UNICODE的实现方式。

utf-8最大的特点就是:它是一种可变长度的编码

image.png

utf-8对于不同范围的unicode编码,都有不同的长度来表示,分别使用1-4个字节表示一个符号

对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的

对于N字节的符号,第一个字节前N位都设为1,第N+1位设为0,后面的两个字节一律设为10,剩下没有提及的二进制全部为这个符号的Unicode码。

这样说是有点抽象,我们来举一个栗子详细说明:

以汉字“严”为例,严的unicode码为4E25,转成二进制就是100111000100101,4E25处于00000800-0000FFFF中,所以严的utf-8的编码应为3个字节。接下来,将严的unicode二进制表示从最后一位开始依次从后向前填入格式中的X,多出的位补0,最终得到严的utf-8的表示:11100100 10111000 10100101,转为十六进制就是E4B8A5。

最后再来总结一下utf-8的优缺点:

优点:对于单字节编码,utf-8与ASCII是一样的,可以说utf-8就是ASCII的超集,所以大量只支持AASCII的遗留软件可以在UTF-8环境下继续工作;

   对于纯英文文本,相较于UNICODE,utf-8节约了一半的存储空间

缺点:比如刚才的栗子,“严”的unicode只需要两个字节,但是utf-8却需要3个字节,可见,在存储中文时,要花费1.5-2倍的空间

三、两种乱码情况

说了这么多,我们只是为了要避免一个问题,那就是乱码。造成乱码的原因有许多种,在这里只先向大家介绍两种:

第一种:中文成了看不懂的字符

String chineseStr = "哈哈";
//编码
byte[] gbks = chineseStr.getBytes("GBK");
//解码
String gbksStr = new String(gbks,"ISO-8859-1");
System.out.println("使用GBK进行编码,使用ISO-8859-1进行解码"+gbksStr );

运行结果:

使用GBK进行编码,使用ISO-8859-1进行解码¹þ¹þ

运行结果显示,中文变成了我们看不懂的字符,那么,这种情况往往是由于,编码和解码使用了两种不兼容的编码集,导致中文变成了其他语言符号。

底层运行过程如下:

image.png

字符串转为字符数组,之后字符数组通过GBK转为字节数组,由于ISO和GBK的编码方式完全不同,所以将字节数组转为字符数组时“曲解”了语意。

第二种:中文变成了“?”

String chineseStr = "哈哈";
//编码
byte[] iso = chineseStr.getBytes("ISO-8859-1");
//解码
String isoStr = new String(iso,"ISO-8859-1");
System.out.println("使用ISO-8859-1进行编码,使用ISO-8859-1进行解码"+isoStr);

运行结果:

使用ISO-8859-1进行编码,使用ISO-8859-1进行解码??

运行结果显示,中文统一变成了“?”。那这又是为什么呢?编码和解码的过程中使用的相同的编码集怎么还会乱码呢?那是因为中文需要多字节编码,而ASCII是单字节编码,由于ASCII无法识别多字节,所以进行了过滤,将所有的中文都过滤成了“?”,底层执行原理如下:

image.png