天天看點

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讀取的。