[概述]
在编程中字符编码绝对是个值得重视的问题,当读取一个文件或是得到一个输入流,你需要分析数据的编码方式、形态,以便能正确的处理、显示数据所表示的字符。
[细节]
1) 在简体中文操作系统中,从键盘输入的原始字符采用的是GBK编码方式,对应到其他操作系统,采用的应是系统默认的本地字符集。而在程序设计语言中,字符和字符串则通常是使用Unicode编码方式,这一点可以用下列代码说明(使用Java语言描述)。
int ch = System.in.read(); //从键盘输入中读取一个字节的数据
如果输入“中国”两个字符,使用上面的代码将所有的数据读取,将得到“D6 D0 B9 FA”,这写数据正是“中国”两个字符的GBK编码值。
String tmp = "中国"; //定义字符串并赋值
for(int i=0;i<tmp.length();i++)
{
//将字符串中的每个字符编码值以十六进制形式显示
System.out.println(Integer.toHexString(ch));
}
上面代码最后显示的内容是“4E2D 56FD”,而这正是“中国”两个字符的Unicode编码值。
2) 不仅是输入的原始字符采用GBK编码,屏幕输出的最终数据也要采用GBK编码,下面的代码能说明问题。
String tmp = "中国"; //定义字符串并赋值
System.out.println(tmp); //将字符串tmp输出到显示屏
代码运行后,在显示屏上显然能看到“中国”两个字符,而字符串tmp分明是采用Unicode进行编码的,是不是刚才提出的命题站不住脚呢?其实,在调用println()方法后,该方法自动的将字符串tmp的编码方式从Unicode转换成了本地编码GBK,这样才能在屏幕上正常的显示中文。如果你仍然怀疑,请继续往下看。
byte [] buf1 = tmp.getBytes("Unicode"); //将字符串tmp以Unicode编码方式储存在字节数组中
byte [] buf2 = tmp.getBytes("GB2312"); //将字符串tmp以UGBK编码方式储存在字节数组中
//在屏幕输出流中直接写字节数组
System.out.write(buf1);
System.out.write(buf2);
这样的作法将会得到什么结果呢?结果也许会令你感到惊讶,buf1的数据输出后显示为乱码,而buf2的数据输出后赫然显示为“中国”两个字符。可以把命题说得明确点:如果要在屏幕上输出汉字,那么字符的最终编码方式必须是GBK编码方式。对于数字和英文字母,以及ASCII编码集中包含的符号,字符的最终编码方式可以是ASCII,这种情况下如果使用Unicode编码,那么显示的结果会是这样“1 2 3 a b c ”,本来想显示的内容是“123abc”。显示结果字符间多了个貌似空格的字符,这是因为ASCII编码使用一个字节,Unicode编码使用两个字节,在ASCII编码转换为Unicode时候,只是单纯地在编码值前面补充一个全为0的字节,这个字节在最终显示的时候被看做是空字符NUL。
3) 在涉及网页、网络流和关系数据库方面编程的时候,字符编码总喜欢戏弄编程人员,不花心思去驯服它的结果将是得到一堆乱码。例如在读取数据数据时候,数据库中的内容是中文字符,如果数据库没有考虑到中文支持问题就很容易得到乱码。再例如程序运行的平台默认编码并非GBK,在获取GBK编码的字符数据时候,程序会将数据看作默认编码,这样也容易产生乱码。在上述情况中编写程序的时候,就应该耐心的分析数据的编码方式,合理的编写代码防止乱码。
[例子]
记得在《Java手机程序设计入门与应用》(王森 编著)一书的第13章-MIDP网络程序设计中有一段使用HTTP进行网络连接的实例代码,部分代码如下所示。
String url = "
http://127.0.0.1/test.html
";
HttpConnection hc = (HttpConnection)Connector.open(url);
DataInputStream dis = new DataInputStream(hc.openInputStream());
String content = "";
int ic;
while((ic = dis.read()) != -1)
{
content += (char)ic;
}
Form f = new Form("HTTP Test");
f.append(content);
Display.getDisplay(this).setCurrent(f);
这段代码让手机通过HTTP协议与网络中的主机进行通信,然后获得网络主机上的文件test.html并将文件内容读取到字符串变量中,最后显示到程序窗体中。如果程序这般执行的话,你会发现MIDlet显示出来中文都是乱码。作者称“之所以会有这种结果,原因在于我们的仿真器支持Unicode的缘故。”,作者的意思似乎是MIDlet将本地编码的字符数据误认为了Unicode编码的数据,因此不能正常显示,然后推荐了一种解决方法:使用ASCII形态的Unicode。
所谓ASCII形态的Unicode指的是使用ASCII编码的字符来表示Unicode编码值,反过来说就是将Unicode的编码值看做字符,再用ASCII对这些字符进行编码存放。比如“中国”这两个字符的ASCII形态的Unicode编码字符为“\u4e2d\u56fd”,0x4E2D 0x56FD 分别为“中”和“国”的Unicode编码值,将编码值作为字符,然后在前面添加“\u”标识符,以便进行还原。再对这些字符进行ASCII编码就得到了ASCII形态的Unicode编码值,最终的值为“5C 75 34 65 32 64 5C 75 35 36 66 64”,一共12个字节的数据,分别对应了“\u4e2d\u56fd”中的一个字符。使用jdk*\bin文件夹下的native2ascii.exe程序可以很方便的将一个文件转换为ASCII形态的Unicode编码。将文件test.html转换形态后,MIDlet中需要再次将ASCII形态的Unicode转换为Unicode编码,这个转换方法需要自己写,最后MIDlet中显示出来的就是正常的中文字符。
在我看来,那本书的作者没有把握住问题的真正原因,也或许是我们使用的模拟器和平台不同。如果把握住真正的原因,问题的解决方法就变得很简单了。前面说过,要在屏幕上显示出中文,字符的最终编码形式必须是GBK,在中国大陆发现的手机都能显示中文,也就说明手机中都支持GBK编码。那为什么会出现中文字符乱码的问题呢?我的理由是手机中采用的默认编码是ISO8859-1,对于从网络中读入的字符数据,在没有指明的情况下,MIDlet一律将它们看作是ISO8859-1编码的数据。而test.html的编码方式是GBK,MIDlet犯了个错误,它将GBK编码的数据误认为了ISO8859-1编码的数据,然后在显示的时候又进行了一次ISO8859-1到GBK的编码转换,这样的结果是数据遭到了破坏,显示出来的中文也就变成了乱码。
把握住了原因,解决起来就十分方便了。既然MIDlet将GBK编码的数据误认为ISO8859-1编码的数据,那么我们只要在程序中指明数据的编码方式就可以了,而不用使用“ASCII形态的Unicode”这样的舍本求末的方法。下面是解决MIDlet网络连接中文乱码问题的代码,这些代码将证明我的观点。
http://127.0.0.1/test.html;
HttpConnection hc = (HttpConnection)Connector.open(url);
byte [] buf = new byte[1024];
int len = hc.openInputStream().read(buf); //读取网络数据
String content = new String(buf,0,len,"GB2312"); //指定数据为GBK编码
Form f = new Form("HTTP Test");
f.append(content);
display.setCurrent(f);
上述代码中关键的一句是:
String content = new String(buf,0,len,"GB2312");
这句代码告诉MIDlet从网络中读取的数据使用的是GBK编码方式,然后MIDlet便能争取处理和显示这些数据。
如果将这句代码改写为:
String content = new String(buf,0,len);
或者是:
String content = new String(buf,0,len,"ISO8859-1");
都将出现同样的中文乱码现象,由此断定错误的原因是手机默认编码使用ISO8859-1,MIDlet将从网络中读取的GBK编码的数据误认成了ISO8859-1编码的数据。