本文介紹JavaME中文編碼的相關(guān)問(wèn)題,這個(gè)問(wèn)題一度是互聯(lián)網(wǎng)上的開(kāi)發(fā)者們討論的熱門(mén)話(huà)題。本文整理和綜合了網(wǎng)上眾多相關(guān)內(nèi)容,盡可能的為開(kāi)發(fā)者提供一個(gè)全面、系統(tǒng)的認(rèn)識(shí)。
文中的代碼僅用來(lái)說(shuō)明原理,可能很不完整,缺乏變量定義或者返回值,請(qǐng)諒解。部分代碼直接來(lái)源于網(wǎng)上的其他資料。
感謝眾多開(kāi)發(fā)者在中文編碼問(wèn)題上做出的努力與探索。總結(jié)中有什么問(wèn)題的話(huà),歡迎大家指正:)
2. 術(shù)語(yǔ)介紹
2.1 ASCII
基于羅馬字母表的一套電子計(jì)算器編碼系統(tǒng),是單字節(jié)的編碼方式,每個(gè)ASCII字符占用1個(gè)字節(jié)(8bits),所以ASCII編碼最多可以表示256個(gè)字符。它是美國(guó)信息交換標(biāo)準(zhǔn)委員會(huì)(American Standards Committee for Information Interchange)的縮寫(xiě), 為美國(guó)英語(yǔ)通信所設(shè)計(jì)。
顯然ASCII編碼用來(lái)表示英文字母和字符是足夠了的,但是對(duì)于中文和日文等眾多的文字來(lái)說(shuō),是遠(yuǎn)遠(yuǎn)不夠的。
跟ASCII類(lèi)似的編碼還有ISO8859-1。
2.2 UNICODE
雙字節(jié)編碼方式,它為每種語(yǔ)言中的每個(gè)字符設(shè)定了統(tǒng)一并且唯一的二進(jìn)制編碼,以滿(mǎn)足跨語(yǔ)言、跨平臺(tái)進(jìn)行文本基準(zhǔn)轉(zhuǎn)換、處理的要求。UNICODE支持歐洲、非洲、中東、亞洲(包括統(tǒng)一標(biāo)準(zhǔn)的東亞像形漢字和韓國(guó)像形文字)的文字。
UNICODE又可以分為“高位在前”和“低位在前”的兩種格式,這和CPU的處理方式有點(diǎn)關(guān)系。這一點(diǎn)可以通過(guò)BOM(Byte Order Mark)來(lái)標(biāo)示,若采用 “低位在前”方式編碼,BOM 會(huì)表示為 0xFF 0xFE,而在 Unicode 的定義中是不存在 U+FFFE 這個(gè)字符的。若采用高位在前方式編碼,BOM 會(huì)表示為 0xFE 0xFF,而 U+FEFF 剛好是在 Unicode 中的有效字符,代表的是一個(gè)不占空間的 space 符號(hào),所以即使沒(méi)被解釋為 BOM,也不會(huì)對(duì)閱覽者產(chǎn)生錯(cuò)誤的信息。
但UINICODE也帶來(lái)一些問(wèn)題,當(dāng)美國(guó)人看見(jiàn)自己每天最常用的字符需要用兩倍的空間來(lái)保存時(shí),自然會(huì)覺(jué)得這是一種浪費(fèi),他們一定會(huì)說(shuō):看看那堆0。于是新的編碼方式又誕生了。
2.3 UTF-8
UTF的全稱(chēng)是UCS Transformation Format,即把Unicode轉(zhuǎn)做某種格式的意思。目前存在的UTF格式有:UTF-7, UTF-7.5, UTF-8, UTF-16, 以及 UTF-32,本文討論UTF-8格式。
UTF-8是UNICODE的一種變長(zhǎng)字符編碼,理論上使用1~6個(gè)字節(jié)來(lái)編碼UNICODE。
雖然理論上UTF-8最多為6個(gè)字節(jié),但是,由于雙字節(jié)的Unicode最大為0XFFFF,所以雙字節(jié)的Unicode轉(zhuǎn)為UTF-8后最長(zhǎng)為3個(gè)字節(jié)。
下列字節(jié)串用來(lái)表示一個(gè)字符。 用到哪個(gè)串取決于該字符在 Unicode 中的序號(hào)。
U-00000000 - U-0000007F: 0xxxxxxx U-00000080 - U-000007FF: 110xxxxx 10xxxxxx U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
上表中的xxx為Unicode編碼的二進(jìn)制數(shù)據(jù)。例如:“中”字,Unicode編碼為4E2D。
Unicode: 4E 2D 01001110 00101101 UTF-8: E4 B8 AD 11100100 10111000 10101101
對(duì)于英文來(lái)說(shuō),UTF-8跟ISO8859-1一樣節(jié)約;但顯然中文等字符將為UTF-8付出更多。
2.4 GB2312
GB2312又稱(chēng)國(guó)標(biāo)碼,由國(guó)家標(biāo)準(zhǔn)總局發(fā)布,1981年5月1日實(shí)施,通行于大陸。新加坡等地也使用此編碼。它是一個(gè)簡(jiǎn)化字的編碼規(guī)范,當(dāng)然也包括其他的符號(hào)、字母、日文假名等,共7445個(gè)圖形字符,其中漢字占6763個(gè)。我們平時(shí)說(shuō)6768個(gè)漢字,實(shí)際上里邊有5個(gè)編碼為空白,所以總共有6763個(gè)漢字。
GB2312規(guī)定“對(duì)任意一個(gè)圖形字符都采用兩個(gè)字節(jié)表示,每個(gè)字節(jié)均采用七位編碼表示”,習(xí)慣上稱(chēng)第一個(gè)字節(jié)為“高字節(jié)”,第二個(gè)字節(jié)為“低字節(jié)”。GB2312中漢字的編碼范圍為,第一字節(jié)0xB0-0xF7(對(duì)應(yīng)十進(jìn)制為176-247),第二個(gè)字節(jié)0xA0-0xFE(對(duì)應(yīng)十進(jìn)制為160-254)。
GB2312具有很多擴(kuò)展,其中GBK是微軟對(duì)GB2312的擴(kuò)展,GB18030則是2000年發(fā)布的國(guó)家標(biāo)準(zhǔn),是到目前為止最新的國(guó)標(biāo)漢字編碼。
3.JavaME中的字符編解碼
3.1 編解碼方法
說(shuō)到JavaME中的字符編碼問(wèn)題,自然要從String類(lèi)入手,在String類(lèi)中我們可以找到字符編解碼的相關(guān)方法:
1. 解碼:
public String(byte[] bytes, String enc) throws UnsupportedEncodingException
2. 編碼:
public byte[] getBytes(String enc) throws UnsupportedEncodingException
舉例來(lái)說(shuō),“諾基亞”三個(gè)漢字的GB2312編碼為C5 B5 BB F9 D1 C7。
代碼段一,解碼試驗(yàn):
byte[] codes = {0XC5, 0XB5, 0XBB, 0XF9, 0XD1, 0XC7}; String string = new String(codes, “gb2312”); testForm.append(string);
得到的結(jié)果為:諾基亞
代碼段二,編碼試驗(yàn):
byte[] codes = “諾基亞”.getBytes(“gb2312”); for (int i = 0, t = codes.length; i < t; i ++) { String hexByte = Integer.getHexString(codes[i]); if (hexByte.length() > 2) { hexByte = hexByte.subString(hexByte.length() - 2); } testForm.append(“0X”+ hexByte.subString + “, ”); }
得到的結(jié)果為:0XC5, 0XB5, 0XBB, 0XF9, 0XD1, 0XC7,
3.2 檢查設(shè)備的編碼支持情況
一個(gè)最直接的獲取編碼支持的方法是使用System.getProperty(“microedition.encoding”),可以得到設(shè)備的默認(rèn)的字符編碼,以NOKIA設(shè)備為例,得到的屬性值為ISO8859-1。
然而,通過(guò)這個(gè)方法的意義并不大。首先,它只能獲取到一個(gè)編碼格式,而一般設(shè)備都會(huì)支持很多種編碼規(guī)范;其次,這個(gè)屬性的數(shù)值與虛擬機(jī)的實(shí)現(xiàn)有很大關(guān)系,同樣以NOKIA S40v2為例,不論設(shè)備的目標(biāo)市場(chǎng)使用什么語(yǔ)言,這個(gè)屬性統(tǒng)一為ISO8859-1[參考資料5],顯然ISO8859-1對(duì)于中文來(lái)說(shuō)是毫無(wú)意義的。
再回過(guò)頭來(lái)看看上一節(jié)中的兩個(gè)方法,它們都會(huì)拋出一個(gè)UnsupportedEncodingException。利用這一點(diǎn),我們可以自己來(lái)做一個(gè)設(shè)備支持編碼規(guī)范情況的測(cè)試。
boolean isEncodingSupported (String encoding) { try { "諾基亞".getBytes(encoding); return true; } catch (UnsupportedEncodingException uee) { return false; } }
這里需要提醒的是,對(duì)于同一個(gè)編碼格式來(lái)說(shuō),可能會(huì)有很多種不同的名稱(chēng),例如Unicode在NOKIA的設(shè)備上用的是ucs-2,再例如utf-8來(lái)說(shuō),utf-8、utf8和utf_8都會(huì)有可能。對(duì)于這一點(diǎn),CLDC的規(guī)范中并沒(méi)有給出嚴(yán)格的定義。所以在實(shí)際測(cè)試的過(guò)程中需要充分考慮到這個(gè)情況。
3.3 readUTF() 和 writeUTF(String)
CLDC中還有兩個(gè)方法跟字符編碼有關(guān)系:DataInputStream中的readUTF()和DataOutputStream中的writeUTF(String)。根據(jù)兩個(gè)方法的Java Doc,writeUTF(String)首先會(huì)向輸出流中寫(xiě)入字符串編碼成UTF8格式后的byte數(shù)組長(zhǎng)度(2個(gè)字節(jié)),然后再將這個(gè)UTF8的byte數(shù)組寫(xiě)入。而readUTF()則是先從輸入流中讀取2個(gè)字節(jié),組成一個(gè)short數(shù)值,在從輸入流中讀出這個(gè)數(shù)值長(zhǎng)度的byte數(shù)據(jù),再將這個(gè)byte數(shù)組解碼成字符串。詳細(xì)說(shuō)明請(qǐng)參考DataInputStream和DataOutputStream的Java Document。
4.具體問(wèn)題的解決
上一部分中介紹了CLDC平臺(tái)上所有跟字符編解碼相關(guān)的API。了解了這些內(nèi)容以后,就可以結(jié)合具體的情況來(lái)考慮如何解決中文編碼的問(wèn)題了。
首先,中文編碼問(wèn)題中最常見(jiàn)的情況就是亂碼,那么亂碼是如何產(chǎn)生的呢?無(wú)非是以下幾種情況:
- 指定的編碼格式與數(shù)據(jù)實(shí)際的編碼格式不符,造成數(shù)據(jù)被編碼成指定的替代符號(hào)或被解釋成與源字符完全不同的字符;
- 指定的編碼格式正確,數(shù)據(jù)不完整或被篡改,造成數(shù)據(jù)無(wú)法在字符集中找到與其對(duì)應(yīng)的編碼;
所以,解決中文編碼問(wèn)題的幾個(gè)基本思路是:
- 存儲(chǔ)或傳輸前,確保數(shù)據(jù)被正確編碼;
- 確保存儲(chǔ)、讀取和傳輸?shù)倪^(guò)程完整、正確;
- 解碼時(shí),使用與編碼時(shí)同樣的編碼格式;
- 確認(rèn)編碼格式是否被設(shè)備支持;
4.1 RMS存儲(chǔ)的中文問(wèn)題
這個(gè)問(wèn)題完全可以使用readUTF和writeUTF來(lái)解決。
用UTF8編碼向RecordStore中寫(xiě)入中文:
String content = "中文字符"; ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); dos.writeUTF(content); byte[] bytes = bos.toByteArray(); rs.addRecord(bytes, 0, bytes.length);
從RecordStore中讀出UTF8編碼的中文:
byte utfBytes[] = rs.getRecord(dbid); DataInputStream dis=new DataInputStream(new ByteArrayInputStream(utfBytes)); String content = dis.readUTF();
當(dāng)然,使用UTF8對(duì)于中文來(lái)說(shuō)意味著更大的存儲(chǔ)空間,所以也可以使用類(lèi)似Unicode和GB2312等2字節(jié)的編碼。在此之前,請(qǐng)通過(guò)3.2中描述的方式檢測(cè)編碼是否被支持。
用GB2312編碼向RecordStore中寫(xiě)入中文:
String content = "中文字符"; byte[] bytes = content.getBytes("gb2312"); rs.addRecord(bytes, 0, bytes.length);
從RecordStore中讀出GB2312編碼的中文:
byte gb2312Bytes[] = rs.getRecord(dbid); String content = new String(gb2312Bytes, "gb2312");
4.2 從Resource文件中讀取中文
大概可以分為兩種情況,一是這個(gè)文件遵循某種特定的格式,例如RPG游戲的關(guān)卡文件,其中包含地圖、事件和對(duì)話(huà)等數(shù)據(jù),文件由特定的程序生成,為自有格式。這種情況基本上可以使用與上一小節(jié)中同樣的方式來(lái)解決。關(guān)鍵是,存儲(chǔ)和讀取的過(guò)程遵從同樣的數(shù)據(jù)和編碼格式。
另一種情況是txt文件,可能通過(guò)一些文本編輯工具生成。Txt文件中常見(jiàn)的編碼格式有Unicode、UTF8、Unicode Big Endian等。在我們讀取txt文件之前,最先要確認(rèn)的就是這個(gè)txt文件所使用的編碼格式。
以UTF8為例:
in = getClass().getResourceAsStream(filename); in.read(word_utf); in.close(); string =new String(word_utf,"UTF-8");
對(duì)于Unicode來(lái)說(shuō),這里引用了參考1中的一段代碼,這段代碼實(shí)際上是在處理“低位在前”的Unicode:
public static String unicodeBytesToString(byte abyte0[], int i) { StringBuffer stringbuffer = new StringBuffer(""); for(int j = 0; j < i; ) { int k = abyte0[j++]; //注意在這個(gè)地方進(jìn)行了碼制的轉(zhuǎn)換 if(k < 0) k += 256; int l = abyte0[j++]; if(l < 0) l += 256; char c = (char)(k + (l << 8));//把高位和低位數(shù)組裝起來(lái) stringbuffer.append(c); } }
對(duì)于高位在前的Unicode,可以使用和UTF8類(lèi)似的方式。請(qǐng)注意,這里的"ucs-2"是針對(duì)諾基亞設(shè)備的,其他廠商設(shè)備可能與此不同,請(qǐng)查閱相關(guān)文檔或自行測(cè)試。
in = getClass().getResourceAsStream(filename); in.read(word_ unicode); in.close(); string =new String(word_unicode, "ucs-2");
實(shí)際上經(jīng)過(guò)我在NOKIA設(shè)備上的測(cè)試,對(duì)于低位在前的Unicode,也可以使用這個(gè)方式。前提是需要確保在數(shù)據(jù)的最前端添加低位在前的BOM(0XFFFE)。
繼續(xù)使用“諾基亞”為例,高位在前和低位在前的的Unicode編碼分別為:
nokiaBE = {(byte)0x8b, (byte)0xfa, (byte)0x57, (byte)0xfa, (byte)0x4e, (byte)0x9a,} nokiaLE = {(byte)0xfa, (byte)0x8b, (byte)0xfa, (byte)0x57, (byte)0x9a, (byte)0x4e,};
new String(nokiaBE, "ucs-2")的結(jié)果是“諾基亞”,而new String(nokiaLE, "ucs-2")的結(jié)果則是亂碼。然后,我們對(duì)nokiaLE做出修改:
nokiaLE = {(byte)0xff, (byte)0xfe, (byte)0xfa, (byte)0x8b, (byte)0xfa, (byte)0x57, (byte)0x9a, (byte)0x4e,};
修改后,再次執(zhí)行new String(nokiaLE, "ucs-2",則得到的結(jié)果也是“諾基亞”。
BTW,對(duì)于Resouce文件來(lái)說(shuō),雖然使用Unicode編碼存儲(chǔ)中文看起來(lái)像是比UTF-8要更節(jié)約,但是當(dāng)Resource資源被打成Jar包時(shí),壓縮后的文件大小可能很接近。
4.3通過(guò)網(wǎng)絡(luò)讀取中文
和前面描述的一樣,避免亂碼的關(guān)鍵同樣是保證編解碼使用同樣的格式,也就是客戶(hù)端與服務(wù)器段保持同樣的字符編碼。
對(duì)于socket連接來(lái)說(shuō),傳輸?shù)膬?nèi)容可以使用自定義的數(shù)據(jù)格式,所以處理的方式完全和前面兩節(jié)是相同的,甚至可以向http學(xué)習(xí),在數(shù)據(jù)的開(kāi)頭約定字符編碼。
對(duì)于http連接,在http數(shù)據(jù)頭中已經(jīng)約定了編碼格式,使用這個(gè)編碼格式解碼即可。另外一個(gè)很常見(jiàn)的問(wèn)題,就是從xml文件中解析中文。對(duì)于這一點(diǎn),在kxml2中已經(jīng)有了很好的解決方案。
org.kxml2.io.KXmlParser.setInput(InputStream is, String _enc)
你可以通過(guò)_enc指定一個(gè)編碼格式,如果_enc為null,則Parser會(huì)根據(jù)數(shù)據(jù)的特性自動(dòng)嘗試各種編碼格式。由于kxml為開(kāi)源項(xiàng)目,如果這里的處理方式需要調(diào)整,你也可以自己動(dòng)手去完善它的功能。
在kxml2中也增加了對(duì)wml文件的解析,有興趣的可以研究一下,這一部分我沒(méi)有作過(guò)嘗試。
4.4 JAD中的中文
JAD文件如果需要使用中文,則需要使用UTF8格式。關(guān)于如何在JAD中寫(xiě)入U(xiǎn)TF8數(shù)據(jù),可以有很多方法,可以使用一些Ultral Editor之類(lèi)的文本工具,或使用一些JAD/MENIFEST生成工具。你甚至可以自己編寫(xiě)一個(gè)JAD/MENIFEST的生成工具,對(duì)于JavaSE平臺(tái)來(lái)說(shuō),這并不是一件太難的工作,你還可以給這個(gè)工具賦予更多的功能,例如自動(dòng)填寫(xiě)vendor和version之類(lèi)的字段。
特別說(shuō)明,對(duì)于NOKIA Series 40v2等設(shè)備來(lái)說(shuō),通過(guò)OTA下載Jad/Jar時(shí),需要在服務(wù)器端為JAD添加MIME type時(shí)標(biāo)明encoding為UTF8,否則即使你的JAD確實(shí)使用了UTF8格式,安裝之后仍然會(huì)是亂碼。
4.5 Unicode和UTF8的互轉(zhuǎn)
在網(wǎng)上有一些Unicode和UTF8互相轉(zhuǎn)換的代碼,是直接通過(guò)編碼格式去實(shí)現(xiàn)的,從前文敘述的Unicode和UTF8之間的關(guān)系來(lái)看,Unicode和UTF8之間很容易互相轉(zhuǎn)換,計(jì)算量也不會(huì)太大。這里就不在把這些代碼貼出來(lái)了,有興趣的可以自己找一下,或者自己寫(xiě)一個(gè)。
但是,實(shí)際上Unicode和UTF8這兩個(gè)編碼格式基本是所有設(shè)備都會(huì)支持的,直接通過(guò)String類(lèi)的編解碼方法互轉(zhuǎn)就可以了。
4.6設(shè)備不支持的編碼格式
如果需要使用設(shè)備不支持的編碼格式,那么必然將付出一些額外的代價(jià)。例如,如果設(shè)備不支持GB2312,則你可以有如下的幾個(gè)選項(xiàng)中選擇其一:
- 在你的程序中包含一個(gè)GB2312的字符對(duì)應(yīng)表;(也許GB2312你還可以接收,但是GB18030要怎么辦呢?)
- 做一個(gè)代理服務(wù)器,將GB2312轉(zhuǎn)解碼成可以被設(shè)備支持的編碼;
- 放棄GB2312,選擇其他編碼;