3. Java文件与编码
Java运用得如此广泛,以致于Java文件可能是采用任意一种字符编码的,如果不知道Java文件的编码标准是什么,就可能给我们的javac MyClass.java带来尴尬。所有文件的储存是都是字节的储存,在磁盘上保留的并不是文件的字
图 2-5 JVM输出
符而是先把字符编码成字节,再储存这些字节到磁盘。在读取文件(特别是文本文件)时,也是一个字节一个字节地读取以形成字节序列,所以读取文件又可能涉及到字节解码到字符,如果javac在读取java文件时,没有采取正确的decoding,就如同我们用UTF-8对GB2312字节流解码样,如果Java源程序中存在字符串常量(String str = “我是中国人”;),而这些常量又是非英文字符的话,那么javac将不能正确解码而形成乱码,甚至javac报Java文件语法错误。由于java.exe的options并没有关于encoding或decoding的选项,所以可以肯定在javac MyClass.java生成的class文件(byte code)中,字符串常量在编译Java源文件时,就被按照某种固定的编码标准编码在class文件中,按JVM(Java Virtual Machine, Java虚拟机)的规范,这个固定的编码就是UTF-8,在class文件中使用CONSTANT_Utf8_info结构表示常量字符串值。图 2-6就是用Dos命令debug.exe查看一个class文件(debug.exe不直接支持扩展名大于三个ACSII字符的文件,将Show.class拷贝到Show.txt),用红线标记出的正是字符串”我是中国人”的UTF-8编码结果。
图 2-6 用debug查看class文件
让我们再来做一些实验吧。
public class Show
{
static
{
String str = "我是中国人";
System.out.println(str);
}
public static void main(String args[]){}
}
我使用的操作系统的default charset是GBK,所以我的Show.java存储时的编码也是GBK,而javac的default decoding与系统所使用的decoding相同,也就是说我不必给javac的options添加”-encoding GBK”,javac也能对正确Show.java解码。可是如果我添加了”-encoding UTF-8”或” -encoding ISO8859-1”将会怎样呢,你可能已经知道了一些结果,如图 7-2,也许你在那里面发现了眼熟的东西,但我要说的并不是和图 2-5里面相同的乱码,为什么不看看图 2-1呢,但请不要把两个乱码字符串进行比较,因为它们本来就是不正确的字符串,对本来就不正确的东西进行比较,很多时候都是没有意义的,你根本就不能确认它们是什么,即使它们看起来是显示相同的字符串。
图 2-7对javac Show.java采取不同的解码
先不要着急,你可以先思考一下我说的东西,再进行下面的一些实验。
用记事本打开Show.java,然后用同名另存它,不过在另存时编码选择“UTF-8”(Win 2000及更高版本的Windows中的记事本,都可以这么做)。你已经知道了,我们现在该用javac –encoding UTF-8 Show.java命令行了,可是我们又遇到尴尬了,如图 2-8所示
图 2-8编译报错——我们又一次尴尬了
javac报出了我的语法错误,这可有些让人着实想不通了,找来找去都找不到错误出处,是的,你找不到,不管你是用记事本打开这个Show.java还是用MS的InterDev中的VJ++都找不出错误所在,一切都好好的,报错所指的第一行中,根本就不存在那两个字符。还好一切都逃不过debug.exe的眼睛,原来Show.java文件的前三个字节是0xEF、0xBB和0xBF,虽然不懂它的意思,但还是可以理解的,因为在未打开文件前,Windows不知道它所要处理的文件是用什么编码的,这不像在Internet上,我们可以从信息的附加信息中知道信息所使用的编码,而Windows却不能(也许它应该这么做了),大概是为了标识该文件是用UTF-8编码的吧,系统只好在文件前加上了那三个字节(我们实在无法就凭着几个字节就能判断或确定出它是用什么编码编码的,XML中不也是要指定encoding的么)。可这下javac就不同意了(也许这种做法是MS自己想出来的吧),它仍将文件的前三个字节当作有效数据而不是特殊的标识,这下就错了。用debug把前面那三个字节删除(见图 2 –11)就行了,但当我们用GBK或GB2312解码时,javac报语法错误了,这是应该的;用ISO8859-1虽通过了编译,却又出现了乱码,这也是应该的。如图2-9
图 2-9 Show. java是用UTF-8编码的
我们还可以得出一个结论的:如果Java文件中不存在非英文字符的字符串常量,我们是有理由不去关心javac的options中的-encoding了,不管这个Java类在将来是否会去处理非英文字符,因为我们已经完全生成了正确的class,剩下的有关编码或decoding的事,为什么不交给它去做呢,说到这里,我们有必要再做一个实验。
4. 文件的编码
这个实验是用段Java小程序读取中文文件的,不过在这之前,你最好先看看这几个类的Java Document吧,java.io.FileInputStream, java.io.InputStreamReader, java.io.BufferedReader,我从Sun的《JavaTM 2 SDK, Standard Edition Documentation Version 1.4.0》中摘抄了一点很令我们振奋的Document:
An InputStreamReader is a bridge from byte streams to character streams: It reads bytes and decodes them into characters using a specified charset. The charset that it uses may be specified by name or may be given explicitly, or the platform's default charset may be accepted.
Each invocation of one of an InputStreamReader's read() methods may cause one or more bytes to be read from the underlying byte-input stream. To enable the efficient conversion of bytes to characters, more bytes may be read ahead from the underlying stream than are necessary to satisfy the current read operation.
…
public int read()
throws IOException
Read a single character.
通过InputStreamReader来读取文件或其他输入流,我们已经不直接读取到byte了,而我们所读取到的char也是InputStreamReader使用“自以为正确的解码”来解码字节流(type stream)所得的结果,因为如果我们在构造InputStreamReader对象时,没有使用参数charset来指定字节流的编码(编码字符流时所采用的encoding),它将使用缺省的编码(encoding)来作为解码(decoding);OutputStreamWriter有着相同的机制却执行着相反的动作。Utf_8File.java的源代码:
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.UnsupportedEncodingException;
public class Utf_8File
{
public static void main(String args[])
throws FileNotFoundException,IOException,UnsupportedEncodingException
{
if(args.length < 3){
System.out.println("cmd encoding decoding file");
return;
}
FileInputStream fis = new FileInputStream(args[2]);
InputStreamReader isr = new InputStreamReader(fis, args[0]);
BufferedReader br = new BufferedReader(isr);
String str;
System.out.println("File content:");
while((str = br.readLine()) != null)
System.out.println(str);
br.close();
File f = new File(args[2]);
fis = new FileInputStream(args[2]);
byte bs[] = new byte[(int)f.length()];
int b, index = 0;
while((b = fis.read()) != -1)
bs[index++] = (byte)b;
fis.close();
System.out.println("File content:");
System.out.print(new String(bs, args[1]));
}
}
用记事本编辑一个Show.txt文件,里面只有五个汉字:我是中国人,保存的时候用UTF-8编码,如图 2-10,请注意我的每一个命令行。
不错,第一次读取Show.txt时出现了乱码,都是文件前面那三个讨厌的流氓字节惹的祸,我们用debug把前面那三个字节删除,如图 2-11,第二次就好好的了。除此之外我们好像并没有发现乱码,但你也会发现“java Utf_8File utf-8 gb2312 Show.txt”的第二个“File content:”后什么也没有,我们用GB2312解码UTF-8编码的字节流是失败了(我并没有说对其他的UTF-8编码字节流用GB2312解码也不会得到什么字符)。
图 2-10 Java程序读取Show.txt
图 2-11 再见了,你们这些小流氓
你很细心,发现了图 2-10中的那个MalformedInputException,这是可以解释的,正是因为“java Utf_8File utf-8 gb2312 Show.txt”的第二个“File content:”后什么也没有,说过的,我们在这里没有得到任何字符,也许在其他地方可能会得到字符,但那是乱码(Malformed Character)。
图 2-12 我们的确收到了这个Exception
现在,你也可以明白为什么System.out和System.in可以乖乖地干活了吧,也可以知道我们以往不考虑这些东西好像也没出错的原因了吧,JVM用缺省的encoding/decoding帮我们做好了这些。那好,为什么不用GB2312编码另存Show.txt试试。
现在我们可以直面图 2-1所遇的尴尬了。
5. JSP文件与编码
是的,我的JSP文件的编码是GB2312,如果我使用的Tomcat编译这个JSP文件没有使用GB2312作为decoding,又或是discomfiture$jsp被编译时,没有使用GB2312作为decoding,那么含非英文字符的常量字符串将不会被正确编译,也就会出现了乱码,因为我们可以肯定System.out是不会把正确有字符输出错误的。其实问题没有这么多,JSP引擎(JSP engine)在将JSP文件编译成Java文件后,javac编译这个Java文件时所用的encoding参数是UTF-8,也就是说JSP引擎所生成discomfiture$jsp.javap,而这个文件的存储时的编码(encoding)是UTF-8就行了,而我们所在乎的就只是JSP引擎编译JSP文件所使用的编码。
先让JSP引擎正确编译discomfiture.jsp,我们得看看discomfiture$jsp.java到底是不是这么回事,图 2-13.a是用记事本打开的discomfiture$jsp.java的拷贝,图 2-13.b和2-13.c都是用debug查看discomfiture$jsp.java的拷贝,记事本和debug都是很可爱的小东西,我们可以一直信任她们。
图 2-13.a这儿没有乱码
图 2-13.b这儿没有流氓
图 2-13.c我是中国人让JSP$jsp.java采用固定的UTF-8编码是很明智的,因为我们可以很容易控制JSP文件的存储编码,而又可以轻松地把各种存储的编码告诉给JSP引擎,同时也完全照顾到了英文字符和非英文字符的利益,也许UTF-8使用起来较复杂,但JSP$jsp.java仅仅是一个temp,我们应该可以容忍的。
是的,我们可以很容易把JSP文件的编码告诉给JSP引擎,这样在编译JSP文件时,含非英文字符的常量字符串就不会再被编译错了。
图 2-14 海纳百川
JSP文件中的page指令中的pageEncoding属性,就把JSP文件的编码告诉给了JSP引擎,比如:
<%@ page pageEncoding=”gb2312” %>
page指令中的属性都是可选属性,pageEnoding的默认值是contentType中的charset的值,所以如果我们使用了:
<%@ page contentType=”text/html;charset=gb2312” %>
而没有明确指出pageEncoding,则pageEncoding的值也是GB2312了,所以我们常常没有在意这个属性,JSP文件中的含非英文字符的常量字符串也没有编译错。如果连这个属性也没有设置,那可有点糟糕,contentType的默认值是text/html;charset=ISO8859-1,我们的JSP文件的编码也成了ISO8859-1,所以就出来了图 2-1。可那时候浏览器为什么乖乖的呢?让我们看看”我是中国人”的流程吧,图 2-15
图 2-15 常量字符串流程图
JSP引擎对一个已知编码的JSP文件如何处理对我们来说可以当作透明的,我们也完全信任它会正确处理的。我们的discomfiture.jsp文件被以GBK字节流保存着,常量字符串”我是中国人”,被编码成:
0xCE0xD20xCA0xC70xD60xD00xB90xFA0xC80xCB
当JSP引擎读取这个文件的字节流时,因为我们并没有指定pageEncoding或contentType的charset,JSP文件的编码被认为是ISO8859-1,则JSP引擎也用ISO8859-1对这些字节解码。按照ISO8859-1的标准,每个字节被解码成一个数值相等的字符,这时JSP引擎认为这个常量字符串(注意Java采用的是Unicode字符集)是:
\u00CE\u00D2\u00CA\u00C7\u00D6\u00D0\u00B9\u00FA\u00C8\u00CB
一直到Servlet discomfiture$jsp的执行时,都是如此。当Servlet把这个字符串用JVM在服务器本地输出时,被按照缺省GBK解码,然后给底层操作系统输出GBK字节流,这就乱码了。我们可以用下面这段JSP代码证明这个推测:
<%-- discomfiture2.jsp --%>
<%
String str = "我是中国人";
System.out.println(str);
char chs[] = str.toCharArray();
for(int i = 0; i < chs.length; i++)
{
System.out.print(Integer.toHexString((int)chs[i]));
System.out.print(" ");
}
out.println(str);
%>
服务器端输出如图 2-16
图 2-16 字符串中每个字符的Unicode码
所以我们在discomfiture1.jsp中直接给字符串中的每个字符用Unicode码赋值,Servlet中也就是正确的字符串”我是中国人”了,也能正确输出到System.out。当我们没有设JSP的page指令中的contentType时,Servlet输出到网络的字节流按缺省的编码ISO8859-1编码,这种编码方式直接丢弃Unicode字符的高位为0的字节,将低位字节写入输出流中(当高位不为0时,不能正确编码则将字符’\u3f’写入字节流中,所以我们得到了字符’?’),则在客户端,我们又得到了字节流:
0xCE0xD20xCA0xC70xD60xD00xB90xFA0xC80xCB
在浏览器输出这些字节流所代表的字符串(浏览器输出的当然是字符不会是字节了)时,被按照系统缺省编码方式编码,我们又得到了字符串”我是中国人”,其实这是一种错误与错误的耦合。现在我们也就不难理解图 2-3的乱码——五个’?’了。