使用Java语言时,遇到中文乱码是最令人头疼的问题之一。很多从VC、C#转过来的程序员尤其不适应。鉴于目前网上大多数文章治标不治本,有必要写一篇文章来专门释疑。
1. 从Byte开始
首先看一段代码:
- public static void startWithByte() {
- byte[] unicode_b = new byte[4];
- unicode_b[0] = (byte) 0XFE;
- unicode_b[1] = (byte) 0XFF;
- unicode_b[2] = (byte) 0X4E;
- unicode_b[3] = (byte) 0X2D;
- String unicode_str;
- byte[] gbk_b = new byte[2];
- gbk_b[0]=(byte) 0XD6;
- gbk_b[1]=(byte) 0XD0;
- String gbk_str;
- byte[] utf_b=new byte[3];
- utf_b[0]=(byte)0XE4;
- utf_b[1]=(byte)0XB8;
- utf_b[2]=(byte)0XAD;
- String utf_str;
- try {
- unicode_str = new String(unicode_b, "unicode");
- System.out.println("unicode string is:"+unicode_str);
- gbk_str = new String(gbk_b,"gbk");
- System.out.println("gbk string is:"+gbk_str);
- utf_str=new String(utf_b,"utf-8");
- System.out.println("utf-8 string is:"+utf_str);
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- }
- }
运行这段代码会得到如下输出:
unicode string is:中中
gbk string is:中中
utf-8 string is:中
这说明,那三组byte数组,都存储了同一个汉字“中”。在Unicode中,使用了四个字节FE FF 4E 2D(事实上,前两个字节是用来指示字节高低位的,在Unicode中,总是使用两个字节来表示一个字符,不管是中文还是ASCII字符);在GBK中,使用了两个字节D6 D0;在UTF-8中,使用了三个字节E4 B8 AD。
需要说明的是,在文件中也是如此存储。你可以使用例如UltraEdit之类的工具,使用十六进制模式输入以上的几组字节,保存为文本文件。然后使用Java读入字节数组,再转化为String,可得到同样的结果。
2. 字节和字符
Java只处理两种信息,字节和字符。其中字节是无意义的,就是0和1的集合;而字符是有意义的,一个字符可以代表一个ASCII字符、一个汉字或者一个日本字。某些字节是可以转化为字符的,其翻译形式就是编码,UNICODE是一种编码,GBK和UTF-8也各是一种编码。所有的字符和字符串都可以翻译为字节,其翻译形式是解码,与编码对应,可以对字符串进行UNICODE、GBK或者UTF-8解码。
需要注意的是,Java程序内部总是以UNICODE的方式来理解字符。因此每个Java字符都有两个字节的长度,即16位。看下面的程序:
- public static void byteAndChar() {
- char c1 = 0X4E2D;
- System.out.println(c1);
- char c2 = 20013;
- System.out.println(c2);
- byte[] b = new byte[2];
- b[0] = 0X4E;
- b[1] = 0x2D;
- String str = null;
- try {
- str = new String(b, "unicode");
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- }
- char c3 = str.charAt(0);
- System.out.println(c3);
- String str2="中a";
- System.out.println("length of str2 is :"+str2.length());
- }
运行程序可得:
中
中
中
length of str2 is :2
因此我们可以知道,汉字“中”在java程序中的值就是20013(或者十六进制的0X4E2D),Java程序在对待“中”和ASCII的字符a时并无不同,都是看作一个Unicode字符。在计算字符串长度时也是如此。
3. Java的基本变量
了解以上基础后,再来复习一下Java的基本变量(以下表格摘自《Java编程思想》第四版)。Java语言的特点之一就是基本变量的长度在不同的平台下保持不变:
基本类型 | 大小 | 最小值 | 最大值 | 包装器类型 |
boolean | - | - | - | Boolean |
char | 16-bit | Unicode0 | Unicode216-1 | Character |
byte | 8-bit | -128 | 127 | Byte |
short | 16-bit | -215 | 215-1 | Short |
int | 32-bit | -231 | 231-1 | Integer |
long | 64-bit | -263 | 263-1 | Long |
float | 32-bit | IEEE754 | IEEE754 | Float |
double | 64-bit | IEEE754 | IEEE754 | Double |
void | - | - | - | Void |
从以上表格中我们发现,byte永远是8位,一个字节;char是16位,两个字节,正好放下一个Unicode字符;short也是16位,因此在很多时候可以将short和char进行无损的转换。
4. 字符串String
首先陈述几个事实:
1.String是由一个char的原生数组封装而成,String中存储的是一个个的char变量;
2.由于每个char中存储了一个双字节的Unicode字符,因此String中所存储的都是Unicode字符;
3.String和byte[]可以相互转换,编码使用new String(byte[],charset);解码使用String.getBytes();
4.若在String中出现乱码,只有一个原因,就是String中的Unicode字符没有被正确理解,或者其中的字符不是Unicode字符;
5.推论是,用何种编码格式进行编码,就应该使用相同的格式进行解码。
看下面的例子:
- public static void stringExample() {
- String str = "中国ABC";
- try {
- byte[] unicode_b = str.getBytes("unicode");
- for (byte b : unicode_b) {
- System.out.format("%02X ", b);
- }
- System.out.println();
- String unicode_str = new String(unicode_b, "unicode");
- System.out.println(unicode_str);
- byte[] utf_b = str.getBytes("utf-8");
- for (byte b : utf_b) {
- System.out.format("%02X ", b);
- }
- System.out.println();
- String utf_str = new String(utf_b, "utf-8");
- System.out.println(utf_str);
- byte[] gbk_b = str.getBytes("gbk");
- for (byte b : gbk_b) {
- System.out.format("%02X ", b);
- }
- System.out.println();
- String gbk_str = new String(gbk_b, "gbk");
- System.out.println(gbk_str);
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- }
- }
运行结果:
FE FF 4E 2D 56 FD 00 41 00 42 00 43
中国ABC
E4 B8 AD E5 9B BD 41 42 43
中国ABC
D6 D0 B9 FA 41 42 43
中国ABC
由结果可知,Unicode中每个字符由两个字节存储,
中=4E 2D
国=56 FD
A=41 00
B=42 00
C=43 00
UTF-8中,汉字由多个字节(2,3,4个)存储,但ASCII字符由一个字节存储:
中=E4 B8 AD
国=E5 9B BD
A=41
B=42
C=43
GBK中,汉字由两个字节存储,ASCII字符由一个字节存储:
中=D6 D0
国=B9 FA
A=41
B=42
C=43
5. Java如何使用编码
好了,现在我们知道从字节到字符进行转换时,需要使用编码,那么Java会默认使用什么编码呢?
看下面的程序:
- public static void getEncoding(){
- String encodeName = System.getProperty("file.encoding");
- System.out.println("System file encoding is " + encodeName);
- }
输出为:
System file encoding is UTF-8
这说明我当前的这个java程序默认使用的是UTF-8编码。值得一提的是,中文Windows操作系统多使用GBK编码。但是,在Eclipse等开发工具中可以设定默认编码,比如我的Eclipse的默认编码设定为UTF-8。如此一来,此程序在Eclipse中运行时使用UTF-8编码,但是编译为class文件在Windows中运行时则使用GBK编码。
因此,为了避免混乱,最好在任何编码解码过程中都明确指定编码格式。
6. 如何产生乱码
要产生乱码很简单,将一些字节以错误的编码读入,则可以产生乱码。如下:
- public static void getEncodingError() {
- String str = "产生乱码";
- byte[] bs;
- try {
- bs = str.getBytes("unicode");
- String strError = new String(bs, "utf-8");
- System.out.println(strError);
- } catch (UnsupportedEncodingException e) {
- e.printStackTrace();
- }
- }
输出如下:
��N�u_Nqx_
7. 源文件的乱码问题
对于用来编译的java源文件来讲,同样存在乱码问题。例如A.java是用unicode格式来存储的,那么在一个默认编码为GBK的系统上使用javac命令进行编译就会报错。例如一个源代码为A.java
- publicclass A {
- String s = "Unicode编码中文";
- }
如果在GBK平台下编译,则报错信息如下:
A.java:1:错误: 编码GBK的不可映射字符
?......
如果要正确编译,则必须使用-encoding参数。例如:
Javac –encoding unicode A.java
8. 字节流和字符流
Java程序员都非常熟悉字节流和字符流,那么在使用这两种流的时候,要注意哪些编码相关的问题呢?
首先是,若程序并不需要编码解码时,尽量使用字节流。例如拷贝一个带有中文字符的文件,虽然其中有中文字符,但是拷贝文件并不需要编码解码,只需要将字节原封不动的拷贝过去即可,因此使用字节流即可。
其次,正如《java夜未眠》一书中提到的一样,转码只发生在Reader/InputStream的交界处和Writer/OutputStream的交界处,所以是有InputStreamReader和OutputStreamWriter两个类负责的。程序员需要在使用这两个类时小心处理编码与解码的类型。
9. 如何避免乱码
相信如果你已经认真读完此文,另外也读完了我推荐的《java夜未眠》中的“java繁体中文处理完全攻略”一节,那么你应该知道如何避免乱码了。