Java 繁體中文處理完全攻略 許多人用 Java 處理到中文資料時(shí),常會(huì)出現(xiàn)亂碼。關(guān)于 Java 和中文兼容性的問(wèn)題,實(shí)在讓許多程序員為此傷透腦筋,相關(guān)的問(wèn)題每隔幾天就會(huì)出現(xiàn)在網(wǎng)絡(luò)上。為了舒緩您緊蹙的眉頭,我特別寫(xiě)了這系列文章,解說(shuō) Java 牽涉到文字時(shí)的內(nèi)部處理方式,供讀者參考。讀完本系列文章之后,不求甚解者可以治標(biāo),充分理解者可以治本。本文貴在原理解說(shuō),別光是囫圇吞棗。 快速解決之道 如果你目前正遭遇到 Java 和中文不兼容的問(wèn)題,請(qǐng)你注意下面這幾點(diǎn),說(shuō)不定問(wèn)題能馬上迎刃而解: 1. 檢查操作系統(tǒng)設(shè)定:先檢查你的操作系統(tǒng),確定國(guó)籍語(yǔ)言資料是「Traditional Chinese(Taiwan)」。國(guó)籍語(yǔ)言資料的設(shè)定會(huì)影響 Java 編譯器與JRE的判斷。我之前就是因?yàn)閲?guó)籍資料設(shè)定不正確,出了一堆 Java 和中文不兼容的怪事。 2. 更新 Java 環(huán)境版本:改用最新版的 JDK,新版本的 JDK 說(shuō)不定已經(jīng)解決你原有的問(wèn)題。請(qǐng)注意:某些 Java IDE 所用的編譯器和 JRE 是不兼容于中文的(我遇過(guò)這樣的情形),你最好能把 Java IDE 的 JDK 指到新版的 JDK。另外,如果數(shù)據(jù)庫(kù)取回的資料是亂碼,換別套或者更新 JDBC 驅(qū)動(dòng)程序試試看。 如果還是無(wú)法解決,請(qǐng)?jiān)敿?xì)閱讀下面各小節(jié)的內(nèi)容,仔細(xì)推敲你的錯(cuò)誤所在。 Unicode、UTF-16、UTF-8 Java 內(nèi)部處理字符使用的字序方式是 Unicode,這是一種通行全球的編碼方式。Unicode 因?yàn)楸仨殞⒅小㈨n、日、英、法、阿拉伯……等許多國(guó)家所使用的文字都納入,目前已經(jīng)包含了六萬(wàn)多個(gè)字符,所以 Unicode 使用了 16 個(gè)位來(lái)為字符編碼。因?yàn)?nbsp;Unicode 使用了 16 位編碼,所以每個(gè)字符都用 16 位來(lái)儲(chǔ)存或傳輸是很自然的事,這種儲(chǔ)存或傳輸?shù)母袷椒Q(chēng)為 UTF-16(是不是很像戰(zhàn)斗機(jī)的名字)。如果你使用到的字符都是西方字符,那么你一定不會(huì)想用 UTF-16 的格式,因?yàn)轶w積比 8 位的 Latin-1(一種擴(kuò)充 ASCII 的編碼)多了一倍。所以 Unicode 另有一種儲(chǔ)存或傳輸?shù)母袷?,叫?nbsp;UTF-8。UTF-8 的格式在編碼英文時(shí),只需要 8 位,但是中文則是 24 位,所以中文字出現(xiàn)比例高的地方還是使用 UTF-16 比較節(jié)省空間。Java 的 Class File(也就是 bytecode)中有一字段叫做常數(shù)區(qū)(Constant Pool),一律使用 UTF-8 為字符編碼。 關(guān)于 Unicode 的編碼,請(qǐng)查閱「The Unicode Standard, Version 3.0」一書(shū)(Addison-Wesley 出版);關(guān)于 UTF-8 編碼,請(qǐng)查閱「Java I/O」一書(shū)的 399 頁(yè)(O‘Reilly 出版)。關(guān)于 Java Class File 的格式與 Constant Pool,請(qǐng)查閱「Java Virtual Machine」一書(shū)(O‘Reilly出版)。 Unicode 與繁體中文編碼的互轉(zhuǎn) 雖然 Java 內(nèi)部完整地使用 Unicode,但是你所使用的操作系統(tǒng)可不見(jiàn)得。以繁體中文版的 Windows 98 來(lái)說(shuō),預(yù)設(shè)的編碼方式是 MS950,這是一種兼容于 Big 5的編碼方式。字符串?dāng)?shù)據(jù)從 Windows 一送進(jìn) JRE,JRE 的轉(zhuǎn)碼系統(tǒng)馬上先把字符串編碼由 MS950 轉(zhuǎn)成 Unicode,才能進(jìn)行處理。字符串資料由 JRE 一送出給 Windows,JRE 的轉(zhuǎn)碼系統(tǒng)馬上先將其由 Unicode 轉(zhuǎn)成 MS950,操作系統(tǒng)才能處理。 想知道你的 JDK 或 JRE 會(huì)用什么樣的編碼方式來(lái)和操作系統(tǒng)溝通,請(qǐng)執(zhí)行下面的 Java 程序: public class ShowNativeEncoding { public static void main(String[] args) { String enc = System.getProperty("file.encoding"); System.out.println(enc); } } 如果執(zhí)行結(jié)果不是下面的字符串之一,那么你的操作系統(tǒng)國(guó)籍語(yǔ)言設(shè)定可能就有問(wèn)題了: Big5:這是繁體中文 de facto 標(biāo)準(zhǔn)。 CNS11643:臺(tái)灣的官方標(biāo)準(zhǔn)繁體中文編碼。 Cp937:繁體中文加上 6204 個(gè)使用者自定的字符 Cp948:繁體中文版 IBM OS/2 用的編碼方式。 Cp964:繁體中文版 IBM AIX 用的編碼方式。 EUC_TW:臺(tái)灣的加強(qiáng)版 Unicode。 ISO2022CN:編碼中文的一套標(biāo)準(zhǔn)。 ISO2022CN_CNS:編碼中文的一套標(biāo)準(zhǔn),繁體版,襲自 CNS11643。 MS950 或 Cp950:ASCII + Big5,用于臺(tái)灣和香港的繁體中文 MS Windows操作系統(tǒng)。 Unicode:有次序記號(hào)的 Unicode。次序記號(hào)占用兩個(gè) byte,如果其值是0xFEFF,表示使用 big-endian(由大到?。┑拇涡?yàn)?nbsp;Unicode 編碼;如果其值是 0xFFFF,表示使用 little-endian(由小到大)的次序?yàn)?nbsp;Unicode 編碼。 UnicodeBig:使用 big-endian(由大到?。┑拇涡?yàn)?nbsp;Unicode 編碼。 UnicodeLittle:使用 little-endian(由小到大)的次序?yàn)?nbsp;Unicode 編碼。 UTF8:使用 UTF-8 為 Unicode 編碼。 關(guān)于 Big 5 編碼,請(qǐng)查閱「CJKV Information Processing」一書(shū)的附錄 H(O‘Reilly出版)。 編譯時(shí)的注意事項(xiàng) 編譯的時(shí)候,如果你不說(shuō)明原始文件編碼方式的話(huà), javac 編譯器在讀進(jìn)此原始程序文件,開(kāi)始編譯之前,會(huì)先去詢(xún)問(wèn)操作系統(tǒng)檔案預(yù)設(shè)的編碼方式為何。以繁體中文 Windows 98 來(lái)說(shuō),javac 會(huì)先詢(xún)問(wèn) Windows 98,得知檔案是用 MS950 的方式編碼。然后就可以將檔案由 MS950 轉(zhuǎn)成 Unicode 編碼方式,開(kāi)始進(jìn)行編譯。 通常在編譯階段,會(huì)造成的錯(cuò)誤有下列幾種可能: 1. 如果操作系統(tǒng)的國(guó)籍資料設(shè)定錯(cuò)誤,會(huì)造成 javac 編譯器取得的編碼信息是錯(cuò)的。 2. 較差勁的編譯器可能沒(méi)有主動(dòng)詢(xún)問(wèn)操作系統(tǒng)的編碼方式,而是采用編譯器預(yù)設(shè)的編碼方式。 3. 如果原始程序不是用編譯當(dāng)時(shí)操作系統(tǒng)預(yù)設(shè)的編碼方式存盤(pán)的,也會(huì)造成錯(cuò)誤。比方說(shuō),原始程序文件是臺(tái)灣程序員寫(xiě)的,在繁體中文版的 Windows上以 MS950 編碼存盤(pán),再經(jīng)由網(wǎng)絡(luò)傳送到泰國(guó),在泰文版的 Windows 上編譯(泰文版 Windows 預(yù)設(shè)的檔案編碼方式是 MS874)。 這種因?yàn)樵汲绦蛭募幋a方式和編譯器無(wú)法匹配所造成的問(wèn)題,輕則編譯成功但執(zhí)行時(shí)文字出現(xiàn)亂碼或出現(xiàn) Error/Exception,重則無(wú)法成功編譯。這時(shí)候,你需要主動(dòng)透過(guò)「-encoding」選項(xiàng)來(lái)指定原始程序的編碼方式,編譯器會(huì)以你指定的編碼為主,不會(huì)再去詢(xún)問(wèn)操作系統(tǒng)。下面的例子,我們告訴編譯器「TaiwanClass.java」是以繁體中文版 Windows 的「MS950」編碼的: javac ?encoding MS950 TaiwanClass.java 如果你手上只有某 class 文件,沒(méi)有原始程序文件,而且你確定其 constant pool 的UTF-8 字段編碼錯(cuò)誤,你有兩種方式可以用來(lái)修正編碼: 1. 先反編譯,取得原始程序,再修改,編譯。 2. 或者直接利用 bytecode 編輯軟件,直接修改 class 文件。 I/O 轉(zhuǎn)碼 Java 現(xiàn)行的 IO 一律使用 Stream 的方式,相關(guān)的類(lèi)別都放在 java.io 中。輸出 binary 的資料使用 OutputStream 的子類(lèi)別,輸入 binary 的資料使用 InputStream 的子類(lèi)別,輸出文字的資料使用 Writer 的子類(lèi)別,輸入文字的資料使用 Reader 的子類(lèi)別。 你可能會(huì)覺(jué)得很奇怪:「有必要用不同的方式來(lái)處理文字和 binary 嗎?文字資料不也是 binary 的一種?」沒(méi)錯(cuò),其實(shí)他們非常類(lèi)似,最大的差異在于,InputStream/OutputStream 會(huì)原封不動(dòng)地傳送資料,但是 Reader/Writer 會(huì)將資料當(dāng)作文字對(duì)待,所以 Reader/Writer 在「必要時(shí)」會(huì)把(文字)資料轉(zhuǎn)碼。什么時(shí)候才是所謂的「必要時(shí)」呢? Java 的 Stream(包括 Reader 和 Writer)是可以互相串接的。當(dāng) Reader 的資料來(lái)源是另一個(gè) Reader 時(shí),不轉(zhuǎn)碼,當(dāng) Reader 的資料來(lái)源是一個(gè) InputStream 時(shí),就會(huì)轉(zhuǎn)碼。當(dāng) Writer 的資料去處是另一個(gè) Writer 時(shí),不轉(zhuǎn)碼,當(dāng) Writer 的資料去處是一個(gè) OutputStream 時(shí),就會(huì)轉(zhuǎn)碼。 由什么碼轉(zhuǎn)成什么碼?這是可以指定的。因?yàn)檗D(zhuǎn)碼只發(fā)生在 Reader/InputStream 的交界處與 Writer/OutputStream 的交界處,所以正是由 InputStreamReader 和 OutputStreamWriter 此二類(lèi)別負(fù)責(zé),下面兩個(gè) constructor 的第二個(gè)參數(shù),正是用來(lái)指定轉(zhuǎn)碼的方式。 public InputStreamReader(InputStream in, String enc) throws UnsupportedEncodingException; public OutputStreamWriter(OutputStream out, String enc) throws UnsupportedEncodingException; InputStreamReader 負(fù)責(zé)將 enc 的編碼方式轉(zhuǎn)成 Unicode(因?yàn)橘Y料是從「外部」送過(guò)來(lái)給「內(nèi)部」的),OutputStreamWriter 負(fù)責(zé)將 Unicode 的編碼方式轉(zhuǎn)成 enc(因?yàn)橘Y料要從「內(nèi)部」送給「外部」)。JRE 內(nèi)部當(dāng)然都一定是用 Unicode 編碼,而外部的編碼就不一定,要看當(dāng)時(shí)的環(huán)境為何。你可以透過(guò) getEncoding() 的 method,來(lái)得知 InputStreamReader 與 OutputStreamWriter 的編碼方式。 請(qǐng)注意:即使你沒(méi)用到 InputStreamReader 與 OutputStreamWriter,只有用到其它的 Reader 和 Writer,但是這些 Reader 和 Writer 內(nèi)部也很有可能(但非絕對(duì))是直接或間接通到 InputStreamReader 與 OutputStreamWriter。比方說(shuō):FileReader 內(nèi)部其實(shí)是透過(guò)一個(gè) InputStreamReader 的中介來(lái)將資料從 FileInputStream 取過(guò)來(lái)的,此時(shí) InputStreamReader 的轉(zhuǎn)碼方式是采用 OS 的文字編碼(以繁體中文的 Windows 為例,就是「MS950」)轉(zhuǎn)成 Unicode。 如果你清楚地知道你要讀寫(xiě)的檔案(或資料來(lái)源 / 去處)是采用某種編碼方式,你也可以主動(dòng)指定編碼方式。但是,請(qǐng)記得抓取可能導(dǎo)致的 UnsupportedEncodingException,并務(wù)必處理之,不可對(duì)此例外置之不理,因?yàn)樵?nbsp;JRE 有可能沒(méi)有附上此種編碼表(也有可能你的編碼名稱(chēng)給錯(cuò))。 檔案 I/O 轉(zhuǎn)碼 如果你是在泰文版的 Windows 上,想讀取用 MS950 編碼的繁體中文文字文件,你就必須主動(dòng)指定編碼,不可以直接用 FileReader,否則無(wú)法成功讀取。方法如下: FileInputStream fis = new FileInputStream(fileName); InputStreamReader reader = new InputStreamReader(fis, "MS950"); 然后,透過(guò) Reader 讀出來(lái)的就會(huì)是正確的中文。 網(wǎng)絡(luò) I/O 轉(zhuǎn)碼 如果你的網(wǎng)絡(luò)程序采用 TCP,那么你可以透過(guò) Socket 類(lèi)別所提供的 getInputStream() 和 getOutputStream() 來(lái)得到 InputStream 和 OutputStream 對(duì)象。如果你是在泰文版的 Windows 上,想讀取用 MS950 編碼的繁體中文文字 TCP 網(wǎng)絡(luò)串流,你可以用類(lèi)似上面的技巧來(lái)轉(zhuǎn)碼。方法如下: InputStream is = mySocket.getInputStream(); InputStreamReader reader = new InputStreamReader(is, "MS950"); 如果你的網(wǎng)絡(luò)程序采用 UDP,你必須把中文字符串轉(zhuǎn)成(或轉(zhuǎn)自)byte 數(shù)組。請(qǐng)看下一節(jié)「 字符串和 byte 數(shù)組的轉(zhuǎn)碼 」。 如果你的網(wǎng)絡(luò)程序采用 RMI,那你完全不用為這部分的轉(zhuǎn)碼操心,字符串直接用 Unicode 在網(wǎng)絡(luò)上傳遞給另一個(gè) JRE,不需要轉(zhuǎn)碼。 保持刑案現(xiàn)場(chǎng) 如果你不知道你的 I/O 資料來(lái)源或去處是用何種編碼方式,那么你最好不要用 Reader 和 Writer,而應(yīng)該直接用 InputStream 和 OutputStream,因?yàn)榕c其被 Reader 和 Writer 胡亂編碼之后造成信息遺失或錯(cuò)亂,不如保持資料的完整不變,留待以后進(jìn)一步解讀。 字符串和 byte 數(shù)組的轉(zhuǎn)碼 java.lang.String 類(lèi)別是 Java 字符串對(duì)象的類(lèi)別,Java 字符串對(duì)象既然是活在 JRE 內(nèi)部,當(dāng)然就一定是用 Unicode 編碼。如果你需要將 String 對(duì)象和 byte 數(shù)組互轉(zhuǎn),你可以使用: String(byte[] bytes, int offset, int length, String enc); 或 String(byte[] bytes, String enc); 來(lái)將用 enc 編碼的 byte 數(shù)組,轉(zhuǎn)成 Unicode 的 String 對(duì)象。你也可以使用 String 對(duì)象所提供的: byte[] getBytes(String enc) 來(lái)將 String 對(duì)象轉(zhuǎn)成 byte 數(shù)組。 另外,你也可以透過(guò) ByteArrayInputStream 或 ByteArrayOutputStream 串接到 InputStreamReader 或 OutputStreamWriter,來(lái)達(dá)到轉(zhuǎn)碼的目的。 |
|