工作中经常遇到 java 编码问题,由于缺乏研究,总是无法给出确切的答案,这个周末在网上查了一些资料,在此做些汇总。
问题一:在 java 中读取文件时应该采用什么编码?
Java 读取文件的方式总体可以分为两类:按字节读取和按字符读取。按字节读取就是采用 InputStream.read() 方法来读取字节,然后保存到一个 byte[] 数组中,最后经常用 new String(byte[]); 把字节数组转换成 String 。在最后一步隐藏了一个编码的细节, new String(byte[]); 会使用操作系统默认的字符集来解码字节数组,中文操作系统就是 GBK 。而我们从输入流里读取的字节很可能就不是 GBK 编码的,因为从输入流里读取的字节编码取决于被读取的文件自身的编码。举个例子:我们在 D: 盘新建一个名为 demo.txt 的文件,写入 ” 我们。 ” ,并保存。此时 demo.txt 编码是 ANSI ,中文操作系统下就是 GBK 。此时我们用输入字节流读取该文件所得到的字节就是使用 GBK 方式编码的字节。那么我们最终 new String(byte[]); 时采用平台默认的 GBK 来编码成 String 也是没有问题的 ( 字节编码和默认解码一致 ) 。试想一下,如果在保存 demo.txt 文件时,我们选择 UTF-8 编码,那么该文件的编码就不在是 ANSI 了,而变成了 UTF-8 。仍然采用输入字节流来读取,那么此时读取的字节和上一次就不一样了,这次的字节是 UTF-8 编码的字节。两次的字节显然不一样,一个很明显的区别就是: GBK 每个汉字两个字节,而 UTF-8 每个汉字三个字节。如何我们最后还使用 new String(byte[]); 来构造 String 对象,则会出现乱码,原因很简单,因为构造时采用的默认解码 GBK ,而我们的字节是 UTF-8 字节。正确的办法就是使用 new String(byte[],”UTF-8”); 来构造 String 对象。此时我们的字节编码和构造使用的解码是一致的,不会出现乱码问题了。
说完字节输入流,再来说说字节输出流。
我们知道如果采用字节输出流把字节输出到某个文件,我们是无法指定生成文件的编码的 ( 假设文件以前不存在 ) ,那么生成的文件是什么编码的呢?经过测试发现,其实这取决于写入的字节编码格式。比如以下代码:
OutputStream out = new FileOutputStream("d:\\demo.txt");
out.write(" 我们 ".getBytes());
getBytes() 会采用操作系统默认的字符集来编码字节,这里就是 GBK ,所以我们写入 demo.txt 文件的是 GBK 编码的字节。那么这个文件的编码就是 GBK 。如果稍微修改一下程序: out.write(" 我们 ".getBytes(“UTF-8”)); 此时我们写入的字节就是 UTF-8 的,那么 demo.txt 文件编码就是 UTF-8 。这里还有一点,如果把 ” 我们 ” 换成 123 或 abc 之类的 ascii 码字符,那么无论是采用 getBytes() 或者 getBytes(“UTF-8”) 那么生成的文件都将是 GBK 编码的。
这里可以总结一下, InputStream 中的字节编码取决文件本身的编码,而 OutputStream 生成文件的编码取决于字节的编码。
下面说说采用字符输入流来读取文件。
首先,我们需要理解一下字符流。其实字符流可以看做是一种包装流,它的底层还是采用字节流来读取字节,然后它使用指定的编码方式将读取字节解码为字符。说起字符流,不得不提的就是 InputStreamReader 。以下是 java api 对它的说明: InputStreamReader 是字节流通向字符流的桥梁:它使用指定的 charset 读取字节并将其 解码为字符 。它使用的字符集可以由名称指定或显式给定,否则可能接受平台默认的字符集。说到这里其实很明白了, InputStreamReader 在底层还是采用字节流来读取字节,读取字节后它需要一个编码格式来解码读取的字节,如果我们在构造 InputStreamReader 没有传入编码方式,那么会采用操作系统默认的 GBK 来解码读取的字节。还用上面 demo.txt 的例子,假设 demo.txt 编码方式为 GBK ,我们使用如下代码来读取文件:
InputStreamReader in = new InputStreamReader(new FileInputStream(“demo.txt”));
那么我们读取不会产生乱码,因为文件采用 GBK 编码,所以读出的字节也是 GBK 编码的,而 InputStreamReader 默认采用解码也是 GBK 。如果把 demo.txt 编码方式换成 UTF-8, 那么我们采用这种方式读取就会产生乱码。这是因为字节编码 (UTF-8) 和我们的解码编码 (GBK) 造成的。解决办法如下:
InputStreamReader in = new InputStreamReader(new FileInputStream(“demo.txt”),”UTF-8”);
给 InputStreamReader 指定解码编码,这样二者统一就不会出现乱码了。
下面说说字符输出流。
字符输出流的原理和字符输入流的原理一样,也可以看做是包装流,其底层还是采用字节输出流来写文件。只是字符输出流根据指定的编码将字符转换为字节的。字符输出流的主要类是: OutputStreamWriter 。 Java api 解释如下: OutputStreamWriter 是字符流通向字节流的桥梁:使用指定的 charset 将要向其写入的字符编码为字节。它使用的字符集可以由名称指定或显式给定,否则可能接受平台默认的字符集。说的很明白了,它需要一个编码将写入的字符转换为字节,如果没有指定则采用 GBK 编码,那么输出的字节都将是 GBK 编码,生成的文件也是 GBK 编码的。如果采用以下方式构造 OutputStreamWriter :
OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(“dd.txt”),”UTF-8”);
那么写入的字符将被编码为 UTF-8 的字节 , 生成的文件也将是 UTF-8 格式的。
问题二: 既然读文件要使用和文件编码一致的编码,那么 javac 编译文件也需要读取文件,它使用什么编码呢?
这个问题从来就没想过,也从没当做是什么问题。正是因为问题一而引发的思考,其实这里还是有东西可以挖掘的。下面分三种情况来探讨,这三种情况也是我们常用的编译 java 源文件的方法。
1.javac 在控制台编译 java 类文件。
通常我们手动建立一个 java 文件 Demo.java ,并保存。此时 Demo.java 文件的编码为 ANSI, 中文操作系统下就是 GBK. 然后使用 javac 命令来编译该源文件。 ”javac Demo.java” 。 Javac 也需要读取 java 文件,那么 javac 是使用什么编码来解码我们读取的字节呢?其实 javac 采用了操作系统默认的 GBK 编码解码我们读取的字节,这个编码正好也是 Demo.java 文件的编码,二者一致,所以不会出现乱码情况。让我们来做点手脚,在保存 Demo.java 文件时,我们选择 UTF-8 保存。此时 Demo.java 文件编码就是 UTF-8 了。我们再使用 ”javac Demo.java” 来编译,如果 Demo.java 里含有中文字符,此时控制台会出现警告信息,也出现了乱码。究其原因,就是因为 javac 采用了 GBK 编码解码我们读取的字节。因为我们的字节是 UTF-8 编码的,所以会出现乱码。如果不信的话你可以自己试试。那么解决办法呢?解决办法就是使用 javac 的 encoding 参数来制定我们的解码编码。如下: javac -encoding UTF-8 Demo.java 。 这里我们指定了使用 UTF-8 来解码读取的字节,由于这个编码和 Demo.java 文件编码一致,所以不会出现乱码情况了。
2.Eclipse 中编译 java 文件。
我习惯把 Eclipse 的编码设置成 UTF-8 。那么每个项目中的 java 源文件的编码就是 UTF-8 。这样编译也从没有问题,也没有出现过乱码。正是因为这样才掩盖了使用 javac 可能出现的乱码。那么 Eclipse 是如何正确编译文件编码为 UTF-8 的 java 源文件的呢?唯一的解释就是 Eclipse 自动识别了我们 java 源文件的文件编码,然后采取了正确的 encoding 参数来编译我们的 java 源文件。功劳都归功于 IDE 的强大了。
3. 使用 Ant 来编译 java 文件。
Ant 也是我常用的编译 java 文件的工具。首先,必须知道 Ant 在后台其实也是采用 javac 来编译 java 源文件的,那么可想而知, 1 会出现的问题在 Ant 中也会存在。如果我们使用 Ant 来编译 UTF-8 编码的 java 源文件,并且不指定如何编码,那么也会出现乱码的情况。所以 Ant 的编译命令 <javac> 有一个属性 ” encoding” 允许我们指定编码,如果我们要编译源文件编码为 UTF-8 的 java 文件,那么我们的命令应该如下:
<javac destdir="${classes}" target="1.4" source="1.4" deprecation="off" debug="on" debuglevel="lines,vars,source" optimize="off" encoding="UTF-8" >
指定了编码也就相当于 ”javac –encoding” 了,所以不会出现乱码了。
问题三: tomcat 中编译 jsp 的情况。
这个话题也是由问题二引出的。既然 javac 编译 java 源文件需要采用正确的编码,那么 tomcat 编译 jsp 时也要读取文件,此时 tomcat 采用什么编码来读取文件?会出现乱码情况吗?下面我们来分析。
我们通常会在 jsp 开头写上如下代码:
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
我常常不写 pageEncoding 这个属于,也不明白它的作用,但是不写也没出现过乱码情况。其实这个属性就是告诉 tomcat 采用什么编码来读取 jsp 文件的。它应该和 jsp 文件本身的编码一致。比如我们新建个 jsp 文件,设置文件编码为 GBK, 那么此时我们的 pageEncoding 应该设置为 GBK, 这样我们写入文件的字符就是 GBK 编码的, tomcat 读取文件时采用也是 GBK 编码,所以能保证正确的解码读取的字节。不会出现乱码。如果把 pageEncoding 设置为 UTF-8 ,那么读取 jsp 文件过程中转码就出现了乱码。上面说我常常不写 pageEncoding 这个属性,但是也没出现过乱码,这是怎么回事呢?那是因为如果没有 pageEncoding 属性, tomcat 会采用 contentType 中 charset 编码来读取 jsp 文件,我的 jsp 文件编码通常设置为 UTF-8,contentType 的 charset 也设置为 UTF-8, 这样 tomcat 使用 UTF-8 编码来解码读取的 jsp 文件,二者编码一致也不会出现乱码。这只是 contentType 中 charset 的一个作用,它还有两个作用,后面再说。可能有人会问:如果我既不设置 pageEncoding 属性,也不设置 contentType 的 charset 属性,那么 tomcat 会采取什么编码来解码读取的 jsp 文件呢?答案是 iso-8859-1 ,这是 tomcat 读取文件采用的默认编码,如果用这种编码来读取文件显然会出现乱码。
问题四:输出。
问题二和问题三分析的过程其实就是从源文件 à class 文件过程中的转码情况。最终的 class 文件都是以 unicode 编码的,我们前面所做的工作就是把各种不同的编码转换为 unicode 编码,比如从 GBK 转换为 unicode, 从 UTF-8 转换为 unicode 。因为只有采用正确的编码来转码才能保证不出现乱码。 Jvm 在运行时其内部都是采用 unicode 编码的,其实在输出时,又会做一次编码的转换。让我们分两种情况来讨论。
1.java 中采用 Sysout.out.println 输出。
比如: Sysout.out.println(“ 我们 ”) 。经过正确的解码后 ” 我们 ” 是 unicode 保存在内存中的,但是在向标准输出 ( 控制台 ) 输出时, jvm 又做了一次转码,它会采用操作系统默认编码 ( 中文操作系统是 GBK) ,将内存中的 unicode 编码转换为 GBK 编码,然后输出到控制台。因为我们操作系统是中文系统,所以往终端显示设备上打印字符时使用的也是 GBK 编码。因为终端的编码无法手动改变,所以这个过程对我们来说是透明的,只要编译时能正确转码,最终的输出都将是正确的,不会出现乱码。在 Eclipse 中可以设置控制台的字符编码,具体位置在 Run Configuration 对话框的 Common 标签里 , 我们可以试着设置为 UTF-8, 此时的输出就是乱码了。因为输出时是采用 GBK 编码的,而显示却是使用 UTF-8 ,编码不同,所以出现乱码。
2.jsp 中使用 out.println() 输出到客户端浏览器。
Jsp 编译成 class 后,如果输出到客户端,也有个转码的过程。 Java 会采用操作系统默认的编码来转码,那么 tomcat 采用什么编码来转码呢?其实 tomcat 是根据 <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> 中 contentType 的 charset 参数来转码的, contentType 用来设置 tomcat 往浏览器发送 HTML 内容所使用的编码。 Tomcat 根据这个编码来转码内存中的 unicode 。经过转码后 tomcat 输出到客户端的字符编码就是 utf-8 了。那么浏览器怎么知道采取什么编码格式来显示接收到的内容呢?这就是 contentType 的 charset 属性的第三个作用了:这个编码会在 HTTP 响应头中指定以通知浏览器。浏览器使用 http 响应头的 contentType 的 charset 属性来显示接收到的内容。
总结一下 contentType charset 的三个作用:
1). 在没有 pageEncoding 属性时, tomcat 使用它来解码读取的 jsp 文件。
2).tomcat 向客户端输出时,使用它来编码发送的内容。
3). 通知浏览器,应该以什么编码来显示接收到的内容。
为了能更好的理解上面所说的解码和转码过程,我们举一个例子。
新建一个 index.jsp 文件,该文件编码为 GBK, 在 jsp 开头我们写上如下代码:
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="GBK"%>
这里的 charset 和 pageEncoding 不同,但是也不会出现乱码,我来解释一下。首先 tomcat 读取 jsp 内容,并根据 pageEncoding 指定的 GBK 编码将读取的 GBK 字节解码并转换为 unicode 字节码保存在 class 文件中。然后 tomcat 在输出时 (out.println()) 使用 charset 属性将内存中的 unicode 转换为 utf-8 编码,并在响应头中通知浏览器,浏览器以 utf-8 显示接收到的内容。整个过程没有一次转码错误,所以就不会出现乱码情况。
问题五: Properties 和 ResourceBundle 使用的解码编码。
以上两个是我们常用的类,他们在读取文件过程中并不允许我们指定解码编码,那么它们采取什么解码方式呢?查看源码后发现都是采用 iso-8859-1 编码来解码的。这样的话我们也不难理解我们写的 properties 文件为什么都是 iso-8859-1 的了。因为采取任何一个别的编码都将产生乱码。因为 iso-8859-1 编码是没
有中文的,所以我们输入的中文要转换为 unicode ,通常我们使用插件来完成,也可以使用 jdk 自带的 native2ascii 工具。