`

编码字符集与Java -Java World乱码问题根源之所在

    博客分类:
  • java
阅读更多

摘自:http://www.blogjava.net/ramon/archive/2006/09/24/71505.html  

 

 

本文介绍了编码字符集的概念以及Java与编码字符集之间的关系,文章的内容来自于本人工作过程中的经验积累以及网络中的相关文章介绍,如果文章中有任何纰漏欢迎读者指正,让我们共同讨论学习 J

 

 

1. 字符

字符是抽象的最小文本单位。它没有固定的形状(可能是一个字形),而且没有值。“ A ”是一个字符,“€”(德国、法国和许多其他欧洲国家通用货币的标志)也是一个字符。“中”“国”这是两个汉字字符。字符仅仅代表一个符号,没有任何实际值的意义

2. 字符集

 

字符集是字符的集合。例如,汉字字符是中国人最先发明的字符,在中文、日文、韩文和越南文的书写中使用。这也说明了字符和字符集之间的关系,字符组成字符集。

3. 编码字符集

 

编码字符集是一个字符集(有时候也被简称位字符集),它为每一个字符分配一个唯一数字。最早的编码是 iso8859-1 ,和 ascii 编码相似。但为了方便表示各种各样的语言,逐渐出现了很多标准编码。

 

 

iso8859-1: 属于单字节编码字符集,最多能表示的字符范围是 0-255 ,应用于英文系列,除了 iso8859-1 以外还有其他 iso8859 系列的编码,这些编码都是为了满足欧洲国家语言字符的需要而设计的。  

 

GB2312/GBK/ GB18030: 前面提到的 iso8859-1 最多只能表示 256 个字符,这对于汉字来说实在是有些抱歉,所以就有了现在要介绍的汉字国标码,专门用来表示汉字,是双字节编码字符集,而英文字母和 iso8859-1 一致(兼容 iso8859-1 编码)。其中 GBK 编码能够用来同时表示繁体字和简体字,而 GB2312 只能表示简体字, GBK 是兼容 GB2312 编码的。而 GB18030-2000 则是一个更复杂的字符集,采用变长字节的编码方式,能够支持更多的字符。需要注意的是中国政府要求所有在中国出售的软件必须支持 GB18030  

 

Unicode 这是最统一的编码字符集,可以用来表示所有语言的字符 ,不兼容任何前面提到的编码字符集。 Unicode 标准始终使用十六进制数字,而且在书写时在前面加上前缀“ U+ ”,所以“ A ”的编码书写为“ U+0041 注意:在 JAVA 语言中书写时应该使用转义符‘ \u ’表示,如 char charA = ‘\u0041’; 这种表示方法等与 char charA = ‘A’;

 

ASCII (英文) ==> 西欧文字 ==> 东欧字符集(俄文,希腊语等) ==> 东亚字符集( GB2312 BIG5 SJIS 等) ==> 扩展字符集 GBK GB18030 这个发展过程基本上也反映了字符集标准的发展过程,但这么随着时间的推移,尤其是互联网让跨语言的信息的交互变得越来越多的时候,太多多针对本地语言的编码标准的出现导致一个应用程序的国际化变得成本非常高 。尤其是你要编写一个同时包含法文和简体中文的文档,这时候一般都会想到要是用一个通用的字符集能够显示所有语言的所有文字就好了,而且这样做应用也能够比较方便的国际化,为了达到这个目标,即使应用牺牲一些空间和程序效率也是非常值得的。 UNICODE 就是这样一个通用的解决方案。  

 

4. Unicode 编码字符集

 

Unicode 因为必须将中、韩、日、英、法、阿拉伯……等许多国家所使用的文字都纳入,目前已经包含了六万多个字符,所以 Unicode 使用了 16 个位来为字符编码。因为 Unicode 使用了 16 位编码,所以每个字符都用 16 位来储存或传输是很自然的事,这种储存或传输的格式 称为 UTF-16 (一种 Unicode 的字符编码方案,在这里所说的 UTF-16 并不涉及增补字符的表示,本文将会在稍后介绍)。但是如果你使用到的字符都是西方字符,那么你一定不会想用 UTF-16 的格式,因为体积比 8 位的 iso8859-1 多了一倍,如此一来就必须考虑程序运行时各种字符在内存中所占空间的性能问题,这便引入了字符编码方案的概念:

字符编码方案是从一个或多个编码字符集到一个或多个固定宽度代码单元序列的映射。

 

最常用的代码单元是字节,所以可以简单的认为字符编码方案是为了告诉计算机如何将编码字符集(如 Unicode )映射到计算机可以识别的数据格式中,如字节。这种编码方案往往能够为他所对应的字符集在计算机处理时提供更为优化的空间以及性能上的解决方案。 Unicode 编码字符集有三种字符编码方案,下面将逐一介绍:  

 

l         UTF-32* 即将每一个 Unicode 编码表示为相同值的 32 位整数。很明显,它是内部处理最方便的表达方式,但是,如果作为一般字符串表达方式,则要消耗更多的内存。显而易见,对于英文字母的表示将需要多个 0 字节,仅仅因为我们需要 4 个字节 32 位来表示一个 Unicode 字符。

 

l         UTF-16 使用一个或两个未分配的 16 位代码单元的序列对 Unicode 编码进行编码。值 U+0000 U+FFFF 编码为一个相同值的 16 位单元。增补字符 * 编码为两个代码单元,第一个单元来自于高代理范围( U+D800 U+DBFF ),第二个单元来自于低代理范围( U+DC00 U+DFFF )。这在概念上可能看起来类似于多字节编码,但是其中有一个重要区别:值 U+D800 U+DFFF 保留用于 UTF-16 ;没有这些值分配字符作为代码点。这意味着,对于一个字符串中的每个单独的代码单元,软件可以识别是否该代码单元表示某个单单元字符,或者是否该代码单元是某个双单元字符的第一个或第二单元。这相当于某些传统的多字节字符编码来说是一个显著的改进,在传统的多字节字符编码中,字节值 0x41 既可能表示字母“ A ”,也可能是一个双字节字符的第二个字节。  

 

l         UTF-8 使用一至四个字节的序列对编码 Unicode 进行编码。 U+0000 U+007F 使用一个字节编码, U+0080 U+07FF 使用两个字节, U+0800 U+FFFF 使用三个字节,而 U+10000 U+10FFFF 使用四个字节。 UTF-8 设计原理为:字节值 0x00 0x7F 始终表示代码点 U+0000 U+007F Basic Latin 字符子集,它对应 ASCII 字符集)。这些字节值永远不会表示其他 Unicode 编码字符,这一特性使 UTF-8 可以很方便地在软件中将特殊的含义赋予某些 ASCII 字符。 UTF-8 的格式在编码英文时,只需要 8 位,但是中文则是 24 位,其他更加偏僻的字符才又可能是 32 位,这也是 UTF-8 最大的编码特点,可以最高效率的利用计算机空间,因为在计算机处理的时候大多数情况下还是只使用英文进行运算和处理,这也是为什么还需要 UTF-8 的主要原因,因为毕竟互联网 70 %以上的信息仍然是英文。如果连英文都用 2 个字节存取 (UCS-2) ,空间浪费不就太多了?    

 

*   UTF­-32 表示 Unicode Transformation Form 32-bit form UTF-16 UTF-8 依此类推。

 

*   Unicode 最初设计是作为一种固定宽度的 16 位字符编码。在 Java 编程语言中,基本数据类型 char 初衷是通过提供一种简单的、能够包含任何字符的数据类型来充分利用这种设计的优点。不过,现在看来, 16 位编码的所有 65,536 个字符并不能完全表示全世界所有正在使用或曾经使用的字符。于是, Unicode 标准已扩展到包含多达 1,112,064 个字符。那些超出原来的 16 位限制的字符被称作增补字符。

5. Java 与编码字符集

 

从上面的介绍我们知道了 Unicode 编码字符集可以用来表示世界上所有的语言文字。 Java 内部处理字符使用的字序方式是 Unicode ,这是一种通行全球的编码方式,他使 Java 语言能够描述世界上所有的文字。在 Java 程序中对各种字符在内存中处理是使用 Unicode UTF-8 编码方式,这也是因为 UTF-8 的特点所决定的, Class File (也就是 bytecode )中有一栏位叫做常数区( Constant Pool ),一律使用 UTF-8 为子元编码。

 

这看起来一切正常, Java 可以处理世界上所有的字符,一切都是按照秩序在运行,但是,从前面的讨论我们知道,世界上并不是仅仅只有 Unicode 编码字符集,同时存在的还有 iso8859-1 GBK 等编码字符集,就是在 Unicode 中也同样存在着 UTF-8 UTF-16 UTF-32 等多种编码,如果传入的字节编码采用的是 GB18030 ,而采用的解码方式为 UTF-8 那会有什么后果呢,看看下面的代码片段:  

public static final String TEST_RESOURCE = " 你好 ";      

public static void testEncoding() {        

    try {            

        byte[] bytes = TEST_RESOURCE.getBytes("GB18030");            

        String result = new String(bytes, " UTF-8 ");            

        System.out.println("Receive value: [" + result + "].");        

    } catch (UnsupportedEncodingException e) {            

        / / TODO Auto-generated catch block            

        e.printStackTrace();        

    }    

}

执行以上的代码片段,在我的机器( Win XP 中文版)上面得到的结果是:

Receive value: [���].

明白了吧,这就是久负盛名的乱码问题的根源,目前在市面上存在有多种编码字符集,以及编码字符集的编码方案,所以虽然在 Java 中内部是以 Unicode UTF-8 来处理各种字符的表示以及运算,但是这仅仅是在 Java 内部而以,如果 Java 程序需要和外部应用系统进行交互,比如与操作系统,数据库系统之间的交互,那么在这些交互过程中如何处理字符集的编码解码是解决好 Java 应用程序乱码问题的根源。 如果将上面的代码块修改成如下的代码块:    

public static void testEncoding() {        

    try {            

        byte[] bytes = TEST_RESOURCE.getBytes("GB18030");            

        String result = new String(bytes, " GB18030 ");            

        System.out.println("Receive value: [" + result + "].");        

    } catch (UnsupportedEncodingException e) {            

        // TODO Auto-generated catch block            

        e.printStackTrace();        

    }    

}

 

注意红色标注的地方,执行以上的代码块将会受到预期的结果:

Receive value: [ 你好 ].

 

统一字符的编码类型和解码类型 ,如此一来任何乱码问题都不会再是问题了。在网上可以搜索到 N 多的关于如何解决 J2EE 乱码问题的文章,我在这里也就不废话了,我只是想说说 Java 乱码问题的根源之所在。

 

如果你仔细想想 Java 的开发过程,原文件编写、 javac 编译、 java 执行,这每一步骤都会涉及到编码的转换过程,这个过程总是存在的,只是有的时候用默认的参数进行。

 

我们从 javac 这个命令来开始我们的分析,编译的时候,如果你不说明源文件编码方式的话, javac 编译器在读进此原始程序文件开始编译之前,会先去询问操作系统档案预设的编码方式为何。以我的操作系统 WIN XP 中文版来说, javac 会先询问 WIN XP ,得知当前的编码是用 GB18030 的方式编码。然后就可以将源文件由 GB18030 转成 Unicode 编码方式,开始进行编译。在这里就会发生一下一些编码问题:

 

 

l         如果操作系统的国籍资料设定错误,会造成 javac 编译器取得的编码信息是错误的,这里也有可能由于系统属性 file.encoding 设置错误,在我的系统中该属性为 GB18030 ,可以通过代码 System.out.println(System.getProperties()); 输出可能的系统属性。  

 

l         较差劲的编译器可能没有主动询问操作系统的编码方式,而是采用编译器预设的编码方式,当然这种情况对于目前先进的编译器来说已经不存在了,但是这确实是一种可能的原因。

 

 

l         源代码是在英文操作系统上书写采用编码 iso8859-1 ,写好以后再将源代码传递给中文操作系统进行编译,这样由于两个操作系统的编码方式不同,也会造成 javac 执行错误。  

 

明白了吧,这些问题在我们日常的代码编写过程中,往往由于默认的属性都正好能满足我们的需要,即源代码的书写以及编译都采用操作系统默认的编码方式,所以可能很多人到目前为止都没有遇见过诸如此类的问题,但是我们要知道,这些问题确实是存在的。

 

Java 编译器在执行过程中给我们提供了可选的 encoding 参数来告诉编译器该采用何种编码方式将读入的源文件转换成 Unicode 编码方式,然后再进行后续的编译工作。

javac –encoding GB18030 ….  

 

6. Inside

OK ,通过前面的介绍希望读者能够对 Java 以及各种字符编码之间的关系有个简单的了解,下面我在继续总结一下:

l         Javac 是以系统默认编码( file.encoding 系统属性)读入源文件,然后按 Unicode 进行编码的。  

l         JAVA 运行的时候, JAVA 也是采用 Unicode 编码的,为了高度利用内存空间提高效率对 Unicode 字符编码采用了 UTF-8 的方式编码,并且默认输入和输出的都是操作系统的默认编码。  

l         也就是说在 new String(bytes,encode) 中,系统认为输入的是编码为 encode 的字节流,换句话说,如果按 encode 来翻译 bytes 才能得到正确的结果;而在 new String(bytes) 中采用的就是根据 file.encoding 系统属性读入的编码方式来进行编码,同样也必须根据系统默认的编码才能得到正确的结果,这个结果最后要在 JAVA 中保存,它还是要从这个 encode 转换成 Unicode ,因为在 JAVA 中各种字符均是以 Unicode 的形式来处理的。  

l         也就是说有 bytes-->encode 字符 -->Unicode 字符的转换;而在 String.getBytes([encode]) 中,系统要做一个 Unicode 字符 -->encode 字符 -->bytes 的转换。  

 

希望通过本文的介绍能够使你对字符集编码的概念以及 Java 与字符集编码之间的关系有个清楚的认识,我相信,如果搞清楚了他们之间的关系,那个在 Java world 鼎鼎有名的乱码问题将一去不再复返了 J

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics