一直有个困惑,为什么计算机系统搞那么多字符编码,就一个Unicode统一天下不就得了,后来看了篇文章,才多少理解一丁点。
英语的国家,只要一个字节就可以表示全部的字符,一个无符合的字节可以表示256个字符,对于英语的国家而言绰绰有余了。但是其它国家的字符就费劲了,如果使用Unicode编码,囊括全部的字符,要统一天下,那么一个字节就远远不够了,需要很多字节来表示一个字符(比如4个字节或者更多字节)才行,因为全球的字符实在TMD太多了。
好了咱们就这么定吧,Unicode编码来表示全部的字符好吗?居然有人反对了,说英文字母只用一个字节表示就够了,如果Unicode统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。总之就是没法达成一致,既然如此,那么就各家自己设计字符编码方案吧,于是乎才有那么一堆UTF-8,GB2312,ISO 8859-1,GBK等等。
但是计算机内存只能保存Unicode编码,这点则是统一的,而且必须统一,否则问题就大了。
如果没有Unicode,会怎么样,举栗子说明下:
中国使用自己的字符集编码GBK对字符串china
编码,假设编码后的二进制码就是:0111 0101
,那么计算机就按0111 0101
存储china
。那么美国用户想在他们自己的计算机打开文件查看,就必须使用GBK解码(他们的计算机必须安装GBK编码)才能看到china
,这是肯定的。那么这种编码和解码的关系如下图所示:
计算机是根据GBK编码把中文字符串转换成二进制码后进行存储的,假设GBK编码表如下图所示:
如上图可以明确知道,字符编码就是规定每个字符对应的二进制码是什么。
文档的内容其实是英文字母,对于美国用户而言他们习惯使用UTF-8字符编码,他们的计算机默认是使用UTF-8编码和解码的,他不希望每次打开这份文档都要指定GBK解码才能正常看内容,那么就必须把这份文档改成UTF-8编码保存才行,这时候问题来了,没有中间码(Unicode),那么怎么把文档的编码改成UTF-8呢?因为字符串china
在UTF-8的规则里面,对应的二进制码是1010 0010
,如下图所示:
就是说,计算机是根据GBK的规则把0111 0101
转换成字符串china
显示给用户看的,也就是说计算机对文档的数据理解就是根据GBK编码规则来理解的。现在要按UTF-8保存文档,就是把china
转换成10100010
存储,那么01110101
如何变成10100010
呢?没法变,对吧!所以没有中间码(Unicode)就无法把GBK的文档变成UTF-8的文档。
再举个栗子说明下:
计算机和编程语言都是西方国家发明的,这些计算机对编程语言的理解是基于二进制码,因此计算机对这些二进制码的含义认知应该已经固定和达成统一了,计算机的理解过程用下图简单表示:
int x = 1;
int y = 1;
System.out.println(x + y);
上面的代码根据计算机本来设计的编码规则理解,可以得到如下图所示的字节码:
就是说JVM遇到0110
,知道是int
数据类型,遇到0010
知道是=
,遇到0111
知道是+
,所以根据这样的理解,最终计算机知道代码的含义,从而计算得到结果2
并且打印输出。
如果计算机没有这样默认的编码规则,那怎么玩?GBK,UTF-8等等这些编码方案都是后面才设计出来的,而且相同的二进制码对应不同的编码方案其含义还完全不同,所以你让系统的指令集如何去理解这些二进制代码。举个比较形象的栗子:好比计算机是个“神人”,中国用户用自己的语言把需求描述清楚了,日本用户用自己的语言也把需求描述清楚了,这两份需求要提交给“神人”去完成,“神人”怎么理解这两份需求呢?你是不是要翻译成“神人”理解的语言才行呀!“神人”只理解Unicode的,所以中国和日本用户必须把需求翻译成Unicode后,“神人”就理解两位用户的需求是啥了,于是“神人”就用自己的能力去完成需求的任务,得到两位用户想要的结果。你如果不要这个“神人”帮忙,想自己完成特定的需求任务可以吗?当然可以,那你自己去创造个“神人”,来理解你的需求就行了。也就是说你自己创造个“机器”,它可以理解你用自己特定的编码规则提交的代码从而完成相应的任务,而且这个“机器”只能理解你的代码,日本用户的代码理解不了,德国的代码也理解不了。这样你觉得满意吗?
所以说计算机必须有个初始的、固定的编码规则,而GBK、UTF-8等编码规则都只是把自己国家的字符所对应的Unicode转换成自己的字符编码而已。如下图所示:
我再举个例子,大家如果理解了就基本ok了,假设浏览器指定字符编码是UTF-8,那我们看看String str = "小妹"的编码是如何变化的:
代码如下:
String c = "小妹";byte[] bs = c.getBytes("UTF-8"); // ① 使用UTF-8对"小妹"进行编码,得到UTF-8编码String c1 = new String(bs, "ISO-8859-1"); //② 使用ISO-8859-1对UTF-8的编码进行解码,得到错误的Unicode编码byte[] bs1 = c1.getBytes("UNICODE"); // 获取错误的Unicode编码,其实就是字节数组byte[] bs2 = c1.getBytes("ISO-8859-1"); //③ 把错误的Unicode编码,重新转换成原来的UTF-8编码,String c2 = new String(bs2,"UTF-8"); // ④使用UTF-8重新解码得到正确的Unicode编码System.out.println(c2); // ⑤小妹
解读如下:
-
字符串“小妹”在内存中的Unicode编码是:
11111110 11111111 01011100 00001111 01011001 10111001
-
接着浏览器使用UTF-8对“小妹”的Unicode进行编码得到UTF-8编码:
11100101 10110000 10001111 11100101 10100110 10111001
,接着就传送给了服务器 -
服务器默认使用ISO-8859-1解码,得到了错误的Unicode编码:
11111110 11111111 00000000 11100101 00000000 10110000 00000000 10001111 00000000 11100101 00000000 10100110 00000000 10111001
-
如果想要获得正确的Unicode编码,必须把错误的Unicode编码重新编码成原来的UTF-8编码,因为原来解码是用ISO-8859-1,所以错误的Unicode编码要转换成原来的UTF-8编码,就必须使用ISO-8859-1进行编码才行,得到了UTF-8编码之后,再使用UTF-8重新解码成正确的Unicode编码就可以了。
上述编码的变化过程可以看下图:
P.S 解码行为就是编码行为,为了区别不同的编码方向和编码含义,所以规定字符从初始编码转换成其它编码叫编码,而从其它编码转换成初始编码叫解码
任何字符在计算机内存中都默认以Unicode编码存在,所以“祖国”这个字符串初始是以Unicode编码存在于内存中的,那么java程序用UTF-8把“祖国”从Unicode(就是字节数组)转换成UTF-8编码(就是字节数组)的过程叫“编码”,java程序再用UTF-8(就是字节数组)将“祖国”从UTF-8编码转换回Unicode编码(就是字节数组)的过程叫“解码”。过程如下图所示:
通常字符是以某种编码保存在硬盘(磁盘)中,例如GBK,UTF-8,ISO-8859-1等,但是java程序在读取字符到内存时,就必须以Unicode编码存放,换句话说java程序在内存中处理字符的时候,会用到Unicode编码,持久化存储的时候都是以其它编码方案保存。
用图表述如下:
磁盘文件(假如以UTF-8编码保存)加载至内存,JVM会以UTF-8解码转成Unicode编码。
out.println()写入response,容器会将response中的数据发送给浏览器,这样数据会离开服务器内存一段时间再到用户电脑的内存中,Unicode只在内存中出现,所以写入response也要按某种字符编码(假如也是UTF-8)对文档的数据进行编码后保存,浏览器拿到服务器发送过来的数据后,会以UTF-8解码成Unicode编码,再正常显示出来。