全面分析Java相关的“编码”问题
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
全面分析Java相关的“编码”问题
本文说明
Java程序员遇到的一个很头痛的问题就是编码问题。一旦开发与系统平台及网络相关的程序而需要传输中文字符时,若不能正确掌握编码的知识,将频频出现乱码问题而扰乱开发计划。目前已经存在大量的关于编码问题的讲解。个人觉得这些文档主要是教“怎样解决目前问题”而非“为什么有这个问题”。希望本文能够帮助读者从根本上了解编码原理,以及软件开发过程中出现乱码的原因,而非仅仅是解决某个具体问题。
字符集与编码标准
字符集UNICODE
从概念上讲,字符集并不完全等同与编码标准,这两个概念的区别很模糊。(未经过文献证实,只是以掌握的知识这么认为)。大家都清楚Java使用的是UNICODE字符集。请注意这里使用的是“字符集”关键字。下面以UNICODE为例浅谈字符集与编码标准。
UNICODE是一套字符集而非编码标准。UNICODE字符集并非一直不变,目前流行使用的版本是使用2个字节来存储理论数量为256x256=65535(个)的字符集。两个字节能表示的范围也就是这个数字,以16进制表示就是0x0000-0xFFFF。这65535(个)已经足够收录目前世界上主要语言的大多数字符了,保证日常通信是没问题的。最新的UNICODE标准已经可以支持几百万个字符了,当然随之而来的则是一个字符占用的字节数将更大。
在目前使用的JRE版本中,一个中文字符是使用两个字节的内存空间来存储的。在Java 中测试UNICODE的相关功能是非常容易的:
首先是查看一个中文字符的UNICODE编码值(特别注意,这里指的是字符集中一个字符所代表的数字,它是存放在内存中的)。运行如下代码将可以得到:
char ch1 = '汉' ;
int nch1 = (int)ch1 ;
System.out.println("汉:" + Integer.toHexString(nch1));
运行结果为:汉:6c49。
也就是说“汉”这个字符在内存中实际上是占用了两个字节的空间,一个字节存放的是“6C”而另一个字节存放的是“49”。这个十六进制数“0x6c49”所代表的数字则是在UNICODE 字符集标准中“汉”这个字对应的数字值。
Java中提供了对“转码”相当好的支持,使用的时候也非常方便。请运行下面的代码:String str = “\u6c49” ;
System.out.println("6C49 --> " + str);
运行结果:6C49 --> 汉。
“\U”是将十六进制数转化成字符的转义字符。它只对它后面紧跟上的四个字符起作用。比如说:(‘a’的UNICODE数字编码是61)
String str = “\u61” ; //这句代码是通不过编译的,”\u后面至少要4位十六进制的数字字符”
String str = “\ugf61” ; //代码无法通过编译,提示不正确的UNICODE编码。
String str = “\u0061”; //代码能够运行,打印str的结果为”a” 。
在JRE的bin目录下有一个native2ascii.exe程序,它可以提供对中文字符的转码,转化成上述的\uXXXX的格式。因为这种格式的字符串可以在完全不支持中文的环境中存储和传输,并且也可以被Java程序轻松的还原(String str = “\u6c49” ;这样就还原了,够简单了)。实际上手工编写一个native2ascii.exe的功能也是很简单的,前面已经介绍了其中的原理了。
Java中UNICODE字符集是表示在内存中的字符的编码,是和String类紧密相关的。一旦一个字符串建立,这些字符在内存中所存放的十六进制数就是根据UNICODE而来。在不需要写入文件和网络传输的情况下它的编码都是统一的。换句话说String类在处理字符的时候会将对象自身存储的“字节数组”以UNICODE来分析,两两字节来读取,将两个字节作为一组当成一个字符来处理。(注意这里)
这种不需要转换编码的字符串处理过程是程序员非常希望见到的。但实际上Java系统的运行其实到处都存在转码的处理。因为不可能数据只存在于内存中吧,只在内存中处理就没任何意义了,用户需要对数据进行保存,读取,打印,输入等等操作,每一样操作都是存在“转码”处理的,只是一般我们不会去关心,JRE已经帮你做好了很多事。
下面进入下一个重点知识,JRE与操作系统字符编码转换。
Windows操作系统默认支持的是GBK,它是一种编码标准。GBK我没深入研究过,它肯定是一种编码标准,因为它的编码可以写入文件。但是不是字符集笔者也不曾深入研究。我们不讨论它的编码原理,只需要知道它是和UNICODE不一样的,也就是说同一个字符在它们之间存放的十六进制数是不一样的。
JRE运行在WINDOWS中。我们从键盘输入的字符是以windows默认的编码标准接收的,也就是你输入的任何东西先是进入到操作系统又才会进入JRE。操作系统读取数据的时候内
存中存放的是操作系统的默认支持的编码标准。这种编码格式不是JRE默认支持的。所以数据再进入JRE的时候就需要一次转码。
java.nio.charset.Charset.defaultCharset().name()方法可以得到当前操作系统默认使用的编码标准。JRE与操作系统之间进行数据通信时,在未特别指定的情况下都以这个编码进行转化,实际上就是UNICODE和这个方法返回的结果的那个编码进行转换。
通常情况下,当JRE读取控制台输入的字符时不需要进行编码指定,因为控制台接收数据后所存放的编码不会被用户更改,所以JRE只要用系统默认的编码进行转化就可以正确处理。(这个过程是:操作系统中存在一个字符串,它是以GBK编码的。而JRE需要将它读到JRE的内存里头,就需要通过JRE里面已经编写好了的GBK->UNICODE算法进行转化。String 类中提供了转化的方法,详细请看ng.String类的多种构造方法)。
当处理的是文件的时候就复杂一点了,因为文件中字符串的编码并不肯定是系统默认的编码。这个时候用户在读取的时候可以指定某一种编码来读取,使用InputStreamReader类,它有设置编码的构造方法。
由于JRE封装好了UNICODE与系统平台之间的编码转换,所以对于不经常使用编码的人员来说,编码问题就不重要了。
UTF-8编码
Java中经常使用的编码标准莫过于“UTF-8”了。它是编码标准而非单单是字符集,它也不是UNICODE。UNICODE中是以固定长存放字符的,而UTF-8是变长的。这其中又有什么关系呢?其实UTF-8和GBK、GB2312等等属于同一类,只是一种编码标准而已。它的功能是将内存中的字符保存到硬盘或者发送到网络传输线的时候使用的编码。这里需要注意理解一点,UNICODE字符集所用来表示字符的编码是“不能”直接写入硬盘和发送当网络传输线上去的。也就是说“6c49”这个4个十六进制字符所代表的字符是“汉”,但如果将“汉”这个字写入到文件,无论以哪种编码格式写入,在文件里保存的二进制编码中绝对不是6c49所表示的二进制编码。(以上为个人理解和测试的结果)。
内存中的“汉”这个字,所占用的两个字节里面存放的是“6c”和“49”。如果要以UTF-8格式写入到文件中,它的字节数组将是什么呢?下面通过测试得到这个字节数组的值。(具体这些编码标准什么编码的不是本文讨论的问题,因为它怎么编其实并不直接影响乱码的出现,而是编码之间的转化才导致了乱码)。
String str = "汉" ;
byte [] str_utf8_bytes = str.getBytes("UTF-8");