前言
上次对计算机中的“字符集”和“编码”分别进行了总结,并指出二者之间的区别,不要搞混了,不清楚的再回到上一章看一下。今天再总结下java中是如何使用字符集(主要是Unicode字符集,其他常用字符集都只有一种编码规则),以及是如何使用utf-8、utf-16、utf-32对Unicode字符集进行编码的。
java中的char类型
java中的char类型占用两个字节、用于定义字符,这些字符只覆盖了Unicode字符集中的第0个平面中定义的符号(该平面中定义的符号 都是地球人最常用的65536个),也就是说其他16个平面中的符号是没办法有java的char类型表示的。
char c0 = 'A'; char c1='天'; char c2='星'; char c3 = 'XXX';//编译错误
提示:这个字iteye无法识别,导致文章提交被截断,文中所有’XXX’都是表示这个字。
前三个char类型赋值没有问题,第四个赋值直接编译错误。’XXX’这个字的Unicode码值是:10 10001000 10111011,可以看出使用两个字节是放不下的。如果使用utf-16进行编码,编码后的二进制值如下(需要4个字节,编码格式可以参考上一篇文章):
1101100 00110001 01101110 010111011
这也就是为什么不能把'XXX'赋值给java char类型的原因。
Java中字符串转字节
Java中的字符串(String)在网络传输或者存储硬盘的真实内容是 通过字符集编码转换后的二进制数字。在String类中定义了几个getBytes重载方法,来获取字符串对应的字节数组,并且默认使用的是Unicode字符集的utf-8编码:测试代码如下:
byte[] bytes = "XXX".getBytes(); System.out.println("默认编码:"+new BigInteger(1,bytes).toString(2)); byte[] bytes3 = "XXX".getBytes("utf-8"); System.out.println("使用utf-8编码:"+new BigInteger(1,bytes3).toString(2));
打印内容为:
默认编码:11110000101010001010001010111011 使用utf-8编码:11110000101010001010001010111011
可以发现使用不带参数和带"utf-8"参数的结果是完全一致的。即可说明:getBytes不带参数的默认方法使用的是Unicode字符集的utf-8编码规则进行编码的。"XXX"不是常用的汉字,这里使用了4字节,常用汉字使用utf-8编码一般三个字节。
在上一章中讲到过utf-16是使用2或者4字节进行存储,我们来看下在java语言中的表现:
byte[] bytes1 = "XXX".getBytes("Unicode"); System.out.println("使用Unicde字符集的默认编码:"+new BigInteger(1,bytes1).toString(2)); byte[] bytes2 = "XXX".getBytes("utf-16"); System.out.println("使用utf-16编码:"+new BigInteger(1,bytes2).toString(2)); byte[] bytes4 = "XXX".getBytes("ASCII"); System.out.println("使用ASCII字符集:"+new BigInteger(1,bytes4).toString(2));
打印结果为:
使用Unicde字符集的默认编码:111111101111111111011000011000101101110010111011 使用utf-16编码:111111101111111111011000011000101101110010111011 使用ASCII字符集:111111
注意这里区别:
调用getBytes("Unicode")方法 表示使用Unicode字符集的默认编码方式进行编码。这个默认编码方式一般由操作系统指定,我的是win7,显示跟utf-8编码相同;
调用getBytes("utf-16")方法 表示使用默认Unicode字符集的utf-16编码方式进行编码。
另外我们上一章说过,utf-16使用2或者4个字节存储,"XXX"不在第0平面,应该占用4个字节。但实际打印出来的是11111110 11111111 11011000 01100010 11011100 10111011,一共6个字节,怎么多了两个字节呢?我们还可以发现无论是什么字符串,通过调用getBytes("utf-16")方法,打印出来的前面都会固定多两个字节:11111110 11111111,换算成16进制就是0x FEFF。这里有一个大端序和小端序的概念。
大端序和小端序
前一篇文章也提到过utf-8、utf-16、utf-32有一个区别就是一次最少读入的字节数,utf-8是一次最少读入一个字节(8个bit),utf-16是2两字节,utf-32是4个字节。其中utf-16和utf-32一次需要读入多个字节,根据读取顺序的不同,分为大端序和小端序。
大端序:简单的理解就是从左往右依次读入两个(utf-16)或者4个(utf-32)字节
小端序:简单的理解就是从右往做反向依次读入两个(utf-16)或者4个(utf-32)字节
大端序编码:UTF-16Be、UTF-32Be;小端序:UTF-16Le、UTF-32Le;另外在java中还可以使使用UnicodeBig、UnicodeLittle,表示使用默认的UTF-16大、小端序编码,与UTF-16Le、UTF-32Le基本等效,区别就是UnicodeBig、UnicodeLittle的前面会自动加上两字节,用于表示大小端序。
UnicodeBig自动在编码后的最前面加上:1111111011111111 换成16进制为 FEFF;
UnicodeLittle自动在编码后的最前面加上:1111111111111110 换成16进制为FFFE。
测试代码如下:
byte[] bytes2 = "XXX".getBytes("utf-16"); System.out.println("使用utf-16编码:"+new BigInteger(1,bytes2).toString(2)); byte[] bytes6 = "XXX".getBytes("utf-16le"); System.out.println("使用utf-16小端序编码:"+new BigInteger(1,bytes6).toString(2)); byte[] bytes7 = "XXX".getBytes("utf-16be"); System.out.println("使用utf-16大端序编码:"+new BigInteger(1,bytes7).toString(2)); byte[] bytesx = "XXX".getBytes("utf-32be"); System.out.println("使用utf-32大端序编码:"+new BigInteger(1,bytesx).toString(2)); byte[] bytesy = "XXX".getBytes("utf-32le"); System.out.println("使用utf-32小端序编码:"+new BigInteger(1,bytesy).toString(2)); byte[] bytesz = "XXX".getBytes("utf-32"); System.out.println("使用utf-32编码:"+new BigInteger(1,bytesz).toString(2)); byte[] bytes8 = "XXX".getBytes("UnicodeBig"); System.out.println("使用Unicode大端序编码:"+new BigInteger(1,bytes8).toString(2)); byte[] bytes9 = "XXX".getBytes("UnicodeLittle"); System.out.println("使用Unicode小端序编码:"+new BigInteger(1,bytes9).toString(2));
运行结果为:
使用utf-16编码:111111101111111111011000011000101101110010111011 使用utf-16小端序编码:1100010110110001011101111011100 使用utf-16大端序编码:11011000011000101101110010111011 使用utf-32大端序编码:101000100010111011 使用utf-32小端序编码:10111011100010000000001000000000 使用utf-32编码:101000100010111011 使用Unicode大端序编码:111111101111111111011000011000101101110010111011 使用Unicode小端序编码:111111111111111001100010110110001011101111011100
用BigInteger没有打印出完整的二进制,它把前面是0的全部去掉了。另外我们可以发现在在不指定大小端序的情况下默认是使用的大端序,在上一节中直接使用getBytes("utf-16")或者getBytes("Unicode")时就可以看出,前面多的两个字节刚好就是用来表示默认是“大端序”FEFF。
为什么要有大小端序呢,原因很简单:不同的操作系统的不同实现罢了,有些操作系统默认是大端序(比如我的win7下),有些默认又是小端序。所以最好指定清楚,否则容易引起乱码。
另外在window下utf-8还有带与不带BOM的区别,也就是在字符串的最前面会多出FEFF,在其他系统上显示就会有乱码,在linux操作系统下很少见到使用,这里就不深入了。
Java解码--字节数组转字符串
Java中解码,直接调用String的带字节数组的构造方法解码,生成人类能识别的符号。如果人类不能识别就是我们所谓的乱码。同时该构造方法还有另外一个参数,表示使用的字符集编码,看一个例子:
byte[] b16 = "天星".getBytes("utf-16"); String n8=new String(b16,"utf-8"); System.out.println("使用utf-8解码 utf-16编码的字符串:"+n8); byte[] b8 = n8.getBytes("utf-8"); String n16 = new String(b8,"utf-16"); System.out.println("还原:"+n16);
这个例子首先使用utf-16编码对字符串进行编码;
然后使用了utf-8编码进行解码,此时由于编解码不一致,发现打印出来有乱码;
接着试图把这个乱码还原回去,再使用utf-16进行解码。
也许你期望的应该是没有乱码,可以还原,但真实的打印结果如下:
使用utf-8解码 utf-16编码的字符串:��Y)f 还原:뷯뾽天星
发现还是有乱码,没有真正还原。这是为什么呢?这其实就是默认大端序会加两个字节引起的的问题,我们把程序改下,指定在16进制时使用大端序:
byte[] b16 = "天星".getBytes("utf-16Be"); String n8=new String(b16,"utf-8"); System.out.println("使用utf-8解码 utf-16编码的字符串:"+n8); byte[] b8 = n8.getBytes("utf-8"); String n16 = new String(b8,"utf-16Be"); System.out.println("还原:"+n16);
运行结果:
使用utf-8解码 utf-16编码的字符串:Y)f 还原:天星
发现已经神奇的还原了。
导致乱码的根本原因是编码和解码使用了不同的字符集,或者相同的字符集下使用了不同的编码规则进行编解码。但反过来确不成立,也就是说在有些情况下即便是使用了不同的字符集或编码规则进行编解码,也不会出现乱码,比如我上一个例子。这就要求我们对字符集的编码规则要相当熟悉,并能灵活运用。
理论上在编码时只要可以保证数据不丢失,都可以先还原回去,再使用正确的字符集编码进行解码,可以得到正常的结果。比如下面的例子:
byte[] b16 = "天星".getBytes("ASCII"); String n8=new String(b16,"utf-8"); System.out.println("使用utf-8解码 utf-16编码的字符串:"+n8); byte[] b8 = n8.getBytes("utf-8"); String n16 = new String(b8,"ASCII"); System.out.println("还原:"+n16);
运行结果是乱码,主要原因就是在"天星".getBytes("ASCII")这一步产生了数据丢失,后面再牛逼的人也无力回天了。但如果把“天星”改成“abc”是可以还原的,因为这些字符本身就是ASCII中定义的字符。这也算是英文的优势吧。
总结
关于java中的字符集和编码就总结到这里,这次总结的起因是要分析 恶意用户的恶意请求参数。他使用了utf-16进行编码,而我们日志服务器默认是使用utf-8进行解码,从而导致乱码。但如果熟悉了我上面讲解的内容,相信你也可以把真实的内容还原回来。当然中间还使用另外一些加密手段,这里就不再一一详解了。
提示:这个字iteye无法识别,导致文章提交被截断,文中所有’XXX’都是表示这个字。