从Properties乱码来学习编码
最近使用到java中的Properties
来获取一些变量信息,但如果变量值中有中文,那么最终录入到内存中的字符将会变乱码,那么是什么原因使得中文变成乱码呢?
1 字节与字符
为了了解中文乱码,我们先捋清字节和字符的概念。
我们平常说的字节(byte
),实际上是在计算机信息技术中计量存储容量的一种计量单位。在计算机中,能够识别和处理的数据都是二进制,而字节就是二进制位数的单位,一般情况下,一个字节表示8位二进制。
而字符(char
),则是计算机中使用的字母、数字、字和符号。由于计算机识别的数据都是是二进制,所以字符在计算机中的处理也是由二进制来进行。为了方便人们统计处理,一般用字节来衡量字符所占用的二进制位数。
需要注意的是,字符的长度在不同的系统中不同的语言中是不一样的,例如在C语言中一个字符(char
)就是一个字节(byte
)大小,而在Java语言中一个字符(char
)占用两个字节(byte
)大小。
2 字符集
在第一节中,我们缕清了字节和字符的关系,那么很多人接下来就想知道为什么字符0
在序号上是48(编码:0x30)、字符A
在序号上是65(编码:0x41)。
这里就要引入一个字符集的概念(编码的问题在下一章节细讲),1967年,为了统一不同计算机在相互通信时的字符编码标准,美国国家标准协会定制了一套字符集。ASCII ((American Standard Code for Information Interchange): 美国信息交换标准代码),这套字符集中包含了英语和一些其他小部分欧洲语言,共计128个字符。
随着计算机信息技术的发展,计算机系统需要处理的字符越来越多,像中文、日文和韩文等等。这时,ASCII字符集就已经不能满足各个国家的需求了,于是各个国家均开始了本国文字字符集标准的建设,像日文的字符集标准有:Shift_JIS、EUC-JP、ISO-2022-JP,中文的字符集标准有:GBK、BIG-5、GB2312等等,欧洲国家也制定了ISO-8859-1标准,扩展ASCII字符集到256位,其中包含了大部分欧洲语言。
在这些各国的文字字符集标准中,大多都对ASCII字符集做了兼容,所以在各个标准中,大家看到的字符0
、A
的序号都是48、65。
与此同时,国际组织随之制定了能够将全球字符纳入的字符集:Unicode。Java中的字符(char
)就采用的Unicode,之前我们说Java中的字符占两个字节,而Unicode中包含的字符远超65535,因此在Unicode中序号超过65535的字符就用Java中的两个字符(char
)表示。
很多人在这里就有疑问了,如果有一个Unicode字符占了两个Java的字符(char
),那么String.length()
方法岂不是有问题了?实际上,这个问题的确存在,但我们平常处理大部分的字符都在65535以内,所以平时也不用纠结String.length()
是否有问题。那如何得到真正的字符长度呢?String类提供了int codePointCount(int beginIndex, int endIndex)
方法。
下面是实际演示的例子:
3 字符编码集
第二节我们讨论了字符集,但我有意忽略了字符编码集,因为字符集和字符编码集很容易搞混。为了沟通顺畅,在这里我们对这两个概念做一解释。字符集中只有字符与序号的对应关系,例如字符0
在ASCII字符集中序号是48。字符编码集指的是为了在计算机中进行处理,字符与在计算机中的二进制编码的对应关系。
为什么二者容易搞混?原因是序号和编码在计算机中都是用二进制,而在很多数字符集中,字符集和字符编码集的关系就是一一对应的。例如字符0
在ASCII字符集中序号是48,它的16进制编码就是0x30。
但还有一些字符集的编码方式却和序号不是意义对应的,例如GB2312,举例来说,"啊"字是GB2312编码中的第一个汉字,它位于16区的01位,所以它的区位码(序号)就是1601。GB2312规定对收录的每个字符采用两个字节表示,第一个字节为“高字节”,对应94个区;第二个字节为“低字节”,对应94个位。所以它的区位码范围是:0101-9494。区号和位号分别加上0xA0就是GB2312编码。例如最后一个码位是9494,区号和位号分别转换成十六进制是5E5E,0x5E+0xA0=0xFE,所以'啊'字在GB2312字符集中的编码是0xFEFE。
需要注意的是,有些字符集的计算机编码规范只有一种,例如GB2312。但还有一些字符集的计算机编码方式却有多种,例如我们平常使用最多的UTF8编码,它是Unicode字符集中的一种编码方式,Unicode的编码方式还有UTF16、UTF32等等。为什么Unicode字符集会有多种编码方式?其中一个重要的原因就是Unicode字符集包含的字符太多,如果直接一一映射,那么每个字符需要占用4字节。为了减少字节占用,于是出现了UTF8编码。UTF8编码针对Unicode的一种可变长度字符编码,对于常用的英文字符只占用一个字节,对于中文常用的字符,只占用两个字节,这样做的好处是在IO时,需要传输的字节长度将会大大降低。
由于在Java中,字符集只支持Unicode,所以在Java的编码函数中,只有Unicode字符到各个字符集对应编码的映射关系,不存在各个字符集对应编码再映射回各个字符集中的序号的能力。例如"啊",在Java中可以编码为GB2312的字符编码0xFEFE,也可以从0xFEFE映射回Unicode的序号\u554a
,但就是没有映射回GB2312序号1601的需要和能力。
4 Properties的问题
在捋清楚字节、字符集和字符编码集后,我们来看看Properties为什么会中文乱码。Java从文件读取字符串的流程如下:
- 获取文件对象
- 读取其中的字节(现在的文件编码大多是UTF-8)
- 将字节按照字符集的编码规范翻译成Unicode序号并产生字符(
char
) - 将字符组成字符串
而JDK中Properties的load方法有这样的一个注释。
Reads a property list (key and element pairs) from the input byte stream. The input stream is in a simple line-oriented format as specified in load(Reader) and is assumed to use the ISO 8859-1 character encoding; that is each byte is one Latin1 character. Characters not in Latin1, and certain special characters, are represented in keys and elements using Unicode escapes as defined in section 3.3 of The Java™ Language Specification.
The specified stream remains open after this method returns.
也就是说使用Properties加载文件数据时,并没有默认以UTF-8的编码规范来翻译字符到Unicode,而是以ISO-8859-1的编码规范来翻译字符到Unicode。由于ISO-8859-1编码规范中并不包含汉字,因此UTF8编码的字节将会变成ISO-8859-1字符集中的英文或拉丁文字,从而让人感觉是乱码。
4 Properties乱码解决
那么如何解决这个问题?该注释说明了解决办法,就是说如果要用到ISO-8859-1字符集以外的字符,就要使用Unicode转义,而Properties内部会将转义字符串再转回Unicode字符。
另外,可以使用Properties的synchronized void load(Reader reader)
方法来加载文件数据。Reader接口使用FileReader即可。因为Reader接口返回的是Unicode序号(也就是char
),而如果使用别的load方法,Properties内部将使用内部的LineReader来获取char,这个LineReader则默认以ISO-8859-1编码来处理生成Unicode序号(也就是char
)。
5 小结
字符集和字符编码集之前没有细究,这次趁排查Properties的机会,再次梳理了字符集和字符编码集的关系。
Java中全局使用的字符集是Unicode,而各类Charset的编码与解码,均不牵扯其他字符集,只是Unicode字符序号和各字符集计算机编码的映射。