工作中經常遇到java編碼問題,由於缺乏研究,總是無法給出確切的答案,這個周末在網上查了一些資料,在此做些匯總。
問題一:在java中讀取檔案時應該採用什麼編碼。
Java讀取檔案的方式總體可以分為兩類:按位元組讀取和按字元讀取。按位元組讀取就是採用InputStream.read()方法來讀取位元組,然後儲存到一個byte[]數組中,最後經常用new String(byte[]);把位元組數群組轉換成String。在最後一步隱藏了一個編碼的細節,new String(byte[]);會使用作業系統預設的字元集來解碼位元組數組,中文作業系統就是GBK。而我們從輸入資料流裡讀取的位元組很可能就不是GBK編碼的,因為從輸入資料流裡讀取的位元組編碼取決於被讀取的檔案自身的編碼。舉個例子:我們在D:盤建立一個名為demo.txt的檔案,寫入”我們。”,並儲存。此時demo.txt編碼是ANSI,中文作業系統下就是GBK。此時我們用輸入位元組流讀取該檔案所得到的位元組就是使用GBK方式編碼的位元組。那麼我們最終new String(byte[]);時採用平台預設的GBK來編碼成String也是沒有問題的(位元組編碼和預設解碼一致)。試想一下,如果在儲存demo.txt檔案時,我們選擇UTF-8編碼,那麼該檔案的編碼就不在是ANSI了,而變成了UTF-8。仍然採用輸入位元組流來讀取,那麼此時讀取的位元組和上一次就不一樣了,這次的位元組是UTF-8編碼的位元組。兩次的位元組顯然不一樣,一個很明顯的區別就是:GBK每個漢字兩個位元組,而UTF-8每個漢字三個位元組。如何我們最後還使用new String(byte[]);來構造String對象,則會出現亂碼,原因很簡單,因為構造時採用的預設解碼GBK,而我們的位元組是UTF-8位元組。正確的辦法就是使用new String(byte[],”UTF-8”);來構造String對象。此時我們的位元組編碼和構造使用的解碼是一致的,不會出現亂碼問題了。
說完位元組輸入資料流,再來說說位元組輸出資料流。
我們知道如果採用位元組輸出資料流把位元組輸出到某個檔案,我們是無法指定組建檔案的編碼的(假設檔案以前不存在),那麼產生的檔案是什麼編碼的呢。經過測試發現,其實這取決於寫入的位元組編碼格式。比如以下代碼:
OutputStream out = new FileOutputStream("d:\\demo.txt");
out.write("我們".getBytes());
getBytes()會採用作業系統預設的字元集來編碼位元組,這裡就是GBK,所以我們寫入demo.txt檔案的是GBK編碼的位元組。那麼這個檔案的編碼就是GBK。如果稍微修改一下程式:out.write("我們".getBytes(“UTF-8”));此時我們寫入的位元組就是UTF-8的,那麼demo.txt檔案編碼就是UTF-8。這裡還有一點,如果把”我們”換成123或abc之類的ascii碼字元,那麼無論是採用getBytes()或者getBytes(“UTF-8”)那麼產生的檔案都將是GBK編碼的。
這裡可以總結一下,InputStream中的位元組編碼取決檔案本身的編碼,而OutputStream組建檔案的編碼取決於位元組的編碼。
下面說說採用字元輸入資料流來讀取檔案。
首先,我們需要理解一下字元流。其實字元流可以看做是一種封裝流,它的底層還是採用位元組流來讀取位元組,然後它使用指定的編碼方式將讀取位元組解碼為字元。說起字元流,不得不提的就是InputStreamReader。以下是java api對它的說明: InputStreamReader是位元組流通向字元流的橋樑:它使用指定的 charset 讀取位元組並將其解碼為字元。它使用的字元集可以由名稱指定或顯式給定,否則可能接受平台預設的字元集。說到這裡其實很明白了,InputStreamReader在底層還是採用位元組流來讀取位元組,讀取位元組後它需要一個編碼格式來解碼讀取的位元組,如果我們在構造InputStreamReader沒有傳入編碼方式,那麼會採用作業系統預設的GBK來解碼讀取的位元組。還用上面demo.txt的例子,假設demo.txt編碼方式為GBK,我們使用如下代碼來讀取檔案:
InputStreamReader in = new InputStreamReader(new FileInputStream(“demo.txt”));
那麼我們讀取不會產生亂碼,因為檔案採用GBK編碼,所以讀出的位元組也是GBK編碼的,而InputStreamReader預設採用解碼也是GBK。如果把demo.txt編碼方式換成UTF-8,那麼我們採用這種方式讀取就會產生亂碼。這是因為位元組編碼(UTF-8)和我們的解碼編碼(GBK)造成的。解決辦法如下:
InputStreamReader in = new InputStreamReader(new FileInputStream(“demo.txt”),”UTF-8”);
給InputStreamReader指定解碼編碼,這樣二者統一就不會出現亂碼了。
下面說說字元輸出資料流。
字元輸出資料流的原理和字元輸入資料流的原理一樣,也可以看做是封裝流,其底層還是採用位元組輸出資料流來寫檔案。只是字元輸出資料流根據指定的編碼將字元轉換為位元組的。字元輸出資料流的主要類是:OutputStreamWriter。Java api解釋如下:OutputStreamWriter 是字元流通向位元組流的橋樑:使用指定的 charset 將要向其寫入的字元編碼為位元組。它使用的字元集可以由名稱指定或顯式給定,否則可能接受平台預設的字元集。說的很明白了,它需要一個編碼將寫入的字元轉換為位元組,如果沒有指定則採用GBK編碼,那麼輸出的位元組都將是GBK編碼,產生的檔案也是GBK編碼的。如果採用以下方式構造OutputStreamWriter:
OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(“dd.txt”),”UTF-8”);
那麼寫入的字元將被編碼為UTF-8的位元組,產生的檔案也將是UTF-8格式的。
問題二: 既然讀檔案要使用和檔案編碼一致的編碼,那麼javac編譯檔案也需要讀取檔案,它使用什麼編碼呢。
這個問題從來就沒想過,也從沒當做是什麼問題。正是因為問題一而引發的思考,其實這裡還是有東西可以挖掘的。下面分三種情況來探討,這三種情況也是我們常用的編譯java源檔案的方法。
1.javac在控制台編譯java類檔案。
通常我們手動建立一個java檔案Demo.java,並儲存。此時Demo.java檔案的編碼為ANSI,中文作業系統下就是GBK.然後使用javac命令來編譯該源檔案。”javac Demo.java”。Javac也需要讀取java檔案,那麼javac是使用什麼編碼來解碼我們讀取的位元組呢。其實javac採用了作業系統預設的GBK編碼解碼我們讀取的位元組,這個編碼正好也是Demo.java檔案的編碼,二者一致,所以不會出現亂碼情況。讓我們來做點手腳,在儲存Demo.java檔案時,我們選擇UTF-8儲存。此時Demo.java檔案編碼就是UTF-8了。我們再使用”javac Demo.java”來編譯,如果Demo.java