“与C/C++不同,Java中的字符数据是16位无符号型数据,它表示Unicode集,而不仅仅是ASCII集”①。这是一个很好的做法,它解决了www上更多的程序设计问题,比如说低成本的国际化(International),然而用16位的字符,却带来了浪费,毕竟Java所处理的信息,绝大多数都是英文,对它们来说7位的ACSII码已经足够了,而Unicode却需要双倍的空间,所以Java的这种兼顾各种语言的做法却是与存储资源及效率的妥协。而对中国的Java程序员(特别是初级的)来说,Java采用Unicode字符,却给我们带来了尴尬甚至噩梦——Web页面上显示的不是中文,而是乱码。
一、 常见字符集简介
字符集就是字符内码到字符的表现形式之间的映射的集合。ASCII字符A是就内码0x41的表现形式,所以在很多程序语言中,字符变量和整型变量仅在一念之差。
1. ISO8859系列
ISO8859包括诸如ISO8859-1,ISO8859-2之类的一系列字符集,它们都是8位的字符集,0~0x7F仍与ASCII字符集保持兼容,大于0x7F的是各种拉丁字符或欧洲字符的扩展。
2. GB2312字符集
如果像ISO8859系列一样,大于0x7F的字符用来表示汉字,则最多表示128个,这显然不够,于是就有了GB2312标准所产生的字符集,如果当前字节(8 bit)小于0X80,则仍当它为英文字符;如果它大于等于0x80,则它和紧接着它的下一个字节构成一个汉字字符,这样,GB2312字符集可包含大约4000多个常用简体汉字和其他汉字中的特殊符号(如①㈠之类)。其他类似的汉字字符集还有GBK(GB2312的扩展),GB18030,Big5(繁,台湾省用),详细规范介绍可参考:http://www.unihan.com.cn/cjk/ana17.htm
3. Unicode字符集
Unicode字符最初是16位的(出于需要,后来增加了代用对),它和7位的US-ASCII保持兼容,MS的Windows NT/2000/XP和Sun的Java都用它作为默认的字符集,它最初是美国商务联盟的事实上的标准,它遵循国际通用字符(UCS)集标准:ISO/IEC 10646。Unicode的主要目标是提供一个“通用字符集”,这个通用字符集包括世界上所有的语言,字母和文字,所以在Unicode字符集中,不光“I”是字母,“我”也是字母,在写Java时也可以“int 我是中国人 = 0xff;”。毕竟16位的Unicode字符集最多只有216= 65536个字符,还不足以在实际应用中表示所有的字符,而且在以英文为主要信息的互联网时代,它的使用、存储与传输,都极其浪费空间,所以在此基础上出现了UTF-8(Unicode Transformation Form 8-bit form)和UTF-16这两种对Unicode字符编码的规范,在UTF-8中,属于US-ASCII中的字符,仍用一个字节表示,且和US-ASCII兼容,编码其他的字符,则用1(大于0x7F部分)到3个字节。UTF-8的变长性和复杂性,对非ASCII的字符,就不大友好了,也开始违背了Unicode的初衷。而UTF-16则是很简单的编码方式,它完全遵循Unicode标准,用16位的定长空间来表示部分Unicode字符集。关于Unicode的更多规范,请访问Unicode联盟站点:http://www.unicode.org,UTF-8和UTF-16分别定义在IETF的RFC 2279和RFC 2781中,可以通过http://www.ietf.org/rfc2279.txt或http://www.ietf.org/rfc2781.txt访问它们。
一般情况下,字符集名称是大小写不敏感的,所以GB2312也可以写作gb2312或Gb2312。
二、 乱码带来的尴尬
1. 先看一个JSP
JSP(Java Server Page)的实质还是一个Servlet,所以用JSP,也可以说明Servlet中的一些问题,就一般而言,JSP代码比Servlet代码还要简单。
我们先用JSP来做一个实验,下面的这个JSP文件中含有常量字符串“我是中国人”,看看它在浏览器的输出是否会是乱码?
<%-- discomfiture.jsp --%>
<%
String str = "我是中国人";
System.out.println(str);
out.println(str);
%>
从浏览器打开它,并没有乱码,显示的就是“我是中国人”这个字符串。先别乐,再看看服务器的输出窗口吧,如图2-1,服务器监视窗口输出了乱码(红色下划线标出)。
图 2-1 服务器窗口中输出的乱码
虽说只是在服务器端出现了乱码,而客户端浏览器是完全正确显示的,但这里很明显出了什么问题,否则都两边应该是正确的输出。
服务器端输出了乱码,说明服务器Java虚拟机(Java Virtual Machine, JVM)没有“得到”正确的字符串。为了保证JVM能够正确“得到”我们指出的含中文的常量字符串,我们可以直接用字符的Unicode内码代替字符串中的字符,就像
String str=”I am Chinese”;
用
String str = “\u0049\u0020\u0061\u006D\u0020\u0043\u0068\u006E\u0065\u0073\u0065”;
代替一样。明确给JVM指出这些字符串,是否还会出现乱码呢?要得到一个字符的Unicode内码是件很容易的事,Java和JavaScrtipt的字符都是用Unicode字符集的。先看看输出Unicode字符内码的Java程序:
public class getCode
{
public static void main(String args[])
{
char chs[] = args[0].toCharArray();
for(int i = 0; i < chs.length; i++){
System.out.println(chs[i] + " = " + (int)chs[i]);
}
System.out.println(args[0]);
}
}
编译并执行它,结果如图:
图 2-2 JVM输出
不过JavaScript用起来,怎么也比Java来得快,这里也介绍一段JavaScript代码:
<script>
var str = "我是中国人";
for(var i = 0; i < str.length; i++)
{
document.wirte(str.charAt(i) + " = " + str.charCodeAt(i) + "<br>");
}
document.write(str);
</script>
保存为HTML文件,输出如下图:
图 2-2 JavaScript在IE6.0中输出
现在替换discomfiture.jsp中的中文字符串:
<%-- discomfiture1.jsp --%>
<%
String str = "\u6211\u662F\u4E2D\u56FD\u4EBA";
System.out.println(str);
out.println(str);
%>
实验得到了希望的结果,服务器端输出窗口正确输出了字符串,可客户端浏览器却又输出了乱码,如下图:
图 2-3 JSP在IE6.0中出现了乱码
图 2-4 客户端刷新两次的服务器窗口的输出
因为直接使用的Unicode码生成的字符串,保证了在JSP生成的Servlet discomfiture1$jsp中,字符串str的值一定是“我是中国人”,而服务器窗口的输出也证实了这一点,那么也就是说在Servlet discomfiture$jsp中str的值并不是“我是中国人”,因为它在服务器窗口中输出了乱码,如图 2-1可以发现,当时输出了10个字符,即str的长度是10,并不是5。可为什么在浏览器却好好地得到了这个字符的输出呢? 先简要明白两个概念:编码与解码。
2. 编码与解码
编码(Encode)和解码(Decode)是两个相反的动作。编码是把字符按照某种映射标准(字符集),转换成字节,这时我们把执行编码动作时所采用的标准叫编码(encoding)。如我们对Unicode字符串
”我是中国人”
按照GB2312标准编码(byte bsg[] = ”我是中国人”.getBytes(“GB2312”);),就可以得到一个字节序列(bytes sequence),用十六进制的码值表示:
0xCE0xD20xCA0xC70xD60xD00xB90xFA0xC80xCB
按照UTF-8标准编码(byte bsu[] = ”我是中国人”.getBytes(“UTF-8”);),就可以得到字节序列:
0xE60x880x910xE60x980xAF0xE40xB80xAD0xE50x9B0xBD0xE40xBA0xBA
而解码则是将字节序列按照某种字符标准(解码,decoding),转换成字符串。如我们对字节序列:
0xCE0xD20xCA0xC70xD60xD00xB90xFA0xC80xCB
按照GB2312解码(new String(bsg,”GB2312”),或对字节序列:
0xE60x880x910xE60x980xAF0xE40xB80xAD0xE50x9B0xBD0xE40xBA0xBA
按照UTF-8解码(new String(bsu,”UTF-8”),均可得到字符串”我是中国人”,但是如果我们对GB2312编码的字节序列用UTF-8解码,这就乱了套,所得的字符串明显是错误的乱码。
让我们来看一个试验。
import java.io.UnsupportedEncodingException;
public class u2g
{
public static void main(String args[])throws UnsupportedEncodingException
{
String str = args[0];
char chs[] = str.toCharArray();
System.out.println("Unicode characters:");
for(int i = 0; i < chs.length; i++)
System.out.print(chs[i] + " = " + (int)chs[i] + ";");
System.out.println();
String messages[] = {
"Encodes this String into a sequence of bytes using the" +
"\nplatform's default charset.",
"Encodes this String into a sequence of bytes using gb2312.",
"Encodes this String into a sequence of bytes using utf-8."};
String encodings[] = {null,"gb2312","utf-8"};
byte bs[][] = new byte[3][];
for(int h = 0; h < messages.length; h++){
System.out.print(messages[h]);
if(encodings[h] == null)bs[h] = str.getBytes();
else bs[h] = str.getBytes(encodings[h]);
for(int l = 0; l < bs[h].length; l++){
if(l % 4 == 0)System.out.println();
System.out.print("byte[" + l + "] = " +
Integer.toHexString(bs[h][l] & 0xff) + ";");
}
System.out.println();
}
System.out.println("Decodes the sequence of bytes using corresponding encoding.");
for(int i = 0; i < bs.length; i++){
if(encodings[i] == null)System.out.println(new String(bs[i]));
else System.out.println(new String(bs[i], encodings[i]));
}
String messages1[] = {
"Decodes the sequence of bytes encoded by gb2312 into a string\nusing utf-8.",
"Decodes the sequence of bytes encoded by utf-8 into a string\nusing gb2312."};
for(int h = 0; h < 2; h ++){
System.out.println(messages1[h]);
str = new String(bs[h+1], encodings[h == 0 ? 2 : 1]);
chs = str.toCharArray();
System.out.print("Unicode characters:");
for(int i = 0; i < chs.length; i++)
{
if(i % 4 == 0)System.out.println();
System.out.print(chs[i] + " = " + (int)chs[i] + ";");
}
System.out.println();
}
System.out.println("The default encoding of system is " +
System.getProperty("file.encoding"));
}
}
JVM输出如图 2-5所示,很明显,对用UTF-8编码的字节流,用GB2312编码是彻底失败了,我们什么字符也没得到。我所使用的系统是MS Windows 2000 Server,默认字符集是GBK,这个实验也可以看出GBK兼容GB2312。