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é)果如下:

問題分析:
硬盤上有個(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í)行原理如下圖所示:

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

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

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)換流

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

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)換流。

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);//你好
}
}
上述代碼的圖解如下圖所示:

說明:
發(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。