1. 程式人生 > >深入理解IO流中字元編碼問題

深入理解IO流中字元編碼問題

首先了解一下常用的編碼:

ASCIIASCII碼是7位編碼,ASCII字符集包括英文字母、阿拉伯數字和標點符號等字元。專門給英語國家設計的編碼。

GB2312:中文字符集只收錄了6763個常用漢字,字符集中除常用簡體漢字字元外還包括希臘字母、日文等字元,未收錄繁體中文漢字和一些生僻字。

GBK:GBK編碼是GB2312編碼的超集共收錄了21003個漢字向下完全相容GB2312。

又稱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表示前面還有位元組。

E6

B1

89

E5

AD

97

11100110

10110001  

10001001

11100101

10101101

10010111


以上面這個例子說明:“漢字”共佔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編碼的原理,就算出現亂碼也能很快的去解決問題了。