世界上的各地区都有本地的语言。地区差异直接导致了语言环境的差异。在开发一个国际化程序的过程中,处理语言问题就显得很重要了。这是一个世界范围内都存在的问题,所以,java提供了世界性的解决方法。本文描述的方法是用于处理中文的,但是,推而广之,对于处理世界上其它国家和地区的语言同样适用。
汉字是双字节的。所谓双字节是指一个双字要占用两个byte的位置(即16位),分别称为高位和低位。中国规定的汉字编码为gb2312,这是强制性的,目前几乎所有的能处理中文的应用程序都支持gb2312。gb2312包括了一二级汉字和9区符号,高位从0xa1到0xfe,低位也是从0xa1到0xfe,其中,汉字的编码范围为0xb0a1到0xf7fe。
另外有一种编码,叫做gbk,但这是一份规范,不是强制的。gbk提供了20902个汉字,它兼容gb2312,编码范围为0x8140到0xfefe。gbk中的所有字符都可以一一映射到unicode 2.0。
在不久的将来,中国会颁布另一种标准:gb18030-2000(gbk2k)。它收录了藏、蒙等少数民族的字型,从根本上解决了字位不足的问题。注意:它不再是定长的。其二字节部份与gbk兼容,四字节部分是扩充的字符、字形。它的首字节和第三字节从0x81到0xfe,二字节和第四字节从0x30到0x39。
本文不打算介绍unicode,有兴趣的可以浏览“http://www.unicode.org/”查看更多的信息。unicode有一个特性:它包括了世界上所有的字符字形。所以,各个地区的语言都可以建立与unicode的映射关系,而java正是利用了这一点以达到异种语言之间的转换。
在jdk中,与中文相关的编码有:
表1 jdk中与中文相关的编码列表
| 编码名称 | 说明 |
| ascii | 7位,与ascii7相同 |
| iso8859-1 | 8-位,与 8859_1,iso-8859-1,iso_8859-1,latin1...等相同 |
| gb2312-80 | 16位,与gb2312,gb2312-1980,euc_cn,euccn,1381,cp1381, 1383, cp1383, iso2022cn,iso2022cn_gb...等相同 |
| gbk | 与ms936相同,注意:区分大小写 |
| utf8 | 与utf-8相同 |
| gb18030 | 与cp1392、1392相同,目前支持的jdk很少 |
在实际编程时,接触得比较多的是gb2312(gbk)和iso8859-1。
为什么会有“?”号
上文说过,异种语言之间的转换是通过unicode来完成的。假设有两种不同的语言a和b,转换的步骤为:先把a转化为unicode,再把unicode转化为b。
举例说明。有gb2312中有一个汉字“李”,其编码为“c0ee”,欲转化为iso8859-1编码。步骤为:先把“李”字转化为unicode,得到“674e”,再把“674e”转化为iso8859-1字符。当然,这个映射不会成功,因为iso8859-1中根本就没有与“674e”对应的字符。
当映射不成功时,问题就发生了!当从某语言向unicode转化时,如果在某语言中没有该字符,得到的将是unicode的代码“\uffffd”(“\u”表示是unicode编码,)。而从unicode向某语言转化时,如果某语言没有对应的字符,则得到的是“0x3f”(“?”)。这就是“?”的由来。
例如:把字符流buf =“0x80 0x40 0xb0 0xa1”进行new string(buf, "gb2312")操作,得到的结果是“\ufffd\u554a”,再println出来,得到的结果将是“?啊”,因为“0x80 0x40”是gbk中的字符,在gb2312中没有。
再如,把字符串string="\u00d6\u00ec\u00e9\u0046\u00bb\u00f9"进行new string (buf.getbytes("gbk"))操作,得到的结果是“3fa8aca8a6463fa8b4”,其中,“\u00d6”在“gbk”中没有对应的字符,得到“3f”,“\u00ec”对应着“a8ac”,“\u00e9”对应着“a8a6”,“0046”对应着“46”(因为这是ascii字符),“\u00bb”没找到,得到“3f”,最后,“\u00f9”对应着“a8b4”。把这个字符串println一下,得到的结果是“?ìéf?ù”。看到没?这里并不全是问号,因为gbk与unicode映射的内容中除了汉字外还有字符,本例就是最好的明证。
所以,在汉字转码时,如果发生错乱,得到的不一定都是问号噢!不过,错了终究是错了,50步和100步并没有质的差别。
或者会问:如果源字符集中有,而unicode中没有,结果会如何?回答是不知道。因为我手头没有能做这个测试的源字符集。但有一点是肯定的,那就是源字符集不够规范。在java中,如果发生这种情况,是会抛出异常的。
什么是utf
utf,是unicode text format的缩写,意为unicode文本格式。对于utf,是这样定义的:
(1)如果unicode的16位字符的头9位是0,则用一个字节表示,这个字节的首位是“0”,剩下的7位与原字符中的后7位相同,如“\u0034”(0000 0000 0011 0100),用“34” (0011 0100)表示;(与源unicode字符是相同的);
(2)如果unicode的16位字符的头5位是0,则用2个字节表示,首字节是“110”开头,后面的5位与源字符中除去头5个零后的最高5位相同;第二个字节以“10”开头,后面的6位与源字符中的低6位相同。如“\u025d”(0000 0010 0101 1101),转化后为“c99d”(1100 1001 1001 1101);
(3)如果不符合上述两个规则,则用三个字节表示。第一个字节以“1110”开头,后四位为源字符的高四位;第二个字节以“10”开头,后六位为源字符中间的六位;第三个字节以“10”开头,后六位为源字符的低六位;如“\u9da7”(1001 1101 1010 0111),转化为“e9b6a7”(1110 1001 1011 0110 1010 0111);
可以这么描述java程序中unicode与utf的关系,虽然不绝对:字符串在内存中运行时,表现为unicode代码,而当要保存到文件或其它介质中去时,用的是utf。这个转化过程是由writeutf和readutf来完成的。
好了,基础性的论述差不多了,下面进入正题。
先把这个问题想成是一个黑匣子。先看黑匣子的一级表示:
input(charseta)->process(unicode)->output(charsetb)
简单,这就是一个ipo模型,即输入、处理和输出。同样的内容要经过“从charseta到unicode再到charsetb”的转化。
再看二级表示:
sourcefile(jsp,java)->class->output
在这个图中,可以看出,输入的是jsp和java源文件,在处理过程中,以class文件为载体,然后输出。再细化到三级表示:
jsp->temp file->class->browser,os console,db
app,servlet->class->browser,os console,db
这个图就更明白了。jsp文件先生成中间的java文件,再生成class。而servlet和普通app则直接编译生成class。然后,从class再输出到浏览器、控制台或数据库等。
jsp:从源文件到class的过程
jsp的源文件是以“.jsp”结尾的文本文件。在本节中,将阐述jsp文件的解释和编译过程,并跟踪其中的中文变化。
1、jsp/servlet引擎提供的jsp转换工具(jspc)搜索jsp文件中用<%@ page contenttype ="text/html; charset=<jsp-charset>"%>中指定的charset。如果在jsp文件中未指定<jsp-charset>,则取jvm中的默认设置file.encoding,一般情况下,这个值是iso8859-1;
2、jspc用相当于“javac –encoding <jsp-charset>”的命令解释jsp文件中出现的所有字符,包括中文字符和ascii字符,然后把这些字符转换成unicode字符,再转化成utf格式,存为java文件。ascii码字符转化为unicode字符时只是简单地在前面加“00”,如“a”,转化为“\u0041”(不需要理由,unicode的码表就是这么编的)。然后,经过到utf的转换,又变回“41”了!这也就是可以使用普通文本编辑器查看由jsp生成的java文件的原因;
3、引擎用相当于“javac –encoding unicode”的命令,把java文件编译成class文件;
先看一下这些过程中中文字符的转换情况。有如下源代码:
<%@ page contenttype="text/html; charset=gb2312"%>
<html><body>
<%
string a="中文";
out.println(a);
%>
</body></html>
这段代码是在ultraedit for windows上编写的。保存后,“中文”两个字的16进制编码为“d6 d0 ce c4”(gb2312编码)。经查表,“中文”两字的unicode编码为“\u4e2d\u6587”,用 utf表示就是“e4 b8 ad e6 96 87”。打开引擎生成的由jsp文件转变而成的java文件,发现其中的“中文”两个字确实被“e4 b8 ad e6 96 87”替代了,再查看由java文件编译生成的class文件,发现结果与java文件中的完全一样。
再看jsp中指定的charset为iso-8859-1的情况。
<%@ page contenttype="text/html; charset=iso-8859-1"%>
<html><body>
<%
string a="中文";
out.println(a);
%>
</body></html>
同样,该文件是用ultraedit编写的,“中文”这两个字也是存为gb2312编码“d6 d0 ce c4”。先模拟一下生成的java文件和class文件的过程:jspc用iso-8859-1来解释“中文”,并把它映射到unicode。由于iso-8859-1是8位的,且是拉丁语系,其映射规则就是在每个字节前加“00”,所以,映射后的unicode编码应为“\u00d6\u00d0\u00ce\u00c4”,转化成utf后应该是“c3 96 c3 90 c3 8e c3 84”。好,打开文件看一下,java文件和class文件中,“中文”果然都表示为“c3 96 c3 90 c3 8e c3 84”。
如果上述代码中不指定<jsp-charset>,即把第一行写成“<%@ page contenttype="text/html" %>”,jspc会使用file.encoding的设置来解释jsp文件。在redhat 6.2上,其处理结果与指定为iso-8859-1是完全相同的。
到现在为止,已经解释了从jsp文件到class文件的转变过程中中文字符的映射过程。一句话:从“jspcharset到unicode再到utf”。下表总结了这个过程:
表2 “中文”从jsp到class的转化过程
| jsp-charset | jsp文件中 | java文件中 | class文件中 |
| gb2312 | d6 d0 ce c4(gb2312) | 从\u4e2d\u6587(unicode)到e4 b8 ad e6 96 87 (utf) | e4 b8 ad e6 96 87 (utf) |
| iso-8859-1 | d6 d0 ce c4 (gb2312) | 从\u00d6\u00d0\u00ce\u00c4 (unicode)到c3 96 c3 90 c3 8e c3 84 (utf) | c3 96 c3 90 c3 8e c3 84 (utf) |
| 无(默认=file.encoding) | 同iso-8859-1 | 同iso-8859-1 | 同iso-8859-1 |
servlet:从源文件到class的过程
servlet源文件是以“.java”结尾的文本文件。本节将讨论servlet的编译过程并跟踪其中的中文变化。
用“javac”编译servlet源文件。javac可以带“-encoding <compile-charset>”参数,意思是“用< compile-charset >中指定的编码来解释serlvet源文件”。
源文件在编译时,用<compile-charset>来解释所有字符,包括中文字符和ascii字符。然后把字符常量转变成unicode字符,最后,把unicode转变成utf。
在servlet中,还有一个地方设置输出流的charset。通常在输出结果前,调用httpservletresponse的setcontenttype方法来达到与在jsp中设置<jsp-charset>一样的效果,称之为<servlet-charset>。
注意,文中一共提到了三个变量:<jsp-charset>、<compile-charset>和<servlet-charset>。其中,jsp文件只与<jsp-charset>有关,而<compile-charset>和<servlet-charset>只与servlet有关。
看下例:
class testservlet extends httpservlet
{
public void doget(httpservletrequest req,httpservletresponse resp)
throws servletexception,java.io.ioexception
{
resp.setcontenttype("text/html; charset=gb2312");
java.io.printwriter out=resp.getwriter();
out.println("<html>");
out.println("#中文#");
out.println("</html>");
}
}
该文件也是用ultraedit for windows编写的,其中的“中文”两个字保存为“d6 d0 ce c4”(gb2312编码)。
开始编译。下表是<compile-charset>不同时,class文件中“中文”两字的十六进制码。在编译过程中,<servlet-charset>不起任何作用。<servlet-charset>只对class文件的输出产生影响,实际上是<servlet-charset>和<compile-charset>一起,达到与jsp文件中的<jsp-charset>相同的效果,因为<jsp-charset>对编译和class文件的输出都会产生影响。
表3 “中文”从servlet源文件到class的转变过程
| compile-charset | servlet源文件中 | class文件中 | 等效的unicode码 |
| gb2312 | d6 d0 ce c4 (gb2312) | e4 b8 ad e6 96 87 (utf) | \u4e2d\u6587 (在unicode中=“中文”) |
| iso-8859-1 | d6 d0 ce c4 (gb2312) | c3 96 c3 90 c3 8e c3 84 (utf) | \u00d6 \u00d0 \u00ce \u00c4 (在d6 d0 ce c4前面各加了一个00) |
| 无(默认) | d6 d0 ce c4 (gb2312) | 同iso-8859-1 | 同iso-8859-1 |
| 序号 | 步骤说明 | 结果 |
| 1 | 编写jsp源文件,且存为gb2312格式 | d6 d0 ce c4 (d6d0=中 cec4=文) |
| 2 | jspc把jsp源文件转化为临时java文件,并把字符串按照gb2312映射到unicode,并用utf格式写入java文件中 | e4 b8 ad e6 96 87 |
| 3 | 把临时java文件编译成class文件 | e4 b8 ad e6 96 87 |
| 4 | 运行时,先从class文件中用readutf读出字符串,在内存中的是unicode编码 | 4e 2d 65 87(在unicode中4e2d=中 6587=文) |
| 5 | 根据jsp-charset=gb2312把unicode转化为字节流 | d6 d0 ce c4 |
| 6 | 把字节流输出到ie中,并设置ie的编码为gb2312(作者按:这个信息隐藏在http头中) | d6 d0 ce c4 |
| 7 | ie用“简体中文”查看结果 | “中文”(正确显示) |
| 序号 | 步骤说明 | 结果 |
| 1 | 编写jsp源文件,且存为gb2312格式 | d6 d0 ce c4 (d6d0=中 cec4=文) |
| 2 | jspc把jsp源文件转化为临时java文件,并把字符串按照iso8859-1映射到unicode,并用utf格式写入java文件中 | c3 96 c3 90 c3 8e c3 84 |
| 3 | 把临时java文件编译成class文件 | c3 96 c3 90 c3 8e c3 84 |
| 4 | 运行时,先从class文件中用readutf读出字符串,在内存中的是unicode编码 | 00 d6 00 d0 00 ce 00 c4 (啥都不是!!!) |
| 5 | 根据jsp-charset=iso8859-1把unicode转化为字节流 | d6 d0 ce c4 |
| 6 | 把字节流输出到ie中,并设置ie的编码为iso8859-1(作者按:这个信息隐藏在http头中) | d6 d0 ce c4 |
| 7 | ie用“西欧字符”查看结果 | 乱码,其实是四个ascii字符,但由于大于128,所以显示出来的怪模怪样 |
| 8 | 改变ie的页面编码为“简体中文” | “中文”(正确显示) |
| 序号 | 步骤说明 | 结果 |
| 1 | 编写jsp源文件,且存为gb2312格式 | d6 d0 ce c4 (d6d0=中 cec4=文) |
| 2 | jspc把jsp源文件转化为临时java文件,并把字符串按照iso8859-1映射到unicode,并用utf格式写入java文件中 | c3 96 c3 90 c3 8e c3 84 |
| 3 | 把临时java文件编译成class文件 | c3 96 c3 90 c3 8e c3 84 |
| 4 | 运行时,先从class文件中用readutf读出字符串,在内存中的是unicode编码 | 00 d6 00 d0 00 ce 00 c4 |
| 5 | 根据jsp-charset=iso8859-1把unicode转化为字节流 | d6 d0 ce c4 |
| 6 | 把字节流输出到ie中 | d6 d0 ce c4 |
| 7 | ie用发出请求时的页面的编码查看结果 | 视情况而定。如果是简体中文,则能正确显示,否则,需执行表5中的第8步 |
| 序号 | 步骤说明 | 结果 |
| 1 | 编写servlet源文件,且存为gb2312格式 | d6 d0 ce c4 (d6d0=中 cec4=文) |
| 2 | 用javac –encoding gb2312把java源文件编译成class文件 | e4 b8 ad e6 96 87 (utf) |
| 3 | 运行时,先从class文件中用readutf读出字符串,在内存中的是unicode编码 | 4e 2d 65 87 (unicode) |
| 4 | 根据servlet-charset=gb2312把unicode转化为字节流 | d6 d0 ce c4 (gb2312) |
| 5 | 把字节流输出到ie中并设置ie的编码属性为servlet-charset=gb2312 | d6 d0 ce c4 (gb2312) |
| 6 | ie用“简体中文”查看结果 | “中文”(正确显示) |
| 序号 | 步骤说明 | 结果 |
| 1 | 编写servlet源文件,且存为gb2312格式 | d6 d0 ce c4 (d6d0=中 cec4=文) |
| 2 | 用javac –encoding iso8859-1把java源文件编译成class文件 | c3 96 c3 90 c3 8e c3 84 (utf) |
| 3 | 运行时,先从class文件中用readutf读出字符串,在内存中的是unicode编码 | 00 d6 00 d0 00 ce 00 c4 |
| 4 | 根据servlet-charset=iso8859-1把unicode转化为字节流 | d6 d0 ce c4 |
| 5 | 把字节流输出到ie中并设置ie的编码属性为servlet-charset=iso8859-1 | d6 d0 ce c4 (gb2312) |
| 6 | ie用“西欧字符”查看结果 | 乱码(原因同表5) |
| 7 | 改变ie的页面编码为“简体中文” | “中文”(正确显示) |
| 序号 | 步骤说明 | 结果 | 域 |
| 1 | 在ie中输入“中文” | d6 d0 ce c4 | ie |
| 2 | ie把字符串转变成utf,并送入传输流中 | e4 b8 ad e6 96 87 | |
| 3 | servlet接收到输入流,用readutf读取 | 4e 2d 65 87(unicode) | servlet |
| 4 | 编程者在servlet中必须把字符串根据gb2312还原为字节流 | d6 d0 ce c4 | |
| 5 | 编程者根据数据库内码iso8859-1生成新的字符串 | 00 d6 00 d0 00 ce 00 c4 | |
| 6 | 把新生成的字符串提交给jdbc | 00 d6 00 d0 00 ce 00 c4 | |
| 7 | jdbc检测到数据库内码为iso8859-1 | 00 d6 00 d0 00 ce 00 c4 | jdbc |
| 8 | jdbc把接收到的字符串按照iso8859-1生成字节流 | d6 d0 ce c4 | |
| 9 | jdbc把字节流写入数据库中 | d6 d0 ce c4 | |
| 10 | 完成数据存储工作 | d6 d0 ce c4 数据库 | |
以下是从数据库中取出数的过程 | |||
| 11 | jdbc从数据库中取出字节流 | d6 d0 ce c4 | jdbc |
| 12 | jdbc按照数据库的字符集iso8859-1生成字符串,并提交给servlet | 00 d6 00 d0 00 ce 00 c4 (unicode) | |
| 13 | servlet获得字符串 | 00 d6 00 d0 00 ce 00 c4 (unicode) | servlet |
| 14 | 编程者必须根据数据库的内码iso8859-1还原成原始字节流 | d6 d0 ce c4 | |
| 15 | 编程者必须根据客户端字符集gb2312生成新的字符串 | 4e 2d 65 87 (unicode) | |
servlet准备把字符串输出到客户端 | |||
| 16 | servlet根据<servlet-charset>生成字节流 | d6d0 ce c4 | servlet |
| 17 | servlet把字节流输出到ie中,如果已指定<servlet-charset>,还会设置ie的编码为<servlet-charset> | d6 d0 ce c4 | |
| 18 | ie根据指定的编码或默认编码查看结果 | “中文”(正确显示) | ie |
Java Asp PHP .Net XML C/C++ CGI VB Jsp J2ee J2se J2me EJB Servlet Tomcat Resin Struts Weblogic Eclipse ANT GUI JMS Web servise IDEA Webphere Hibernate Spring Jboss Applet Swing Socket Javamail Perl Ajax P2P 安全 模式 框架 测试 开源 游戏
Windows XP Windows 2000 Windows 2003 Windows Me Windows 9.x Linux UNIX 注册表 操作系统 服务器 应用服务器