JavaME中文编码(转)
原文地址:
本文介绍JavaME中文编码的相关问题,这个问题一度是互联网上的开发者们讨论的热门话题。本文整理和综合了网上众多相关内容,尽可能的为开发者提供一个全面、系统的认识。
文中的代码仅用来说明原理,可能很不完整,缺乏变量定义或者返回值,请谅解。部分代码直接来源于网上的其他资料。
感谢众多开发者在中文编码问题上做出的努力与探索。总结中有什么问题的话,欢迎大家指正:)
基于罗马字母表的一套电子计算器编码系统,是单字节的编码方式,每个ASCII字符占用1个字节(8bits),所以ASCII编码最多可以表示256个字符。它是美国信息交换标准委员会(American Standards Committee for Information Interchange)的缩写, 为美国英语通信所设计。
显然ASCII编码用来表示英文字母和字符是足够了的,但是对于中文和日文等众多的文字来说,是远远不够的。
跟ASCII类似的编码还有ISO8859-1。
双字节编码方式,它为每种语言中的每个字符设定了统一并且{wy}的二进制编码,以满足跨语言、跨平台进行文本基准转换、处理的要求。UNICODE支持欧洲、非洲、中东、亚洲(包括统一标准的东亚像形汉字和韩国像形文字)的文字。
UNICODE又可以分为“高位在前”和“低位在前”的两种格式,这和CPU的处理方式有点关系。这一点可以通过BOM(Byte Order Mark)来标示,若采用 “低位在前”方式编码,BOM 会表示为 0xFF 0xFE,而在 Unicode 的定义中是不存在 U+FFFE 这个字符的。若采用高位在前方式编码,BOM 会表示为 0xFE 0xFF,而 U+FEFF 刚好是在 Unicode 中的有效字符,代表的是一个不占空间的 space 符号,所以即使没被解释为 BOM,也不会对阅览者产生错误的信息。
但UINICODE也带来一些问题,当美国人看见自己每天最常用的字符需要用两倍的空间来保存时,自然会觉得这是一种浪费,他们一定会说:看看那堆0。于是新的编码方式又诞生了。
UTF的全称是UCS Transformation Format,即把Unicode转做某种格式的意思。目前存在的UTF格式有:UTF-7, UTF-7.5, UTF-8, UTF-16, 以及 UTF-32,本文讨论UTF-8格式。
UTF-8是UNICODE的一种变长字符编码,理论上使用1~6个字节来编码UNICODE。
虽然理论上UTF-8最多为6个字节,但是,由于双字节的Unicode{zd0}为0XFFFF,所以双字节的Unicode转为UTF-8后最长为3个字节。
下列字节串用来表示一个字符。 用到哪个串取决于该字符在 Unicode 中的序号。
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编码的二进制数据。例如:“中”字,Unicode编码为4E2D。
Unicode: 4E 2D 01001110 00101101 UTF-8: E4 B8 AD 11100100 10111000 10101101
对于英文来说,UTF-8跟ISO8859-1一样节约;但显然中文等字符将为UTF-8付出更多。
GB2312又称国标码,由国家标准总局发布,1981年5月1日实施,通行于大陆。新加坡等地也使用此编码。它是一个简化字的编码规范,当然也包括其他的符号、字母、日文假名等,共7445个图形字符,其中汉字占6763个。我们平时说6768个汉字,实际上里边有5个编码为空白,所以总共有6763个汉字。
GB2312规定“对任意一个图形字符都采用两个字节表示,每个字节均采用七位编码表示”,习惯上称{dy}个字节为“高字节”,第二个字节为“低字节”。GB2312中汉字的编码范围为,{dy}字节0xB0-0xF7(对应十进制为176-247),第二个字节0xA0-0xFE(对应十进制为160-254)。
GB2312具有很多扩展,其中GBK是微软对GB2312的扩展,GB18030则是2000年发布的国家标准,是到目前为止{zx1}的国标汉字编码。
说到JavaME中的字符编码问题,自然要从String类入手,在String类中我们可以找到字符编解码的相关方法:
1. 解码:
public String(byte[] bytes, String enc) throws UnsupportedEncodingException
2. 编码:
public byte[] getBytes(String enc) throws UnsupportedEncodingException
举例来说,“诺基亚”三个汉字的GB2312编码为C5 B5 BB F9 D1 C7。
代码段一,解码试验:
byte[] codes = {0XC5, 0XB5, 0XBB, 0XF9, 0XD1, 0XC7}; String string = new String(codes, “gb2312”); testForm.append(string);
得到的结果为:诺基亚
代码段二,编码试验:
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 + “, ”); }
得到的结果为:0XC5, 0XB5, 0XBB, 0XF9, 0XD1, 0XC7,
一个最直接的获取编码支持的方法是使用System.getProperty(“microedition.encoding”),可以得到设备的默认的字符编码,以NOKIA设备为例,得到的属性值为ISO8859-1。
然而,通过这个方法的意义并不大。首先,它只能获取到一个编码格式,而一般设备都会支持很多种编码规范;其次,这个属性的数值与虚拟机的实现有很大关系,同样以NOKIA S40v2为例,不论设备的目标市场使用什么语言,这个属性统一为ISO8859-1[参考资料5],显然ISO8859-1对于中文来说是毫无意义的。
再回过头来看看上一节中的两个方法,它们都会抛出一个UnsupportedEncodingExcep
boolean isEncodingSupported (String encoding) { try { "诺基亚".getBytes(encoding); return true; } catch (UnsupportedEncodingException uee) { return false; } }
这里需要提醒的是,对于同一个编码格式来说,可能会有很多种不同的名称,例如Unicode在NOKIA的设备上用的是ucs-2,再例如utf-8来说,utf-8、utf8和utf_8都会有可能。对于这一点,CLDC的规范中并没有给出严格的定义。所以在实际测试的过程中需要充分考虑到这个情况。
CLDC中还有两个方法跟字符编码有关系:DataInputStream中的readUTF()和DataOutputStream中的writeUTF(String)。根据两个方法的Java Doc,writeUTF(String)首先会向输出流中写入字符串编码成UTF8格式后的byte数组长度(2个字节),然后再将这个UTF8的byte数组写入。而readUTF()则是先从输入流中读取2个字节,组成一个short数值,在从输入流中读出这个数值长度的byte数据,再将这个byte数组解码成字符串。详细说明请参考DataInputStream和DataOutputStream的Java Document。
上一部分中介绍了CLDC平台上所有跟字符编解码相关的API。了解了这些内容以后,就可以结合具体的情况来考虑如何解决中文编码的问题了。
首先,中文编码问题中最常见的情况就是乱码,那么乱码是如何产生的呢?无非是以下几种情况:
- 指定的编码格式与数据实际的编码格式不符,造成数据被编码成指定的替代符号或被解释成与源字符xx不同的字符;
- 指定的编码格式正确,数据不完整或被篡改,造成数据无法在字符集中找到与其对应的编码;
所以,解决中文编码问题的几个基本思路是:
- 存储或传输前,确保数据被正确编码;
- 确保存储、读取和传输的过程完整、正确;
- 解码时,使用与编码时同样的编码格式;
- 确认编码格式是否被设备支持;
这个问题xx可以使用readUTF和writeUTF来解决。
用UTF8编码向RecordStore中写入中文:
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();
当然,使用UTF8对于中文来说意味着更大的存储空间,所以也可以使用类似Unicode和GB2312等2字节的编码。在此之前,请通过3.2中描述的方式检测编码是否被支持。
用GB2312编码向RecordStore中写入中文:
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");
大概可以分为两种情况,一是这个文件遵循某种特定的格式,例如RPG游戏的关卡文件,其中包含地图、事件和对话等数据,文件由特定的程序生成,为自有格式。这种情况基本上可以使用与上一小节中同样的方式来解决。关键是,存储和读取的过程遵从同样的数据和编码格式。
另一种情况是txt文件,可能通过一些文本编辑工具生成。Txt文件中常见的编码格式有Unicode、UTF8、Unicode Big Endian等。在我们读取txt文件之前,{zx0}要确认的就是这个txt文件所使用的编码格式。
以UTF8为例:
in = getClass().getResourceAsStream(filename); in.read(word_utf); in.close(); string =new String(word_utf,"UTF-8");
对于Unicode来说,这里引用了参考1中的一段代码,这段代码实际上是在处理“低位在前”的Unicode:
public static String unicodeBytesToString(byte abyte0[], int i) { StringBuffer stringbuffer = new StringBuffer(""); for(int j = 0; j < i; ) { int k = abyte0[j++]; //注意在这个地方进行了码制的转换 if(k < 0) k += 256; int l = abyte0[j++]; if(l < 0) l += 256; char c = (char)(k + (l << 8));//把高位和低位数组装起来 stringbuffer.append(c); } }
对于高位在前的Unicode,可以使用和UTF8类似的方式。请注意,这里的"ucs-2"是针对诺基亚设备的,其他厂商设备可能与此不同,请查阅相关文档或自行测试。
in = getClass().getResourceAsStream(filename); in.read(word_ unicode); in.close(); string =new String(word_unicode, "ucs-2");
实际上经过我在NOKIA设备上的测试,对于低位在前的Unicode,也可以使用这个方式。前提是需要确保在数据的最前端添加低位在前的BOM(0XFFFE)。
继续使用“诺基亚”为例,高位在前和低位在前的的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")的结果是“诺基亚”,而new String(nokiaLE, "ucs-2")的结果则是乱码。然后,我们对nokiaLE做出修改:
nokiaLE = {(byte)0xff, (byte)0xfe, (byte)0xfa, (byte)0x8b, (byte)0xfa, (byte)0x57, (byte)0x9a, (byte)0x4e,};
修改后,再次执行new String(nokiaLE, "ucs-2",则得到的结果也是“诺基亚”。
BTW,对于Resouce文件来说,虽然使用Unicode编码存储中文看起来像是比UTF-8要更节约,但是当Resource资源被打成Jar包时,压缩后的文件大小可能很接近。
和前面描述的一样,避免乱码的关键同样是保证编解码使用同样的格式,也就是客户端与服务器段保持同样的字符编码。
对于socket连接来说,传输的内容可以使用自定义的数据格式,所以处理的方式xx和前面两节是相同的,甚至可以向http学习,在数据的开头约定字符编码。
对于http连接,在http数据头中已经约定了编码格式,使用这个编码格式解码即可。另外一个很常见的问题,就是从xml文件中解析中文。对于这一点,在kxml2中已经有了很好的解决方案。
org.kxml2.io.KXmlParser.setInput(InputStream is, String _enc)
你可以通过_enc指定一个编码格式,如果_enc为null,则Parser会根据数据的特性自动尝试各种编码格式。由于kxml为开源项目,如果这里的处理方式需要调整,你也可以自己动手去完善它的功能。
在kxml2中也增加了对wml文件的解析,有兴趣的可以研究一下,这一部分我没有作过尝试。
JAD文件如果需要使用中文,则需要使用UTF8格式。关于如何在JAD中写入UTF8数据,可以有很多方法,可以使用一些Ultral Editor之类的文本工具,或使用一些JAD/MENIFEST生成工具。你甚至可以自己编写一个JAD/MENIFEST的生成工具,对于JavaSE平台来说,这并不是一件太难的工作,你还可以给这个工具赋予更多的功能,例如自动填写vendor和version之类的字段。
特别说明,对于NOKIA Series 40v2等设备来说,通过OTA下载Jad/Jar时,需要在服务器端为JAD添加MIME type时标明encoding为UTF8,否则即使你的JAD确实使用了UTF8格式,安装之后仍然会是乱码。
在网上有一些Unicode和UTF8互相转换的代码,是直接通过编码格式去实现的,从前文叙述的Unicode和UTF8之间的关系来看,Unicode和UTF8之间很容易互相转换,计算量也不会太大。这里就不在把这些代码贴出来了,有兴趣的可以自己找一下,或者自己写一个。
但是,实际上Unicode和UTF8这两个编码格式基本是所有设备都会支持的,直接通过String类的编解码方法互转就可以了。
如果需要使用设备不支持的编码格式,那么必然将付出一些额外的代价。例如,如果设备不支持GB2312,则你可以有如下的几个选项中选择其一:
- 在你的程序中包含一个GB2312的字符对应表;(也许GB2312你还可以接收,但是GB18030要怎么办呢?)
- 做一个代理服务器,将GB2312转解码成可以被设备支持的编码;
- 放弃GB2312,选择其他编码;