版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明
http://www.chedong.com/tech/hello_unicode.html
关键词:linux java mutlibyte encoding locale i18n i10n chinese iso-8859-1 gb2312 big5 gbk unicode
内容摘要:
不知道你有没有这样的感受:为什么php很少有乱码问题而用java做web应用却这么麻烦呢?为什么在google上能用简体中文查到繁体中文,甚至日文的结果?而且用google的时候发现它居然能自动根据我使用浏览器的语言选择自动调出中文界面?
很多国际化应用的让我理解了这么一个道理:unicode是为更方便的做国际化应用设计的,而java核心的字符是基于unicode的,这一机制为应用提供了对中文“字”的控制(而不是字节)。但如果不仔细理解其中的规范,这种自由反而会成为累赘,从而导致更多的乱码问题:
- 关于字符集的一些基本概念;
- 试验1:显示系统的环境设置和支持的编码方式;
- 试验2:系统缺省编码方式对java应用的输入输出影响;
- 试验3:在web应用中输出和输出中的字符集问题;
关于字符集的准备知识:
iso-8859-1 gb2312 big5 gbk gb18030 unicode 为什么会有这么多字符集编码方式?
注意:以下说明不是严格定义,一些比喻仅作为方便理解使用。
假设一个字符就是棋盘上的一个棋子,有其固定的坐标,如果需要区别所有的字符,就需要有足够的棋格容纳不同的“字符”。
英文和欧洲其他语言的单字节字符集(singlebyte charsets):
首先对于iso-8859系列的字符集都想象成一个:2^8 = 16 * 16 = 256个格子的棋盘,这样所有的西文字符(英文)用这样一个16×16的坐标系就基本可以覆盖全了。而英文实际上只用其中小于128(/x80)的部分就够了。利用大于128部分的空间的不同定义规则形成了真对其他欧洲语言的扩展字符集:iso-8859-2 iso-8859-4等……
| iso-8859-1 | iso-8859-7 | 其他语言 | ||||||||||||
|
|
|
对于亚洲语言来说:汉字这么多,用这么一个256格的小棋盘肯定放不下,所以要区别成千上万的汉字解决办法就是用2个字节(坐标)来定位一个“字”在棋盘上的位置,将以上规则做一个扩展:
- 如果第1个字符是小于128(/x80)的仍和英文字符集编码方式保持兼容;
- 如果第1个字符是大于128(/x80)的,就当成是汉字的第1个字节,这个自己和后面紧跟的1个字节组成一个汉字;
其结果相当于在位于128以上的小棋格里每个小棋格又划分出了一个16×16的小棋盘。这样一个棋盘中的格子数(可能容纳的字符数)就变成了128 + 128 * 256。按照类似的方式有了简体中文的gb2312标准,繁体中文的big5字符集和日文的sjis字符集等,gb2312字符集包含大约有六仟多个常用简体汉字。
| 简体中文 | 日文sjis | 繁体中文 | ||||||||||||||||||||||||||||||
|
|
|
由此可以看出,所有这些从ascii扩展式的编码方式中:英文部分都是兼容的,但扩展部分的编码方式是不兼容的,虽然很多字在3种体系中写法一致(比如“中文”这2个字)但在相应字符集中的坐标不一致,所以gb2312编写的页面用big5看就变得面目全非了。而且有时候经常在浏览其他非英语国家的页面时(比如包含有德语的人名时)经常出现奇怪的汉字,其实就是扩展位的编码冲突造成的。
我把gbk和gb18030理解成一个小unicode:gbk字符集是gb2312的扩展(k),gbk里大约有贰万玖仟多个字符,除了保持和gb2312兼容外,繁体中文字,甚至连日文的假名字符也能显示。而gb18030-2000则是一个更复杂的字符集,采用变长字节的编码方式,能够支持更多的字符。关于汉字的编码方式比较详细的定义规范可以参考:
http://www.unihan.com.cn/cjk/ana17.htm
ascii(英文) ==> 西欧文字 ==> 东欧字符集(俄文,希腊语等) ==> 东亚字符集(gb2312 big5 sjis等)==> 扩展字符集gbk gb18030这个发展过程基本上也反映了字符集标准的发展过程,但这么随着时间的推移,尤其是互联网让跨语言的信息的交互变得越来越多的时候,太多多针对本地语言的编码标准的出现导致一个应用程序的国际化变得成本非常高。尤其是你要编写一个同时包含法文和简体中文的文档,这时候一般都会想到要是用一个通用的字符集能够显示所有语言的所有文字就好了,而且这样做应用也能够比较方便的国际化,为了达到这个目标,即使应用牺牲一些空间和程序效率也是非常值得的。unicode就是这样一个通用的解决方案。
unicode双字节字符集
所以你可以把unicode想象成这样:让所有的字符(包括英文)都用2个字节(2个8位)表示,这样就有了一个2^(8*2) = 256 * 256 = 65536个格子的大棋盘。在这个棋盘中,这样中(简繁)日韩(还包括越南)文字作为cjk字符集都放在一定的区位内,为了减少重复,各种语言中写法一样的字共享一个“棋格”。详细的区位见附录a
unicode:(doublebyte charsets)
| 西 | c中 | 其 | |
| 欧 | j日 | 它 | |
| 英 | k韩 | 语 | |
| 文 | 言 |
什么还要有utf-8?毕竟互联网70%以上的信息仍然是英文。如果连英文都用2个字节存取(ucs-2),空间浪费不就太多了?所谓utf-8就是这样一个为了提高英文存取效率的字符集转换格式:unicode transformation form 8-bit form。用utf-8,unicode的2字节字符用变长个(1-3个字节)表示:
- 对英文,仍然和ascii一样用1个字节表示,这个字节的值小于128(/x80);
- 对其他语言的用一个值位于128-256之间的字节开始,再加后面紧跟的2个字节表示,一个字符一共是3个字节;
因此,在应用中程序处理过程中所有字符都是16位(双字节),但在存取转换成字节流时使用utf-8格式转换,对于英文字符来说和原来用ascii方式存取时相比大小仍然是一样的,而对中文来说和原来的gb2312编码方式相比,大小为:(3字节/2字节)=1.5倍
小节:
假设英文字符集是一个16×16的棋盘,么其他语言的字符集就是把高位区重新分割的(> 128)的中等棋盘,多种字符集之间互不兼容而unicode本身就相当于一个256×256的大棋盘,通过一定规则将英文和其他所有语言的字符都包含在内。
试验1:操作系统语言环境设置对java应用缺省编码方式的影响
为了了解java应用的编码处理的机制,首先要了解操作系统对jvm缺省编码方式的影响,因此我做了一个env.java,用于打印显示不同系统下jvm的属性和系统支持的locale。程序很简单:
/*
* copyright (c) 2002 email: chedongatbigfoot.com/chedongatchedong.com
* $id: hello_unicode.html,v 1.6 2003/11/09 07:57:11 chedong exp $
*/
import java.util.*;
import java.text.*;
/**
* 目的:
* 显示环境变量和jvm的缺省属性
* 输入:无
* 输出:
* 1 支持的locale
* 2 jvm的缺省属性
*/
public class env {
/**
* main entrance
*/
public static void main(string[] args) {
system.out.println("hello, it's: " + new date());
//print available locales
locale list[] = dateformat.getavailablelocales();
system.out.println("======system available locales:======== ");
for (int i = 0; i < list.length; i++) {
system.out.println(list[i].tostring() + "/t" + list[i].getdisplayname());
}
//print jvm default properties
system.out.println("======system property======== ");
system.getproperties().list(system.out);
}
}
最需要注意的是jvm的file.encoding属性,这个属性确定了jvm的缺省的编码/解码方式:从而影响应用中所有字节流==>字符流的解码方式 ,字符流==>字节流的编码方式。
linux下的locale可以通过 lang=zh_cn; lc_all=zh_cn.gbk; export lang lc_all 设置。locale 命令可以显示系统当前的环境设置
windows的locale可以通过 控制面板==>区域设置 设置实现
| gnu/linux 2.4.x (j2se1.3.1) lang=en_us lc_all=en_us | gnu/linux 2.4.x (j2se1.3.1) lang=zh_cn lc_all=zh_cn.gbk | windows 2000(j2se1.3.0) 区域设置:中国 中文 | windows 2000(j2se1.3.0) 区域设置:英国 英文 |
hello, it's: tue jul 30 11:05:44 cst 2002 | hello, it's: tue jul 30 11:07:34 cst 2002 | hello, it's: tue jul 30 11:49:36 cst 2002 | hello, it's: tue jul 30 11:53:27 cst 2002 |
结论1:
jvm的缺省编码方式由系统的“本地语言环境”设置确定,和操作系统的类型无关。所以当设置成相同的locale时,linux和windows下的缺省编码方式是没有区别的(可以认为cp1252=iso-8859-1都是一样的西文编码方式,只包含255以下的拉丁字符),因此后面的测试2我只列出了gnu/linux下locale分别设置成zh_cn和en_us的测试结果输出。以下测试如果在windows下分别按照不同的区域和字符集设置后试验的输出是一样的。
试验2:java的输入输出过程中的字节流到字符流的转换过程
通过这个hellounicode.java程序,演示说明"hello world 世界你好"这个字符串(16个字符)在不同缺省系统编码方式下的处理效果。在编码/解码的每个步骤之后,都打印出了相应字符串每个字符(charactor)的byte值,short值和所在的unicode区间。
| lang=en_us lc_all=en_us | lang=zh_cn lc_all=zh_cn.gbk |
========testing1: write hello world to files======== | ========testing1: write hello world to files======== |
试验2的一些结论:
- 所有的应用都是按照字节流=>字符流=>字节流方式进行的处理的:
byte_stream ==[input decoding]==> unicode_char_stream ==[output encoding]==> byte_stream; - 在java字节流到字符流(或者反之)都是含有隐含的解码处理的(缺省是按照系统缺省编码方式);
- 最早的字节流解码过程从javac的代码编译就开始了;
- java中的字符character存储单位是双字节的unicode;
试验3:web应用中的输入输出中的编码问题:java是为做国际化应用设计的,servlet应根据浏览器语言设置自动切换字符集配置
首先一个概念:即使是基于java的web应用,在服务器和客户端之间传递的仍然是字节流,比如我从一个中文客户端的浏览器表单中提交“世界你好”这4个中文字到服务器时:首先浏览器按照gbk方式编码成字节流ca c0 bd e7 c4 e3 ba c3,然后8个字节按照urlencoding的规范转成:%ca%c0%bd%e7%c4%e3%ba%c3,服务器端的servlet接收到请求后应该按什么解码处理,输出时又应该按什么方式编码行字节流呢?在目前的servlet的规范中,如果不指定的话通过web提交时的输入servletrequest和输出时的servletresponse缺省都是iso-8859-1方式编/码解码的(注意,这里的编码/解码方式是和操作系统环境中的语言环境是无关的)。因此,即使服务器操作系统的语言环境是中文,上面输入的请求仍然按英文解码成8个unicode字符,输出时仍按照英文再编码成8个字节,虽然这样在浏览器端如果设置是中文能够正确显示,但实际上读写的是“字节”,正确的方式是应该根据客户端浏览器设置servletrequest和servletresponse用相应语言的编码方式进行输入解码/输入编码,hellounicodeservlet.java就是这样一个监测客户端浏览器语言设置的例子:
当根据浏览器的头信息中的"accept-language"为zh-cn(中文)时,设置请求的解码方式和输出的字符集编码方式使用gbk:
//auto detect broswer's languages输出为:
string clientlanguage = req.getheader("accept-language");
//for simplied chinese
if ( clientlanguage.equals("zh-cn") ) {
req.setcharacterencoding("gbk");
res.setcontenttype("text/html; charset=gbk");
}
'世界你好' length=4
servletrequest's charset encoding = gbk
servletresponse's charset encoding = gbk
char[0]='世' byte=22 /u16 short=19990 /u4e16 cjk_unified_ideographs
char[1]='界' byte=76 /u4c short=30028 /u754c cjk_unified_ideographs
char[2]='你' byte=96 /u60 short=20320 /u4f60 cjk_unified_ideographs
char[3]='好' byte=125 /u7d short=22909 /u597d cjk_unified_ideographs
再做一个试验:把程序开头部分的浏览器自动检测功能注释掉,再次的输出结果就是和目前很多应用一样其实是按iso-8859-1方式解码/编码的“字节应用”了:
'世界你好' length=8虽然这样的输出结果如果在浏览器中设置用中文字符集也能正确显示,但实际上处理的已经是“字节”而不是处理中文“字符”了。servletrequest 和 servletresponse 缺省使用iso-8859-1字符集解码/编码的具体定义请参考:
servletrequest's charset encoding = null
servletresponse's charset encoding = iso-8859-1
char[0]='? byte=-54 /uffffffca short=202 /uca latin_1_supplement
char[1]='? byte=-64 /uffffffc0 short=192 /uc0 latin_1_supplement
char[2]='? byte=-67 /uffffffbd short=189 /ubd latin_1_supplement
char[3]='? byte=-25 /uffffffe7 short=231 /ue7 latin_1_supplement
char[4]='? byte=-60 /uffffffc4 short=196 /uc4 latin_1_supplement
char[5]='? byte=-29 /uffffffe3 short=227 /ue3 latin_1_supplement
char[6]='? byte=-70 /uffffffba short=186 /uba latin_1_supplement
char[7]='? byte=-61 /uffffffc3 short=195 /uc3 latin_1_supplement
http://java.sun.com/products/servlet/2.3/javadoc/javax/servlet/servletrequest.html#setcharacterencoding(java.lang.string)
http://java.sun.com/products/servlet/2.3/javadoc/javax/servlet/servletresponse.html#setcontenttype()
以前能够配置让一个web应用能够在gbk方式编码的中文windows2000服务器上和按iso-8859-1方式编码的gnu/linux上都能够正确的显示中文一直让我迷惑了很久。我仔细想了一下,后来终于想明白了,在一个国际化的应用中:servletrequest和servletresponse的编码/解码方式的确不应该根据服务器设置成固定的字符集,而应该是面向客户端语言环境进行输入/输出编码方式的自适应。一个按照国际化规范设计的web应用中:
- 在servlet的源代码中尽量不要有中文:因为在mvc模式中,servlet主要是控制器(c)的角色,因此,应该通过resourcebundle机制由servlet控制转向到相应的显示器(jsp或者xslt)中,所以应该将与本地界面语言相关的界面显示的部分从servlet和后台的模块中完全剥离出来,放到相应的resourcebundle文件中或者xslt文件中。这样源程序里完全是英文,编译时完全不需要考虑字符集的问题。
如果servlet实在需要包含中文,则需要设置应用服务器的javac编译选项,加上-encoding选项成系统缺省的字符集,如果把用中文编写的字符按照英文方式解码编译,然后再按照英文方式输出,虽然结果表面正确,实际上都成了面向“字节”编程。 - 在servlet层,应该像google搜索引擎那样,设计成能够根据客户端浏览器的语言环境自适应输出,为了判断浏览器的语言servlet中应该有类似以下的代码:
public void doget (httpservletrequest req, httpservletresponse res)
throws servletexception, ioexception {
//从http请求的头信息中获取客户端的语言设置
string clientlanguage = req.getheader("accept-language");
//简体中文浏览器
if ( clientlanguage.equals("zh-cn") ) {
req.setcharacterencoding("gbk");
res.setcontenttype("text/html; charset=gbk");
}
//繁体中文浏览器
else if ( clientlanguage.equals("zh-tw") ) {
req.setcharacterencoding("big5");
res.setcontenttype("text/html; charset=big5");
}
//日文浏览器
else if ( clientlanguage.equals("jp") ) {
req.setcharacterencoding("sjis");
res.setcontenttype("text/html; charset=sjis");
}
//缺省认为是英文浏览器
else {
req.setcharacterencoding("iso-8859-1");
res.setcontenttype("text/html; charset=iso-8859-1");
}
...
//设置好request的解码方式和response的编码方式后,进行后续的操作。
//比如再转向到helloworld.gbk.jsp helloworld.big5.jsp helloworld.jis.jsp等
}
而servlet缺省将字符集设置为iso-8859-1也许是标准制定者认为英文的浏览器占大多数吧(而且按照iso-8859-1方式输出界面往往也是正确的)。
结论:
过以上几个java试验程序得出的
闽公网安备 35060202000074号