背景基础知识:
1、字符编码的相关知识(转自http://blog.csdn.net/llwan/article/details/7567906)
1.1、 “字符”是由数字来表示的
先来重新了解一下计算机是如何处理“字符”的,这个原理是大家必须记住的,特别是在用JAVA写程序的时候,万万不可模糊。我们知道,计算机把任何东西都用数字来表示,“字符”也不例外。比如我们要显示一个阿拉伯数字“3”,在我们的PC里,其实并不是仅仅用一个数字3来代表我们要写的“3”,而是以十六进制的0x33来代表,包括放在内存或者是写到文件里,其实都是写着0x33的,不信你可以编辑一个文本文件,写一个“3”,然后用ultraEdit看他的原始码。
1.2、 一切“字符”都必定用数字+编码表表示。
这时候,有一个问题:为什么一定要用0x33来代表“3”呢?而不用0x43来代表呢?或者是直接用0x03来代替?其实用什么来代表都可以,只不过大家都习惯了用ASCII编码表(是美国国家信息交换表)来确定各字符应该是用什么数字代表的。同样,为了表示中国字,我国也指定了中文的编码表,其中最广泛使用的是GB2312。比如中文的“当”字,就是用0xB5,
0xB1这两个八位的数字来表示的。所以如果显示字符的程序不知道一列数字到底是按什么编码表编码的,他也无法去判断到底这些是什么文字。如果随便用一个不对的编码表来处理这些数字,处理出来的字符很可能完全是错的。比如在英文系统上,没有GB2312编码表,送给他一个0xB5,0xB1,他就傻傻的当作ASCII来处理(操作系统通常都有自己默认的编码表),结果显示出来就是两个奇怪的符号,因为这两个字在ASCII表里就是那两个符号。同样在繁体中文系统里,他的编码表是BIG5,显示出来也是一个奇怪的中文,不是“当”字。
1.3、 UNICODE让全世界都说一种语言
看完上面的文字,是否觉得,世界有那么多语言,每个都有自己的一套编码表,很麻烦呢?就算是中文,也有两套流行的编码表,一个是GB2312,一个是BIG5。要使用不同中文的编码的字符时,还要转来转去,的确很麻烦。不光这个,如果想要写一篇包含很多过国文字的文章,就麻烦了,必须要让处理这个文章的程序知道,哪个字是什么编码标准的。如果你想要在文章里找一个字,也必须指定你要找的是哪种编码的哪个字。否则,你要找一个0xB5,0xB1的中文“当”字,很可能把同样数字表示的日文、波兰文这些不相干的字一起给你找出来,够麻烦的吧!
所以人们想,不如大家都用同一个编码标准吧,各种文字都在编码表里有一席之地,处理文字的程序只需要都按这个编码表来处理就可以了。不过要一个编码表里包含所有的文字,这张表就大了,本来英文字+数字一共只有128个以内。但加上中文后,忽然就多了数万个,所以存放一个字符需要的大小也大了很多。现在UNICODE规定了一个字符必须由2个8位数字来表示(即用一个十六进制数表示),想想,8x8x8x8x
= 65536
,是多大的一个数字啊!所以全世界的文字才能都包含进去。当然拉,也有人说中国字可能都不止6万个拉,还要包括别的文字,但人家外国人觉得你们中国人常用的也没那么多,所以就这么定了,我们也没办法。需要注意的是GB2312和UNICODE虽然都是用两个8位数来代表一个中文字,但具体的规格可不一样,比如0xB5,0xB1在UNICODE里面可不是“当”字,而是另外一国的文字来的。
1.4、JAVA是是如何处理字符的。
世界总会进步的,JAVA就是一个例子。JAVA终于有了String类了,它是解决字符问题的最好工具。在JAVA里,一个基本的要点是:String类对象是不需要指定编码表的!为什么它会自己知道一堆数字各代表什么字符呢?就是因为String里的字符信息是用UNICODE编码存放的。而JAVA为了表示字符(注意是单个字符),也有char这个数据类型,而且他的大小是固定2个8位16进制数字长度,也就是0~65535罗。为的就是对应UNICODE里面的一个字符。大家如果想取一个String里的按UNICODE数字,可以用getChars(int
srcBegin, int srcEnd, char[] dst, int dstBegin)
方法取得一个char[],这个char[]里就是表示String字符的,按UNICODE编码表编码的数字。
可惜现在绝大多数的系统和程序都不是按UNICODE来处理字符,而JAVA程序总是要和别的程序和系统交换数据的,所以在接收一个字符,或者是发送一个字符的时候,就必须要留意当前系统和UNICODE的关系了。比如你从网络或者文件接受到一数字:0xB5,0xB1,JAVA程序并不知道这两个字到底是中文呢?还是日文,或者英文。你如果不指明这个两个数字的编码表,JAVA就会按当前系统默认的编码表来处理。如果这两个数字是从中文WIN98发出去的,JAVA程序又是在英文LINUX上运行的,那就出现了所谓的乱码问题了。也就是JAVA按英文的编码表ASCII来处理这两个数字,当通过new
String({0xB5,0xB1})得到的String的时候,这个String代表的已经不是中文的“当”字,而是两个英文的奇怪字符了。不过如果你知道这两个数字一定是中文的话,就可以指定用new
String({0xB5,0xB1},"GB2312")来处理,这时候新建立的String才真的是一个“当”字。当然拉,如果你要把一个“当”字的JAVA的String显示在中文WIN98上,必须把这个字输出成两个8位数字:0xB5,0xB1,不管是写成文件还是输出到浏览器上,都必须是0xB5,0xB1。如何把“当”字用GB2312输出?String.getBytes("GB2312")就可以拉!所以有一点要记住:和外界交换任何信息都是以byte[]来进行的!。你可以留意一下JAVA大多数的I/O类,都有以byte[]作为参数和返回值的方法。不过,也有很多写的比较糊涂的程序,没有提供byte[]交换信息的方法,害的不同文字平台的程序员很头疼。Servlet的HttpRequest.getParameter()就是这样。好在有的JSP/SERVLET容易还提供先指定编码表的方法,才能比较简单的解决这个问题。
1.5、网上关于JAVA中文问题的一些错误处理方法。
一个是最常见的,不管什么内容,都用new
String(...,"ISO-8859-1")来建立字符串,然后使用的时候按默认的编码格式(通常在服务器上都是英文系统)输出字符串。这样其实你使用的String并不是按UNICODE来代表真正的字符,而是强行把BYTE数组复制到String的char[]里,一旦你的运行环境改变,你就被迫要修改一大堆的代码。而且也无法在同一个字符串里处理几种不同编码的文字。
另一个是把一种编码格式的字符串,比如是GB2312,转换成另一种格式的字符串,比如UTF-8,然后不指明是UTF-8编码,而直接用new
String(...)来建立String,这样放在String里面的字符也是无法确定的,它在不同的系统上代表不同的字符。如果要求别人用“UTF-8格式”的String来交换信息的时候,其实已经破坏了JAVA为了兼容各种语言所做的规定。这种错误的本质思想是还按写C语言的方式,把字符串纯粹当作可以自己自由编码的存储器使用,而忽略了JAVA字符串只有一种编码格式。如果真的想自由编码,用byte[]或者char[]就完全了解决问题的了。
1.6、小结(个人总结,非转载)
1.6.1、对字符编码的理解
字符编码就是一种将二进制数据解析为我们日常使用的语言字符的东西,例如0100 0001(二进制)在ASCII的规则下表示A这个字符,但是在Unicode的规则下并不表示这个字符
1.6.2、出现乱码情况的原因
1.6.2.1、2进制不对(getBytes(str,"GB2312/UNICODE..."))getBytes(str)使用系统默认的编码方式
假如在你将UNICODE编码的汉字"我"变成二进制表示的时候,却使用了GB2312的编码规则,这个时候得到的二进制肯定不是你希望的,等到你再使用这个错的二进制按照Unicode编码规则得到的就不会是"我"了,出现乱码
1.6.2.2、编码方式不对(New String(bytes,"GB2312/UNICODE..."))new String(bytes)使用系统默认的编码方式
假如用new String({0xB5,0xB1}),这个时候j因为你没有指定编码方式,java会使用当前系统默认的编码方式进行处理,这个时候因为0xB5,0xB1在你希望的编码方式下是正确的字符,但是如果默认的编码方式下则不一定是你希望的结果,所以出现乱码
1.6.3、一些测试
1.6.3.1Java String由char组成,从String中的函数CharAt(int index)便可知
1.6.3.2.String类中存放的是UNICODE编码,每个Char是16位,一个Char可能是传统的ASCII字符,也可能是一个汉字,在内存中都 是占据两个字节
1.6.3.3.String类和Byte[]之间的关系
String s = new String("霜之哀伤");
byte[] array1 = s.getBytes("utf-8");
byte[] array2 = s.getBytes("gbk");
byte[] array3 = s.getBytes("unicode");
byte[] array4 = s.getBytes();// 默认是GBK
printBytes(array1);
printBytes(array2);
printBytes(array3);
printBytes(array4);
程序输出:
0xE9 0x9C 0x9C 0xE4 0xB9 0x8B 0xE5 0x93 0x80 0xE4 0xBC 0xA4
0xCB 0xAA 0xD6 0xAE 0xB0 0xA7 0xC9 0xCB
0xFE 0xFF 0x97 0x1C 0x4E 0x4B 0x54 0xC0 0x4F 0x24
0xCB 0xAA 0xD6 0xAE 0xB0 0xA7 0xC9 0xCB
2、十六进制(参考 https://my.oschina.net/xinxingegeya/blog/287476)
十六进制(简写为hex或下标16)在数学中是一种逢16进1的进位制,一般用数字0到9和字母A到F表示(其中:A~F即10~15)。
例如十进制数57,在二进制写作111001,在16进制写作39。
像java,c这样的语言为了区分十六进制和十进制数值,会在十六进制数的前面加上 0x,比如0x20是十进制的32,而不是十进制的20
十六进制字符用4个二进制位来表示
3、java中byte[]和十六进制字符串相互转换
Java中byte用二进制表示占用8位,而我们知道16进制的每个字符需要用4位二进制位来表示。
所以我们就可以把每个byte转换成两个相应的16进制字符
同理,相反的转换也是将两个16进制字符转换成一个byte,原理同上。
根据以上原理,我们就可以将byte[] 数组转换为16进制字符串了,当然也可以将16进制字符串转换为byte[]数组了。
/*
*为了将byte转换为int,必须 &0xFF(1111 1111)否则会因为原码、补码的问题导致错误,例如:-1的byte(负数用补码表示) 1111 1111,直接强转成int(32bit)为1111....1111 1111 ,这时将1111....1111 1111进行Integer.toHexString
就会得到0xffffffff这显然不是我们期望的答案,而使用&0xff可以将前面24位清0,得到0xff,还有一个小知识点,当系统检测到byte可能会转化成int或者说byte与int类型进行运算的时候,就会将byte的内存空间高位补1(也就是按符号位补位)扩充到32位,再参与运算。
这就是为什么不会出现 1111 1111 & 0xff 然后再补位,而是先将1111 1111补位到1111...1111 1111 ,在进行&0xff运算得到000...1111 1111(我们可以发现,这个时候字面量值已经发生了变化,即由-1变成了255,但是保证了二进制补码的一致性)
*/
public String toHexString0(byte[] bytes){ int b ; StringBuffer sb =new StringBuffer(); for(int i=0;i<bytes.length;i++){ b = bytes[i] & 0xFF; sb.append(Integer.toHexString(b)); } return sb.toString(); }
测试
@org.junit.Test public void test4() throws Exception { System.out.println(toHexString0(new byte[]{109,111})); }
结果