本文介绍JavaME中文编码的相关问题,这个问题一度是互联网上的开发者们讨论的热门话题。本文整理和综合了网上众多相关内容,尽可能的为开发者提供一个全面、系统的认识。
2.3 UTF-8
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最大为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付出更多。
3.1 编解码方法
说到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,
3.2 检查设备的编码支持情况
一个最直接的获取编码支持的方法是使用System.getProperty(“microedition.encoding”),可以得到设备的默认的字符编码,以NOKIA设备为例,得到的属性值为ISO8859-1。
然而,通过这个方法的意义并不大。首先,它只能获取到一个编码格式,而一般设备都会支持很多种编码规范;其次,这个属性的数值与虚拟机的实现有很大关系,同样以NOKIA S40v2为例,不论设备的目标市场使用什么语言,这个属性统一为ISO8859-1[参考资料5],显然ISO8859-1对于中文来说是毫无意义的。
再回过头来看看上一节中的两个方法,它们都会抛出一个UnsupportedEncodingException。利用这一点,我们可以自己来做一个设备支持编码规范情况的测试。
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的规范中并没有给出严格的定义。所以在实际测试的过程中需要充分考虑到这个情况。
4.1 RMS存储的中文问题
这个问题完全可以使用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");
4.2 从Resource文件中读取中文
大概可以分为两种情况,一是这个文件遵循某种特定的格式,例如RPG游戏的关卡文件,其中包含地图、事件和对话等数据,文件由特定的程序生成,为自有格式。这种情况基本上可以使用与上一小节中同样的方式来解决。关键是,存储和读取的过程遵从同样的数据和编码格式。
另一种情况是txt文件,可能通过一些文本编辑工具生成。Txt文件中常见的编码格式有Unicode、UTF8、Unicode Big Endian等。在我们读取txt文件之前,最先要确认的就是这个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包时,压缩后的文件大小可能很接近。
4.3通过网络读取中文
和前面描述的一样,避免乱码的关键同样是保证编解码使用同样的格式,也就是客户端与服务器段保持同样的字符编码。
对于socket连接来说,传输的内容可以使用自定义的数据格式,所以处理的方式完全和前面两节是相同的,甚至可以向http学习,在数据的开头约定字符编码。
对于http连接,在http数据头中已经约定了编码格式,使用这个编码格式解码即可。另外一个很常见的问题,就是从xml文件中解析中文。对于这一点,在kxml2中已经有了很好的解决方案。
org.kxml2.io.KXmlParser.setInput(InputStream is, String _enc)
你可以通过_enc指定一个编码格式,如果_enc为null,则Parser会根据数据的特性自动尝试各种编码格式。由于kxml为开源项目,如果这里的处理方式需要调整,你也可以自己动手去完善它的功能。
在kxml2中也增加了对wml文件的解析,有兴趣的可以研究一下,这一部分我没有作过尝试。