Java 中文 Unicode 編碼轉換

 
我頂 字號:
Java作爲支持多平臺的高級程序設計語言自然要支持多種編碼方式才能滿足程序設計的需要。但是在處理中文&其他編碼之間的轉換問題時往往出現各種問題,另程序員大傷腦筋。本文着重闡述了Java中文與Unicode編碼之間進行相互轉化的機理&方法,以求拋磚引玉。

關鍵字
約定:本文中的編碼(encoding)和字符集(charset)概念相同

一、Appetite

在進行詳細的編碼轉換原理闡述之前,我們要作兩件事情:

1。首先檢查操作系統用的語言。以Windows 2003 Server爲例,可以在“控制面板”中的“區域和語言設置”中選擇你的國家、語言,還有你的操作系統必須支持的語言。國籍&語言的設定會影響JRE的判斷情況。也許適當的設定能夠幫你解決不少Java語言編碼的問題。

2。更新新版本的JDK。因爲新版本的JDK往往能夠更好的支持新的特性,達到良好的語言遲遲效果。例如JDK5.0就已經更形了JDK1.2中的很多語言問題。

二、正餐

2.1 Unicode編碼

2.1.1 Unicode——Java默認的編碼


毫無疑問,Unicode作爲容納全球所有語言字符的超級字符集,是Java的首選字符集。Unicode使用兩個字節作爲編碼方式,總共容納有6萬多個字符。因爲使用16位進行字符編碼,所以也稱爲UTF-16。然而即使這樣,UTF-16也並不能充分囊括所有全世界正在使用或者曾經使用的字符,所以必須對其進行擴充。於是後來的Unicode版本已經擴充到了1,112,064個字符,這種規模已經相當大了。但是這樣仍然不能滿足Unicode在世界上的需求,所以必須進行必要的擴充。相比預Unicode1.0,後來的2.0版本已經支持擴展字符了,但是並沒有真正的加入擴展字符集,這種狀況一直持續到了Unicode3.1版,才第一次在Unicode中引入了擴展字符集。但是Unicode的發展腳步並沒有停滯,後來出現了Unicode4.0 標準,而這也正好是現在Java5.0版所必須而且已經提供支持的字符集。

顯然“對增補字符的支持也可能會成爲東亞市場的一個普遍商業要求。政府應用程序會需要這些增補字符,以正確表示一些包含罕見中文字符的姓名。出版應用程序可能會需要這些增補字符,以表示所有的古代字符和變體字符。中國政府要求支持 GB18030(一種對整個 Unicode 字符集進行編碼的字符編碼標準),因此,如果是 Unicode 3.1 版或更新版本,則將包括增補字符。臺灣標準 CNS-11643 包含的許多字符在 Unicode 3.1 中列爲增補字符。香港政府定義了一種針對粵語的字符集,其中的一些字符是 Unicode 中的增補字符。最後,日本的一些供應商正計劃利用增補字符空間中大量的專用空間收入 50,000 多個日文漢字字符變體,以便從其專有系統遷移至基於 Java 平臺的解決方案。

因此,Java 平臺不僅需要支持增補字符,而且必須使應用程序能夠方便地做到這一點。由於增補字符打破了 Java 編程語言的基礎設計構想,而且可能要求對編程模型進行根本性的修改,因此,Java Community Process 召集了一個專家組,以期找到一個適當的解決方案。該小組被稱爲 JSR-204 專家組,使用 Unicode 增補字符支持的 Java 技術規範請求的編號。從技術上來說,該專家組的決定僅適用於 J2SE 平臺,但是由於 Java 2 平臺企業版 (J2EE) 處於 J2SE 平臺的最上層,因此它可以直接受益,我們期望 Java 2 平臺袖珍版 (J2ME) 的配置也採用相同的設計方法。”

UTF-16的編碼方式:

UTF-16 使用一個或兩個未分配的 16 位代碼單元的序列對 Unicode 代碼點進行編碼。值 0x0000 至 0xFFFF 編碼爲一個相同值的 16 位單元。增補字符編碼爲兩個代碼單元,第一個單元來自於高代理範圍(0xD800 至 0xDBFF),第二個單元來自於低代理範圍(U+DC00 至 U+DFFF)。這在概念上可能看起來類似於多字節編碼,但是其中有一個重要區別:值 0xD800 至 0xDFFF 保留用於 UTF-16;沒有這些值分配字符作爲代碼點。這意味着,對於一個字符串中的每個單獨的代碼單元,軟件可以識別是否該代碼單元表示某個單單元字符,或者是否該代碼單元是某個雙單元字符的第一個或第二單元。這相當於某些傳統的多字節字符編碼來說是一個顯著的改進,在傳統的多字節字符編碼中,字節值 0x41 既可能表示字母“A”,也可能是一個雙字節字符的第二個字節。

2.1.2 節省空間的UTF-8

“如果我只能吃一塊巧克力,我絕對不會買上一箱子的巧克力。”

是的,很多時候,特別是我們在處理程序的時候,所使用的並非是所有的Unicode字符,而僅僅是他們其中很小的一個部分,確切的說,這個部分不會比 ASCII多上多少。但是因爲使用UTF-16,卻不得不爲此付出很多的存儲空間來存儲這些字符,這是一種可恥的浪費。因此,爲了便於節省空間,無論是在存儲或者傳輸過程中,如果你只使用到了英文或者拉丁文,那麼只需要8位來表示字符就足夠了。這就是UTF-8的設計思想。但是,如果是在漢字或者亞洲語言使用頻率很高的地方,UTF-16依然將是首選。

但是值得注意的是,因爲Unicode本身一直在進行版本更新,UTF-8當然也並非一成不變。對於經修改的過得UTF-8編碼,在某些Java API的調用中會出現種種問題,特別是要注意在開發包含增補字符的文本與UTF-8進行轉換的時候,可能會出現嚴重錯誤。

雖然Java本身對官方修訂的UTF-8很熟悉,但是因爲Java內部含有一套使用規範編碼的機制,因此實際上,Java在使用UTF-8的時候,就並非使用的是Unicode的UTF-8,而是一種叫做“Java modified UTF-8”(經 Java 修訂的 UTF-8)或(錯誤地)直接稱爲“UTF-8”。而在J2SE5.0種,這種編碼被統稱爲“modified UTF-8”(經修訂的 UTF-8)。

“經修訂的 UTF-8 和標準 UTF-8 之間之所以不兼容,其原因有兩點。其一,經修訂的 UTF-8 將字符 U+0000 表示爲雙字節序列 0xC0 0x80,而標準 UTF-8 使用單字節值 0x0。其二,經修訂的 UTF-8 通過對其 UTF-16 表示法的兩個代理代碼單元單獨進行編碼表示增補字符 。每個代理代碼單元由三個字節來表示,共有六個字節。而標準 UTF-8 使用單個四字節序列表示整個字符。

Java 虛擬機及其附帶的接口(如 Java 本機接口、多種工具接口或 Java 類文件)在 java.io.DataInputDataOutput 接口和類中使用經修訂的 UTF-8 實現或使用這些接口和類 ,並進行序列化。Java 本機接口提供與經修訂的 UTF-8 之間進行轉換的例程。而標準 UTF-8 由 String 類、java.io.InputStreamReaderOutputStreamWriter 類、java.nio.charset 設施 (facility) 以及許多其上層的 API 提供支持。

由於經修訂的 UTF-8 與標準的 UTF-8 不兼容,因此切勿同時使用這兩種版本的編碼。經修訂的 UTF-8 只能與上述的 Java 接口配合使用。在任何其他情況下,尤其對於可能來自非基於 Java 平臺的軟件的或可能通過其編譯的數據流,必須使用標準的 UTF-8。需要使用標準的 UTF-8 時,則不能使用 Java 本機接口例程與經修訂的 UTF-8 進行轉換。”


UTF-8的編碼方式:

UTF-8 使用一至四個字節的序列對編碼 Unicode 代碼點進行編碼。U+0000 至 U+007F 使用一個字節編碼,U+0080 至 U+07FF 使用兩個字節,U+0800 至 U+FFFF 使用三個字節,而 U+10000 至 U+10FFFF 使用四個字節。UTF-8 設計原理爲:字節值 0x00 至 0x7F 始終表示代碼點 U+0000 至 U+007F(Basic Latin 字符子集,它對應 ASCII 字符集)。這些字節值永遠不會表示其他代碼點,這一特性使 UTF-8 可以很方便地在軟件中將特殊的含義賦予某些 ASCII 字符。

2.1.3 同胞兄弟——UTF32

如果要問在Unicode家族誰的肚量最大,毫無疑問的是UTF-32。因爲採用32位編碼方式,所以會使得他的容量特別大!因爲會有2的32次方個字符!同樣的,會帶來相應的問題,就是UTF-32的空間浪費的也比較嚴重。所以,比般情況下很少使用這種編碼。

UTF-32的編碼方式:

UTF-32 即將每一個 Unicode 代碼點表示爲相同值的 32 位整數。很明顯,它是內部處理最方便的表達方式,但是,如果作爲一般字符串表達方式,則要消耗更多的內存。

2.1.4 三種編碼方式的比較

Unicode 代碼點
U+0041
U+00DF
U+6771
U+10400
表示字形
UTF-32 代碼單元
00000041
000000DF
00006771
00010400
UTF-16 代碼單元
0041
00DF
6771
D801 DC00
UTF-8 代碼單元
41
C3 9F
E6 9D B1
F0 90 90 80

更多的信息可以參見:

關於 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出版)。

2.2 Unicode與中文相互轉化的問題來源
2.2.1 識別你的文件編碼
雖然Java能夠在其內部支持Unicode,但是我們的操作系統並非這樣。如果是比較老的windows98 簡體中文版,我們只能使用GB2312編碼。當我們運行程序的時候,字符串是OS支持的編碼,在送進JRE之後,JRE會根據當前操作系統所使用字符集的情況,將字符串轉換爲unicode進行處理,處理之後,再把他們轉化爲系統能夠識別的字符集中的字符,送出JRE到OS。

如果想要知道你的系統到底使用什麼樣的字符集與字符打交道,可以使用如下代碼片斷得到字符集名稱:

String enc = System.getProperty"file.encoding");
System.out.println(enc);

可能會得到下列字符集的名稱:

GB2313:這是簡體中文的標準。
GB18083:這是中文的擴展字符集。
HZ:同樣是一種中文標準。
Big5:這是繁體中文標準。
CNS11643:臺灣的官方標準繁體中文編碼。
Cp937:繁體中文加上 6204 個使用者自定的字符
Cp948:繁體中文版 IBM OS/2 用的編碼方式。
Cp964:繁體中文版 IBM AIX 用的編碼方式。
EUC_TW:臺灣的加強版 Unicode。
ISO2022CN:編碼中文的一套標準。
ISO2022CN_CNS:編碼中文的一套標準,繁體版,襲自 CNS11643。
MS950 或 Cp950:ASCII + Big5,用於臺灣和香港的繁體中文 MS Windows操作系統。

2.2.2 問題來源

在Javac編譯期間,也會先從OS中取得現在使用的字符集,此處設爲A,之後把送入的字符串轉化爲Unicode編碼,在編譯之後再從Unicode轉化爲A型字符集。因此:

  1. 當你的操作系統國際設定錯誤,編譯時就會產出錯誤的字符集編碼。
  2. 一些比較lj的編譯器會按照預先設定的字符集,而非OS所使用的字符集進行編碼。
  3. 原是文件存盤時使用的字符集與編譯器所使用的字符集無法匹配也會產生錯誤。

對於1和2,很好理解。對於3,例如我們使用的OS時GB2312,但是存盤時使用的編碼字符集時UTF-8,這樣java編譯器編譯文件的時候,就把UTF-8字符集當成GB2312字符集來處理,這樣當然會出錯。

可以使用一下代碼片斷來以制定的編碼方式編譯Java文件。

javac -encoding GB2312 TestEncoding.java
然而,有時候不得不面臨另外一種不幸的情況,即我們手頭只有字節碼文件,但是原來類中的常量中肯定存在編碼問題。只有先反編譯字節碼文件,修改文件之後再重新編譯。
2.3 解決之道
2.3.1 I/O神功
幸好,因爲Java中強大的IO接口,我們纔有機會將上面的不幸化解。
Java中所有的IO都是通過流來完成的。對於二進制數據的輸入,InputStream是所有輸入流的基類;而對於所有的二進制輸出,OutputStream則是所有輸出流的基類。在java.io包中的所有類幾乎都與這兩個類有着千絲萬縷的聯繫。而對於各種文字數據,Writer類則是所有文字輸出的祖先類,Reader也一樣是所有文字輸入類的祖先類。
但是文字畢竟也是“binary”,爲什麼要單獨給它們編寫Reader和Writer類呢?問題在於, InputStream與OutputStream會照本宣科的解讀所有輸入的數據爲binary,而Reader和Writer才真正的把文字當成文字,並且在需要的時候將其轉換。這種需要的轉換的情況存在與XXXXer類與XXXXStream作爲對口時纔會發生。例如,當Reader類的來源是一個InputStream時,或者Writer的數據目標是一個OutputStream時就會發生轉碼。由此可知,這種轉換實際上發生在Reader與 InputStream或者是Writer與OutputStream的交界處。幸運的是Java強大而龐大的類庫爲我們提供了這種轉換機制,函數原形如下:
public InputStreamReader(InputStream in, String encoding) throws UnsupportedEncodingException;
public OutputStreamWriter(OutputStream out, String encoding) throws UnsupportedEncodingException;
勿庸置疑,JRE內部使用Unicode編碼,但是外部環境的編碼方式就不一定了。可以使用
getEncoding()方法得知外界使用的編碼方式。
當然,如果你清楚的知道文檔的來去和系統的編碼方式,你可以自己指定。代碼如下:
FileInputStream fis = new FilInputStream(new File("hello.txt"));
InputStreamReader isr = new InputStreamReader(fis,"GB2312");
這樣可以正確的讀出文件中的字符。
如果是除了RMI以外的網絡連接方式進行讀取,也需要使用相應的方法獲取相應的輸入輸出流,之後代碼實現類似上例。但是如果你使用的是UDP,那麼情況就例外了:你必須把中文字符串轉換爲數組。那麼RMI爲什麼不用進行編碼轉換呢?很簡單,因爲RMI是把 Unicode傳給另外一個遠程主機,所以不存在編碼轉換!
注意:
  1. 如果你不能確定你的數據來源或者流向,那麼最好不使用Reader和Writer,因爲這樣可能造成不必要的信息損失。與其這樣,不如保持其二進制編碼的完整性,留作以後進一步處理。
  2. 有時候,Reader和Writer之間進行通信的時候也可能出現編碼錯誤,原因在於他們之間直接或者間接的使用到了I/O流,這樣就可能導致編碼轉換時出現不統一的情況。

2.3.2 字符串與字節數組

Java的String類提供了非常豐富的功能借助與此,我們也能達到編碼轉換的功能。

常用的String構造函數如下:

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

以上方法可以通過byte數組創建指定字符集的字符串,而下面的方法:

byte[] getBytes(String charset);

則可以將String轉化爲指定字符集的byte數組。

此外,還可以通過ByteArrayInputStream 或 ByteArrayOutputStream 串接到 InputStreamReader 或 OutputStreamWriter,來達到轉碼的目的。

2.4 其他問題和解決辦法

然而Java本身涉及到編碼的問題不止這些。曾經有位朋友編寫一個可視花的zip應用程序。非常不幸的是,由於Java 本身的編碼問題,使得他的程序在存儲文件時,如果文件名是含有中文的,那麼存儲後的文件名不能夠正確顯示。這個問題困擾了他很久,雖然使用了本文中所提到過的方法,但是依然不能夠解決問題。無奈,在網絡上查找了相關資料,發現如果不用java自己的zip包而改用Apache的zip包問題能夠得到解決。

這就提示我們說,有的時候,當你面臨Java的編碼問題時,不妨利用第三方的工具包嘗試解決往往能夠收到不錯的效果。

三 總結

綜上,本文討論的Java字符編碼問題的來龍去脈,並且給出了相應的解決方法。相信憑藉着對問題根源的瞭解,Java的字符編碼問題一定能夠在實際中得到解決。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章