您的位置:首页 > 编程语言 > Java开发

Java中文乱码相关问题深入剖析

2014-03-16 02:06 435 查看
使用Java语言时,遇到中文乱码是最令人头疼的问题之一。很多从VC、C#转过来的程序员尤其不适应。鉴于目前网上大多数文章治标不治本,有必要写一篇文章来专门释疑。

1. 从Byte开始

首先看一段代码:

[java] view
plaincopy

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位。看下面的程序:

[java] view
plaincopy

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.推论是,用何种编码格式进行编码,就应该使用相同的格式进行解码。

看下面的例子:

[java] view
plaincopy

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会默认使用什么编码呢?

看下面的程序:

[java] view
plaincopy

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. 如何产生乱码

要产生乱码很简单,将一些字节以错误的编码读入,则可以产生乱码。如下:

[java] view
plaincopy

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

[java] view
plaincopy

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繁体中文处理完全攻略”一节,那么你应该知道如何避免乱码了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: