简述字符编码和解码(字符编码的定义及编码形式)

2023-03-02 12:03:42 密码用途 思思

字符编码简述

众所周知,java中如果要计算一个字符串的长度,可以直接利用String的length方法。如下:

显然,这里的length方法计算的字符数,一个英文字母按一个字符计算,一个中文汉字也是按照一个字符进行计算的。

不过,如果想要获取字符串的字节数呢?String依然提供了现成的方法供我们使用,如下所示:

这里,可以看到几个注意点:

先来看第一点,也是本文主要想讨论的问题:UTF-8、GBK的区别是什么,为什么会导致最终获取的字节数不一样?

要解答上面的问题,需要先知道GBK和UTF-8分别是什么。

简单的说,GBK和UTF-8是两种字符的编码方式。那么,问题又来了,什么是字符的编码方式呢?除了GBK和UTF-8,有没有其他的编码方式呢?其中的区别又在哪里?

关于字符的编码方式,姑且可以简单的理解为,将一个字符表示成一串bit流的规则(这个说法是不太准确的,下文会有详细解释)。比如说,UTF-8就是一种非常常用的字符编码方式,“汉”字以UTF-8的规则计算后表示出来的bit流就是“11100110 10110001 10001001”。

有些时候,编码方式,还会被称为编码规则、编码方案。

实际上,从计算机不再单纯地拿来进行数字计算开始,字符的编码方式就一直在不断的演进,现在就借着这一段历史,来对包括GBK、UTF-8在内的几种常见字符编码方式进行下介绍。

计算机刚出世的时候,美国人为了交流通信方便,约定了一套字符编码方式,就是ASCII码。

ASCII全称为American Standard Code for Information Interchange,即美国信息互换标准码。

ASCII码的字符集中包含了26个英文字母、10个数字(0-9)、一些常见的符号(@、#、!),基本能够满足在英语环境下的需求。ASCII字符集里面只有128个字符,每个字符都有一个编号,也就是0-127。而当时大家已经习惯于用8个bit来表示一个字节,所以干脆取一个字节来表示一个字符。其中,最高位置为0,其他位全部用上,总共128个位置,刚好能够与ASCII字符集一一对应。

举个例子,在ASCII码中,‘A’对应的编号是65,用一个字节表示就是“01000001”。

这里对引入的两个新概念做下解释:

字符集 :字面上理解就是字符的集合。

编号字符集 :指带有数字编号的字符集合,有时候也简称为字符集。例如:[1:a, 2:b, 3:c],在此字符集中,包含三个字符:a、b、c,并且其编号分别为1,2,3。

不过,后来计算机传到了欧洲,不少欧洲国家的语言使用ASCII码无法完整地进行表示,比如德语、法语。上文可以看到,在ASCII编码中,一个ASCII字符,是用一个字节来表示的。一个字节实际上能够表示256个数字,也就至少能够表示256个字符,而ASCII字符集只有128个字符。所以这时候出现了多种基于ASCII的编码方式。大家的基本思路都是一样的:还是使用一个字节表示一个字符,0-127依然用来表示ASCII字符集(字符编号与ASCII码保持一致),128-255拿来表示自己语言中的特殊字符。

显然,这么搞出来的多个编码方式互不兼容,大家会很痛苦。所以最后出现了两套统一的编码方案,能够对欧洲各国的字符都进行支持。这两套编码方案分别是:EASCII(Extended ASCII)字符编码方案,ISO/IEC 8859字符编码方案。

这两套方案也是沿用上面的思路:0-127依然用来表示ASCII字符集(字符编号与ASCII码保持一致),128-255用来表示欧洲各国的特殊字符(这部分字符集又被称为扩展字符集)。

由于在这两种编码方案中,ASCII字符集中的字符,保留了与ASCII码相同的字符编号,所以 这两种编码方案都是对ASCII编码完美兼容的 。

不过,与ASCII、EASCII属于单个独立字符集不同,ISO/IEC 8859是一组字符集的统称。其下共有15个字符集,即ISO/IEC 8859-n,n=1,2,3 …… 15,16(其中12未定义,所以共15个)。

到现在为止,EASCII已经很少有人用了,ISO/IEC 8859却是被广泛使用,其中ISO/IEC 8859-1被使用的最为普遍。而ISO/IEC 8859-1又被简称为ISO 8859-1,而且它还有一个Latin-1(也写作Latin1)的简称。

终于,计算机来到了中国。如上文所述,仿照ASCII码的规则,1个字节最多也就只能表示256个字符。但是,中国汉字有几万个,常用字就有几千个,这样的话,1个字节是完全不够用的。所以,当时的全国信息技术标准化技术委员会搞了一套自己的编码方案:用两个字节表示一个字符。这就是GB系列编码。“GB”是“国标”的拼音首字母缩写,意为“国家标准”。

最早的GB编码就是GB2312,收录了6763个汉字和682个符号,基本能够满足日常需求。

GB2312规定,一个汉字的编号必须大于127,并且编号大于127的字符必须用两个字节来表示。而0-127,仍然用来表示之前的ASCII字符集,这部分字符的编号依旧与ASCII码保持一致,并且只有一个字节来表示。

所以,GB2312对ASCII码是完全兼容的。不过GB2312对ISO是不兼容的,因为它舍弃了ISO中128-255之间的字符映射。

同时,也可以认为,在GB2312中,英文字符只占一个字节,而中文字符会占两个字节。

而计算机在依照GB2312编码进行字符识别时,会先判断第一个字节的第一个bit位是否为0,如果是,则读取1个字节,进行编码解析;如果不是,则读取两个字节,进行编码解析。

此外,当时出于种种原因考虑,GB2312对ASCII码中的西文字母、数字、标点等特殊符号进行了重新编码,用两个字节来进行表示。所以,这类字符在GB2312中就有了两种编码表示,其中小于128的编码(用1个字节表示),就被称为半角字符,大于128的编码(用2个字节表示),就被称为全角字符。

到目前为止,由于当时导致全角字符出现的历史原因已经不再存在,所以只有很少的一些全角字符还在使用(比如中文的逗号,问号,感叹号,空格等),其他的许多全角字符已经很少用了。

虽然GB2312能够满足基本的日常需求,但是毕竟收录的汉字还是太少,繁体字、生僻字是不包含在GB2312字符集中的。由此,有关部门对GB2312进行了扩展,推出了GBK编码。

GBK与GB2312基本一致,都是使用两个字节来表示汉字。不过有一点不一样:在GB2312中,表示汉字的两个字节中,其首位必须都是1;而在GBK中,只要求第一个字节(高字节)的首位为1,对于第二个字节(低字节),没做要求。当然,如果首位为0,都是用来表示ASCII字符集里的内容。

GBK可以认为是对GB2312的扩展,其对GB2312是完美兼容的。所以,GBK对ASCII码也是完美兼容的。

GB18030是对GBK的进一步扩展,在扩展现有汉字的基础上,收录了数千个少数民族的字符。其由中国国家质量技术监督局于2000年3月17日推出,用以取代GBK。

GB18030同样保持向下兼容,其对GBK、GB2312、ASCII编码完美兼容。

诸如GB2312、GBK、GB18030之类的编码格式,被程序员们称为DBCS(Double Byte Charecter Set:双字节字符集)。在DBCS的标准里,英文字符用一个字节表示,并且这个字节的第一位必然为0(英文字符对应的字号小于128);中文字符用两个字节表示,第一个字节的第一位必然为1。

如上文所述,在计算机的传播途中,为了兼容各地的语言,出现了许许多多的编码方案。但是遗憾的是,这些编码方案互不兼容,直接影响到了信息的传播,这也催生了能够兼容全球各种字符的统一编码方案的出现。

历史上存在两个独立的尝试创立单一字符集的组织:

不过在1991年前后,两个项目组发现没必要存在两个不兼容的字符集,所以它们开始合并双方成果,约定使用统一的编码表。从Unicode 2.0开始,Unicode项目采用了与ISO 10646-1相同的字库与字码,ISO也承诺,ISO将不会替超出U+10FFFF的UCS-4编码赋值,以使得两者保持一致(UCS的概念下文会有详述,此处不必过于关注)。

目前,这两个项目组仍独立存在,并独立地发布各自的标准,不过二者约定保持双方的标准码表兼容,并共同调整任何未来的扩展。

ISO 10646标准,只是一个简单的字符集表。它定义了一些编码的别名,指定了一些与标准有关的术语,并包括了规范说明,指定了怎样使用UCS链接其他ISO标准的实现,比如ISO/IEC 6429和ISO/IEC 2022。还有一些与ISO紧密相关的,比如ISO/IEC 14651是关于UCS字符串排序的。

Unicode标准,额外定义了许多与字符有关的语义符号学内容,并详细说明了绘制某些语言(如阿拉伯语)表达形式的算法、处理双向文字(比如拉丁文和希伯来文的混合文字)的算法、排序与字符串比较所需的算法等。

在书写Unicode编码时,规定以十六进制数来进行表示,并需要加上“U+”前缀。比如“汉”字的Unicode编码为“U+6C49”。

为了能够更方便地介绍后续的内容,这里需要先解释清楚几个名词(个人认为这几个概念有助于理解后续的内容,如果不想看,可以直接跳过此节)。

编号字符集(CCS:Coded Character Set) :指带有数字编号的字符集合。上文已经介绍过了。

字符编码方式(CEF:Character Encoding Form) :将字符集的数字编号转换为字节流的规则。

还是上文中的例子,Unicode字符集中的“汉”字,在Unicode字符集中的编号是0x6C49,在UTF-8编码中,需要使用3个字节来表示,表示成二进制则是“11100110 10110001 10001001”(UTF-8的具体编码规则,下文会有详述)。

在这个例子中,Unicode就是所谓的编号字符集(CCS),UTF-8编码便是字符编码方式(CEF)。

实际上,在unicode字符集出世之前,字符集与编码方式往往是耦合在一起的,一套字符集往往也只有一套编码规则,这两个概念也没必要严格区分,人们也经常进行混用。比如ASCII码既可以认为是一套字符集,也可以认为是一种字符编码方式。

但是,Unicode字符集出现之后,字符集和编码方式被分离解耦了。此时,一套字符集可能有多套的编码规则,我们所熟知的UTF-8、UTF-16就是建立在Unicode字符集上的字符编码方式。

编码规则大致上可以分为两类:直接映射与间接映射。

直接映射 ,是指字符在字符集中的数字编号与编码后的编码串是一样的。比如ASCII字符集中,‘A’对应的字符编号是65,换算成二进制为“1000001”,按照ASCII码编码后,用一个字节来表示,就是“01000001”,也就是十进制中的65。编码前后,其实可以视为是一样的。

间接映射 ,就是字符在字符集中的数字编号与编码后的编码串不一定一样。还是上面的例子,unicode字符集中“汉”字的字符编号为0x6C49,如果换算成二进制就是“01101100 01001001”,但是UTF-8编码后要用三个字节来表示,表示成二进制就是“11100110 10110001 10001001”。编码前后,数值不一样。

其实,Unicode出现之前,大家一直用的都是直接映射,编码前后数值是一样的,这也是一直没有明确区分字符集和编码方式这两个概念的一个原因。

解释清楚了这几个概念,下面我们继续:

UCS全称为“Unicode Character Set”,是由ISO制定的ISO 10646标准所定义的标准字符集。

UCS又称“Universal Multiple-Octet Coded Character Set”,译为通用多八位编码字符集。

相对应的,Unicode项目所使用的标准字符集通常被称为Unicode字符集。

如上文所述,Unicode 2.0发布时,Unicode字符集与UCS字符集基本保持了一致,之后虽然二者独立存在,但是一直在保持互相的兼容。

在ISO与unicode合并之前,ISO就有一套字符编码模式,也就是UCS-2。

UCS-2的规则就是用两个字节来表示字符集中的字符,并且它使用的是直接映射的方式。所以可以简单理解为,UCS-2就是将字符的数字编号直接转化为二进制,然后用两个字节来进行存储。

与ASCII类似,此时的UCS-2其实可以视为一套字符集,也可以视为一套编码规则。

UCS-2用两个字节来表示一个字符,所能容纳的字符数量为2^16 = 65536个。

在ISO与Unicode合并字符集之后,双方约定字符集需要容纳的字符数量远远超过65535个(到目前为止,Unicode字符集可容纳的字符量为2^16 * 17 = 1114112个),此时UCS-2显然不够用了,所以ISO推出了新的规则,就是UCS-4.

UCS-4与UCS-2基本一样,唯一的不同点是,UCS-4使用4个字节来表示一个字符。

同样,UCS-4可以认为是一套字符集,也可以认为是一套编码规则。

在有些文章里,UCS-4有广义和狭义两种含义,广义上UCS-4包含UCS-2,狭义上不包含。个人理解,在指代字符集的时候,UCS-4包含UCS-2,但是在指代编码规则时,UCS-4不包含UCS-2。

UCS-2全称2-byte Universal Character Set,直译为2字节通用字符集。

UCS-4全称4-byte Universal Character Set,直译为4字节通用字符集。

注意:UCS-2和UCS-4组成的UCS字符集,都可以采用UTF-8、UTF-16、UTF-32进行编码。所以UCS-2与UTF-16并不等同,UCS-4与UTF-32也不等同。

如上文所述,ISO与Unicode合并之后,ISO推出了UCS-4。但是Unicode推出的却是另外一套编码规则:UTF-16.

UTF-16源于UCS-2,但是与UCS-2不太一样。UCS-2属于定长编码方式,永远使用两个字节来表示一个字符。而UTF-16属于变长编码方式,对于UCS-2字符集中的字符(0x0000~0xFFFF)使用2个字节来表示,对于UCS-4字符集中除开UCS-2里的字符(0x10000~0x10FFFFF),使用4个字节来表示。

UTF-16的编码规则属于间接映射。对于UCS-2字符集里面的内容,保持字符编号与生成的编码串相同,但是对于UCS-4中的其他字符(指除开UCS-2中的字符),字符编号与最终的编码串并不相同。这里采取了一套计算算法:代理机制。不过本文对此不做深究。

虽然UTF-16能够满足需求,但是一来对于ASCII字符集中的字符,UTF-16仍然需要使用两个字节来存储(这样会有一个字节的空间被浪费),并且ASCII中的字符,其UTF-16编码的第一个字节将永远是0x00,而C语言中又因为会将此字节视为字符串末尾导致字符串无法正常解析。所以UTF-16刚推出的时候,就受到了很多的抵制。

由此,UTF-8出现了。

UTF-8也是一种变长编码方式,它使用1到4个字节来表示一个字符。

字符编号为0~127(十进制)的字符,使用一个字节进行表示。

字符编号为128~2047(十进制)的字符,使用两个字节进行表示。

字符编号为2048~65535(十进制)的字符,使用三个字节进行表示。

字符编号为65536~2097151(十进制)的字符,使用四个字节进行表示。

UTF-8和UTF-16,都属于间接映射。也就是说,字符编号与最终的编码并不完全是一样的。

实际上,UTF-8的编码规则如下:

还是上文中的例子,Unicode字符集中的“汉”字,字符编号以16进制表示为“0x6C49”,换算成十进制就是27721,所以需要使用三个字节进行表示。而“0x6C49”换算成二进制就是“110110001001001”,代入上图中三字节的编码规则(“1110xxx 10xxxxxx 10xxxxxx”),最终得到的就是"1110110 10110001 10001001"。

当然,对于ASCII字符集里面的字符(字符编号小于128),UTF-8只需要一个字节即可表示。与UTF-16的两个字节相比,空间利用率更高(同样,在进行数据传输时,效率也更高)。

也因此,UTF-8对于ASCII码属于完美兼容,而UTF-16只能算是间接兼容(毕竟多了一个字节,解析的时候还需要进行转化)。考虑到计算机世界里ASCII字符的广泛性,这一点意义重大。

顺便说一句,虽然上面并没有介绍UTF-16的代理机制,但是可以说明的是,这个代理机制的算法要比UTF-8的算法更加复杂,一定程度上也导致了UTF-16进行编码和解码需要耗费更多的资源。

此外,可以看到,UTF-8编码产出的字节,都带有固定的前缀。这样做有几个好处:

第一,字符使用UTF-8编码之后,第一个字节的前面的几位,可以明确标识出来,此字符需要几个字节才能表示出来。这样的话,解码程序在读入每一个字节的时候,就能够知道当前字节是否为一个字符的首字节;如果是首字节的话,立刻就能知道还需要读入几个字节才能解析出来这个字符。

第二,字符经UTF-8编码之后,生成做到多个字节中,第一个字节的固定前缀与后续字节的固定前缀都不一样。这样就保证,在传输过程中,如果出现了局部的字节错误,比如增加、丢失、修改了某些字节。将只会影响到有限个字符,并不会导致后续的所有的字符都解析错误。这一点是UTF-16、UTF-32、GB系列都做不到的事情。

第三,同样因为编码后,首字节的前缀与后续字节的前缀都不同,所以从UTF-8字节流中的任一字节开始,往后或者往前都可以很轻易的找到当前字符或者临近字符的起始位置。

第四,依照目前的规则(检查首字节,在第一个0出现之前,有几个1,就代表当前字符需要用多少个字节进行表示),UTF-8可以很轻易地扩展到5个字节、6个字节,甚至是7个字节和8个字节。这就保证了UTF-8可以很轻易地支持Unicode字符集的不断扩充。

与UTF-8和UTF-16相比,UTF-32就比较简单了。

UTF-32的编码规则属于直接映射,并且每个字符都使用四个字节来表示。

因此,UTF-32比UTF-16更浪费空间。但是因为使用的是定长编码(每个字符都是四个字节),所以文本处理速度上要比UTF-8和UTF-16快一些。

在三大UTF编码中,UTF-32既不是最早出现的(UTF-16),也不是最优设计(目前公认UTF-8为最优设计),所以目前已经很少有地方在用了。

上文聊到一个内容,UTF-16编码,有可能使用两个或者四个字节来表示一个字符。那么问题来了,假设存在一个字符,其用UTF-16编码之后,对应的字节流,用16进制表示为0xFA 0xFB。这时候,在计算机存储与传输中,到底应该是0xFA放前面呢,还是应该0xFB放前面呢?

比较遗憾的是,在计算机发展历程中,出于各种各样的原因,大家并没有形成统一,而是出现了多种方案,比较常见的是如下两种:

一、大端序(Big-Endian):又称高尾端序,即数据的尾端存储在内存的高地址;数据的头端存储在内存的低地址。

二、小端序(Little-Endian):又称低尾端序,即数据的尾端存储在内存的低地址;数据的头端存储在内存的高地址。

为了方便理解记忆,这里用几个例子来对大端序和小端序进行下简单的说明。

首先,我们在阅读和书写二进制串时,总是高位在前,低位在后。比如,拿“汉字”为例,其中“汉”对应的unicode编码为“U+6C49”,“字”对应的unicode编码为“U+5B57”,如下所示:

而计算机内存的地址增长,我们设定为从左到右,如下图所示:

那么这种情况下,大端序,就是将写入内存时,字节顺序不变。如下所示:

而小端序,就需要将字节串前后颠倒一下顺序,再写入内存,如下所示:

注意:

不过,问题来了,上面举的例子中,“汉”和“字”在UTF-16编码下,都只需要两个字节就能表示。那对于需要四个字节才能表示的字符呢?这里选取两个字符,对应的unicode编码分别为"U+129024"( )与“U+4E00”( )。其中第一个字符使用UTF-16进行编码时需要做间接映射,需要用4个字节来表示,而第二个字节做直接映射即可。如下:

此时,在两种字节顺序中的表现如下:

大端序:

小端序:

可以看到,在UTF-16中,即使对于需要使用四个字节来表示的字符,大端序和小端序的作用范围还是被限制到了两个字节。

实际上,这里有一个码元(code unit)的概念。

在解释码元之前,需要先解释另外一个概念:CES。

CES,全称Character Encoding Scheme,可以直译为字符编码模式,是指将字节流转换为最终的bit流的规则。

而上文中,提到过两个相关的概念:CCS(编号字符集)和CEF(字符编码方式)。

CCS(Coded Character Set):编号字符集,指带有数字编号的字符集合。

CEF(Character Encoding Form):字符编码方式,将字符集的数字编号转换为字节流的规则。

三者之间的关系如下:

举个例子(为了方便阅读,最终的bit流以16进制的方式展示):

其中,CEF得出的字节流可以理解为数字编号在计算机中逻辑表示方式,我们前面介绍到的UTF-8、UTF-16都是CEF;而CES的得出bit流序列可以理解为数字编号在计算机中的物理表现方式,上面提到的字节序(大端序、小端序等),就可以认为是字符编码中的CES。

回到码元的概念。码元,可以认为是CEF在将字节流转变为bit流时的最小操作单元。

举个例子,UTF-16中,以2个字节为一个码元,所以在生成bit流时,只会在2个字节内执行大端序和小端序的排序规则。

类似的,在UTF-32中,以4个字节为一个码元。但是,在UTF-8中,以1个字节作为一个码元,所以在使用UTF-8进行编码时,大端序和小端序其实并不会起作用。

由于在使用诸如UTF-16或者UTF-32等以多个字节作为一个码元的编码方式时,对于同一个bit串,使用大端序和小端序解析出来的最终结果很有可能完全不同。所以,在进行数据传输时,数据的生产方必须告知接收方应该使用哪种方式进行解析。而这个告知操作便由BOM(Byte-Order Mark)来实现。

在Unicode中,有一个字符,其编码为U+FEFF,其含义为零宽度不中断空格(ZERO WIDTH NO-BREAK SPACE)。它名义上是个空格,但是宽度为0,所以不可见,也无法被打印出来,换句话说,这个字符其实没啥用。

但是BOM便是借助于这个字符来实现。

为了告知字节流的接收方,这串bit的字节顺序是什么样子的,约定了个办法。就是在每串字节流前面,都要添加一个上述的字符U+FEFF。对于UTF-16如果是大端序,首先读出来的两个字节就会是0xFE 0xFF;如果是小端序,首先读出来的两个字节就会是0xFF 0xFE。这个强行加载字节流最前面,用来表示字节序的字符,就是上文所说的BOM。类似的,对于UTF-32,如果是大端序,首先读出来的就是0x00 0x00 0xFE 0xFF,而如果是小端序,首先读出来的就是0xFF 0xFE 0x00 0x00.

从Unicode 3.2开始,U+FEFF这个字符被规定只能出现在字节流的开头,且只能用于标识字节序,所以这个字符又有了个别名:字节序标记。不过Unicode又添加了个字符用于标识零宽度不中断空格,编码为U+2060。

上文也提到过,对于UTF-8来说,不存在字节序所带来的问题,所以,UTF-8产出的字节流是根本不需要BOM的。不过某些时候,还是会给UTF-8的字节流添加一个BOM注意此时并不是为了标识当前的字节序,而是表示当前字节流是用UTF-8编码完成的(毕竟UTF-8根本没有字节序问题需要BOM解决)。而在UTF-8前面添加的这个BOM,对应的字节流是0xEF 0xBB 0xBF。

对BOM做下简单的整理,如下:

现在,回到文章最初时提的两个问题:

Q:为什么同一个字符串,使用GBK和UTF-8进行编码后的字节数不一样?

A:因为GBK对于一个字符,恒定使用两个字节来表示,但是UTF-8会使用1~4个字节来表示。而文章开头时,给出的示例字符串为三个汉字“哈哈哈”,在UTF-8中,一个汉字会用三个字节来表示。所以gbk编码后,字节数为2 * 3 = 6,而UTF-8编码后,字节数为3 * 3 = 9.

Q:为什么在获取字节数时,不指定charset的结果与指定使用UTF-8时相同?

A:可以看一下getByte()的源码,如下:

继续看958行的encode方法:

注意看384行,会取默认的charset,继续跟下去:

看608行,取得时系统属性file.encoding,以此作为默认的编码方式。

验证一下,如下:

CCS、CES、CEF、码元的概念,皆引用自知乎专栏( ),不保证正确性与通用性,不过个人认为这几个概念,对于理解unicode、UTF等有着极大的帮助。

简述字符编码和解码(字符编码的定义及编码形式) 第1张

字符常见的几种编码方式

无论在是在编辑文本文件的时候,还是在制作网页的时候,总会遇到文本编码方式的问题。如果处理不当,就会出现乱码的问题。因此,有必要对文本的编码方式做一个详尽的了解。

常见的一些字符编码方式无非有:Unicode、ASCII、GBK、GB2312、UTF-8。下面先对常见的这一些字符编码方式作下说明:

1.ASCII码

这是美国在19世纪60年代的时候为了建立英文字符和二进制的关系时制定的编码规范,它能表示128个字符,其中包括英文字符、阿拉伯数字、西文字符以及32个控制字符。它用一个字节来表示具体的字符,但它只用后7位来表示字符(2^7=128),最前面的一位统一规定为0。

2.扩展的ASCII码

原本的ASCII码对于英文语言的国家是够用了,但是欧洲国家的一些语言会有拼音,这时7个字节就不够用了。因此一些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使 用的编码体系,可以表示最多256个符号。但这时问题也出现了:不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不一样。比如,130在法语编码 中代表了é,在希伯来语编码中却代表了字母Gimel (?),在俄语编码中又会代表另一个符号。但是不管怎样,所有这些编码方式中,0—127表示的符号是一样的,不一样的只是128—255的这一段。这个问题就直接促使了Unicode编码的产生。

3.Unicode符号集

正如上一节所说,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的符号。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样。而Unicode就是这样一种编码:它包含了世界上所有的符号,并且每一个符号都是独一无二的。比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字“严”。具体的符号对应表,可以查询unicode.org,或者专门的汉字对应表 。很多人都说Unicode编码,但其实Unicode是一个符号集(世界上所有符号的符号集),而不是一种新的编码方式。

但是正因为Unicode包含了所有的字符,而有些国家的字符用一个字节便可以表示,而有些国家的字符要用多个字节才能表示出来。即产生了两个问题:第一,如果有两个字节的数据,那计算机怎么知道这两个字节是表示一个汉字呢?还是表示两个英文字母呢?第二,因为不同字符需要的存储长度不一样,那么如果Unicode规定用2个字节存储字符,那么英文字符存储时前面1个字节都是0,这就大大浪费了存储空间。

上面两个问题造成的结果是:1)出现了unicode的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示unicode。2)unicode在很长一段时间内无法推广,直到互联网的出现。

4.UTF-8

互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种unicode的实现方式。其他实现方式还包括UTF-16和UTF-32,不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8是Unicode的实现方式之一。

UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

UTF-8的编码规则很简单,只有两条:

1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。

2)对于n字节的符号(n1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。

5.GBK/GB2312/GB18030

GBK和GB2312都是针对简体字的编码,只是GB2312只支持六千多个汉字的编码,而GBK支持1万多个汉字编码。而GB18030是用于繁体字的编码。汉字存储时都使用两个字节来储存。

算机中的字符编码方式简述

在计算机系统中,我们平时使用最多的是 ASCII 编码字符集,即:American Standard Code for Information Interchange( 美国信息交换标准代码的英文缩写)。其中(注:以下数值均按十进制数字进行介绍):

从 0 - 31:包含有各种控制字符。例如:我们常见的 7 就是响铃、8就是退格键、9就是TAB键、13就是回车键等。这些功能自己都是可以亲自尝试的,很有意思的。

32 - 126:为可见字符部分。但是 32 特殊一些,它是空格键的 ASCII 码(不可见)。

127:Delete(删除键),作用与 08 相同。

总而言之,从 0 - 127(7F:01111111) 的字符可以看作是最高位为 0 的英文 ASCII 字符。

从 128(80:10000000) - 255(FF:11111111):这些字符的最高位为 1,可以用于汉字操作系统的编码(即:汉字内码)。

什么是字符编码

字符编码(英语:Character encoding)、字集码是把字符集中的字符编码为指定集合中某一对象(例如:比特模式、自然数序列、8位组或者电脉冲),以便文本在计算机中存储和通过通信网络的传递。常见的例子包括将拉丁字母表编码成摩斯电码和ASCII。其中,ASCII将字母、数字和其它符号编号,并用7比特的二进制来表示这个整数。通常会额外使用一个扩充的比特,以便于以1个字节的方式存储。

在计算机技术发展的早期,如ASCII(1963年)和EBCDIC(1964年)这样的字符集逐渐成为标准。但这些字符集的局限很快就变得明显,于是人们开发了许多方法来扩展它们。对于支持包括东亚CJK字符家族在内的写作系统的要求能支持更大量的字符,并且需要一种系统而不是临时的方法实现这些字符的编码。

java中编码与解码分别指什么?

问题一:在java中读取文件时应该采用什么编码?

Java读取文件的方式总体可以分为两类:按字节读取和按字符读取。按字节读取就是采用InputStream.read()方法来读取字节,然后保存到一个byte[]数组中,最后经常用new String(byte[]);把字节数组转换成String。在最后一步隐藏了一个编码的细节,new String(byte[]);会使用操作系统默认的字符集来解码字节数组,中文操作系统就是GBK。而我们从输入流里读取的字节很可能就不是GBK编码的,因为从输入流里读取的字节编码取决于被读取的文件自身的编码。举个例子:我们在D:盘新建一个名为demo.txt的文件,写入”我们。”,并保存。此时demo.txt编码是ANSI,中文操作系统下就是GBK。此时我们用输入字节流读取该文件所得到的字节就是使用GBK方式编码的字节。那么我们最终new String(byte[]);时采用平台默认的GBK来编码成String也是没有问题的(字节编码和默认解码一致)。试想一下,如果在保存demo.txt文件时,我们选择UTF-8编码,那么该文件的编码就不在是ANSI了,而变成了UTF-8。仍然采用输入字节流来读取,那么此时读取的字节和上一次就不一样了,这次的字节是UTF-8编码的字节。两次的字节显然不一样,一个很明显的区别就是:GBK每个汉字两个字节,而UTF-8每个汉字三个字节。如何我们最后还使用new String(byte[]);来构造String对象,则会出现乱码,原因很简单,因为构造时采用的默认解码GBK,而我们的字节是UTF-8字节。正确的办法就是使用new String(byte[],”UTF-8”);来构造String对象。此时我们的字节编码和构造使用的解码是一致的,不会出现乱码问题了。

说完字节输入流,再来说说字节输出流。

我们知道如果采用字节输出流把字节输出到某个文件,我们是无法指定生成文件的编码的(假设文件以前不存在),那么生成的文件是什么编码的呢?经过测试发现,其实这取决于写入的字节编码格式。比如以下代码:

OutputStream out = new FileOutputStream("d:\\demo.txt");

out.write("我们".getBytes());

getBytes()会采用操作系统默认的字符集来编码字节,这里就是GBK,所以我们写入demo.txt文件的是GBK编码的字节。那么这个文件的编码就是GBK。如果稍微修改一下程序:out.write("我们".getBytes(“UTF-8”));此时我们写入的字节就是UTF-8的,那么demo.txt文件编码就是UTF-8。这里还有一点,如果把”我们”换成123或abc之类的ascii码字符,那么无论是采用getBytes()或者getBytes(“UTF-8”)那么生成的文件都将是GBK编码的。

这里可以总结一下,InputStream中的字节编码取决文件本身的编码,而OutputStream生成文件的编码取决于字节的编码。

下面说说采用字符输入流来读取文件。

首先,我们需要理解一下字符流。其实字符流可以看做是一种包装流,它的底层还是采用字节流来读取字节,然后它使用指定的编码方式将读取字节解码为字符。说起字符流,不得不提的就是InputStreamReader。以下是java api对它的说明: InputStreamReader是字节流通向字符流的桥梁:它使用指定的charset 读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,否则可能接受平台默认的字符集。说到这里其实很明白了,InputStreamReader在底层还是采用字节流来读取字节,读取字节后它需要一个编码格式来解码读取的字节,如果我们在构造InputStreamReader没有传入编码方式,那么会采用操作系统默认的GBK来解码读取的字节。还用上面demo.txt的例子,假设demo.txt编码方式为GBK,我们使用如下代码来读取文件:

InputStreamReader in = new InputStreamReader(new FileInputStream(“demo.txt”));

那么我们读取不会产生乱码,因为文件采用GBK编码,所以读出的字节也是GBK编码的,而InputStreamReader默认采用解码也是GBK。如果把demo.txt编码方式换成UTF-8,那么我们采用这种方式读取就会产生乱码。这是因为字节编码(UTF-8)和我们的解码编码(GBK)造成的。解决办法如下:

InputStreamReader in = new InputStreamReader(new FileInputStream(“demo.txt”),”UTF-8”);

给InputStreamReader指定解码编码,这样二者统一就不会出现乱码了。

下面说说字符输出流。

字符输出流的原理和字符输入流的原理一样,也可以看做是包装流,其底层还是采用字节输出流来写文件。只是字符输出流根据指定的编码将字符转换为字节的。字符输出流的主要类是:OutputStreamWriter。Java api解释如下:OutputStreamWriter 是字符流通向字节流的桥梁:使用指定的 charset 将要向其写入的字符编码为字节。它使用的字符集可以由名称指定或显式给定,否则可能接受平台默认的字符集。说的很明白了,它需要一个编码将写入的字符转换为字节,如果没有指定则采用GBK编码,那么输出的字节都将是GBK编码,生成的文件也是GBK编码的。如果采用以下方式构造OutputStreamWriter:

OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(“dd.txt”),”UTF-8”);

那么写入的字符将被编码为UTF-8的字节,生成的文件也将是UTF-8格式的。

问题二: 既然读文件要使用和文件编码一致的编码,那么javac编译文件也需要读取文件,它使用什么编码呢?

这个问题从来就没想过,也从没当做是什么问题。正是因为问题一而引发的思考,其实这里还是有东西可以挖掘的。下面分三种情况来探讨,这三种情况也是我们常用的编译java源文件的方法。

1.javac在控制台编译java类文件。

通常我们手动建立一个java文件Demo.java,并保存。此时Demo.java文件的编码为ANSI,中文操作系统下就是GBK.然后使用javac命令来编译该源文件。”javac Demo.java”。Javac也需要读取java文件,那么javac是使用什么编码来解码我们读取的字节呢?其实javac采用了操作系统默认的GBK编码解码我们读取的字节,这个编码正好也是Demo.java文件的编码,二者一致,所以不会出现乱码情况。让我们来做点手脚,在保存Demo.java文件时,我们选择UTF-8保存。此时Demo.java文件编码就是UTF-8了。我们再使用”javac Demo.java”来编译,如果Demo.java里含有中文字符,此时控制台会出现警告信息,也出现了乱码。究其原因,就是因为javac采用了GBK编码解码我们读取的字节。因为我们的字节是UTF-8编码的,所以会出现乱码。如果不信的话你可以自己试试。那么解决办法呢?解决办法就是使用javac的encoding参数来制定我们的解码编码。如下:javac -encoding UTF-8 Demo.java。这里我们指定了使用UTF-8来解码读取的字节,由于这个编码和Demo.java文件编码一致,所以不会出现乱码情况了。

2.Eclipse中编译java文件。

我习惯把Eclipse的编码设置成UTF-8。那么每个项目中的java源文件的编码就是UTF-8。这样编译也从没有问题,也没有出现过乱码。正是因为这样才掩盖了使用javac可能出现的乱码。那么Eclipse是如何正确编译文件编码为UTF-8的java源文件的呢?唯一的解释就是Eclipse自动识别了我们java源文件的文件编码,然后采取了正确的encoding参数来编译我们的java源文件。功劳都归功于IDE的强大了。

3.使用Ant来编译java文件。

Ant也是我常用的编译java文件的工具。首先,必须知道Ant在后台其实也是采用javac来编译java源文件的,那么可想而知,1会出现的问题在Ant中也会存在。如果我们使用Ant来编译UTF-8编码的java源文件,并且不指定如何编码,那么也会出现乱码的情况。所以Ant的编译命令javac有一个属性” encoding”允许我们指定编码,如果我们要编译源文件编码为UTF-8的java文件,那么我们的命令应该如下:

javac destdir="${classes}" target="1.4" source="1.4" deprecation="off" debug="on" debuglevel="lines,vars,source" optimize="off" encoding="UTF-8"

指定了编码也就相当于”javac –encoding”了,所以不会出现乱码了。

问题三:tomcat中编译jsp的情况。

这个话题也是由问题二引出的。既然javac编译java源文件需要采用正确的编码,那么tomcat编译jsp时也要读取文件,此时tomcat采用什么编码来读取文件?会出现乱码情况吗?下面我们来分析。

我们通常会在jsp开头写上如下代码:

%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%

我常常不写pageEncoding这个属于,也不明白它的作用,但是不写也没出现过乱码情况。其实这个属性就是告诉tomcat采用什么编码来读取jsp文件的。它应该和jsp文件本身的编码一致。比如我们新建个jsp文件,设置文件编码为GBK,那么此时我们的pageEncoding应该设置为GBK,这样我们写入文件的字符就是GBK编码的,tomcat读取文件时采用也是GBK编码,所以能保证正确的解码读取的字节。不会出现乱码。如果把pageEncoding设置为UTF-8,那么读取jsp文件过程中转码就出现了乱码。上面说我常常不写pageEncoding这个属性,但是也没出现过乱码,这是怎么回事呢?那是因为如果没有pageEncoding属性,tomcat会采用contentType中charset编码来读取jsp文件,我的jsp文件编码通常设置为UTF-8,contentType的charset也设置为UTF-8,这样tomcat使用UTF-8编码来解码读取的jsp文件,二者编码一致也不会出现乱码。这只是contentType中charset的一个作用,它还有两个作用,后面再说。可能有人会问:如果我既不设置pageEncoding属性,也不设置contentType的charset属性,那么tomcat会采取什么编码来解码读取的jsp文件呢?答案是iso-8859-1,这是tomcat读取文件采用的默认编码,如果用这种编码来读取文件显然会出现乱码。

问题四:输出。

问题二和问题三分析的过程其实就是从源文件àclass文件过程中的转码情况。最终的class文件都是以unicode编码的,我们前面所做的工作就是把各种不同的编码转换为unicode编码,比如从GBK转换为unicode,从UTF-8转换为unicode。因为只有采用正确的编码来转码才能保证不出现乱码。Jvm在运行时其内部都是采用unicode编码的,其实在输出时,又会做一次编码的转换。让我们分两种情况来讨论。

1.java中采用Sysout.out.println输出。

比如:Sysout.out.println(“我们”)。经过正确的解码后”我们”是unicode保存在内存中的,但是在向标准输出(控制台)输出时,jvm又做了一次转码,它会采用操作系统默认编码(中文操作系统是GBK),将内存中的unicode编码转换为GBK编码,然后输出到控制台。因为我们操作系统是中文系统,所以往终端显示设备上打印字符时使用的也是GBK编码。因为终端的编码无法手动改变,所以这个过程对我们来说是透明的,只要编译时能正确转码,最终的输出都将是正确的,不会出现乱码。在Eclipse中可以设置控制台的字符编码,具体位置在Run Configuration对话框的Common标签里,我们可以试着设置为UTF-8,此时的输出就是乱码了。因为输出时是采用GBK编码的,而显示却是使用UTF-8,编码不同,所以出现乱码。

2.jsp中使用out.println()输出到客户端浏览器。

Jsp编译成class后,如果输出到客户端,也有个转码的过程。Java会采用操作系统默认的编码来转码,那么tomcat采用什么编码来转码呢?其实tomcat是根据%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%中contentType的charset参数来转码的,contentType用来设置tomcat往浏览器发送HTML内容所使用的编码。Tomcat根据这个编码来转码内存中的unicode。经过转码后tomcat输出到客户端的字符编码就是utf-8了。那么浏览器怎么知道采取什么编码格式来显示接收到的内容呢?这就是contentType的charset属性的第三个作用了:这个编码会在HTTP响应头中指定以通知浏览器。浏览器使用http响应头的contentType的charset属性来显示接收到的内容。

总结一下contentType charset的三个作用:

1).在没有pageEncoding属性时,tomcat使用它来解码读取的jsp文件。

2).tomcat向客户端输出时,使用它来编码发送的内容。

3).通知浏览器,应该以什么编码来显示接收到的内容。

为了能更好的理解上面所说的解码和转码过程,我们举一个例子。

新建一个index.jsp文件,该文件编码为GBK,在jsp开头我们写上如下代码:

%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="GBK"%

这里的charset和pageEncoding不同,但是也不会出现乱码,我来解释一下。首先tomcat读取jsp内容,并根据pageEncoding指定的GBK编码将读取的GBK字节解码并转换为unicode字节码保存在class文件中。然后tomcat在输出时(out.println())使用charset属性将内存中的unicode转换为utf-8编码,并在响应头中通知浏览器,浏览器以utf-8显示接收到的内容。整个过程没有一次转码错误,所以就不会出现乱码情况。

问题五:Properties和ResourceBundle使用的解码编码。

以上两个是我们常用的类,他们在读取文件过程中并不允许我们指定解码编码,那么它们采取什么解码方式呢?查看源码后发现都是采用iso-8859-1编码来解码

的。这样的话我们也不难理解我们写的properties文件为什么都是iso-8859-1 的了。因为采取任何一个别的编码都将产生乱码。因为iso-8859-1编码是没

有中文的,所以我们输入的中文要转换为unicode,通常我们使用插件来完成,也可以使用jdk自带的native2ascii工具。