我寫了挺多Python程序,其中涉及到了讀取文本文件並處理。用過Python的人都知道這個過程會出現一些讓人摸不着頭腦的事情,概括的說,就是字符的編碼,解碼以及不期而至的亂碼問題。
在應付Python的str對象與unicode對象的過程中,我找到了Ned Batchelder的一篇文章/演講稿“我怎么能止疼啊?當我在Python中用unicode的時候。”,里面關於Python的字符編程的事情,講解得很詳細,關於字符以及編碼的事情,講得甚至更詳細。也是在用Python之后,我更關注字符,編碼集的處理。
今天我在解決某個問題的時候,需要用Java從UTF-8文本文件中讀取內容,並輸出到另一個文件中,為了調試,也需要輸出到Java控制台。第一次實現的時候,我使用了Java中的FileReader類讀取文件,並使用FileWriter寫文件,輸出到控制台當然只能用System.out.println()語句。算法過程很簡單:
(1)讀取源文件內容, 通過FileReader字符流完成;
(2)向目標文件寫內容,通過FileWriter字符流完成;
(3)向控制台輸出內容;
最后的結果是,目標文件為UTF-8編碼格式,一切正常;控制台得到的輸出,中文字符為亂碼,英文字母,標點符號一切正常。為了解決這個問題,我把MyEcplise的控制台默認編碼(GBK)修改為UTF-8,具體修改位置為右鍵要執行的main函數,選擇“Run As”-->"Run Configurations...",在彈出的窗口中,選擇"common"標簽頁,即可發現更改項。修改完成后,再一次運行,目標文件完全正常,控制台輸出也完全正常。
但是這不算解決問題,在Windows 7的cmd窗口下,不可能更改默認編碼,那么該如何讓控制台輸出的字符顯示正常?
解碼過程
我查閱了Java API文檔,官方在線文檔在這里。結合之前理解的Java輸入流與輸出流的知識,找到了使用另一種方法,在不更改控制台編碼集的情況下,得到正常輸出。
Java的FileReader字符輸入流,是Java中讀取文本文件的重要類,為了理解文本文件讀取具體過程,查閱API doc,發現FileReader類有如下繼承關系
- java.io.Reader
- java.io.InputStreamReader
- java.io.FileReader
- java.io.InputStreamReader
InputStreamReader是一個“轉換流”,將字節輸入流轉換成字符輸入流,FileReader繼承了此類。說明FileReader類在讀取文本文件時,首先將文件作為字節流讀入,然后使用InputStreamReader的類似功能,將字節流轉換為字符流。
這里需要解釋下,所有內容在計算機中的存儲形式,都是二進制形式,原始的二進制文件的基本讀取單位是字節(byte),而對於文本文件,字符使用一個整數來表示的,unicode中,把這個表示字符的整數稱為“code point”。encode(編碼)和decode(解碼)的過程,可用下面的方式理解:
字符.encode()------>二進制字節
二進制字節.decode()--------->字符
InputStreamReader轉換流所實現的功能,就是一個decode的過程。
API doc 中,FileReader類的描述中,有這樣一句:“The constructors of this class assume that the default character encoding and the default byte-buffer size are appropriate”,意即“該類構造器假定默認字符編碼和默認緩沖字節大小是合適的”。說明FileReader在將字節與字符進行encode(編碼)和decode(解碼)的過程中,是假定了一個默認的,“合適”的字符集。
而FileReader的父類InputStreamReader,是這樣描述的“It reads bytes and decodes them into characters using a specified
. The charset that it uses may be specified by name or may be given explicitly, or the platform's default charset may be accepted.”,意即“InputStreamReader讀取字節,並將它們解碼為字符,解碼過程中使用指定的字符集(charset)……如果沒有顯示指定字符接,使用平台的默認字符集。”Windows平台的默認字符集是GBK(ANSI標准),Linux平台的默認字符集是“UTF-8”。而如果一個UTF-8字節編碼的文本文件,使用GBK字符集解碼,那當然就會產生亂碼,反過來也一樣。而英文字母、符號永遠不會出現亂碼的原因,是ASCII字符表是UTF-8的一個子集,也是GBK的一個子集,UTF-8中英文字母的字節碼與GBK字符集中英文字母的字節碼完全相同。charset
因此,另外一種解決方式,是使用InputStreamReader轉換流,將一個字節流轉換為字符流,並在轉換過程中,指定使用“UTF-8”字符集,對字節進行解碼。核心代碼如下:
InputStreamReader ipr = new InputStreamReader(new FileInputStream("HelloTest.java"),Charset.forName("UTF-8");
//使用BufferedReader進行封裝,是為了調用readLine()函數,方便操作 BufferedReader br = new BufferedReader(ipr);
//其余操作......
指定解碼字符集后,目標文件正常,控制台輸出也正常。
等等,那編碼過程呢?
在指定解碼字符集之后,就能得到正常輸出。但是有另外一個問題,第一種解決辦法,是更改了MyEcplise控制台的默認編碼,才得到正常輸出。同時由於Windows的cmd窗口無法更改默認編碼,只能使用GBK,依然是亂碼。為什么在Java代碼中,更改了InputStreamReader的一個參數,在GBK編碼的控制台,依然能正常輸出“UTF-8”字符呢?
Java的標准輸出流System.out,將字符內容輸出到控制台。查閱Java Doc文檔,可以看到,out其實是System類中的一個靜態成員。而out的類型為PrintStream。因此,關鍵是要理解PrintStream類中的方法成員println()/print()的執行原理。在Java Doc文檔中,有描述“All characters printed by a PrintStream
are converted into bytes using the platform's default character encoding. ”意即“被PrintStream打印(print)的所有字符,都使用平台默認的字符編碼集,轉換為字節。”。也就是說,System.out.print()/pinrtln()方法,使用平台默認字符集,將字符串encode(編碼)為字節,然后傳給了控制台,控制台接收到字節后,當然會使用平台默認的字符集,將字節decode(解碼)為字符串。
即Java會將程序中的字符,先編碼,再放入標准輸出流System.out,編碼字符集為平台默認字符集。因此,只要在讀取輸入流的過程中,將字節正確地解碼為字符,后續的標准輸出其實是與原文件編碼格式無關的。
但是之前說過,計算機中所有數據都是二進制字節,文本文件的存儲方式也是二進制字節;那么,在Java程序中,字符是以什么形式保存的?再次查閱Java Doc,發現Java中,char數據類型是用如前所述的 UNICODE 表示,UNICODE標准給每個字符分配一個唯一的整數,使用16進制編碼方式表示,一共可以表示65536個字符,可以理解為無符號的16位整數。因此Java程序中,char類型是可以和Int類型相互轉換的。
所以,被更改的並不是控制台的編碼
通過以上的整理,可以發現,使用第一種方法——更改控制台編碼格式——解決控制台輸出亂碼的問題時,我們並不是因為讓控制台可以輸出"UTF-8"字符,所以消滅了亂碼。而是通過修改控制台編碼,連帶將整個平台的默認字符集編碼改為"UTF-8",導致FileReader在讀取字符輸入流時,使用了UTF-8字符集進行解碼。這一效果其實和
1 InputStreamReader ipr = new InputStreamReader(new FileInputStream("HelloTest.java"),Charset.forName("UTF-8");
完全相同。