Java基礎(chǔ)進(jìn)階 轉(zhuǎn)換流、編碼、解碼、亂碼

1、轉(zhuǎn)換流(掌握)

我們學(xué)習(xí)字符流的時(shí)候知道字符流:它的底層是字節(jié)流和編碼表。
需求:在硬盤上新建一個(gè)文本文件D:\out.txt,輸入”你好”兩個(gè)漢字,并以UTF-8編碼保存,使用程序讀取文件中的數(shù)據(jù)并打印到控制臺(tái)上。
代碼如下:
分析和步驟:
1)創(chuàng)建輸入流FileReader類的對(duì)象fr,D:\out.txt作為參數(shù);
2)定義字符數(shù)組,數(shù)組名是ch,長度是1024;
3)定義一個(gè)變量len=0記錄著讀取的字符個(gè)數(shù);
4)使用循環(huán)來控制次數(shù)并輸出讀取到的數(shù)據(jù);
5)關(guān)閉資源;

/*
 * 轉(zhuǎn)換流的演示
 */
public class ExchangeDemo {
    public static void main(String[] args) throws IOException {
        //創(chuàng)建字符輸入流的對(duì)象
        FileReader fr = new FileReader("D:\\out.txt");
        //定義數(shù)組
        char[] ch=new char[1024];
        int len=0;
        while((len=fr.read(ch))!=-1)
        {
            System.out.println(new String(ch,0,len));
        }
        //關(guān)閉資源
        fr.close();
    }
}

結(jié)果如下:


1.png

問題分析:
硬盤上有個(gè)記事本文件中保存的數(shù)據(jù)是以UTF-8編碼保存的,現(xiàn)在我們使用的FileReader在讀取這個(gè)記事本中的數(shù)據(jù),結(jié)果記事本中保存的“你好”,但使用程序讀取到的“浣犲ソ”,數(shù)據(jù)讀取錯(cuò)誤了。
錯(cuò)誤原因:
出現(xiàn)上述的錯(cuò)誤數(shù)據(jù)的原因是記事本文件中保存的數(shù)據(jù)使用的編碼表和我們程序中讀取數(shù)據(jù)使用的編碼表不一致。導(dǎo)致數(shù)據(jù)錯(cuò)誤了。

而我們知道字符流在讀取數(shù)據(jù)的時(shí)候,是先從底層讀取字節(jié)數(shù)據(jù),然后再結(jié)合編碼表查到對(duì)應(yīng)的字符數(shù)據(jù)?,F(xiàn)在我們希望讀取到正確的數(shù)據(jù),必須使用和保存時(shí)一致編碼表。不能再使用FileReader進(jìn)行讀取,因?yàn)镕ileReader讀取數(shù)據(jù)底層使用的是默認(rèn)的編碼表GBK來讀取數(shù)據(jù)的,而這里我們需要在讀取數(shù)據(jù)的時(shí)候指定編碼表。

那么既然不能使用FileReader來根據(jù)默認(rèn)編碼表進(jìn)行讀取數(shù)據(jù),那么怎樣通過指定的編碼表來進(jìn)行讀取數(shù)據(jù)呢?

Java提供了一個(gè)轉(zhuǎn)換流InputStreamReader可以在讀取數(shù)據(jù)的時(shí)候指定編碼表,然后把字節(jié)數(shù)據(jù)根據(jù)指定編碼表轉(zhuǎn)成字符數(shù)據(jù)。

1.1、轉(zhuǎn)換流介紹

轉(zhuǎn)換流:它的主要功能就是進(jìn)行字節(jié)數(shù)據(jù)和字符數(shù)據(jù)之間的轉(zhuǎn)換的。轉(zhuǎn)換流它就是用來轉(zhuǎn)換的,它不能和底層的文件進(jìn)行關(guān)聯(lián),也就是說轉(zhuǎn)換流不能去讀寫文件中的數(shù)據(jù)。讀取底層的字符數(shù)據(jù)還是得需要我們之前講過的字符流FileReader。向硬盤中的文件中寫數(shù)據(jù)得需要我們之前講過的FileWriter類。

Java中提供了2個(gè)轉(zhuǎn)換流對(duì)象:
InputStreamReader:輸入流(硬盤文件數(shù)據(jù)------》內(nèi)存),它的功能是把讀取到的字節(jié)(硬盤上的數(shù)據(jù)都是字節(jié)數(shù)據(jù))轉(zhuǎn)換轉(zhuǎn)成字符數(shù)據(jù)。字節(jié)轉(zhuǎn)字符輸入轉(zhuǎn)換流。
OutputStreamWriter:輸出流(內(nèi)存---->硬盤文件),它的功能是把字符數(shù)據(jù)轉(zhuǎn)成字節(jié)數(shù)據(jù)并寫到硬盤上指定的文件中。字符轉(zhuǎn)字節(jié)輸出轉(zhuǎn)換流。

轉(zhuǎn)換流的執(zhí)行原理如下圖所示:


2.png

1.2、字節(jié)數(shù)據(jù)轉(zhuǎn)成字符數(shù)據(jù)的輸入轉(zhuǎn)換流

3.png

用來把字節(jié)數(shù)據(jù)轉(zhuǎn)為字符數(shù)據(jù)的轉(zhuǎn)換流。
構(gòu)造函數(shù)如下圖所示:


4.png

InputStreamReader構(gòu)造函數(shù)中的字節(jié)流是負(fù)責(zé)從文件中讀取字節(jié)數(shù)據(jù),然后把字節(jié)數(shù)據(jù)交給轉(zhuǎn)換流。

為了不出現(xiàn)上述亂碼的結(jié)果,我們使用轉(zhuǎn)換流來實(shí)現(xiàn),根據(jù)指定的編碼來讀取文件中的數(shù)據(jù),

代碼如下所示:
分析和步驟:
1)創(chuàng)建字節(jié)輸入流FileInputStream 類的對(duì)象fis,D:\out.txt作為參數(shù);
2)創(chuàng)建字節(jié)轉(zhuǎn)成字符的輸入轉(zhuǎn)換流InputStreamReader類的對(duì)象isr,并指定編碼方式是utf-8;
3)創(chuàng)建字符數(shù)組ch,數(shù)組長度是1024,并同時(shí)定義一個(gè)變量len來記錄讀取字符的個(gè)數(shù);
4)使用while循環(huán)控制循環(huán)次數(shù)來讀取字符并輸出內(nèi)容;
5)關(guān)閉轉(zhuǎn)換流資源;

/*
 * 字節(jié)轉(zhuǎn)成字符的輸入轉(zhuǎn)換流
 */
public class InputStreamReaderDemo {
    public static void main(String[] args) throws IOException {
        //創(chuàng)建字節(jié)輸入流對(duì)象
        FileInputStream fis = new FileInputStream("D:\\out.txt");
        //創(chuàng)建字節(jié)數(shù)據(jù)轉(zhuǎn)成字符數(shù)據(jù)的輸入轉(zhuǎn)換流 如果這里不指定編碼表,那么和FileReader一樣
//      InputStreamReader isr = new InputStreamReader(fis,"gbk");
        InputStreamReader isr = new InputStreamReader(fis,"utf-8");
        //創(chuàng)建數(shù)組
        char[] ch=new char[1024];
        int len=0;
        while((len=isr.read(ch))!=-1)
        {
            System.out.println(new String(ch,0,len));
        }
        //關(guān)閉資源
        isr.close();//關(guān)閉轉(zhuǎn)換流在底層同時(shí)就會(huì)關(guān)閉字節(jié)流
    }
}

1.3、字符數(shù)據(jù)轉(zhuǎn)成字節(jié)數(shù)據(jù)的輸出轉(zhuǎn)換流

5.png

說明:用來把字符數(shù)據(jù)轉(zhuǎn)為字節(jié)數(shù)據(jù)的轉(zhuǎn)換流。

構(gòu)造函數(shù)如下所示:


6.png

OutputStreamWriter:構(gòu)造函數(shù)中接收的這個(gè)OutputStream是負(fù)責(zé)把OutputStreamWriter轉(zhuǎn)后的字節(jié)數(shù)據(jù)寫到文件中的流對(duì)象。

需求:將字符串”你好”按照utf-8編碼方式輸出到D:\out.txt文件中。
實(shí)現(xiàn)代碼如下所示:
分析和步驟:
1)創(chuàng)建一個(gè)可以給文件中寫數(shù)據(jù)的字節(jié)輸出流FileOutputStream類的對(duì)象fos,D:\out.txt作為參數(shù);
2)創(chuàng)建字符轉(zhuǎn)成字節(jié)的輸出轉(zhuǎn)換流OutputStreamWriter類的對(duì)象osw,并指定編碼方式是utf-8;
3)使用osw對(duì)象調(diào)用write()函數(shù)將字符串對(duì)象”你好”寫到指定的文件位置;
4)關(guān)閉轉(zhuǎn)換流資源;

/*
 * 字符轉(zhuǎn)字節(jié)輸出轉(zhuǎn)換流
 */
public class OutputStreamWriterDemo {

    public static void main(String[] args) throws IOException {
        //創(chuàng)建一個(gè)可以給文件中寫數(shù)據(jù)的字節(jié)輸出流對(duì)象
        FileOutputStream fos = new FileOutputStream("D:\\out.txt");
        //創(chuàng)建字符數(shù)據(jù)轉(zhuǎn)字節(jié)數(shù)據(jù)輸出轉(zhuǎn)換流對(duì)象
        OutputStreamWriter osw = new OutputStreamWriter(fos, "utf-8");
        osw.write("你好");
        osw.close();
    }
}

注意:這里也有個(gè)緩沖區(qū),也就是說寫數(shù)據(jù)的時(shí)候先將數(shù)據(jù)寫到緩沖區(qū)中,然后必須要調(diào)用flush()或者close()函數(shù)才能將緩沖區(qū)中的數(shù)據(jù)寫到文件中。

1.4、轉(zhuǎn)換流的細(xì)節(jié)

1、轉(zhuǎn)換流的構(gòu)造函數(shù)中接收的字節(jié)流都是負(fù)責(zé)和底層文件交互的流。
2、如果使用轉(zhuǎn)換流沒有傳遞編碼表,就和FileReader或FileWriter沒有任何區(qū)別。
3、只有輸入的字節(jié)數(shù)據(jù)轉(zhuǎn)換字符數(shù)據(jù)的轉(zhuǎn)換流,沒有輸入的字符數(shù)據(jù)轉(zhuǎn)字節(jié)數(shù)據(jù)的轉(zhuǎn)換流。
4、只有輸出的字符數(shù)據(jù)轉(zhuǎn)字節(jié)數(shù)據(jù)的轉(zhuǎn)換流,沒有字節(jié)數(shù)據(jù)轉(zhuǎn)字符數(shù)據(jù)的輸出轉(zhuǎn)換流。


7.png

2、編碼、解碼、亂碼(掌握)

在轉(zhuǎn)換流中,字符數(shù)據(jù)轉(zhuǎn)字節(jié)數(shù)據(jù),把字符數(shù)據(jù)編碼成字節(jié)。字節(jié)轉(zhuǎn)換成字符,把字節(jié)解碼成字符。

編碼:把字符數(shù)據(jù)通過查閱編碼表,查到對(duì)應(yīng)的編碼值,然后把編碼值轉(zhuǎn)成字節(jié)數(shù)據(jù)的過程。
字符------> 字節(jié)的過程(編碼)
字符可以使用字符串表示,字節(jié)可以使用字節(jié)數(shù)組表示。
編碼:把看得懂的數(shù)據(jù)變成看不懂的數(shù)據(jù)(字符 ---> 字節(jié))。

需求:把字符串”你好”進(jìn)行編碼。
分析:將一個(gè)字符串變?yōu)橐粋€(gè)字節(jié)數(shù)據(jù)需要借助String類中的getBytes()和getBytes(String charsetName)函數(shù),不帶參數(shù)的函數(shù)是按照默認(rèn)的字符集將字符串編碼為字節(jié)數(shù)據(jù),而帶有參數(shù)的函數(shù)是將字符串根據(jù)指定的編碼編碼為字節(jié)數(shù)據(jù)。

byte[] getBytes() 使用平臺(tái)的默認(rèn)字符集將此 String 編碼為 byte 序列
byte[] getBytes(String charsetName) 使用指定的字符集將此 String 編碼為 byte 序列

代碼如下所示:
分析和步驟:
1)定義一個(gè)EncodeDemo 測(cè)試類;
2)在這個(gè)類中定義一個(gè)字符串str,并賦值為”你好”;
3)使用字符串str對(duì)象調(diào)用getBytes()函數(shù)根據(jù)系統(tǒng)默認(rèn)編碼表將字符串”你好”轉(zhuǎn)換成字節(jié)數(shù)組;
4)使用Arrays數(shù)組工具類調(diào)用toString()函數(shù)打印生成的字節(jié)數(shù)組;
5)在使用字符串str對(duì)象調(diào)用getBytes(String charsetName)函數(shù)根據(jù)指定的編碼表將字符串”你好”轉(zhuǎn)換成字節(jié)數(shù)組;

/*
 * 編碼演示
 * 編碼就是把看得懂的數(shù)據(jù)變?yōu)榭床欢臄?shù)據(jù)
 * 字符-----》字節(jié)
 * 需求:把字符串”你好”進(jìn)行編碼。
 */
public class EncodeDemo {
    public static void main(String[] args) throws UnsupportedEncodingException {
        String str="你好";
        //使用getBytes()使用平臺(tái)的默認(rèn)字符集將此 String 編碼為 byte 序列,
        //并將結(jié)果存儲(chǔ)到一個(gè)新的 byte 數(shù)組中。
        byte[] bytes = str.getBytes();//默認(rèn)編碼表 
        System.out.println(Arrays.toString(bytes));//[-60, -29, -70, -61]
        
        //getBytes(Charset charset) 
        //使用給定的 charset 將此 String 編碼到 byte 序列,并將結(jié)果存儲(chǔ)到新的 byte 數(shù)組。
        byte[] bytes2 = str.getBytes("GBK");//GBK編碼
        System.out.println(Arrays.toString(bytes2));//[-60, -29, -70, -61]
        
        byte[] bytes3 = str.getBytes("UTF-8");//UTF-8編碼
        System.out.println(Arrays.toString(bytes3));//[-28, -67, -96, -27, -91, -67]
    }
}

說明:
1)GBK編碼表:一個(gè)中文占兩個(gè)字節(jié);
2)在這里UTF-8編碼表:一個(gè)中文占三個(gè)字節(jié)。

解碼:把看不懂的變成看得懂的(字節(jié) --> 字符)
需求:把上述字符串”你好”生成的字節(jié)數(shù)據(jù)解碼成為字符串。

分析:
解碼:
String(byte[] bytes) 通過使用平臺(tái)的默認(rèn)字符集解碼指定的 byte 數(shù)組,構(gòu)造一個(gè)新的 String。
String(byte[] bytes, String charsetName) 通過使用指定的 charset 解碼指定的 byte 數(shù)組,構(gòu)造一個(gè)新的 String。

代碼如下所示:
分析和步驟:
1)定義一個(gè)DecodeDemo 測(cè)試類;
2)在這個(gè)類中定義一個(gè)byte類型的字節(jié)數(shù)組b,將上述字符串”你好”按照gbk和utf-8生成的編碼分別作為數(shù)組中的數(shù)據(jù);
3)使用String類中的構(gòu)造函數(shù)將字節(jié)作為數(shù)組,按照系統(tǒng)默認(rèn)編碼方式和指定的編碼方式utf-8進(jìn)行解碼生成一個(gè)新的字符串;
4)輸出生成新的字符串;

/*
 * 解碼演示
 * 需求:把上述字符串”你好”生成的字節(jié)數(shù)據(jù)解碼成為字符串。
   解碼:把看不懂的轉(zhuǎn)成看得懂的(字節(jié)-----》字符)
 */
public class DecodeDemo {
    public static void main(String[] args) throws UnsupportedEncodingException {
        //定義一個(gè)字節(jié)數(shù)組 你好的gbk字節(jié)形式
//      byte[] b={-60, -29, -70, -61};
        //你好的UTF-8字節(jié)形式
        byte[] b2={-28, -67, -96, -27, -91, -67};
        /*
         * String(byte[] bytes) 
         * 通過使用平臺(tái)的默認(rèn)字符集解碼指定的 byte 數(shù)組,構(gòu)造一個(gè)新的 String。
         */
//      String s = new String(b);//默認(rèn)編碼表解碼
//      System.out.println(s);//你好
        /*
         * String(byte[] bytes, String charsetName) 
         * 通過使用指定的 charset 解碼指定的 byte 數(shù)組,構(gòu)造一個(gè)新的 String。
         */
//       String s2 = new String(b,"GBK");//你好
        //解碼
         String s2 = new String(b2,"UTF-8");
         System.out.println(s2);//你好
    }
}

亂碼:編碼和解碼的時(shí)候使用的編碼表不一致。導(dǎo)致解碼出來的數(shù)據(jù)是錯(cuò)誤的。

代碼如下所示:
分析和步驟:
1)定義一個(gè)LuanCodeDemo 測(cè)試類;
2)在這個(gè)類中定義一個(gè)字符串str,并賦值為”你好”;
3)使用字符串str對(duì)象調(diào)用getBytes(“UTF-8”)函數(shù)根據(jù)指定的UTF-8編碼表將字符串”你好”轉(zhuǎn)換成字節(jié)數(shù)組bytes;
4)解碼 ,使用String類的構(gòu)造函數(shù)以GBK的編碼表進(jìn)行解碼,但是這樣會(huì)發(fā)生亂碼;
5)由于編碼使用UTF-8方式進(jìn)行編碼,所以解碼也得使用UTF-8,否則會(huì)亂碼;
6)既然上述使用GBK解碼方式已經(jīng)發(fā)生亂碼了,我們應(yīng)該在此基礎(chǔ)上進(jìn)行解決亂碼的問題,我們需要使用生成的字符串對(duì)象調(diào)用getBytes()函數(shù)將字符串以gbk編碼表在轉(zhuǎn)換為字節(jié)數(shù)組進(jìn)行編碼;
7)重新使用正確的編碼表UTF-8進(jìn)行解碼;

/*
 * 亂碼演示
 */
public class LuanCodeDemo {
    public static void main(String[] args) throws UnsupportedEncodingException {
        //定義字符串對(duì)象
        String str="你好";
        //編碼
        byte[] bytes = str.getBytes("UTF-8");
        //解碼 由于編碼采用的是UTF-8,而解碼采用的是GBK這樣會(huì)發(fā)生亂碼
        String str2 = new String(bytes,"GBK");//浣犲ソ
        //解碼 由于編碼采用的是UTF-8,而解碼采用的也是UTF-8這樣不會(huì)發(fā)生亂碼
//      String str3 = new String(bytes,"UTF-8");//你好
        
        //輸出解碼后的結(jié)果 
        System.out.println(str2);//浣犲ソ
//      System.out.println(str3);//你好
        /*
         * 上述亂碼的原因是將字符串str="你好"以UTF-8進(jìn)行編碼
         * 然后以GBK進(jìn)行解碼
         * 編碼和解碼的碼表不一致 所以會(huì)發(fā)生亂碼
         * 那么這里我們只能再次按照GBK編碼表再次編碼
         */
        //編碼
        byte[] bytes2 = str2.getBytes("GBK"); 
        //[-28, -67, -96, -27, -91, -67]
        System.out.println(Arrays.toString(bytes2));
        //重新用正確的編碼進(jìn)行解碼
        String str4 = new String(bytes2,"UTF-8");
        System.out.println(str4);//你好
    }
}

上述代碼的圖解如下圖所示:


8.png

說明:
發(fā)生亂碼:按照反方向編碼回去,編碼的時(shí)候一定要使用導(dǎo)致解碼錯(cuò)誤的編碼表,得到編碼后的數(shù)據(jù),再使用正確的編碼表進(jìn)行解碼。這種方式不是萬能的。一旦解碼中有未知的錯(cuò)誤字符,就無法還原。
比如某些字符串中的漢字 ”聯(lián)通” 就會(huì)發(fā)生未知字符的現(xiàn)象。 結(jié)果是:聯(lián)? 這里的?就表示未知字符。

總結(jié):
上述對(duì)于聯(lián)通解碼不出來,原因是由于在GBK碼表中具有未知字符,而對(duì)于未知字符,解碼的時(shí)候也會(huì)按照未知字符進(jìn)行處理,所以為了發(fā)生上述情況,我們以后學(xué)習(xí)的軟件(比如:服務(wù)器Tomcat)中底層的編碼表全部是沒有未知字符的編碼表ISO-8859-1。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 編碼問題一直困擾著開發(fā)人員,尤其在 Java 中更加明顯,因?yàn)?Java 是跨平臺(tái)語言,不同平臺(tái)之間編碼之間的切換...
    x360閱讀 2,580評(píng)論 1 20
  • 字符是用戶可以讀寫的最小單位。計(jì)算機(jī)所能支持的字符組成的集合,就叫做字符集。字符集通常以二維表的形式存在。二維表的...
    劉惜有閱讀 8,373評(píng)論 2 14
  • 編碼與解碼 碼表: 常見的碼表如下:ASCII : 美國標(biāo)準(zhǔn)信息交換碼。用一個(gè)字節(jié)的7位可以表示。ISO8859-...
    奮斗的老王閱讀 1,287評(píng)論 0 51
  • (20180626) 我選擇沉默的主要原因之一:從話語中,你很少能學(xué)到人性,從沉默中卻能。假如還想學(xué)得更多,那就要...
    永遠(yuǎn)的浩子閱讀 564評(píng)論 0 0
  • 人 ,走著,走著,會(huì)無知的疲倦; 心,飛著,飛著,會(huì)莫名的寂寥。 只愿許我一片青天,我便能自由翱翔于九天之上; 只...
    心靈簡約閱讀 430評(píng)論 0 0

友情鏈接更多精彩內(nèi)容