先澄清这么几个事实:
java中byte是以其补码存储的。ava中的一个byte其范围是-128~127,Integer.toHexString(int)的参数为int型,当byte传入其中时,会自动转换成int,对于负数,会做位扩展,举例来说,一个byte的-1(即0xff),会被转换成int的-1(即0xffffffff),那么转化出的结果就和预期不同。而0xff默认是整形,所以,一个byte跟0xff相与会先将那个byte转化成整形运算,这样,结果中的高的24个比特就会被清0,这样就不影响转换了。
IO里面stream的读写操作,其实只有两种:write(int b),write(byte[] b):read(),read(byte())。其余的均是在这两种方式上进行封装处理。
Java数据处理的基本单元是字节(byte),一个字节8bit,字节能表示的范围为[0,255]。而java里面的byte类型表示的范围是[-128,127]。
对write(b), write(byte[] bs) 的理解应该以bit的方式去理解,不能以Java中的byte类型去理解,即这里的byte可以是高精度类型(int,long)的低八位。
OutputStream的write(int b)是将一个字节写入到流中,而不是将int写入流中,即将int所表示的低8位写入,高24bit舍去。因此通过write(int b)将一个byte类型的b写入时不会有问题。
若byte b = (byte)-1;out.write(b)的转化过程为:btye -> int -> 截取低8位,写入流中。 中间byte->int得到的int的bit序列可能有误,但由于最后截取低8位bit,所以最后得到的bit都是正确的。
同样可以推理出int i = read() 和 byte b =(byte)read(); 由此我们可以看到读写进行的bit处理都是一致的。
使用IO的这些接口时,不用考虑是int 还是byte类型,直接调用即可。
3: byte与unicode, 编码方式(比如UTF8)之间的关系
Java中String 是以unicode的方式存储,也就是说一个String的unicode编码是固定的,其unicode表示的byte序列也是固定。由于存储效率的原因,我们需要使用别的编码方式,比如utf8,那么就需要将unicode的bit序列转化为utf8所表示的bit序列,这要求unicode和utf8之间必须能互相转换。因此可以得出:对同一个unicode使用不同的编码方式得到的bit序列将是不一样的。
首先注意,Java的class文件采用utf8的编码方式,JVM运行时采用utf16。
Java的字符串是unicode编码的。总之,Java采用了unicode字符集,使之易于国际化。
Java支持哪些字符集:即Java能识别哪些字符集并对它进行正确地处理?
查看Charset 类,最新的JDK支持165种字符集,可以通过static方法availableCharsets拿到所有Java支持的字符集。
System.out.println(Charset.defaultCharset()); SortedMap<String,Charset> charsets = Charset.availableCharsets(); System.out.println(charsets.size()); for(String key : charsets.keySet()) { System.out.format("%-30s%s" ,key,charsets.get(key).displayName()); System.out.println(0); }
需要在哪些时候注意编码问题?
1. 从外部资源读取数据:
这跟外部资源采取的编码方式有关,我们需要使用外部资源采用的字符集来读取外部数据:
InputStream is = new FileInputStream("res/input2.data");
InputStreamReader streamReader = new InputStreamReader(is, "GB18030");
这里可以看到,我们采用了GB18030编码读取外部数据,通过查看streamReader的encoding可以印证:
assertEquals("GB18030", streamReader.getEncoding());
正是由于上面我们为外部资源指定了正确的编码,当它转成char数组时才能正确地进行解码(GB18030 -> unicode):
char[] chars = new char[is.available()];
streamReader.read(chars, 0, is.available());
但我们经常写的代码就像下面这样:
InputStream is = new FileInputStream("res/input2.data");
InputStreamReader streamReader = new InputStreamReader(is);
这时候InputStreamReader采用什么编码方式读取外部资源呢?Unicode?不是,这时候采用的编码方式是JVM的默认字符集,这个默认字符集在虚拟机启动时决定,通常根据语言环境和底层操作系统的 charset 来确定。可以通过以下方式得到JVM的默认字符集:
Charset.defaultCharset();
为什么要这样?因为我们从外部资源读取数据,而外部资源的编码方式通常跟操作系统所使用的字符集一样,所以采用这种默认方式是可以理解的。
2. 字符串和字节数组的相互转换
我们通常通过以下代码把字符串转换成字节数组:
"string".getBytes();
但你是否注意过这个转换采用的编码呢?其实上面这句代码跟下面这句是等价的:
"string".getBytes(Charset.defaultCharset());
也就是说它根据JVM的默认编码(而不是你可能以为的unicode)把字符串转换成一个字节数组。
反之,如何从字节数组创建一个字符串呢?
new String("string".getBytes());
同样,这个方法使用平台的默认字符集解码字节的指定数组
看一段常用的解决乱码问题的代码
new String(input.getBytes("ISO-8859-1"), "GB18030")
先看一下这里面用到两个函数的官方解释:
getBytes(Charset charset)
Encodes this String into a sequence of bytes using the given charset, storing the result into a new byte array.
使用参数中给定的字符集将调用该方法的字符串编码为字节序列,结果存储在一个新的字节数组中。
String(byte[] bytes, String charsetName)
Constructs a new String by decoding the specified array of bytes using the specified charset.
通过使用参数中指定的字符集解码参数中指定的字节数组的方式构造一个新的字符串。
这行代码一般用在一下情形:
本应该用GB18030的编码来读取数据并解码成字符串,但结果却采用了ISO-8859-1的编码,导致生成一个错误的字符串。要恢复,就要先把字符串恢复成原始字节数组,然后通过正确的编码GB18030再次解码成字符串(即把以GB18030编码的数据转成unicode的字符串)。注意,字符串永远都是unicode编码的。
但编码转换并不是负负得正那么简单,这里我们之所以可以正确地转换回来,是因为 ISO8859-1 是单字节编码,所以每个字节被按照原样转换为String ,也就是说,虽然这是一个错误的转换,但编码没有改变,所以我们仍然有机会把编码转换回来!
总结:
所以,我们在处理java的编码问题时,要分清楚三个概念:Java采用的编码:unicode,JVM平台默认字符集和外部资源的编码。
一般的数据流程:外部输入 -> Java String -> 外部输出。
由于IO处理的byte(8个bit)类型,因此该流程应该为:读取外部介质-> 输入的byte序列 ->JVM的unicode -> 输出的byte序列 -> 写到到外部介质
因此乱码的问题,就会出现在两个地方:输入的byte序列-> JVM的Unicode和JVM的unicode ->输出byte序列;也就是,将某中编码的byte序列转为unicode和将unicode转化为某种编码的byte序列。
如果你不知道是哪种编码和unicode进行互转,比如:将UTF8的byte序列使用GBK的编码方式转化为unicode,那么得到unicode byte序列可能不对,则在unicode字符集中找到字符和原来就很可能千差万别了。另外通过new String(byte[] bs)是不指定编码方式,则默认使用jvm的字符编码进行处理。 因此上文指出一定要知道外部系统和JVM的编码方式。