Java處理中文化問題詳解...

許多人用 Java 處理到中文資料時,常會出現亂碼。關於 Java 和中文相容性的問題,實在讓許多程式員為此傷透腦筋,相關的問題每隔幾天就會出現在網路上。為了舒緩您緊蹙的眉頭,我特別寫了這系列文章,解說 Java 牽涉到文字時的內部處理方式,供讀者參考。讀完本系列文章之後,不求甚解者可以治標,充分理解者可以治本。本文貴在原理解說,別光是囫圇吞棗。

快速解決之道 

如果你目前正遭遇到 Java 和中文不相容的問題,請你注意下面這幾點,說不定問題能馬上迎刃而解:

1. 檢查作業系統設定:先檢查你的作業系統,確定國籍語言資料是「Traditional Chinese(Taiwan)」。國籍語言資料的設定會影響 Java 編譯器與JRE的判斷。我之前就是因為國籍資料設定不正確,出了一堆 Java 和中文不相容的怪事。

2. 更新 Java 環境版本:改用最新版的 JDK,新版本的 JDK 說不定已經解決你原有的問題。請注意:某些 Java IDE 所用的編譯器和 JRE 是不相容於中文的(我遇過這樣的情形),你最好能把 Java IDE 的 JDK 指到新版的 JDK。另外,如果資料庫取回的資料是亂碼,換別套或者更新 JDBC 驅動程式試試看。

如果還是無法解決,請詳細閱讀下面各小節的內容,仔細推敲你的錯誤所在。

Unicode、UTF-16、UTF-8 

Java 內部處理字元使用的字序方式是 Unicode,這是一種通行全球的編碼方式。Unicode 因為必須將中、韓、日、英、法、阿拉伯……等許多國家所使用的文字都納入,目前已經包含了六萬多個字元,所以 Unicode 使用了 16 個位元來為字元編碼。因為 Unicode 使用了 16 位元編碼,所以每個字元都用 16 位元來儲存或傳輸是很自然的事,這種儲存或傳輸的格式稱為 UTF-16(是不是很像戰鬥機的名字)。如果你使用到的字元都是西方字元,那麼你一定不會想用 UTF-16 的格式,因為體積比 8 位元的 Latin-1(一種擴充 ASCII 的編碼)多了一倍。所以 Unicode 另有一種儲存或傳輸的格式,叫做 UTF-8。UTF-8 的格式在編碼英文時,只需要 8 位元,但是中文則是 24 位元,所以中文字出現比例高的地方還是使用 UTF-16 比較節省空間。Java 的 Class File(也就是 bytecode)中有一欄位叫做常數區(Constant Pool),一律使用 UTF-8 為字元編碼。

關於 Unicode 的編碼,請查閱「The Unicode Standard, Version 3.0」一書(Addison-Wesley 出版);關於 UTF-8 編碼,請查閱「Java I/O」一書的 399 頁(O'Reilly 出版)。關於 Java Class File 的格式與 Constant Pool,請查閱「Java Virtual Machine」一書(O'Reilly出版)。

Unicode 與繁體中文編碼的互轉

雖然 Java 內部完整地使用 Unicode,但是你所使用的作業系統可不見得。以繁體中文版的 Windows 98 來說,預設的編碼方式是 MS950,這是一種相容於 Big 5的編碼方式。字串資料從 Windows 一送進 JRE,JRE 的轉碼系統馬上先把字串編碼由 MS950 轉成 Unicode,才能進行處理。字串資料由 JRE 一送出給 Windows,JRE 的轉碼系統馬上先將其由 Unicode 轉成 MS950,作業系統才能處理。

想知道你的 JDK 或 JRE 會用什麼樣的編碼方式來和作業系統溝通,請執行下面的 Java 程式:

public class ShowNativeEncoding {

public static void main(String[] args) {

String enc = System.getProperty(“file.encoding”);

System.out.println(enc);

}

}

如果執行結果不是下面的字串之一,那麼你的作業系統國籍語言設定可能就有問題了:

Big5:這是繁體中文 de facto 標準。 

CNS11643:台灣的官方標準繁體中文編碼。 

Cp937:繁體中文加上 6204 個使用者自定的字元 

Cp948:繁體中文版 IBM OS/2 用的編碼方式。 

Cp964:繁體中文版 IBM AIX 用的編碼方式。 

EUC_TW:台灣的加強版 Unicode。 

ISO2022CN:編碼中文的一套標準。 

ISO2022CN_CNS:編碼中文的一套標準,繁體版,襲自 CNS11643。 

MS950 或 Cp950:ASCII + Big5,用於台灣和香港的繁體中文 MS Windows作業系統。 

Unicode:有次序記號的 Unicode。次序記號佔用兩個 byte,如果其值是0xFEFF,表示使用 big-endian(由大到小)的次序為 Unicode 編碼;如果其值是 0xFFFF,表示使用 little-endian(由小到大)的次序為 Unicode 編碼。 

UnicodeBig:使用 big-endian(由大到小)的次序為 Unicode 編碼。 

UnicodeLittle:使用 little-endian(由小到大)的次序為 Unicode 編碼。 

UTF8:使用 UTF-8 為 Unicode 編碼。 

關於 Big 5 編碼,請查閱「CJKV Information Processing」一書的附錄 H(O'Reilly出版)。

編譯時的注意事項 

編譯的時候,如果你不說明原始檔編碼方式的話, javac 編譯器在讀進此原始程式檔,開始編譯之前,會先去詢問作業系統檔案預設的編碼方式為何。以繁體中文 Windows 98 來說,javac 會先詢問 Windows 98,得知檔案是用 MS950 的方式編碼。然後就可以將檔案由 MS950 轉成 Unicode 編碼方式,開始進行編譯。

通常在編譯階段,會造成的錯誤有下列幾種可能:

如果作業系統的國籍資料設定錯誤,會造成 javac 編譯器取得的編碼資訊是錯的。 

較差勁的編譯器可能沒有主動詢問作業系統的編碼方式,而是採用編譯器預設的編碼方式。 

如果原始程式不是用編譯當時作業系統預設的編碼方式存檔的,也會造成錯誤。比方說,原始程式檔是台灣程式員寫的,在繁體中文版的 Windows上以 MS950 編碼存檔,再經由網路傳送到泰國,在泰文版的 Windows 上編譯(泰文版 Windows 預設的檔案編碼方式是 MS874)。 

這種因為原始程式檔編碼方式和編譯器無法匹配所造成的問題,輕則編譯成功但執行時文字出現亂碼或出現 Error/Exception,重則無法成功編譯。這時候,你需要主動透過「-encoding」選項來指定原始程式的編碼方式,編譯器會以你指定的編碼為主,不會再去詢問作業系統。下面的例子,我們告訴編譯器「TaiwanClass.java」是以繁體中文版 Windows 的「MS950」編碼的:

javac –encoding MS950 TaiwanClass.java

如果你手上只有某 class 檔,沒有原始程式檔,而且你確定其 constant pool 的UTF-8 欄位編碼錯誤,你有兩種方式可以用來修正編碼:

先反編譯,取得原始程式,再修改,編譯。 

或者直接利用 bytecode 編輯軟體,直接修改 class 檔。



I/O 轉碼 
Java 現行的 IO 一律使用 Stream 的方式,相關的類別都放在 java.io 中。輸出 binary 的資料使用 OutputStream 的子類別,輸入 binary 的資料使用 InputStream 的子類別,輸出文字的資料使用 Writer 的子類別,輸入文字的資料使用 Reader 的子類別。 

你可能會覺得很奇怪:「有必要用不同的方式來處理文字和 binary 嗎?文字資料不也是 binary 的一種?」沒錯,其實他們非常類似,最大的差異在於,InputStream/OutputStream 會原封不動地傳送資料,但是 Reader/Writer 會將資料當作文字對待,所以 Reader/Writer 在「必要時」會把(文字)資料轉碼。什麼時候才是所謂的「必要時」呢? 

Java 的 Stream(包括 Reader 和 Writer)是可以互相串接的。當 Reader 的資料來源是另一個 Reader 時,不轉碼,當 Reader 的資料來源是一個 InputStream 時,就會轉碼。當 Writer 的資料去處是另一個 Writer 時,不轉碼,當 Writer 的資料去處是一個 OutputStream 時,就會轉碼。 

由什麼碼轉成什麼碼?這是可以指定的。因為轉碼只發生在 Reader/InputStream 的交界處與 Writer/OutputStream 的交界處,所以正是由 InputStreamReader 和 OutputStreamWriter 此二類別負責,下面兩個 constructor 的第二個參數,正是用來指定轉碼的方式。 

public InputStreamReader(InputStream in, String enc)
throws UnsupportedEncodingException;
public OutputStreamWriter(OutputStream out, String enc)
throws UnsupportedEncodingException; 

InputStreamReader 負責將 enc 的編碼方式轉成 Unicode(因為資料是從「外部」送過來給「內部」的),OutputStreamWriter 負責將 Unicode 的編碼方式轉成 enc(因為資料要從「內部」送給「外部」)。JRE 內部當然都一定是用 Unicode 編碼,而外部的編碼就不一定,要看當時的環境為何。你可以透過 getEncoding() 的 method,來得知 InputStreamReader 與 OutputStreamWriter 的編碼方式。 

請注意:即使你沒用到 InputStreamReader 與 OutputStreamWriter,只有用到其它的 Reader 和 Writer,但是這些 Reader 和 Writer 內部也很有可能(但非絕對)是直接或間接通到 InputStreamReader 與 OutputStreamWriter。比方說:FileReader 內部其實是透過一個 InputStreamReader 的仲介來將資料從 FileInputStream 取過來的,此時 InputStreamReader 的轉碼方式是採用 OS 的文字編碼(以繁體中文的 Windows 為例,就是「MS950」)轉成 Unicode。 

如果你清楚地知道你要讀寫的檔案(或資料來源 / 去處)是採用某種編碼方式,你也可以主動指定編碼方式。但是,請記得抓取可能導致的 UnsupportedEncodingException,並務必處理之,不可對此例外置之不理,因為該 JRE 有可能沒有附上此種編碼表(也有可能你的編碼名稱給錯)。 

檔案 I/O 轉碼 
如果你是在泰文版的 Windows 上,想讀取用 MS950 編碼的繁體中文文字檔,你就必須主動指定編碼,不可以直接用 FileReader,否則無法成功讀取。方法如下: 

FileInputStream fis = new FileInputStream(fileName);
InputStreamReader reader = new InputStreamReader(fis, "MS950"); 

然後,透過 Reader 讀出來的就會是正確的中文。 

網路 I/O 轉碼 
如果你的網路程式採用 TCP,那麼你可以透過 Socket 類別所提供的 getInputStream() 和 getOutputStream() 來得到 InputStream 和 OutputStream 物件。如果你是在泰文版的 Windows 上,想讀取用 MS950 編碼的繁體中文文字 TCP 網路串流,你可以用類似上面的技巧來轉碼。方法如下: 

InputStream is = mySocket.getInputStream();
InputStreamReader reader = new InputStreamReader(is, "MS950"); 

如果你的網路程式採用 UDP,你必須把中文字串轉成(或轉自)byte 陣列。請看下一節「 字串和 byte 陣列的轉碼 」。 

如果你的網路程式採用 RMI,那你完全不用為這部分的轉碼操心,字串直接用 Unicode 在網路上傳遞給另一個 JRE,不需要轉碼。 

保持刑案現場 
如果你不知道你的 I/O 資料來源或去處是用何種編碼方式,那麼你最好不要用 Reader 和 Writer,而應該直接用 InputStream 和 OutputStream,因為與其被 Reader 和 Writer 胡亂編碼之後造成資訊遺失或錯亂,不如保持資料的完整不變,留待以後進一步解讀。 

字串和 byte 陣列的轉碼 
java.lang.String 類別是 Java 字串物件的類別,Java 字串物件既然是活在 JRE 內部,當然就一定是用 Unicode 編碼。如果你需要將 String 物件和 byte 陣列互轉,你可以使用: 

String(byte[] bytes, int offset, int length, String enc); 

或 

String(byte[] bytes, String enc); 

來將用 enc 編碼的 byte 陣列,轉成 Unicode 的 String 物件。你也可以使用 String 物件所提供的: 

byte[] getBytes(String enc) 

來將 String 物件轉成 byte 陣列。 

另外,你也可以透過 ByteArrayInputStream 或 ByteArrayOutputStream 串接到 InputStreamReader 或 OutputStreamWriter,來達到轉碼的目的。 

本文作者:蔡學鏞
張貼日期:9/23/00 

留言

這個網誌中的熱門文章

Use Case Description(描述使用案例)

列出不重複的隨機亂數

Tomcat 讀取中文檔案的方法