天天看点

String 字符编码 乱码原理讲解 java

string 中文乱码 ,也许大家都经历过,解决方案网上一搜一大堆。有用的无用的,挨个试一下总会有能用的。但是,我们不应该只看中问题的解决方案,更看重的应该是为什么会这么解决,问题产生的原因是什么? 否则,我们永远不会进步。

进入正题,先说一下基本知识,关于字符编码的。众所周知,世界上面有很多的语言,特别多。为了在计算机中存储语言,编码格式就诞生了。

编码格式

通俗的讲呢,编码格式就是 用指定的计算机存储 对应 语言单词 字母之类的。就像ASCII表那样。

乱码的原因

编码格式和解码格式不一样。举个例子,我们约定 一个规则,存数字的时候 ,要先加3,然后再存起来。 读取数字的时候,要先减3 ,才是原本的值。

比如要存5,那么要先加3 ,变成8 ,再存入。 读取时候,取出来的8,要先减3,才能得到5。

我们要按照相同的规则 存取,才能得到正确的值。string 在文件里面底层保存形式是二进制,高级形式就是byte[]。byte[]数组里面的内容可以按照不同的编码格式存放。我们读取字符串的时候,也可以按照不同的解码格式存放。 这样就造成了 乱码。

如何保证不乱码?

从始至终,都用同一种字符格式

方法

认识两个方法 ,下面要用的。

//将调用的string 用给定的编码 转成 字节数组
//charsetName 编码格式 
public byte[] getBytes(String charsetName)
           
//将给定的字节数组,用给定的解码格式 ,生成一个Sting
public String(byte bytes[], String charsetName)
           

这两个方法里面的charsetName参数,其实并没有编码解码之分,只是想让读者便于理解

例子解读

String path = "D:/one.txt";
        File file = new File(path);
        if (!file.exists()){
            file.createNewFile();
        }
        FileOutputStream outputStream = new FileOutputStream(file);
        //原始内容
        String content = "中文englis》》》》";//原始字符串  这时候 与编码格式无关 ,它可以转成任意格式的字节数组

        byte[] utfBytes = content.getBytes("UTF-8");//用UTF-8编码  把content编码成 utf格式字节数组

        byte[] gbkBytes = content.getBytes("GBK");//用GBK编码  把content编码成 gbk格式字节数组

        byte[] isoBytes = content.getBytes("ISO-8859-1");//用ISO-8859-1编码  把content编码成 iso-8859-1格式字节数组

        byte[] gb2312Bytes = content.getBytes("GB2312");//用GB2312编码  把content编码成 gb2312格式字节数组

        outputStream.write(gb2312Bytes);//将gb2312Bytes 数组写入流,这样 在txt文件中 文字是以 GB2312编码格式存储的
        outputStream.flush();
        outputStream.close();
           

上面呢,我们创建了一个txt文件,并且 将"中文englis》》》》"用gb2312编码格式存储了。以后我们从one.txt里面读取这段话的数组,也是gb2312格式的。

String 字符编码 乱码原理讲解 java

读取文件

//以后我们从one.txt文件里面读取流,读到的也是用gb2312格式字节数组
        //这里省略从one.txt文件里面读取流 变成字节数组的过程, 读取出来的数组 是和gb2312Bytes 是一样的

        //只要字节数组编码格式,和解码格式 是相同的就不会中文乱码
        //   gb2312Bytes  用gb2312解码
        String gb2312ToGb2312 = new String(gb2312Bytes, "GB2312");
        System.out.println("gb2312ToGb2312 字节数组编码格式和解码格式相同 "+gb2312ToGb2312);

        //如果 字节数组编码格式,和解码格式 不同 就会产生中文乱码
        //gb2312编码   ISO-8859-1解码  ——》乱码
        String gbk312ToISO = new String(gb2312Bytes, "ISO-8859-1");//gb2312格式字节数组  却用ISO-8859-1的解码
        System.out.println("gb2312ToGbk 字节数组编码格式和解码格式不同 "+gbk2312ToISO);//输出乱码
           

从上面代码可以看出,只要字节数组编码和解码格式相同,就不会产生乱码。

字符串已经乱码(用不一样的解码 构造String)后,如何将乱码字符修复。而不是用原数组 重新构造一个String?

接着上面的代码

//下面 将乱码修复
        byte[] temp = gb2312ToISO.getBytes("ISO-8859-1");//因为上面是用ISO-8859-1解码的,所以 用ISO-8859-1编码 逆转到 原先数组 也就是gb2312Bytes数组
        System.out.println("乱码字符 ,编码后的数组,和原先gb2312数组比较     "+isEqual(temp,gb2312Bytes));//比较 乱码字符 重新编码后的数组  和 原先的gb2312Bytes数组
        System.out.println("乱码字符 ,编码后的数组,和原先iso-8859-1数组比较     "+isEqual(temp,isoBytes));//比较 乱码字符 重新编码后的数组  和 原先的gb2312Bytes数组
        System.out.println("temp 数组 解码  "+new String(temp,"GB2312"));//再用GB2312解码
           
String 字符编码 乱码原理讲解 java

从上面代码可以看见,首先调用getBytes()方法,将gb2312ToISO 再重新用ISO-5589-1编码 转成字节数组。再用gb2312解码重新构造 生成一个的字符串。 可以看见 乱码被修复了。

UTF-8格式的特殊性

为什么要单独强调UTF-8格式呢?

看代码

//注意 如果 你使用UTF-8解码, 意味着 string 只能被 重新编码成 utf-8格式的原数组(解码 编码后 数组相等),不能被重新编码其他格式的数组(解码 编码后 数组不相等)
        String gb2312ToUTF= new String(gb2312Bytes, "UTF-8");//字节数组编码 和第二个参数不一致  乱码
        System.out.println("gb2312ToUTF   编码gb2312  解码UTF-8 "+gb2312ToUTF);//乱码
        //按照上面的方法修复
        byte[] gb2312ToUTFBytes = gb2312ToUTF.getBytes("UTF-8");//乱码 重新编码
        System.out.println("gbk2312原数组, 和先解码UTF-8,再编码 返回的数组比较"+isEqual(gb2312Bytes,gb2312ToUTFBytes));
        System.out.println("utf原数组, 和先解码UTF-8,再编码 返回的数组比较"+isEqual(utfBytes,gb2312ToUTFBytes));
        String one = new String(gb2312ToUTFBytes, "UTF-8");
        System.out.println("one 此时乱码字符已经不能修复 "+one);

        System.out.println("");
        
        //用utf编译的数组,构造GBK格式的字符串
        String o = new String(utfBytes, "GBK");
        System.out.println(o);
        //识图修复
        System.out.println(new String(o.getBytes("GBK"),"UTF-8"));
           
String 字符编码 乱码原理讲解 java

我们首先,构造一个String字符串,原数组是用gb2312编码的,解码是用UTF-8。不出意外,乱码了。当我们按照上面的修复方法 修复时候。发现 这个乱码的String再也不能被修复了。这就是UTF-8的特殊性。你使用UTF-8解码, 意味着 string 只能被 重新编码成 utf-8格式的原数组(解码 编码后 数组相等),不能被重新编码其他格式的数组(解码 编码后 数组不相等)。

总结UTF-8

只要在解码构造String的过程中,字节数组和解码格式 任意一个是UTF-8。那么,生成的乱码字符串将不能被修复。这时候只能 第一次构造String时候, 编码格式和解码格式相等,才能不乱码

BufferedReader和RandomAccessFile的注意事项

System.out.println("\r\nBufferedReader");
        //如果在构造 InputStreamReader的时候 没有指定 解码格式  charsetName,那么会默认的使用jvm的字符编码(在IDEA中),cmd窗口会使用操作系统的字符编码。
        //现在默认的 一般都是 UTF-8
        InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(path));
        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
        String read = null;
        while ((read=bufferedReader.readLine())!=null){
            System.out.println("读取出来的乱码    "+read);
            //运用修复方法
            read = new String(read.getBytes(), "GB2312");
            System.out.println("依旧乱码 不可修复 "+read);
        }
        bufferedReader.close();
        inputStreamReader.close();
        System.out.println("\r\n RandomAccessFile");

        //RandomAccessFile 是用ISO-8859-1 读取的,所以可以修复
        RandomAccessFile randomAccessFile = new RandomAccessFile(path, "rw");
        String result = null;
        while ((result=randomAccessFile.readLine())!=null){
            System.out.println("result "+result);
            //修复
            System.out.println(new String(result.getBytes("ISO-8859-1"), "gb2312"));
        }
           

可以看见,如果使用BufferedReader 读取字符串,那么在构造InputStreamReader最好指定正常的 编码格式。RandomAccessFile则要注意 他是用ISO-8859-1读取的。