[字符集与编码 2] 编码 解码 乱码

编码规则

如果你已经阅读了JavaHipster 1中references提到的两篇文章,你应该明白:从字符集到编码规则的过程,实际上就是从字符集-->到编号-->到编码的过程。
关于编码规则,主要关注两个方面:

  • 所遵循的字符集
  • 存储方式:使用几个字节来存储字符

ASCII编码规则

  • 遵循ASCII字符集,共含有255个字符(有很多字符是保留字符)
  • 使用1个字节存储(定长)

ISO8859-1编码规则

  • 遵循ISO8859-1字符集,共含有255个字符。
  • 使用1个字节存储(定长)
  • ISO8859-1兼容ASCII,也就是说,对于同样的字符,ASCII与ISO8859-1对应的编号(也成为code point码点)都是一样的。

GB2312编码规则

  • 遵循GB2312字符集,支持常见的中文。
  • 使用1个字节存储英文和数字等字符,使用2个字节存储中文字符。
  • GB2312兼容ASCII。

GBK编码规则

  • 遵循GBK字符集,支持常见的中文,罕见中文,繁体中文,日文的假名。
  • 使用1个字节存储英文和数字等字符,使用2个字节存储中文字符。
  • GBK兼容ASCII与GB2312。

UTF-16编码规则

  • 遵循Unicode字符集。支持地球上所有常见的自然语言。
  • 使用2个字节存储各种语言的常用字符,使用4个字节存储其他罕见字符。
  • UTF-16并不兼容ASCII或者ISO8859-1,原因是UTF-16使用2个字节表示英文和西欧字符。
  • 使用UTF-16编码的文件,在文件头部均含有BOM,以说明这个文件使用大端法还是小端法进行存储。
  • 在有一些文本编辑器中(比如notepad++),会把UTF-16称为UCS-2。

UTF-8编码规则

  • 遵循Unicode字符集。支持地球上所有常见的自然语言。
  • 使用1个字节存储英文和数字等字符,使用3个字节存储常用中文,使用4个字符存储罕见字符。
  • UTF-8兼容ASCII,但不兼容UTF-16,因为UTF-8存储字符所需的空间与UTF-16是不一样的。
  • UTF-8默认不带BOM。最好也不要加上BOM。

ANSI

ANSI严格来说并不是一种编码规则,它表示:根据当前操作系统以及操作系统的语言,选择对应的编码规则进行编码。例如,对于简体中文的Windows操作系统,ANSI代表GBK。在繁体中文Windows操作系统中,ANSI代表Big5。在日文Windows操作系统中,ANSI代表Shift_JIS 编码。

文件编码规则与JVM编码规则

对于一个文件而言,我们可以显式的指定文件的编码规则。但当文件中的内容读到JVM内存中后,将会采用JVM的编码规则进行重新编码和存储。例如,在JVM中,Java的char类型和String类型均强制使用UTF-16进行编码。因此对于一个非UTF-16的文件来说,在数据存储层面,数据依旧采用文件本身的编码规则进行编码和存储。但当数据读到JVM中,将采用UTF-16进行编码和存储。

下面的例子将解释:文件编码规则与JVM编码规则的区别

//把下面的代码分别在UTF-8与UTF-16编码格式的文件下进行测试
char ch1 = 'c';
Character character1 = new Character(ch1);
System.out.println(character1.SIZE); //(1)

char ch2 = '中';
Character character2 = new Character(ch2);
System.out.println(character2.SIZE); //(2)

String str1 = "a";
System.out.println(str1.length()); //(3)
System.out.println(str1.toCharArray().length);  //(4)
System.out.println(str1.getBytes().length); //(5)

String str2 = "中";
System.out.println(str2.length()); //(6)
System.out.println(str2.toCharArray().length); //(7)
System.out.println(str2.getBytes().length); //(8)

Character.SIZE返回的是JVM中char类型占多少bit。对于(1)和(2)而言,在UTF8和UTF16环境下,打印结果均是16bit,即两个字符。原因是:无论文件采用哪种编码规则中,在JVM中,char类型均使用2个字节存储字符。

String.length()返回的是JVM中字符串所对应的代码单元长度。在Java中,字符串是由char字符数组所组成的,因此在JVM中字符串也使用UTF16进行存储。
代码单元指一种转换格式中最小的一个分隔,由于UTF16最少使用2个字节表示一个字符,因此对于UTF-16而言,1个代码单元等价于2个字节。而对于UTF-8而言,1个代码单元等价于1个字节。
对于(3)和(6)而言,无论文件使用UTF8还是UTF16进行编码存储,在JVM中,String均使用UTF-16进行存储,因此打印结果都是1,即一个代码单元(UTF-16使用2个字节存储英文和常见中文)。

String.toCharArray().length返回的是:JVM中字符串对应的字符数组的数组长度。由于1个char便能表示英文和常见中文。因此对于(4)和(7),无论文件使用UTF8还是UTF16进行编码存储,在JVM中均使用UTF16进行存储,所以返回的数组长度均是1

String.getBytes()方法返回的是:字符串在数据存储层面所占的字节数。注意:当getBytes()方法不含参数时,则表明遵循文件所采用的编码规则。
对于(5)来说,如果文件采用UTF8进行编码存储,则返回1,即UTF8使用1个字节存储英文。如果采用UTF16进行编码,则返回4,原因是UTF16采用2个字节存储英文,另外2个字节用于存储BOM。对于(8)来说,如果文件采用UTF8进行编码存储,则返回3,即UTF8使用3个字节存储中文。如果采用UTF16进行编码,则返回4,原因是UTF16采用2个字节存储中文,另外2个字节用于存储BOM。

乱码的本质

乱码的本质就是:编码与解码所采用的编码规则不一致

如果你未能清晰的理解什么是编码,什么是解码,请看下面这张神图:

[字符集与编码 2] 编码 解码 乱码_第1张图片
编码与解码的神图

从右到左的这个过程就是编码,从左到右的这个过程就是解码。下面通过两个例子进一步说明。

编码与解码的例子

//类文件本身采用UTF-8格式
String str1 = "中"; //编码
System.out.println(str1); //解码

第一行代码表示编码的过程:

  1. 把"中"字读到JVM内存中,在JVM中,String类型使用UTF-16进行编码。
  2. 由于文件本身采用UTF-8格式,因此JVM将负责把UTF-16转为UTF-8。
  3. 由于UTF-8遵循Unicode字符集,因此再进一步把Unicode码点按照UTF-8的要求进行编码。(黄色阶段)
  4. 最后把二进制数据保存到文件中。

第二行代码则表示解码的过程:

  1. 把文件中的二进制数据读取出来。
  2. 由于文件是UTF-8格式,因此采用UTF-8对二进制数据进行解码,并得到Unicode码点。(黄色阶段)
  3. 由于JVM使用UTF-16格式,因此数据读到JVM后,JVM负责转译。
  4. 转译后,UTF-16格式的的"中"字被打印到控制台。

在上面的两行代码中,由于编码阶段与解码阶段所采用的编码规则都是一致的,所以肯定不会造成乱码。

乱码的例子

String str2 = new String("中".getBytes(), "GBK"); //(1) getBytes()是编码, GBK是解码
System.out.println(str2.toCharArray().length); //lenght=2, 乱码

/*
乱码:UTF8使用3字节存储一个中文,而GBK使用2字节。
因此GBK把前两个字节作为一个中文,最后一个字节作为另一个中文。
但由于unicode与GBK的中文码点不一样,因此造成乱码
*/
for(int i=0; i

在上面的例子中,str2的值其实是一个乱码。原因是在执行第(1)行代码时,经历了编码和解码两个过程:

  1. 执行"中".getBytes();时,返回的是"中"字按照文件的编码格式(即UTF-8)进行编码后的二进制数据。
  2. new String(XXX, "GBK",);的构造函数则是表示对上一步获得的二进制数据,按照GBK编码规则进行解码,已得到对应的字符串。

到这里你应该就明白了,第一步使用的是UTF-8进行编码,第二步则使用了GBK进行解码,肯定会造成乱码啦!

常见疑问

如何确定不同编码规则的兼容性?

可以通过下面几个实验来确定(这里主要是说兼容英文和数字):
实验一:

  1. 在MyEclipse中创建一个类,右键-->properties,设置编码格式为UTF-8(假设这个文件不含中文)。
  2. 把这个文件修改为ASCII, ISO8859-1, GB2312, 你会发现该文件均不会造成乱码。也就是说对于英文和数字,UTF-8与ASCII,ISO8859-1,GB2312是兼容的。
  3. 如果把这个文件从UTF-8修改为UTF-16格式,你会发现文件乱码。也就是说,UTF-8与UTF-16是不兼容的。

实验二:
假设实验一中的类是UTF-16格式的(文件不含中文),修改为UTF-8,ASCII,ISO8859-1,GB2312,你会发现文件都会造成乱码。也就是会所,UTF-16与UTF-8,ASCII,ISO8859-1,GB2312都不兼容。

UTF-8和UTF-16到底是什么关系?

UTF-8与UTF-16均支持Unicode字符集。所以对于同一个字符来说,使用UTF-8编码与UTF-16编码的码点都是一样的。例如“中”字均使用20013作为码点。但是在数据存储层面,UTF-8与UTF-16是不一样的。例如,我们可以直接把20013转为二进制数字进行存储,也可以在20013后面加上后缀000,然后再转为二进制进行存储。从这个例子中便可以知道,如果不同的编码规则实现了同一个字符集,那么该字符的码点应该是相同的,但是在数据存储方面却不一定相同。具体的存储细节详见References的第一篇。

我应该使用哪种编码规则?

建议首选UTF-8。原因如下:

  • 因为UTF-8支持所有自然语言,
  • 在存储空间方面比UTF-16更具有优势。
  • 兼容ASCII,ISO8859-1,方便转换。

什么时候使用UTF-16?

答:建议永远不要选择UTF-16!因为UTF-16含有BOM,BOM非常坑爹的东西,一不小心就会造成各种错误。而且UTF-8完全可以代替UTF-16。

为什么使用UTF-16存储一个常见中文字符却占4个字节?

答:UTF-16存储一个常见中文字符需要2个字节,但是UTF-16本身含有BOM,BOM也需要占据2个字节。

在哪些地方需要设置编码规则?

  1. 对于一个myeclipse项目中,所有的.java, .jsp, .txt, .xml, .js, .css等常见文件,最好都设置为UTF-8编码规则。
  2. 把tomcat等中间件设置为UTF-8编码规则。
  3. 把数据库设置为UTF-8编码规则。
  4. 对于接收的参数,比如前端传过来的参数,均使用UTF-8进行编码和解码。

char类型对中文的支持

Java中的char类型使用2个字节表示中文,英文字符。可能有人会问:对于一个使用UTF-8编码的类,它使用3个字节存储中文,但是我们依然可以在这个类里面,把中文字符保存到2个字节长度的char类型中,这是为什么呢?原因是:在数据存储层面,一个中文字符确实是按照UTF-8的规定,以3个字节的方式保存在文件中。但是当中文字符被读到JVM内存中,该字符会被转为UTF-16,并以2个字节的方式保存在JVM内存中。简单来说就是:在UTF-8文件中,中文字符以UTF-8进行存储,但是读到JVM内存中时,会转换成UTF-16进行存储。另外还需要注意的是,由于char的长度是2个字节,因此char类型无法表示罕见中文字符。

References

  1. http://my.oschina.net/goldenshaw/blog?catalog=536953
  2. http://blog.xieyc.com/common-code-standard-unicode-utf-iso-8859-1-etc/
  3. http://www.regexlab.com/zh/encoding.htm

你可能感兴趣的:([字符集与编码 2] 编码 解码 乱码)