Java作为支持多平台的高级程序设计语言自然要支持多种编码方式才能满足程序设计的需要。但是在处理中文&其他编码之间的转换问题时往往出现各种问题,另程序员大伤脑筋。本文着重阐述了Java中文与Unicode编码之间进行相互转化的机理&方法,以求抛砖引玉。
关键字:约定:本文中的编码(encoding)和字符集(charset)概念相同
一、Appetite
在进行详细的编码转换原理阐述之前,我们要作两件事情:
1。首先检查操作系统用的语言。以Windows 2003 Server为例,可以在“控制面板”中的“区域和语言设置”中选择你的国家、语言,还有你的操作系统必须支持的语言。国籍&语言的设定会影响JRE的判断情况。也许适当的设定能够帮你解决不少Java语言编码的问题。
2。更新新版本的JDK。因为新版本的JDK往往能够更好的支持新的特性,达到良好的语言迟迟效果。例如JDK5.0就已经更形了JDK1.2中的很多语言问题。
二、正餐
2.1 Unicode编码
2.1.1 Unicode——Java默认的编码
毫无疑问,Unicode作为容纳全球所有语言字符的超级字符集,是Java的首选字符集。Unicode使用两个字节作为编码方式,总共容纳有6万多个字符。因为使用16位进行字符编码,所以也称为UTF-16。然而即使这样,UTF-16也并不能充分囊括所有全世界正在使用或者曾经使用的字符,所以必须对其进行扩充。于是后来的Unicode版本已经扩充到了1,112,064个字符,这种规模已经相当大了。但是这样仍然不能满足Unicode在世界上的需求,所以必须进行必要的扩充。相比预Unicode1.0,后来的2.0版本已经支持扩展字符了,但是并没有真正的加入扩展字符集,这种状况一直持续到了Unicode3.1版,才第一次在Unicode中引入了扩展字符集。但是Unicode的发展脚步并没有停滞,后来出现了Unicode4.0 标准,而这也正好是现在Java5.0版所必须而且已经提供支持的字符集。
显然“对增补字符的支持也可能会成为东亚市场的一个普遍商业要求。*应用程序会需要这些增补字符,以正确表示一些包含罕见中文字符的姓名。出版应用程序可能会需要这些增补字符,以表示所有的古代字符和变体字符。中国*要求支持 GB18030(一种对整个 Unicode 字符集进行编码的字符编码标准),因此,如果是 Unicode 3.1 版或更新版本,则将包括增补字符。*标准 CNS-11643 包含的许多字符在 Unicode 3.1 中列为增补字符。香港*定义了一种针对粤语的字符集,其中的一些字符是 Unicode 中的增补字符。最后,日本的一些供应商正计划利用增补字符空间中大量的专用空间收入 50,000 多个日文汉字字符变体,以便从其专有系统迁移至基于 Java 平台的解决方案。
因此,Java 平台不仅需要支持增补字符,而且必须使应用程序能够方便地做到这一点。由于增补字符打破了 Java 编程语言的基础设计构想,而且可能要求对编程模型进行根本性的修改,因此,Java Community Process 召集了一个专家组,以期找到一个适当的解决方案。该小组被称为 JSR-204 专家组,使用 Unicode 增补字符支持的 Java 技术规范请求的编号。从技术上来说,该专家组的决定仅适用于 J2SE 平台,但是由于 Java 2 平台企业版 (J2EE) 处于 J2SE 平台的最上层,因此它可以直接受益,我们期望 Java 2 平台袖珍版 (J2ME) 的配置也采用相同的设计方法。”
UTF-16的编码方式:
UTF-16 使用一个或两个未分配的 16 位代码单元的序列对 Unicode 代码点进行编码。值 0x0000 至 0xFFFF 编码为一个相同值的 16 位单元。增补字符编码为两个代码单元,第一个单元来自于高代理范围(0xD800 至 0xDBFF),第二个单元来自于低代理范围(U+DC00 至 U+DFFF)。这在概念上可能看起来类似于多字节编码,但是其中有一个重要区别:值 0xD800 至 0xDFFF 保留用于 UTF-16;没有这些值分配字符作为代码点。这意味着,对于一个字符串中的每个单独的代码单元,软件可以识别是否该代码单元表示某个单单元字符,或者是否该代码单元是某个双单元字符的第一个或第二单元。这相当于某些传统的多字节字符编码来说是一个显著的改进,在传统的多字节字符编码中,字节值 0x41 既可能表示字母“A”,也可能是一个双字节字符的第二个字节。
2.1.2 节省空间的UTF-8
“如果我只能吃一块巧克力,我绝对不会买上一箱子的巧克力。”
是的,很多时候,特别是我们在处理程序的时候,所使用的并非是所有的Unicode字符,而仅仅是他们其中很小的一个部分,确切的说,这个部分不会比 ASCII多上多少。但是因为使用UTF-16,却不得不为此付出很多的存储空间来存储这些字符,这是一种可耻的浪费。因此,为了便于节省空间,无论是在存储或者传输过程中,如果你只使用到了英文或者拉丁文,那么只需要8位来表示字符就足够了。这就是UTF-8的设计思想。但是,如果是在汉字或者亚洲语言使用频率很高的地方,UTF-16依然将是首选。
但是值得注意的是,因为Unicode本身一直在进行版本更新,UTF-8当然也并非一成不变。对于经修改的过得UTF-8编码,在某些Java API的调用中会出现种种问题,特别是要注意在开发包含增补字符的文本与UTF-8进行转换的时候,可能会出现严重错误。
虽然Java本身对官方修订的UTF-8很熟悉,但是因为Java内部含有一套使用规范编码的机制,因此实际上,Java在使用UTF-8的时候,就并非使用的是Unicode的UTF-8,而是一种叫做“Java modified UTF-8”(经 Java 修订的 UTF-8)或(错误地)直接称为“UTF-8”。而在J2SE5.0种,这种编码被统称为“modified UTF-8”(经修订的 UTF-8)。
“经修订的 UTF-8 和标准 UTF-8 之间之所以不兼容,其原因有两点。其一,经修订的 UTF-8 将字符 U+0000 表示为双字节序列 0xC0 0x80,而标准 UTF-8 使用单字节值 0x0。其二,经修订的 UTF-8 通过对其 UTF-16 表示法的两个代理代码单元单独进行编码表示增补字符 。每个代理代码单元由三个字节来表示,共有六个字节。而标准 UTF-8 使用单个四字节序列表示整个字符。Java 虚拟机及其附带的接口(如 Java 本机接口、多种工具接口或 Java 类文件)在
java.io.DataInput
和 DataOutput
接口和类中使用经修订的 UTF-8 实现或使用这些接口和类 ,并进行序列化。Java 本机接口提供与经修订的 UTF-8 之间进行转换的例程。而标准 UTF-8 由String
类、java.io.InputStreamReader
和OutputStreamWriter
类、java.nio.charset
设施 (facility) 以及许多其上层的 API 提供支持。
由于经修订的 UTF-8 与标准的 UTF-8 不兼容,因此切勿同时使用这两种版本的编码。经修订的 UTF-8 只能与上述的 Java 接口配合使用。在任何其他情况下,尤其对于可能来自非基于 Java 平台的软件的或可能通过其编译的数据流,必须使用标准的 UTF-8。需要使用标准的 UTF-8 时,则不能使用 Java 本机接口例程与经修订的 UTF-8 进行转换。”
UTF-8的编码方式:
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 字符集)。这些字节值永远不会表示其他代码点,这一特性使 UTF-8 可以很方便地在软件中将特殊的含义赋予某些 ASCII 字符。
2.1.3 同胞兄弟——UTF32
如果要问在Unicode家族谁的肚量最大,毫无疑问的是UTF-32。因为采用32位编码方式,所以会使得他的容量特别大!因为会有2的32次方个字符!同样的,会带来相应的问题,就是UTF-32的空间浪费的也比较严重。所以,比般情况下很少使用这种编码。
UTF-32的编码方式:
UTF-32 即将每一个 Unicode 代码点表示为相同值的 32 位整数。很明显,它是内部处理最方便的表达方式,但是,如果作为一般字符串表达方式,则要消耗更多的内存。
2.1.4 三种编码方式的比较
Unicode 代码点 |
U+0041
|
U+00DF
|
U+6771
|
U+10400
|
表示字形 |
|
|
|
|
UTF-32 代码单元 |
|
|
|
|
UTF-16 代码单元 |
|
|
|
|
UTF-8 代码单元 |
|
|
|
|
更多的信息可以参见:
关于 Unicode 的编码,参见“The Unicode Standard, Version 3.0”一书(Addison-Wesley 出版)。
关于 UTF-8 编码,参见“Java I/O”一书的 399 页(O'Reilly 出版)。
关于 Java Class File 的格式与 Constant Pool,参见“Java Virtual Machine”一书(O'Reilly出版)。
2.2 Unicode与中文相互转化的问题来源2.2.1 识别你的文件编码虽然Java能够在其内部支持Unicode,但是我们的操作系统并非这样。如果是比较老的windows98 简体中文版,我们只能使用GB2312编码。当我们运行程序的时候,字符串是OS支持的编码,在送进JRE之后,JRE会根据当前操作系统所使用字符集的情况,将字符串转换为unicode进行处理,处理之后,再把他们转化为系统能够识别的字符集中的字符,送出JRE到OS。如果想要知道你的系统到底使用什么样的字符集与字符打交道,可以使用如下代码片断得到字符集名称:
String enc = System.getProperty"file.encoding"); System.out.println(enc); |
可能会得到下列字符集的名称:
GB2313:这是简体中文的标准。
GB18083:这是中文的扩展字符集。
HZ:同样是一种中文标准。
Big5:这是繁体中文标准。
CNS11643:*的官方标准繁体中文编码。
Cp937:繁体中文加上 6204 个使用者自定的字符
Cp948:繁体中文版 IBM OS/2 用的编码方式。
Cp964:繁体中文版 IBM AIX 用的编码方式。
EUC_TW:*的加强版 Unicode。
ISO2022CN:编码中文的一套标准。
ISO2022CN_CNS:编码中文的一套标准,繁体版,袭自 CNS11643。
MS950 或 Cp950:ASCII + Big5,用于*和香港的繁体中文 MS Windows操作系统。
2.2.2 问题来源
在Javac编译期间,也会先从OS中取得现在使用的字符集,此处设为A,之后把送入的字符串转化为Unicode编码,在编译之后再从Unicode转化为A型字符集。因此:
- 当你的操作系统国际设定错误,编译时就会产出错误的字符集编码。
- 一些比较lj的编译器会按照预先设定的字符集,而非OS所使用的字符集进行编码。
-
原是文件存盘时使用的字符集与编译器所使用的字符集无法匹配也会产生错误。
对于1和2,很好理解。对于3,例如我们使用的OS时GB2312,但是存盘时使用的编码字符集时UTF-8,这样java编译器编译文件的时候,就把UTF-8字符集当成GB2312字符集来处理,这样当然会出错。
可以使用一下代码片断来以制定的编码方式编译Java文件。
javac -encoding GB2312 TestEncoding.java然而,有时候不得不面临另外一种不幸的情况,即我们手头只有字节码文件,但是原来类中的常量中肯定存在编码问题。只有先反编译字节码文件,修改文件之后再重新编译。2.3 解决之道2.3.1 I/O神功幸好,因为Java中强大的IO接口,我们才有机会将上面的不幸化解。Java中所有的IO都是通过流来完成的。对于二进制数据的输入,InputStream是所有输入流的基类;而对于所有的二进制输出,OutputStream则是所有输出流的基类。在java.io包中的所有类几乎都与这两个类有着千丝万缕的联系。而对于各种文字数据,Writer类则是所有文字输出的祖先类,Reader也一样是所有文字输入类的祖先类。但是文字毕竟也是“binary”,为什么要单独给它们编写Reader和Writer类呢?问题在于, InputStream与OutputStream会照本宣科的解读所有输入的数据为binary,而Reader和Writer才真正的把文字当成文字,并且在需要的时候将其转换。这种需要的转换的情况存在与XXXXer类与XXXXStream作为对口时才会发生。例如,当Reader类的来源是一个InputStream时,或者Writer的数据目标是一个OutputStream时就会发生转码。由此可知,这种转换实际上发生在Reader与 InputStream或者是Writer与OutputStream的交界处。幸运的是Java强大而庞大的类库为我们提供了这种转换机制,函数原形如下:public InputStreamReader(InputStream in, String encoding) throws UnsupportedEncodingException;
public OutputStreamWriter(OutputStream out, String encoding) throws UnsupportedEncodingException;
勿庸置疑,JRE内部使用Unicode编码,但是外部环境的编码方式就不一定了。可以使用getEncoding()方法得知外界使用的编码方式。当然,如果你清楚的知道文档的来去和系统的编码方式,你可以自己指定。代码如下:FileInputStream fis = new FilInputStream(new File("hello.txt"));InputStreamReader isr = new InputStreamReader(fis,"GB2312");这样可以正确的读出文件中的字符。如果是除了RMI以外的网络连接方式进行读取,也需要使用相应的方法获取相应的输入输出流,之后代码实现类似上例。但是如果你使用的是UDP,那么情况就例外了:你必须把中文字符串转换为数组。那么RMI为什么不用进行编码转换呢?很简单,因为RMI是把 Unicode传给另外一个远程主机,所以不存在编码转换!注意:
- 如果你不能确定你的数据来源或者流向,那么最好不使用Reader和Writer,因为这样可能造成不必要的信息损失。与其这样,不如保持其二进制编码的完整性,留作以后进一步处理。
- 有时候,Reader和Writer之间进行通信的时候也可能出现编码错误,原因在于他们之间直接或者间接的使用到了I/O流,这样就可能导致编码转换时出现不统一的情况。
2.3.2 字符串与字节数组
Java的String类提供了非常丰富的功能借助与此,我们也能达到编码转换的功能。
常用的String构造函数如下:
String(byte[] bytes, int offset, int length, String charset);
String(byte[] bytes, String charset);
以上方法可以通过byte数组创建指定字符集的字符串,而下面的方法:
byte[] getBytes(String charset);
则可以将String转化为指定字符集的byte数组。
此外,还可以通过ByteArrayInputStream 或 ByteArrayOutputStream 串接到 InputStreamReader 或 OutputStreamWriter,来达到转码的目的。
2.4 其他问题和解决办法
然而Java本身涉及到编码的问题不止这些。曾经有位朋友编写一个可视花的zip应用程序。非常不幸的是,由于Java 本身的编码问题,使得他的程序在存储文件时,如果文件名是含有中文的,那么存储后的文件名不能够正确显示。这个问题困扰了他很久,虽然使用了本文中所提到过的方法,但是依然不能够解决问题。无奈,在网络上查找了相关资料,发现如果不用java自己的zip包而改用Apache的zip包问题能够得到解决。
这就提示我们说,有的时候,当你面临Java的编码问题时,不妨利用第三方的工具包尝试解决往往能够收到不错的效果。
三 总结
综上,本文讨论的Java字符编码问题的来龙去脉,并且给出了相应的解决方法。相信凭借着对问题根源的了解,Java的字符编码问题一定能够在实际中得到解决。