这几日突然想搞清楚常见字符编码(ASCII、GBK、UTF、Unicode、ISO-8859-1)的关系及项目中可能存在的相关隐患,网上搜索了一大堆资料,这里结合代码实测,简单做个小结:
浅谈这几种编码的出现
1.1 ASCII
我们知道,在计算机内部,所有信息最终被表示为二进制字符串,每个二进制位有1和0两种表现形式,位是内存最小存储单位,其中8位构成一个字节,字节是数据存储的最小单元,因此8个二进制位即一个字节可以表示256个符号,从00000000-11111111。
上个世纪60年代,美国制定一套用来表示 英文字符和二进制位映射关系的字符编码集,即ASCII码,
ASCII码一共规定了128个字符的编码,比如空格"SPACE"是32(二进制00100000),大写的字母A是65(二进制 01000001)。这128个符号(包括32个不能打印出来的控制符号,比如00x10表示换行),只占用了一个字节的后 面7位,最前面的1位统一规定为0。这样计算机就可以用不同字节来存储英语的 文字了。大家看到这样,都感觉 很好,于是大家都把这个方案叫做 ANSI 的"Ascii"编码(American Standard Code for Information
Interchange,美国信息互换标准代码)。当时世界上所有的计算机都用同样的ASCII方案来保存英文文字。
1.2 ISO-8859-1
刚才所说的那128个符号用来表示英文符号是够用了,但是有些国家根本不用英语,他们的语言在ASCII码上却 找不到,于是他们决定采用127号之后的空位来表示这些新的字母、符号,还加入了很多画表格时需要用下到的横 线、竖线、交叉等形状,一直把序号编到了最后一个状态255。从128到255这一页的字符集被称"扩展字符集"。因 为ISO-8859-1编码范围使用了单字节内的所有空间,因此把其他任何编码的字节流当作ISO-8859-1编码看待都没有 问题。
1.3 GBK、GB18030:
等中国人们得到计算机时,已经没有可以利用的字节状态来表示汉字,况且有6000多个常用汉字需要保存呢。 但是这难不倒智慧的中国人民,我们不客气地把那些127号之后的奇异符号们直接取消掉,并且规定:一个小于127的 字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节) 从0xA1用到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这 些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标 点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符
了。
中国人民看到这样很不错,于是就把这种汉字方案叫做"GB2312"。GB2312 是对 ASCII 的中文扩展。但中国的汉字, 只这些还是不够,于是干脆不再要求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个 汉字的开始,不管后面跟的是不是扩展字符集里的内容。结果扩展之后的编码方案被称为 GBK 标准,GBK 包括了 GB2312 的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号。
后来少数民族也要用电脑了,于是我们再扩展,又加了几千个新的少数民族的字,GBK 扩成了 GB18030。从此之 后,中华民族的文化就可以在计算机时代中传承了。
中国的程序员们看到这一系列汉字编码的标准是好的,于是通称他们叫做 "DBCS"(Double Byte Charecter Set 双 字节字符集)。在DBCS系列标准里,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案 里,因此他们写的程序为了支持中文处理,必须要注意字串里的每一个字节的值,如果这个值是大于127的,那么就 认为一个双字节字符集里的字符出现了。
因为当时各个国家都像中国这样搞出一套自己的编码标准,结果互相之间谁也不懂谁的编码,谁也不支持别人的编 码,连大陆和台湾这样只相隔了150海里,使用着同一种语言的兄弟地区,也分别采用了不同的 DBCS 编码方案—— 当时的中国人想让电脑显示汉字,就必须装上一个"汉字系统",专门用来处理汉字的显示、输入的问题,但是那个台 湾的愚昧封建人士写的算命程序就必须加装另一套支持 BIG5 编码的什么"倚天汉字系统"才可以用,装错了字符系 统,显示就会乱了套!这怎么办?而且世界民族之林中还有那些一时用不上电脑的穷苦人民,他们的文字又怎么办?
1.4 Unicode
正在这时,ISO (国际标谁化组织)的国际组织决定着手解决这个问题。他们采用的方法很简单:废了所有的地 区性 编码方案,重新搞一个包括了地球上所有文化、所有字母和符号的编码!他们打算叫它"Universal Multiple- Octet Coded Character Set",简称 UCS, 俗称 "UNICODE" 。
UNICODE 开始制订时,计算机的存储器容量极大地发展了,空间再也不成为问题了。于是 ISO 就直接规定必须用两 个字节,也就是16位来统一表示所有的字符,对于ascii里的那些"半角"字符,UNICODE 包持其原编码不变,只是将 其长度由原来的8位扩展为16位,而其他文化和语言的字符则全部重新统一编码。由于"半角"英文符号只需要用到低8 位,所以其高 8位永远是0,因此这种大气的方案在保存英文文本时会多浪费一倍的空间。因此,在UNICODE 中,一 个字符就是两个字节。所以可以说它是不兼容iso8859-1编码的,也不兼容任何编码。这使得 GBK 与UNICODE 在汉字 的内码编排上完全是不一样的,没有一种简单的算术方法可以把文本内容从UNICODE编码和另一种编码进行转换,这 种转换必须通过查表来进行。
如前所述,UNICODE 是用两个字节来表示为一个字符,他总共可以组合出65535不同的字符,这大概已经可以覆盖世 界上所有文化的符号。如果还不够也没有关系,ISO已经准备了UCS-4方案,说简单了就是四个字节来表示一个字符, 这样我们就可以组合出21亿个不同的字符出来(最高位有其他用途),这大概可以用到银河联邦成立那一天吧!
需要说明的是,定长编码便于计算机处理(注意GB2312/GBK不是定长编码),而unicode又可以用来表示所有字符, 所以在很多软件内部是使用unicode编码来处理的,比如java。
1.5 UTF
考虑到unicode编码不兼容iso8859-1编码,而且容易占用更多的空间:因为对于英文字母,unicode也需要两个字 节来表示。所以unicode不便于传输和存储。因此而产生了utf编码,utf编码兼容iso8859-1编码,同时也可以用来表 示所有语言的字符,不过,utf编码是不定长编码,每一个字符的长度从1-6个字节不等。另外,utf编码自带简单的 校验功能。一般来讲,英文字母都是用一个字节表示,而汉字使用三个字节。注意,虽然说utf是为了使用更少的空 间而使用的,但那只是相对于unicode编码来说,如果已经知道是汉字,则使用GB2312/GBK无疑是最节省的。不过另 一方面,值得说明的是,虽然utf编码对汉字使用3个字节,但即使对于汉字网页,utf编码也会比unicode编码节省, 因为网页中包含了很多的英文字符。
2 和java相关
在java项目中,编码问题存在于两个方面:JVM之内和JVM之外
2.1 Java文件编译后形成class
这里Java文件的编码可能有多种多样,但Java编译器会自动将这些编码按照Java文件的编码格式正确读取后产生class文件,这里的class文件编码是Unicode编码。
因此,在Java代码中定义一个字符串:
String s="小能";
不管在编译前java文件使用何种编码,在编译后成class后,他们都是一样的----Unicode编码表示。
2.2 JVM中的编码
JVM加载class文件读取时候使用Unicode编码方式正确读取class文件,那么原来定义的String s="小能";在内存中的表现形式是Unicode编码。
当调用String.getBytes()的时候,其实已经为乱码买下了祸根。因为此方法使用平台默认的字符集来获取字符串对应的字节数组。在Windows7中文版中,使用的默认编码是GBK,不信运行下:
注意,在IDE中,上面代码运行的结果和IDE中的编码设置一直,如果在DOS窗口中运行,得到就是平台的了,因此如果和第三方数据交互时,如果不注意,可能会出现在IDE中运行的时候正常,但部署在其他服务器中可能就会出现乱码。
2.3 内存中字符串的编码
内存中的字符串不仅仅局限于从class代码中直接加载而来的字符串,还有一些字符串是从文本文件中读取的,还有的是通过数据库读取的,还有可能是从字节数组构建的,然而他们基本上都不是Unicode编码的,原因很简单,存储优化。
因此就需要处理各种各样的编码问题,在处理之前,必须明确“源”的编码,然后用指定的编码方式正确读取到内存中。如果是一个方法的参数,实际上必须明确该字符串参数的编码,因为这个参数可能是另外一个日文系统传递过来的。当明确了字符串编码时候,就可以按照要求正确处理字符串,以避免乱码。
在对字符串进行解码编码的时候,应该调用下面的方法:
getBytes(String charsetName)
String(byte[] bytes, String charsetName)
2.4 再看下面这段代码
运行结果:
有了前面的讲解,机智的你肯定能一眼看出问题所在,但是为什么同样都经过两次转换,第一段乱码,而第二段却正常呢?(注:笔者的IDE中字符集设置的GBK(如果是ISO-8859-1那也不会出现问题),当然第二段未必一定会出乱码,不信你把 联通 换成 移动 再试,嘿嘿,有人说这就是联通不如移动的证据(纯玩笑))
这是因为UTF-8编码中,一个汉字占3个字节,而GBK中占2个字节,注意图中输出的字节数组,从23之后,第二段输出中后面是63,这是因为当调用getBytes("UTF-8")时,2个汉字得到了6个字节,而调用new String()解析时,GBK认为这是3个汉字,这结果肯定乱码无疑了,而解析完后最后一位是?,因此当再次用得到这个乱码按UTF-8的规范转换时,?在ASCII码中的代表数字是63,因此就出现了上述结果,而ISO-8859-1没出现这个问题是因为,这种编码是一个字节对应一个字符,因此逆回去的时候未出现错误