Java——字符编码
字符编码1、常见非Unicode编码1.1、ASCII1.2、ISO 8859-11.3、Windows-12521.4、GB23121.5、GBK1.6、GB180301.7、Big51.8、编码汇总2、Unicode编码2.1、UTF-322.2、UTF-162.3、UTF-82.4、Unicode编码小结3、编码转换4、乱码的原因4.1、解析错误4.2、错误的解析和编码转换5、从乱码中恢复5.1、使用UltraEdit5.2、使用Java6、char的真正含义1、常见非Unicode编码1.1、ASCII世界上虽然有各种各样的字符但计算机发明之初没有考虑那么多基本上只考虑了美国的需求。美国大概只需要128个字符所以就规定了128个字符的二进制表示方法。这个方法是一个标准称为ASCII编码全称是American Standard Code forInformationInterchange即美国信息互换标准代码。128个字符用7位刚好可以表示计算机存储的最小单位是byte即8位ASCII码中最高位设置为0用剩下的7位表示字符。这7位可以看作数字0127, ASCII码规定了从0127的每个数字代表什么含义。我们先来看数字32126的含义如图所示除了中 文之外我们平常用的字符基本都涵盖了键盘上的字符大部分也都涵盖了。数字32126表示的字符都是可打印字符031和127表示一些不可以打印的字符这些字符一般用于控制目的这些字符中大部分都是不常用的表列出了其中相对常用的字符。ASCII码对美国是够用了但对其他国家而言却是不够的于是各个国家的各种计算机厂商就发明了各种各种的编码方式以表示自己国家的字符为了保持与ASCII码的兼容性一般都是将最高位设置为1。也就是说当最高位为0时表示ASCII码当为1时就是各个国家自己的字符。在这些扩展的编码中在西欧国家中流行的是ISO8859-1和Windows-1252在中国是GB2312、GBK、GB18030和Big5我们逐个介绍这些编码。1.2、ISO 8859-1ISO 8859-1又称Latin-1它也是使用一个字节表示一个字符其中0127与ASCII一样128255规定了不同的 含义。在128255中128159表示一些控制字符这些字符也不常用就不介绍了。160255表示一些西欧字符如图所示。1.3、Windows-1252ISO 8859-1虽然号称是标准用于西欧国家但它连欧元€这个符号都没有因为欧元比较晚而标准比较早。实际中使用更为广泛的是Windows-1252编码这个编码与ISO 8859-1基本是一样的区别只在于数字128159。Windows-1252使用其中的一些数字表示可打印字符这些数字表示的含义如图所示。这个编码中加入了欧元符号以及一些其他常用的字符。基本上可以认为ISO 8859-1已被Windows-1252取代在很多应用程序中即使文件声明它采用的是ISO 8859-1编码解析的时候依然被当作Windows-1252编码。HTML5甚至明确规定如果文件声明的是ISO 8859-1编码它应该被看作Win-dows-1252编码。为什么要这样呢因为大部分人搞不清楚ISO 8859-1和Windows-1252的区别当他说ISO 8859-1的时候其实他指的是Windows-1252所以标准干脆就这么强制规定了。1.4、GB2312美国和西欧字符用一个字节就够了但中文显然是不够的。中文第一个标准是GB2312。GB2312标准主要针对的是简体中文常见字符包括约7000个汉字和一些罕用词和繁体字。GB2312固定使用两个字节表示汉字在这两个字节中最高位都是1如果是0就认为是ASCII字符。在这两个字节中其中高位字节范围是0xA10xF7低位字节范围是0xA10xFE。比如“老马”的GB2312编码十六进制表示如表所示。老马C0CFC2ED1.5、GBKGBK建立在GB2312的基础上向下兼容GB2312也就是说GB2312编码的字符和二进制表示在GBK编码里是完全一样的。GBK增加了14 000多个汉字共计约21000个汉字其中包括繁体字。GBK同样使用固定的两个字节表示其中高位字节范围是0x810xFE低位字节范围是0x400x7E和0x800xFE。需要注意的是低位字节是从0x40也就是64开始的也就是说低位字节的最高位可能为0。那怎么知道它是汉字的一部分还是一个ASCII字符呢其实很简单因为汉字是用固定两个字节表示的在解析二进制流的时候如果第一个字节的最高位为1那么就将下一个字节读进来一起解析为一个汉字而不用考虑它的最高位解析完后跳到第三个字节继续解析。1.6、GB18030GB18030向下兼容GBK增加了55 000多个字符共76000多个字符包括了很多少数民族字符以及中日韩统一字符。用两个字节已经表示不了GB18030中的所有字符GB18030使用变长编码有的字符是两个字节有的是四个字节。在两字节编码中字节表示范围与GBK一样。在四字节编码中第一个字节的值为0x810xFE第二个字节的值为0x300x39第三个字节的值为0x810xFE第四个字节的值为0x300x39。解析二进制时如何知道是两个字节还是4个字节表示一个字符呢看第二个字节的范围如果是0x300x39就是4个字节表示因为两个字节编码中第二个字节都比这个大。1.7、Big5Big5是针对繁体中文的广泛用于我国台湾地区和我国香港特别行政区等地。Big5包括13 000多个繁体字和GB2312类似一个字符同样固定使用两个字节表示。在这两个字节中高位字节范围是0x810xFE低位字节范围是0x400x7E和0xA10xFE。1.8、编码汇总ASCII码是基础使用一个字节表示最高位设为0其他7位表示128个字符。其他编码都是兼容ASCII的最高位使用1来进行区分。西欧主要使用Windows-1252使用一个字节增加了额外128个字符。我国内地的三个主要编码GB2312、GBK、GB18030有时间先后关系表示的字符数越来越多且后面的兼容前面的GB2312和GBK都是用两个字节表示而GB18030则使用两个或四个字节表示。我国香港特别行政区和我国台湾地区的主要编码是Big5。如果文本里的字符都是ASCII码字符那么采用以上所说的任一编码方式都是一样的。但如果有高位为1的字符除了GB2312、GBK、GB18030外其他编码都是不兼容的。比如Windows-1252和中文的各种编码是不兼容的即使Big5和GB18030都能表示繁体字其表示方式也是不一样的而这就会出现所谓的乱码具体我们稍后介绍。2、Unicode编码以上我们介绍了中文和西欧的字符与编码但世界上还有很多其他国家的字符每个国家的各种计算机厂商都对自己常用的字符进行编码在编码的时候基本忽略了其他国家的字符和编码甚至忽略了同一国家的其他计算机厂商这样造成的结果就是出现了太多的编码且互相不兼容。世界上所有的字符能不能统一编码呢可以这就是Unicode。Unicode 做了一件事就是给世界上所有字符都分配了一个唯一的数字编号这个编号范围从0x0000000x10FFFF包括110多万。但大部分常用字符都在0x00000xFFFF之间即65 536个数字之内。每个字符都有一个Unicode编号这个编号一般写成十六进制在前面加U。大部分中文的编号范围为U4E00U9FFF例如“马”的Unicode是U9A6C。简单理解Unicode主要做了这么一件事就是给所有字符分配了唯一数字编号。它并没有规定这个编号怎么对应到二进制表示这是与上面介绍的其他编码不同的其他编码都既规定了能表示哪些字符又规定了每个字符对应的二进制是什么而Unicode本身只规定了每个字符的数字编号是多少。那编号怎么对应到二进制表示呢有多种方案主要有UTF-32、UTF-16和UTF-8。2.1、UTF-32这个最简单就是字符编号的整数二进制形式4个字节。但有个细节就是字节的排列顺序如果第一个字节是整数二进制中的最高位最后一个字节是整数二进制中的最低位那这种字节序就叫“大端”Big Endian, BE否则就叫“小端”Little Endian, LE。对应的编码方式分别是UTF-32BE和UTF-32LE。可以看出每个字符都用4个字节表示非常浪费空间实际采用的也比较少。2.2、UTF-16UTF-16使用变长字节表示对于编号在U0000UFFFF的字符常用字符集直接用两个字节表示。需要说明的是UD800UDBFF的编号其实是没有定义的。字符值在U10000U10FFFF的字符也叫做增补字符集需要用4个字节表示。前两个字节叫高代理项范围是UD800UDBFF后两个字节叫低代理项范围是UDC00UDFFF。数字编号和这个二进制表示之间有一个转换算法区分是两个字节还是4个字节表示一个字符就看前两个字节的编号范围如果是UD800UDBFF就是4个字节否则就是两个字节。UTF-16也有和UTF-32一样的字节序问题如果高位存放在前面就叫大端BE编码就叫UTF-16BE否则就叫小端编码就叫UTF-16LE。UTF-16常用于系统内部编码UTF-16比UTF-32节省了很多空间但是任何一个字符都至少需要两个字节表示对于美国和西欧国家而言还是很浪费的。2.3、UTF-8UTF-8使用变长字节表示每个字符使用的字节个数与其Unicode编号的大小有关编号小的使用的字节就少编号大的使用的字节就多使用的字节个数为14不等。具体来说各个Unicode编号范围对应的二进制格式如表所示。x表示可以用的二进制位而每个字节开头的1或0是固定的。小于128的编码与ASCII码一样最高位为0。其他编号的第一个字节有特殊含义最高位有几个连续的1就表示用几个字节表示而其他字节都以10开头。对于一个Unicode编号具体怎么编码呢首先将其看作整数转化为二进制形式去掉高位的0然后将二进制位从右向左依次填入对应的二进制格式x中填完后如果对应的二进制格式还有没填的x则设为0。我们来看个例子“马”的Unicode编号是0x9A6C整数编号是39 532其对应的UTF-8二进制格式是1110xxxx 10xxxxxx 10xxxxxx整数编号39 532的二进制格式是1001 101001 101100将这个二进制位从右到左依次填入二进制格式中结果就是其UTF-8编码11101001 10101001 10101100十六进制表示为0xE9A9AC。和UTF-32/UTF-16不同UTF-8是兼容ASCII的对大部分中文而言一个中文字符需要用三个字节表示。2.4、Unicode编码小结Unicode给世界上所有字符都规定了一个统一的编号编号范围达到110多万但大部分字符都在65 536以内。Unicode本身没有规定怎么把这个编号对应到二进制形式。UTF-32/UTF-16/UTF-8都在做一件事就是把Unicode编号对应到二进制形式其对应方法不同而已。UTF-32使用4个字节UTF-16大部分是两个字节少部分是4个字节它们都不兼容ASCII编码都有字节顺序的问题。UTF-8使用14个字节表示兼容ASCII编码英文字符使用1个字节中文字符大多用3个字节。3、编码转换有了Unicode之后每一个字符就有了多种不兼容的编码方式比如说“马”这个字符它的各种编码方式对应的十六进制如表所示。这几种格式之间可以借助Unicode编号进行编码转换。可以认为每种编码都有一个映射表存储其特有的字符编码和Unicode编号之间的对应关系这个映射表是一个简化的说法实际上可能是一个映射或转换方法。编码转换的具体过程可以是一个字符从A编码转到B编码先找到字符的A编码格式通过A的映射表找到其Unicode编号然后通过Unicode编号再查B的映射表找到字符的B编码格式。举例来说“马”从GB18030转到UTF-8先查GB18030-Unicode编号表得到其编号是9A 6C然后查Uncode编号-UTF-8表得到其UTF-8编码E9 A9AC。编码转换改变了字符的二进制内容但并没有改变字符看上去的样子。4、乱码的原因乱码有两种常见原因一种比较简单就是简单的解析错误另外一种比较复杂在错误解析的基础上进行了编码转换。4.1、解析错误看个简单的例子。一个法国人采用Windows-1252编码写了个文件发送给了一个中国人中国人使用GB18030来解析这个字符看到的可能就是乱码。比如法国人发送的是Pékin, Windows-1252的二进制采用十六进制是50 E9 6B 69 6E第二个字节E9对应é其他都是ASCII码中国人收到的也是这个二进制但是他把它看成了GB18030编码GB18030中E9 6B对应的是字符“閗”于是他看到的就是“P閗in”这看来就是一个乱码。反之也是一样的一个GB18030编码的文件如果被看作Windows-1252也是乱码。这种情况下之所以看起来是乱码是因为看待或者说解析数据的方式错了。只要使用正确的编码方式进行解读就可以纠正了。很多文件编辑器如EditPlus、NotePad、UltraEdit都有切换查看编码方式的功能浏览器也都有切换查看编码方式的功能如Fire-fox在菜单“查看”→“文字编码”中即可找到该功能。切换查看编码的方式并没有改变数据的二进制本身而只是改变了解析数据的方式从而改变了数据看起来的样子这与前面提到的编码转换正好相反。很多时候做这样一个编码查看方式的切换就可以解决乱码的问题但有的时候这样是不够的。4.2、错误的解析和编码转换如果怎么改变查看方式都不对那很有可能就不仅仅是解析二进制的方式不对而是文本在错误解析的基础上还进行了编码转换。我们举个例子来说明两个字“老马”本来的编码格式是GB18030编码十六进制是C0 CF C2 ED。这个二进制形式被错误当成了Windows-1252编码解读成了字符“ÀÏÂí”。随后这个字符进行了编码转换转换成了UTF-8编码形式还是“ÀÏÂí”但二进制变成了C3 80 C3 8F C382 C3 AD每个字符两个字节。这个时候再按照GB18030解析字符就变成了乱码形式“脌脧脗铆”而且这时无论怎么切换查看编码的方式这个二进制看起来都是乱码。这种情况是乱码产生的主要原因。这种情况其实很常见计算机程序为了便于统一处理经常会将所有编码转换为一种方式比如UTF-8在转换的时候需要知道原来的编码是什么但可能会搞错而一旦搞错并进行了转换就会出现这种乱码。这种情况下无论怎么切换查看编码方式都是不行的如表所示。虽然有这么多形式但我们看到的乱码形式很可能是“ÀÏÂí”因为在例子中UTF-8是编码转换的目标编码格式既然转换为了UTF-8一般也是要按UTF-8查看。那有没有办法恢复呢如果有怎么恢复呢5、从乱码中恢复“乱”主要是因为发生了一次错误的编码转换所谓恢复是指要恢复两个关键信息一个是原来的二进制编码方式A另一个是错误解读的编码方式B。恢复的基本思路是尝试进行逆向操作假定按一种编码转换方式B获取乱码的二进制格式然后再假定一种编码解读方式A解读这个二进制查看其看上去的形式这要尝试多种编码如果能找到看着正常的字符形式应该就可以恢复。这听上去可能比较抽象我们举个例子来说明假定乱码形式是“ÀÏÂí”尝试多种B和A来看字符形式。我们先使用编辑器以UltraEdit为例然后使用Java编程来看。5.1、使用UltraEditUltraEdit支持编码转换和切换查看编码方式也支持文件的二进制显示和编辑所以我们以UltraEdit为例其他一些编辑器可能也有类似功能。新建一个UTF-8编码的文件复制“ÀÏÂí”到文件中。使用编码转换转换到Win-dows-1252编码执行“文件”→“转换到”→“西欧”→WIN-1252命令。转换完后打开十六进制编辑查看其二进制形式如图所示。可以看出其形式还是“ÀÏÂí”但二进制格式变成了 C0CF C2 ED。这个过程相当于假设B是Windows-1252。这个时候再按照多种编码格式查看这个二进制在UltraEdit中关闭十六进制编辑切换查看编码方式为GB18030执行“视图”→“查看方式文件编码”→“东亚语言”→GB18030命令切换完后同样的二进制神奇地变为了正确的字符形式“老马”打开十六进制编辑器可以看出二进制还是C0 CF C2 ED这个GB18030相当于假设A是GB18030。这个例子我们碰巧第一次就猜对了。实际中可能要做多次尝试过程是类似的先进行编码转换使用B编码然后使用不同编码方式查看使用A编码如果能找到看上去对的形式就恢复了。表列出了主要的B编码格式、对应的二进制以及按A编码解读的各种形式。可以看出第一行是正确的也就是说原来的编码其实是A即GB18030但被错误解读成了B即Windows-1252了。5.2、使用JavaJava中处理字符串的类有String, String中有我们需要的两个重要方法。public byte[] getBytes(String charsetName)这个方法可以获取一个字符串的给定编码格式的二进制形式。public String(byte bytes[], String charsetName)这个构造方法以给定的二进制数组bytes按照编码格式charsetName解读为一个字符串。将A看作GB18030将B看作Windows-1252进行恢复的Java代码如下所示publicclassBase{publicstaticvoidmain(String[]args){StringstrÀÏÂí;StringnewStrnull;try{newStrnewString(str.getBytes(Charset.forName(windows-1252)),GB18030);}catch(UnsupportedEncodingExceptione){e.printStackTrace();}System.out.println(newStr);//老马}}先按照B编码Windows-1252获取字符串的二进制然后按A编码GB18030解读这个二进制得到一个新的字符串然后输出这个字符串的形式输出为“老马”。同样一次碰巧就对了实际中我们可以写一个循环测试不同的A/B编码中的结果形式如代码所示。publicstaticvoidrecover(Stringstr)throwsUnsupportedEncodingException{String[]charsetsnewString[]{windows-1252,GB18030,Big5,UTF-8};for(inti0;icharsets.length;i){for(intj0;jcharsets.length;j){if(i!j){StringsnewString(str.getBytes(charsets[i]),charsets[j]);System.out.println(---- 原来编码(A)假设是 charsets[j], 被错误解读为了(B): charsets[i]);System.out.println(s);System.out.println();}}}}以上代码使用不同的编码格式进行测试如果输出有正确的那么就可以恢复。可以看出恢复的尝试需要进行很多次上面例子尝试了常见编码GB18030、Windows 1252、Big5、UTF-8共12种组合。这4种编码是常见编码在大部分实际应用中应该够了。如果有其他编码可以增加一些尝试。不是所有的乱码形式都是可以恢复的如果形式中有很多不能识别的字符如则很难恢复。另外如果乱码是由于进行了多次解析和转换错误造成的也很难恢复。6、char的真正含义char用于表示一个字符这个字符可以是中文字符也可以是英文字符。赋值时把常量字符用单引号括起来例如charcA;charz马;但为什么字符类型也可以进行算术运算和比较呢它的本质到底是什么呢在Java内部进行字符处理时采用的都是Unicode具体编码格式是UTF-16BE。简单回顾一下UTF-16使用两个或4个字节表示一个字符Unicode编号范围在65 536以内的占两个字节超出范围的占4个字节BE就是先输出高位字节再输出低位字节这与整数的内存表示是一致的。char本质上是一个固定占用两个字节的无符号正整数这个正整数对应于Unicode编号用于表示那个Unicode编号对应的字符。由于固定占用两个字节char只能表示Unicode编号在65 536以内的字符而不能表示超出范围的字符。那超出范围的字符怎么表示呢使用两个char。在这个认识的基础上我们再来看下char的一些行为。char有多种赋值方式1.charcA2.charc马3.charc395324.charc0x9a6c5.charc\u9a6c第1种赋值方式是最常见的将一个能用ASCII码表示的字符赋给一个字符变量。第2种赋值方式也很常见但这里是个中文字符需要注意的是直接写字符常量的时候应该注意文件的编码比如GBK编码的代码文件按UTF-8打开字符会变成乱码赋值的时候是按当前的编码解读方式将这个字符形式对应的Unicode编号值赋给变量“马”对应的Unicode编号是39 532所以第2种赋值方式和第3种赋值方式是一样的。第3种赋值方式是直接将十进制的常量赋给字符。第4种赋值方式是将十六进制常量赋给字符第5种赋值方式是按Unicode字符形式。所以第2、3、4、5种赋值方式都是一样的本质都是将Unicode编号39 532赋给了字符。由于char本质上是一个整数所以可以进行整数能做的一些运算在进行运算时会被看作int但由于char占两个字节运算结果不能直接赋值给char类型需要进行强制类型转换这和byte、short参与整数运算是类似的。char类型的比较就是其Unicode编号的比较。char的加减运算就是按其Unicode编号进行运算一般对字符做加减运算没什么意义但ASCII码字符是有意义的。比如大小写转换大写AZ的编号是6590小写az的编号是97122正好相差32所以大写转小写只需加32而小写转大写只需减32。加减运算的另一个应用是加密和解密将字符进行某种可逆的数学运算可以做加解密。char的位运算可以看作是对应整数的位运算只是它是无符号数也就是说有符号右移和无符号右移的结果是一样的。既然char本质上是整数查看char的二进制表示同样可以用Integer的方法如下所示charc马;System.out.println(Integer.toBinaryString(c));//1001101001101100