在我们日常的开发过程中经常会出现乱码问题,这种问题往往发生在数据的输入和输出过程中。 下面我们从字符编码的角度来了解为什么不同的字符编码之间会出现乱码的问题。
我们都知道在计算机只存储和处理的是二进制数据,所有字符(如文字、符号、数字)都需要被编码为二进制形式,乱码其实是字符的编码和解码二进制数据不匹配导致的问题,也就是当一个字符的编码方式与解码方式不一致时,解码结果会与预期不符,呈现出错误的字符。
1、ASCII码
众所周知,计算机是美国人发明的,美国人将他们国家常用的字符如英文字母、数字和标点符号等可见字符,还有一些日常使用(如打印)中涉及到的不可见字符(如回车键这样的控制字符)都存储到计算机里面。接下来他们按照顺序将不可见字符(33个)、可见字符(95个)列出来,于是就形成了如下图所示的ASCII码表:
图片
给0、1、2、a、b.....这样的字符称之为ASCII码字符集;给ASCII码字符集对应的二进制码称之为ASCII码。
ASCII码如果在美国一直使用是没有问题,但是其他国家学习计算机就会存在问题,因为没有对应的字符编码,于是对ACSII码进行了扩展。
扩展的方法是将原ASCII码的第一位0修改成第一位为1,然后从128个字符扩展到255个字符,这样新增加了128个,给新增加128个字符取名叫做扩展字符集,如下图所示的扩展图:
图片
通过扩展ASCII码暂时解决了西方国家的字符编码问题。随着计算机不断的发展和普及,计算机走向了世界各国,就比如我们中国来说,中国的汉字至少有上千个,这个时候256个字符已经不够用了。
2、GB2312码
由于8位表示一个字符已经无法满足中文字符的实际需求了,所以就设计使用16位表示一个字符,那么中文怎么编码呢?编码的步骤如下所示:
(1)设定字符集
中文字符比较多,采用分区管理的方式管理中文字符集,共分成94个区,每个区含94个位,共8836个码位。如下所示的第1区的字符集:
图片
每个分区的存储的内容如下表整理:
分区范围 | 存储的内容 |
01-09 | 收录除汉字外的682个字符 |
10-15 | 用户自定义符号区(未编码) |
16-55 | 收录3755个一级汉字,按拼音排序 |
56-87 | 收录3008个二级汉字,按部首/笔画排序 |
88-94 | 用户自定义汉字区(未编码) |
(2)定义汉字的位置
每个分区的字符如何确定其字符的位置呢?在GB212中使用分区号+字符的行列号来确定字符的位置,如下图所示的第16分区中的”白“字:
图片
”白“字使用其分区号(16)+行号(3)+ 列号(7)组成1637
(3)计算实际的存储位置
将”白“字的码位1637分别拆分成16、37后转成十六进制,然后对这两个十六进制数再分别与A0相加(加A0的好处是让计算机区分ASCII码和GB2312码),将得到的结果合并就计算出了”白“字的存储位置,如下图所示的计算过程:
图片
通过这种规则的计算,得到”白“字的实际在计算机中存储位置为0xB0C5。
3、GBK
由于我们中国的汉字太多了,导致了很多汉字都不在GB2312码中,为了满足实际的需要,于是对GB2312码进行扩充。
扩充的方法是将之前没有用上的码位都使用上,并且不在规定它的低位一定要大于127,可以小于127,但是必须要保证高位大于127,然后规定计算机只要遇到大于127的字节就表示一个汉字的开始。
图片
通过这种方式新增了近2万个字符和汉字,将这些字符集称为GBK字符集,对应的编码称为GBK码。
3、Unicode
中国可以实现自己的一套编码规则,同样的道理,其他的国家也可以实现自己的一套编码,这样就出现了新的问题,世界上这么多的编码,那么不同国家在进行通信的时候就会出现乱码的现象。于是ISO的组织提出了一套规范,这样就出现了Unicode的标准。Unicode是一个标准,它包含了字符集以及对应的编码规则,目的就是将世界上所有的字符整理到一起并且进行编码。
3.1、UCS-2字符集
初期Unicode使用16位的UCS-2字符集,UCS-2字符集将世界上所有用到的字符罗列到一起,按照顺序标上对应的位置编码然后转成二进制存储,这样UCS-2字符集可以表示65536个字符。如下图所示的UCS-是字符集:
图片
3.2、UCS-4字符集
UCS-2字符集还是无法表示世界上所有的语言字符,于是Unicode推出了UCS-4字符集,UCS-4字符集用32位表示一个字符,如下图所示:
图片
UCS-4字符集可以表示近42亿个字符,基本可以容纳世界上所有的字符了,由于UCS-4字符集占用的空间大,所以它没有被各国很好的接受。
3.3、UTF-8编码
到了互联网飞速发展的阶段,世界各国的交流日益频繁,不同的编码之间无法通信,大家便重新思考unicode编码,于是便推出如下的编码规则:
图片
UTF-32属于定长编码,它的每个字符编码固定占4字节,比如对于英文字母a,UTF-32表示这个字符需要32位,在ASCII的编码中字符a只需8位就可以表示,那么那如果存储的内容主要是英文,使用UTF-32占用的存储空间就是使用ASCII编码占用的存储空间的四倍。
UTF-32编码会造成严重的内存消耗,而UTF-8编码则不存在这个问题,因为它是一种变长的编码,对于英文字符UTF8和ASCII编码是一样的,只占一个字节,对于非英文的字符,UTF-8会使用2~4个字节来表示(如对于中文一般会使用三个字节来表示)。
UTF-8的优势是可以有效的利用存储空间避免浪费,并且UTF-8向后兼容了ASCII编码,UTF-8的编码规则有如下的几种:
图片
1个字节的UTF-8编码,它的最高位固定为0,剩余的七位用来编码,这和ASCII编码是完全一样的,所以为什么UTF-8可以兼容了ASCII就是这个原因。
2个字节的UTF-8编码的首字节的前三位为110,其余字节的开头两位为10。
3个字节的UTF-8编码的首字节的前四位为1110,其余字节的开头两位是10
4个字节的UTF-8编码首字节的前五位为11110,其余四节的开头是10。
从UTF-8的编码规则我们可以看到对于2字节到4字节的编码,它的首字节开头有几个连续的1,那就代表着它这个编码占了几个字节,那这样解码的时候就知道如何对这个二进制数据进行解码。
那么一个汉字是如何转成UTF-8编码呢?以中文的这个”王“字为例,它在UCS-4中的编码是0x0000 738B,其UTF-8的编码过程如下所示:
图片
注意的是,如果汉字的二进制无法填满模板的所有的x空位,则剩余的空位默认都用0来填充,通过这种方式填充完以后就得到了汉字中所对应的UTF-8的编码。同样要解码的话,也只需要逆序执行一次就可以得到对应的汉字。
至此我们了解了编码的发展过程,由于每种编码都有自己的规则,如果不按照它的规则进行解码就得到一串乱码。
在SpringBoot中默认的字符编码通常是UTF-8,这是Java和SpringBoot推荐的标准字符编码(特别是在处理Web请求和响应)。因为UTF-8编码支持广泛的字符集,包括大多数自然语言,并且可以有效地减少数据传输时的空间。