字符编码 以及在java中注意事项

 

一、字符集和编码 

1 基本概念

字符(Character)是文字与符号的总称,包括文字、图形符号、数学符号等。

字符集(Charset)是一组抽象字符的集合。 字符集常常和一种具体的语言文字对应起来,该文字中的所有字符或者大部分常用字符就构成了该文字的字符集,比如英文字符集。 一组有共同特征的字符也可以组成字符集,比如繁体汉字字符集、日文汉字字符集。字符集的子集也是字符集。

计算机要处理各种字符,就需要将字符和二进制内码对应起来,这种对应关系就是字符编码(Encoding)。 

制定编码首先要确定字符集,并将字符集内的字符排序,然后和二进制数字对应起来。根据字符集内字符的多少,会确定用几个字节来编码。 每种编码都限定了一个明确的字符集合,叫做被编码过的字符集(Coded Character Set),这是字符集的另外一个含义。通常所说的字符集大多是这个含义。

 

2 有哪些字符集?

ASCII: 

American Standard Code for Information Interchange,美国信息交换标准码。 目前计算机中用得最广泛的字符集及其编码,由美国国家标准局(ANSI)制定。 它已被国际标准化组织(ISO)定为国际标准,称为ISO 646标准。 

ASCII字符集由控制字符和图形字符组成。 在计算机的存储单元中,一个ASCII码值占一个字节(8个二进制位),其最高位(b7)用作奇偶校验位。

 

ISO 8859-1: 

ISO 8859,全称ISO/IEC 8859,是国际标准化组织(ISO)及国际电工委员会(IEC)联合制定的一系列8位字符集的标准,现时定义了15个字符集。 ASCII收录了空格及94个“可印刷字符”,足以给英语使用。 但是,其他使用拉丁字母的语言(主要是欧洲国家的语言),都有一定数量的变音字母,故可以使用ASCII及控制字符以外的区域来储存及表示。 除了使用拉丁字母的语言外,使用西里尔字母的东欧语言、希腊语、泰语、现代阿拉伯语、希伯来语等,都可以使用这个形式来储存及表示。 

 

UCS

通用字符集(Universal Character Set,UCS)是由ISO制定的ISO 10646(或称ISO/IEC 10646)标准所定义的字符编码方式,采用4字节编码。 UCS包含了已知语言的所有字符。 除了拉丁语、希腊语、斯拉夫语、希伯来语、阿拉伯语、亚美尼亚语、格鲁吉亚语,还包括中文、日文、韩文这样的象形文字,UCS还包括大量的图形、印刷、数学、科学符号。 

* UCS-2: 与unicode的2byte编码基本一样。 

* UCS-4: 4byte编码, 目前是在UCS-2前加上2个全零的byte。 

 

Unicode

Unicode(统一码、万国码、单一码)是一种在计算机上使用的字符编码。 它是http://www.unicode.org制定的编码机制,要将全世界常用文字都函括进去。 它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。 1990年开始研发,1994年正式公布。随着计算机工作能力的增强,Unicode也在面世以来的十多年里得到普及。 但自从unicode2.0开始,unicode采用了与ISO 10646-1相同的字库和字码,ISO也承诺ISO10646将不会给超出0x10FFFF的UCS-4编码赋值,使得两者保持一致。 

Unicode的编码方式与ISO 10646的通用字符集(Universal Character Set,UCS)概念相对应,目前的用于实用的Unicode版本对应于UCS-2,使用16位的编码空间。 也就是每个字符占用2个字节,基本满足各种语言的使用。实际上目前版本的Unicode尚未填充满这16位编码,保留了大量空间作为特殊使用或将来扩展。

 

UTF:  

一个字符的Unicode编码是确定的,但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Translation Format,简称为 UTF,即把Unicode转做某种格式的意思。Unicode是一个字符集, 可以看作为内码,而UTF是一种编码方式。

UTF-8:Unicode的其中一个使用方式。UTF-8便于不同的计算机之间使用网络传输不同语言和编码的文字,使得双字节的Unicode能够在现存的处理单字节的系统上正确传输。UTF-8使用可变长度字节来储存 Unicode字符,例如ASCII字母继续使用1字节储存,重音文字、希腊字母或西里尔字母等使用2字节来储存,而常用的汉字就要使用3字节。辅助平面字符则使用4字节。UTF-8编码具有以下优点:

* 与CPU字节顺序无关, 可以在不同平台之间交流

* 容错能力高, 任何一个字节损坏后, 最多只会导致一个编码码位损失, 不会链锁错误(如GB码错一个字节就会整行乱码) 

UTF-16:16bit编码, 是变长码, 大致相当于20位编码, 值在0到0×10FFFF之间, 基本上就是unicode编码的实现. 它是变长码, 与CPU字序有关, 但因为最省空间, 常作为网络传输的外码。UTF-16是unicode的preferred encoding. 

UTF-32:仅使用了unicode范围(0到0×10FFFF)的32位编码, 相当于UCS-4的子集.

 

汉字编码: 

GB2312:全称为GB2312(80)字集。GB2312收录简化汉字及一般符号、序号、数字、拉丁字母、日文假名、希腊字母、俄文字母、汉语拼音符号、汉语注音字母,共 7445 个图形字符,其中包括国标简体汉字6763个。 使用双字节编码,两个字节中前面的字节为第一字节,后面的字节为第二字节。习惯上称第一字节为“高字节” ,而称第二字节为“低字节”。 “高位字节”使用了0xA1-0xF7(把01-87区的区号加上0xA0),“低位字节”使用了0xA1-0xFE(把01-94加上0xA0)。

BIG5:台湾繁体字集,共包括国标繁体汉字13053个。 Big5码使用了双字节储存方法,以两个字节来编码一个字。第一个字节称为“高位字节”,第二个字节称为“低位字节”。高位字节的编码范围0xA1-0xF9,低位字节的编码范围0x40-0x7E及0xA1-0xFE。

GBK:简繁字集,包括了GB字集、BIG5字集和一些符号,共包括21003个字符。 
       GB18030:是国家制定的一个强制性大字集标准,全称为GB18030-2000,它的推出使汉字集有了一个“大一统”的标准。GB 18030字符集标准解决汉字、日文假名、朝鲜语和中国少数民族文字组成的大字符集计算机编码问题。该标准的字符总编码空间超过150万个编码位,收录了27484个汉字,覆盖中文、日文、朝鲜语和中国少数民族文字。满足中国大陆、香港、台湾、日本和韩国等东亚地区信息交换多文种、大字量、多用途、统一编码格式的要求。并且与Unicode 3.0版本兼容,填补Unicode扩展字符字汇“统一汉字扩展A”的内容。并且与以前的国家字符编码标准(GB2312,GB13000.1)兼容。GB 18030标准采用单字节、双字节和四字节三种方式对字符编码。

 

二、编程语言与编码

C、C++、Python2内部字符串都是使用当前系统默认编码 

Python3、Java内部字符串用Unicode保存 

Ruby有一个内部变量$KCODE用来表示可识别的多字节字符串的编码,变量值为EUC SJIS UTF8 NONE之一。 $KCODE的值为EUC时,将假定字符串或正则表达式的编码为EUC-JP。 同样地,若为SJIS时则认定为Shift JIS。若为UTF8时则认定为UTF-8。 若为NONE时,将不会识别多字节字符串。 在向该变量赋值时,只有第1个字节起作用,且不区分大小写字母。 e E 代表 EUC,s S 代表 SJIS,u U 代表 UTF8,而n N 则代表 NONE。默认值为NONE。 即默认情况下Ruby把字符串当成单字节序列来处理。

 

三、 Java与unicode

Java号称国际化的语言,它的class文件采用UTF-8,JVM运行时使用UTF-16,字符串是unicode编码的。总之,Java采用了unicode字符集,使之易于国际化。因此,在Java中可以使用世界上任何国家的语言来为变量名、方法名、类起名,如下面代码如下:

class 中国 

    { 

        public String 崛起() 

        { 

            return "中国崛起"; 

        } 

    } 

     

    中国 祖国 = new 中国(); 

System.out.println(祖国.崛起());

当我们从IDE输入“中国”时,用的是java源代码文件保存的格式,一般是GBK,有时也可是utf-8,而在Java编译程序时,会不由分说地将所有的编码格式转换成utf-8编码。

实际上,由于Java内部使用的是UCS2编码格式,因为,Java并不关心所使用的是哪种语言,而只要这种语言在UCS2中有定义就可以。

1 java中的编码转换

不同编码之间的转换是通过Unicode来完成的。假设有两种不同的编码A和B,转换的步骤为:先把A转化为Unicode,再把Unicode转化为B。

举例说明。有GB2312中有一个汉字“李”,其编码为“C0EE”,欲转化为ISO8859-1编码。步骤为:先把“李”字转化为Unicode,得到“674E”,再把“674E”转化为ISO8859-1字符。当然,这个映射不会成功,因为ISO8859-1中根本就没有与“674E”对应的字符。当映射不成功时,问题就发生了!当从某编码向Unicode转化时,如果在该编码中没有该字符,得到的将是Unicode的代码“/uffffd”(“/u”表示是Unicode编码,)。而从Unicode向某编码转化时,如果该编码没有对应的字符,则得到的是“0x3f”(“?”)。这就是“?”的由来。例如:把字符流buf =“0x80 0x40 0xb0 0xa1”进行new String(buf, "gb2312")操作,得到的结果是“/ufffd/u554a”,再println出来,得到的结果将是“?啊”,因为“0x80 0x40”是GBK中的字符,在GB2312中没有。

再如,把字符串String="/u00d6/u00ec/u00e9/u0046/u00bb/u00f9"进行new String (buf.getBytes("GBK"))操作,得到的结果是“3fa8aca8a6463fa8b4”,其中,“/u00d6”在“GBK”中没有对应的字符,得到“3f”,“/u00ec”对应着“a8ac”,“/u00e9”对应着“a8a6”,“0046”对应着“46”(因为这是ASCII字符),“/u00bb”没找到,得到“3f”,最后,“/u00f9”对应着“a8b4”。把这个字符串println一下,得到的结果是“?ìéF?ù”。看到没?这里并不全是问号,因为GBK与Unicode映射的内容中除了汉字外还有字符。

2 需要在哪些时候注意编码问题?

a从外部资源读取数据 

这跟外部资源采取的编码方式有关,我们需要使用外部资源采用的字符集来读取外部数据: 

 

InputStream is = new FileInputStream("res/input2.data");   

InputStreamReader streamReader = new InputStreamReader(is, "GB18030");  

 

这里可以看到,我们采用了GB18030编码读取外部数据,通过查看streamReader的encoding可以印证: 

 

assertEquals("GB18030", streamReader.getEncoding());  

 

正是由于上面我们为外部资源指定了正确的编码,当它转成char数组时才能正确地进行解码(GB18030 -> unicode): 

 

char[] chars = new char[is.available()];   

streamReader.read(chars, 0, is.available());  

 

 但我们经常写的代码就像下面这样: 

InputStream is = new FileInputStream("res/input2.data");   

InputStreamReader streamReader = new InputStreamReader(is);  

 

这时候InputStreamReader采用什么编码方式读取外部资源呢?Unicode?不是,这时候采用的编码方式是JVM的默认字符集,这个默认字符集在虚拟机启动时决定,通常根据语言环境和底层操作系统的 charset 来确定。可以通过以下方式得到JVM的默认字符集: 

 

Charset.defaultCharset();  

 

为什么要这样?因为我们从外部资源读取数据,而外部资源的编码方式通常跟操作系统所使用的字符集一样,所以采用这种默认方式是可以理解的。 

好吧,那么我通过我的IDE Ideas创建了一个文件,并以JVM默认的编码方式从这个文件读取数据,但读出来的数据竟然是乱码。为何?呵呵,其实是因为通过Ideas创建的文件是以utf-8编码的。要得到一个JVM默认编码的文件,通过手工创建一个txt文件试试吧。

 

b. 字符串和字节数组的相互转换 

我们通常通过以下代码把字符串转换成字节数组: 

"string".getBytes();  

 但你是否注意过这个转换采用的编码呢?其实上面这句代码跟下面这句是等价的: 

"string".getBytes(Charset.defaultCharset());  

 也就是说它根据JVM的默认编码(而不是你可能以为的unicode)把字符串转换成一个字节数组。 

反之,如何从字节数组创建一个字符串呢? 

new String("string".getBytes());  

 同样,这个方法使用平台的默认字符集解码字节的指定数组(这里的解码指从一种字符集到unicode)。 

 

c字符串编码迷思: 

new String(input.getBytes("ISO-8859-1"), "GB18030")  

 

上面这段代码代表什么?有人会说: “把input字符串从ISO-8859-1编码方式转换成GB18030编码方式”。如果这种说法正确,那么又如何解释我们刚提到的java字符串都采用unicode编码呢? 

这种说法不仅是欠妥的,而且是大错特错的,让我们一一来分析,其实事实是这样的:我们本应该用GB18030的编码来读取数据并解码成字符串,但结果却采用了ISO-8859-1的编码,导致生成一个错误的字符串。要恢复,就要先把字符串恢复成原始字节数组,然后通过正确的编码GB18030再次解码成字符串(即把以GB18030编码的数据转成unicode的字符串)。注意,字符串永远都是unicode编码的。 

但编码转换并不是负负得正那么简单,这里我们之所以可以正确地转换回来,是因为 ISO8859-1 是单字节编码,所以每个字节被按照原样 转换为 String ,也就是说,虽然这是一个错误的转换,但编码没有改变,所以我们仍然有机会把编码转换回来!

 

四、为什么会乱码

乱码是个老问题,从上面我们知道,字符在保存时的编码格式如果和要显示的编码格式不一样的话,就会出现乱码问题。

我们的Web系统,从底层数据库编码、Web应用程序编码到HTML页面编码,如果有一项不一致的话,就会出现乱码。

所以,解决乱码问题说难也难说简单也简单,关键是让交互系统之间编码一致。

在如此多种编码和字符集弄的我们眼花缭乱的情况下,我们只需选择一种兼容性最好的编码方式和字符集,让它成为我们程序子系统之间。交互的编码契约,那么从此恼人的乱码问题即将远离我们而去 -- 这种兼容性最好的编码就是UTF-8! 毕竟GBK/GB2312是国内的标准,当我们大量使用国外的开源软件时,UTF-8才是编码界最通用的语言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值