深入理解IO流中字符编码问题

原创
2014/12/09 19:43
阅读数 379

首先了解一下常用的编码:

ASCIIASCII码是7位编码,ASCII字符集包括英文字母、阿拉伯数字和标点符号等字符。专门给英语国家设计的编码。

GB2312:中文字符集只收录了6763个常用汉字,字符集中除常用简体汉字字符外还包括希腊字母、日文等字符,未收录繁体中文汉字和一些生僻字。

GBK:GBK编码是GB2312编码的超集共收录了21003个汉字向下完全兼容GB2312。

ISO8859-1又称Latin-1或“西欧语言”。它以ASCII为基础,在空置的0xA0-0xFF的范围内,加入96个字母及符号,藉以供使用变音符号的拉丁字母语言使用

Unicode :又称万国码,顾名思义,unicode中收录了世界各国语言,用以解决传统编码的局限性。它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

UTF-8:(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码。

对于不同编码之间有很重要的两点需要知道:

1、不同编码表示一个字符所占用字节是不相同的,其中ASCII码占一个字节,GB2312、GBK、unicode都用2字节表示一个字符。而UTF-8是可变长的,英文字母用1字节,汉字用3字节表示。

2、同一个字符在不同编码表中的位置也是不同的,比如汉字‘中’在GBK中是D6D0,而在unicode中是4E2D。也就导致了当汉字的解码方式和编码方式不同时会产生乱码。

Unicode编码

每个Java程序员都应该记住,Java使用的是Unicode编码。所有的字符在JVM中(内存中)只有一个存在形式就是Unicode。所以一个char占用2字节。

编码只发生在JVM和底层操作系统(以及网络传输)之间进行数据传输时,如果程序中没有IO操作,那么所有的String和Char都以unicode编码。当从磁盘读取文件或者往磁盘写入文件时使用的编码要一致,也就是编码和解码使用的字符集要一样才不会出现乱码!

public static void main(String[] args) throws IOException {  
    File file=new File("d:\\utf.txt");  
    //用utf-8编码写入文件  
    BufferedWriter out=new BufferedWriter(  
            new OutputStreamWriter(new FileOutputStream("d:\\utf.txt"),"utf-8"));  
    out.write("汉字");  
    out.close();  
    //用gbk编码进行解码,会出现乱码  
    System.out.println("文件大小:"+file.length()+"字节");  
    BufferedReader in=new BufferedReader(  
            new InputStreamReader(new FileInputStream("d:\\utf.txt"),"gbk"));  
    String line=in.readLine();  
    System.out.println(line);  
    //再次用utf-8编码,得到原文  
    byte[] bs=line.getBytes("gbk");  
    System.out.println(new String(bs,"utf-8"));  
}

我们来看一下程序中涉及到的编码转换过程。

首先,Java会以默认的Unicode编码来对”汉字“编码,占用两个字节(6C49 5B57)存储在内存中,当要写入文件中时,就按程序指定的UTF-8编码对“汉字”重新编码,UTF-8一个汉字占用3个字节,得到E6 B1 89 E5 AD 97(11100110 10110001 10001001 11100101 10101101 10010111)存储在utf.txt中。因为UTF-8是可变长度的,那么用UTF-8解码文件时,怎么知道的几个字节表示一个字符呢?原来UTF-8编码时每个字节的高位部分都有表示头,如果一个字节高位是1110,则表示3个字节表示一个字符,应该再往后找2个以10开始的字节。字节高位是10表示前面还有字节。

以上面这个例子说明:“汉字”共占6个字节,第一个字节是11100110,高位是1110表示后边还有2个字节,再往后读2个字节,3个字节解码成一个汉字——”汉“。同理解码”字“。再例如有个字节是11010101,高位是110说明是2个字节表示一个字符,后面还有一个以10开始的字节。如果高位是0开始,表示是单字节字符,abc字母都占用一个字节。

程序中用GBK编码来解码UTF-8编码的”汉字“就会出现乱码,因为GBK编码无论是字母还是汉字都占用2个字节,它在解码时就会按顺序2个字符解码为一个字符,刚好E6B1在GBK编码表中是汉字“姹”,89E5是“夊”,AD97是“瓧”,最后解码成了“姹夊瓧”这个神奇的东西,如果对应位置GBK表中没有字符可能就显示“?”(ISO-8859-1就会这么干),所以啊,用什么编码就得用什么解码。

那乱码还能恢复原来的内容吗?那是肯定滴,因为只是解码时出现了问题,文件并没有改变,还是这些01的二进制。通过乱码“姹夊瓧”重新得到其字节文件,然后再次用UTF-8去解码就可以了。

byte[] bs=line.getBytes("gbk"); //得到GBK编码中的字节  
System.out.println(new String(bs,"utf-8"));//用UTF-8去正确解码

如果在读写文件时没有在程序中显示指定编码方式,则使用操作系统默认的编码格式。注意是OS默认的编码格式,不是JVM默认的unicode,中文系统Windows默认是GBK,英文系统默认UTF-8。

Java的IO体系中面向字符的IO类只有Reader和Writer,但是最常用的FileReader和FileWriter类不支持自定义编码类型,只能使用系统默认编码。这样一来,读写文件的编码就一定一致了,也就减少了乱码的可能性。个人理解,这么做可能是强制帮助用户完成编码一致,降低乱码率。如果要自定义编码,要用其父类InputStreamRreader和OutputStreamWriter。这些类的具体区别和用法,会在以后的文章中讲到。

在实际开发中,只要设置编码一致,就很少出现乱码问题。只要你深刻的理解了Java编码的原理,就算出现乱码也能很快的去解决问题了。



展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部