写在前边的

相信大家经常遇见乱码,我用java就经常遇到,尤其是servlet接收参数时,当然python、js、mysql都有这问题,乱码这个问题说解决也挺简单,百度一下也许就解决了,但是下次出现仍然不知道哪里出现的问题,这个时候就该想想应该彻底把它搞懂了!

问题

本文仍按照以往风格,以问题为导向,解答疑惑

  • 程序为什么会出现乱码?
  • unicode、gbk、iso8859-1、ascii、utf-8、utf-16、utf-32,分别代表什么?它们之间有什么关系?

其实,只要弄懂了字符集编码,就懂了乱码的全部。

字符集&编码

字符集就是字符的集合,相当于一本字典,我们都知道字典有英语字典、汉语字典,如果去英语字典里找汉字肯定找不到,所以就出现了乱码。经常见的字符集有ascii、iso8859-1、gb2312、gbk、big5、unicode。
字符集只规定了字符对应的数值(专业说法叫code point代码点,代码点就是个数值),但没规定存储格式,比如对一个字符可以用1个字节表示也可以用2个字节表示,这就是编码方案,编码就是用将字符变为数值,注意编码方案是对应某种字符集而言的,当然如果使用了某种编码方案默认也就是用了其对应的字符集

ascii、iso8859-1、iso8859-n

ascii(American Standard Code for Information Interchange,美国信息交换标准代码)是最早出现的字符集,它仅含有常用的英文字母、数字及英文标点符号,共计128个字符,1个字节最多表示256个数值,所以1个字节足以表示所有ascii字符,所以每个字符占用1个字节
iso8859-1是西欧字符的集合,它兼容ascii,另外多了一些西欧的字符。既然有iso8859-1,那有没有iso8859-2、-3…?答案是有,iso8859-2、、iso8859-3一直到.iso8859-16,用来表示中欧、南欧、东欧等地的字符,注意iso8859-n之间是不兼容的,或者说部分兼容,因为iso8859-1的总数量也没超过256,所以仍然用1个字节表示所有的iso8859-1字符,也就意味着每个字符占用1个字节,注意这里要理解iso8859-1兼容ascii这句话,兼容的意思就是iso8859-1囊括了ascii的所有字符,并且对同一个字符的数值是一样的,比如字母a,在ascii中的数值是97,在iso8859-1中数值也是97。
综上,iso8859-1既是字符集也是编码方案,所有字符都占用1个字节。

gbk、big5

既然欧美都出版了本国的字符集,那我国也不能落后。
gb2312是中文的字符集,它兼容ascii,收录了大约6000个字符,主要是简体字及常见字,繁体字、古文书上的生僻字没包括在内,为了解决这个问题,陆续出来了gbk、gb18030,gbk的k是扩展的拼音首字母,收录了大约2万个字符和图形,gb18030收录的字符就更多了,包括蒙古语、朝鲜语等少数民族语言,gb18030、gbk、gb2312依次向前兼容。gbk兼容ascii,对英文字字母和数字使用单字节表示,对汉字采用双字节表示。
big5包含中文的繁体字,主要在港澳台等地流行;

unicode

可以看到每个国家和地区都有自己的字符集,在日常交流中,字符集之间需要转换,比较麻烦,而且如果其他国家再造一套本国的就更麻烦了。所以国际组织就创造了unicode。
unicode又称万国码,顾名思义就是包括世界上所有国家的字符,英文、中文、日文、韩文、英文、拉丁文、希伯来人、斯拉夫文等等。unicode也是兼容ascii的,不过它比较特殊有多套编码方案,utf-8、utf-16、utf-32。

utf-8、utf-16、utf-32

utf-8是变长编码方案,它以8-bit为编码单元,英语、数字占用1个字节,绝大多数汉语占3个字节。
utf-16也是变长编码方案,它以16-bit为编码单元,最开始计划用16-bit(也就是2个字节)表示unicode中的所有字符(基本多语言平台中的字符),但是后来unicode扩容了(扩容的部分称为辅助平面),16-bit不够用的,对于辅助平面中的字符,就用2个16-bit表示,也就是4个字节。
utf-32是定长编码方案,所有字符都占用32-bit,也就是4个字节。
可以看到utf-8是比较省空间的编码方案,所以现在的网络传输、文本保存基本用utf-8。但是它也有缺点,就是找一段文本中的第n个字符需要从头找,因为字符占用的字节数是变长的。

小总结

综上所述,
unicode是字符集,对应的编码方案是utf-8、utf-16、utf-32。
gbk,既是字符集也是编码方案,英文字母、数字占用1个字节,汉字占用2个字节。
iso8859-1是字符集也是编码方案,已有的字符集基本都兼容它。

回答问题

现在再回头看最开始的问题,

  • 程序为什么会出现乱码?
  • 乱码产生的根本原因是编码方案与解码方案不一致导致;比如字符a用utf-8编码的,如果用utf-16解码,虽然用的同一个字符集unicode,但肯定会出现乱码,更不用说用utf-8编码,而用gbk去解码了。
  • unicode、gbk、iso8859-1、ascii、utf-8、utf-16、utf-32,这几个名词,代表什么?它们之间有什么关系?
  • unicode是字符集,utf-8、utf-16、utf-32是unicode的编码方案。gbk既是字符集也是编码方案、ascii、iso8859-1是英文字母、数字的字符集和编码,unicode、iso8859-1都兼容ascii

我们来实战一下,

String s = "万";
//将字符编码,相当于客户端发的数据
 byte[] bytes = s.getBytes("utf-8");
 System.out.println(Arrays.toString(bytes));
 //服务器端收到字节流了,将字节流解码为字符
 String decodedUTF8 = new String(bytes, "utf-8");
 String decodedGBK = new String(bytes, "gbk");
 System.out.println(decodedUTF8);
 System.out.println(decodedGBK);

输出

[-28, -72, -121]   //绝大多数汉字在utf-8中占3个字节,所以这里是3个数值
万 //utf-8编码的,再用utf-8解码,正常
涓� //utf-8编码的,用其他编码方案解码,乱码!

还有一种常见的错误如下

String s = "万";
        //将字符编码
        byte[] bytes = s.getBytes("utf-8");
        System.out.println(Arrays.toString(bytes));
        String decodedISO = new String(bytes, "iso8859-1"); //应该在这里修正,改为utf-8
        System.out.println(decodedISO);  //乱码了
        String fixStr = new String(decodedISO.getBytes("iso8859-1"), "utf-8");  //再转为utf-8修正,这是不对的!应该在第一次字节流转字符处,改正
        System.out.println(fixStr);

输出

[-28, -72, -121]
万    //乱码了
万

后记

编码、解码的世界里还有 byte order mark,简称BOM,为什么有字节序?它跟大端序、小端序有什么关系?为什么utf-8可以没有bom,而utf-16、utf-32却必须有bom?大端序、小端序又指的是什么?下篇再讲吧。

参考

asciiiso8859-1gb2312gbkgb18030unicodeunicode FAQ