coding……
但行好事 莫问前程

Java编程拾遗『搞定编码』

提到编码,也许很多人都有概念,就是普通字符转化位计算机能识别的二进制码的过程。但是具体到各种字符集,编码方式,为什么会有这些编码方式,再到Unicode和UTF-8、UTF-16是什么关系,相信很多人都答不上来。字符集和编码也是很多人比较头疼的问题,我一度也搞不明白。直到之前写String类那篇文章,读了Character源码,并查阅了一堆资料,才真正搞懂编码的来龙去脉,搞明白字符是如何在计算机和程序之间交互的。现在将相关的知识梳理以下,希望对大家有帮助。

1. 字符集

字符(Character)是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。字符集(Character set)是多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同,计算机中常见字符集有:ASCII字符集、GB2312字符集、 GB18030字符集、Unicode字符集等。我们都知道对于计算机而言,它只能识别01串,不管是在内存中还是外部存储设备上,我们所看到的文字、图片、视频等等“数据”在计算机中都是已二进制形式存在的。那么为了能够让计算机识别我们的各种字符,肯定要存在一种规范,把字符映射成某个数字,然后再通过特定的规则,将这个数字转化成计算机能够识别的二进制串。而字符集可以理解位就是用来管理字符到数字的映射的,字符集就像一个字典,我们通过字符就可以查到字符在该字符集下对应的数字(String类那篇文章提到的码位(code point)就是这个意思)。而将这个数字转化成计算机能够识别的二进制串的过程就是编码,这个过程使用的规则就是编码方式。

1.1 ASCII字符集

在早期的计算机系统中,使用的字符是非常少的,一般只包括26个英文字母、数字符号和一些常用符号,对于这些字符进行编码,用1个字节就足够了。这时候,ASCII字符集就出现了。ASCII(American Standard Code for Information Interchange)是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其它西欧语言,它是现今最通用的单字节编码系统。ASCII字符集可以表示128或者256种可能的字符,包含128个字符的ASCII字符集称为标准ASCII码,包含256个字符的ASCII字符集称为扩展ASCII码。

1.1.1 标准ASCII

标准ASCII码可以表示128个字符,主要包括大小写字母、阿拉伯数字、标点符合、预算符号以及一些控制字符。

0~31、127(共33个)是控制字符或者通信专用字符,如控制符:LF(换行)、CR(回车)、DEL(删除)等;通信专用字符:SOH(文头)、EOT(文尾)、ACK(确认)等。ASCII值为8、9、10、13分别表示退格、制表、换号、回车字符;

32~126(共95个)字符,32为空格、48~57为阿拉伯数字、65~90为大写字母、97~122为小写字母,其余为一些标点符号和运算符号!

1.1.2 扩展ASCII

ASCII是单字节字符集,理论上应该可以表示256个字符,但是标准ASCII只表示了128个字符。这是因为标准的ASCII码只使用七位来表示字符,而最高位则是用作奇偶校验的。标准ASCII只能显示26个基本拉丁字母、阿拉伯数目字和英式标点符号,基本上只能应用于现代美国英语,对于其他国家,128个字符肯定不够。于是,这些欧洲国家决定利用字节中闲置的最高位编入新的符号,这样一来,可以表达的字符数最多就为256个,但是随着产生的问题也就来了:不同的国家有不同的字母,可能同一个编码在不同的国家所表示的字符不同。但是不管怎么样,在这些编码中0~127所表示的字符肯定是一样的,不一样的也只是128~255这一段。

 8位的ASCII在欧洲国家表现的不尽人意,那么在其他国家就更加不用说了。比如我国的汉字多达十几万,所以一个字节8位表示的256个字符肯定是不够的,所以为了表示汉字至少需要两个字节,我们常见的汉字就是用两个字节表示的,如GB2312。

1.2 GB*

对于欧美国家来说,ASCII一般能够很好的满足用户的需求,但是当我们使用计算机时,ASCII明显就不满足需求了,我们拥有十多万的汉字,所以为了显示中文,我们必须设计一套编码规则用于将汉字转换为计算机可以接受的数字系统的数。显示中文的常用字符集有:GB2312、GBK、GB18030。

1.2.1 GB2312

GB2312,中国国家标准简体中文字符集,全称《信息交换用汉字编码字符集·基本集》,由中国国家标准总局发布,1981年5月1日实施,使用1/2字节变长编码(因为兼容ASCII)。

在GB2312中,GB2312共收录6763个汉字,其中一级汉字3755个,二级汉字3008个,还收录了拉丁字母、希腊字母、日文等682个全角字符。由于GB2312的出现,它基本上解决了我们日常的需要,它所收录的汉子已经覆盖了中国大陆99.75%的使用平率。但是我国文化博大精深,对于人名、古汉语等方面出现的罕用字,GB2312还是不能处理,于是后面的GBK和GB18030汉字字符集出现了。

1.2.2 GBK

GBK,全称《汉字内码扩展规范》,由中华人民共和国全国信息技术标准化技术委员会1995年12月1日制订,也是汉字编码的标准之一,使用1/2字节变长编码(因为兼容ASCII)。

GBK是GB2312的扩展,它向下与GB2312兼容,向上支持 ISO 10646.1国际标准,是前者向后者过渡过程中的一个承上启下的标准。

1.2.3 GB18030

GB18030,国家标准GB18030《信息技术 中文编码字符集》,是我国计算机系统必须遵循的基础性标准之一。它有两个版本:GB18030-2000、GB18030-2005。其中GB18030-2000仅规定了常用非汉字符号和27533个汉字(包括部首、部件等)的编码,而GB18030-2005是全文强制性标准,市场上销售的产品必须符合,它是GB18030-2000的基础上增加了42711个汉字和多种我国少数民族文字的编码。GB18030标准采用单字节、双字节和四字节三种方式对字符编码。

1.3 Unicode

随着计算机的发展、普及,世界各国为了适应本国的语言和字符都会自己设计一套自己的编码风格,正是由于这种乱,导致存在很多种编码方式,以至于同一个二进制数字可能会被解释成不同的符号。为了解决这种不兼容的问题,Unicode字符集应时而生!上面讲过字符集可以理解为一种字典,那么Unicode字符集可以理解为世界通用字典,无论哪个国家的字符,都能在Unicode字符集中找到对应的编码数值,可以满足跨语言、跨平台进行文本转换、处理的要求。Unicode至今仍在不断增修,迄今而至已收入超过十万个字符,它备受业界认可,并广泛地应用于电脑软件的国际化与本地化过程。像ASCII、GB*字符集都只有一种编码方式,但Unicode有多重编码方式,比如UTF-8、UTF-16、UTF-32,关于编码的问题,下面会详细讲解。这里只需要理解Unicode是个世界通用的字符集就可以了。

2. 编码

上面讲到,为了能够让计算机识别我们的各种字符,肯定要存在一种规范,把字符映射成某个数字,这个就是字符集的作用,专业的表述就是获取字符对应的码位(code point)。但是有了码位之后,还差一步,那就是将码位转成二进制,转换成二进制最简单的方法就是什么规定都没有,直接把十进制转成二进制,这种在单字符集编码中也许还好使,因为计算机可以知道八位就是一个字符。对应那种变长的字符集,假如直接通过十进制转二进制拿到一个二进制串,计算机怎么知道多少位在一起是一个字符。所以可以肯定的是,编码肯定不是直接进行二进制转化,肯定有特殊的规定,这个规定的目的是按照一定的规则把码位翻译成二进制串

字符集和字符编码一般都是成对出现的,如ASCII、IOS-8859-1、GB2312、GBK、GB18030,都既表示了字符集又表示对应的字符编码。Unicode比较特殊,Unicode表示字符集,也可以表示编码(通过读查阅一堆资料,我发现Unicode编码的意思其实就是UTF-16编码,比如Java中char使用unicode编码其实就是在讲char使用UTF-16编码),但是Unicode中不止有一种编码方式,UTF-8、UTF-16、USC、UTF-32都是Unicode字符集的编码方式。

2.1 ASCII编码

2.1.1 标准ASCII编码

标准的ASCII编码使用的是7(2^7 = 128)位二进制数来表示所有的大小写字母、数字和标点符号已经一些特殊的控制字符,最前面的一位统一规定为0。其中0~31及127(共33个)是控制字符或通信专用字符,32~126(共95个)是字符(32是空格),其中48~57为0到9十个阿拉伯数字,65~90为26个大写英文字母,97~122号为26个小写英文字母,其余为一些标点符号、运算符号等。最高位用于奇偶校验,再标准ASCII中最高为都是0。

2.1.1 扩展ASCII编码(EASCII编码)

EASCII编码将标准ASCII编码中的最高为表示码位,将标准ASCII码由7位扩充为8位而成。EASCII的内码是由0到255共有256个字符组成。EASCII码比ASCII码扩充出来的符号包括表格符号、计算符号、希腊字母和特殊的拉丁符号。

2.2 GB*

2.2.1 GB2312编码

GB2312编码是一种1/2字节变长编码方式,它是在ASCII编码的基础上进行扩充的,它规定了:ASCII的字符完整的包含在GB2312里,编码不变,仍然是以0开头,用一个字节来表示一个字符;对于ASCII没有的字符,就用1开头来区分,用两个字节合起来表示一个字符。前面的一个字节(高字节)从0xA1到 0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000个简体汉字了。在这些编码里,还把数学符号、罗马希腊的 字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的“全角”字符,而原来在127 号以下的那些就叫“半角”字符了。在GB2312中,GB2312共收录6763个汉字,其中一级汉字3755个,二级汉字3008个,还收录了拉丁字母、希腊字母、日文等682个全角字符。由于GB2312的出现,它基本上解决了我们日常的需要,它所收录的汉字已经覆盖了中国大陆99.75%的使用频率。GB2312编码就是将这些码位直接转成8位或16位的二进制串。但是对于人名、古汉语等方面出现的罕用字,GB2312还是不能处理,于是后面的GBK和GB18030汉字字符集出现了,GB2312简体中文编码表

2.2.2 GBK编码

GBK编码是一种1/2字节变长编码方式,编码方式跟GB2312一致。GBK字符集编码范围从8140至FEFE(剔除xx7F),首字节在 81-FE 之间,尾字节在 40-FE 之间,共23940个码位,共收录了21003个汉字,相比于GB2312,只是表示范围的改变,编码方式是一样的。GBK编码就是将这些码位直接转成8位或16位的二进制串。

2.2.3 GB18030编码

GB18030编码是一种单字节、双字节和四字节变长编码方式,单字节部分采用GB/T 11383的编码结构与规则,使用0×00至0×7F码位(对应于ASCII码的相应码位)。双字节部分,首字节码位从0×81至0×FE,尾字节码位分别是0×40至0×7E和0×80至0×FE。四字节部分采用GB/T 11383未采用的0×30到0×39作为对双字节编码扩充的后缀,这样扩充的四字节编码,其范围为0×81308130到0×FE39FE39。其中第一、三个字节编码码位均为0×81至0×FE,第二、四个字节编码码位均为0×30至0×39。

对于GB18030编码,比较复杂,我也没有具体去研究其编码方式。但需要清楚:

  • 也是一个多字节编码方案,有一,二,四字节三种变长组合。
  • 的编码空间很大,高达160万,甚至超过了Unicode规定的110万
  • 兼容GB2312、GBK
  • 支持许多少数民族如藏、蒙古、彝、维吾尔等的文字

而且再编程中一般不会使用GB系列编码方式,而会使用Unicode字符集的一些编码,下面详细讲一下UTF系列编码。

2.3 Unicode

2.3.1 Unicode和UTF-8/UTF-16/UTF-32的关系

对于之前讲的ASCII、GB*,概念一般比较独立,字符集跟编码方式一一对应,比如使用GBK字符集,就等同于使用GBK编码。但是讲使用Unicode字符集,却无法确定具体使用哪种编码方式(这点很重要,因为同一个Unicode码位使用不同的编码方式,结果是完全不一样的),因为Unicode字符集有多种编码方式,其中包括UTF-8、UTF-16、UTF-32。这里要特别提一下,通常我们说使用Unicode编码时,其实讲的是使用UTF-16编码

这里再讲一个编码中的一个概念——码元(Code Unit,也称“代码单元”),是指一个已编码的文本中具有最短的比特组合的单元。对于UTF-8来说,码元是8比特长;对于UTF-16来说,码元是16比特长;对于UTF-32来说,码元是32比特长。

2.3.1 UTF-32编码

在Unicode现有的三种编码方式中,UTF-32是最简单的一种编码方式,使用定长4字节编码,一个字符编码过程很简单,只要把该字符的Unicode码位直接转化成32位二进制串即可。

2.3.2 UTF-16编码

UTF-16是一种变长2/4字节编码方式,UTF-16编码会将Unicode字符集中字符的码位编码为16位串(1个码元)或者32位串(2个码元)表示。因为之前讲过Java的char使用Unicode编码(UTF-16),所以这里详细将一下这种编码方式。

Unicode的编码空间从U+0000到U+10FFFF,共有1,112,064个码位(code point)可用来映射字符。Unicode的编码空间可以划分为17个平面(plane),每个平面包含216(65,536)个码位。17个平面的码位可表示为从U+xx0000到U+xxFFFF,其中xx表示十六进制值从0016到1016,共计17个平面。第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0)。其他平面称为辅助平面(Supplementary Planes)。基本多语言平面内,从U+D800到U+DFFF之间的码位区块是永久保留不映射到Unicode字符。UTF-16就利用保留下来的0xD800-0xDFFF区块的码位来对辅助平面的字符的码位进行编码。

从U+0000至U+D7FF以及从U+E000至U+FFFF的码位

第一个Unicode平面(码位从U+0000至U+FFFF)包含了最常用的字符。该平面被称为基本多语言平面,缩写为BMP(Basic Multilingual Plane, BMP)。UTF-16与UCS-2编码这个范围内的码位为16位长的单个码元,编码结果的数值等价于对应的码位。BMP中的这些码位是仅有的可以在UCS-2中表示的码位。UTF-16可看成是UCS-2的父集。在没有辅助平面字符前,UTF-16与UCS-2所指的是同一的意思。但当引入辅助平面字符后,就称为UTF-16了。现在若有软件声称自己支持UCS-2编码,那其实是暗指它不能支持在UTF-16中超过2字节的字集。对于小于0x10000的UCS码,UTF-16编码就等于UCS码。

从U+10000到U+10FFFF的码位

辅助平面(Supplementary Planes)中的码位,在UTF-16中被编码为一对16比特长的码元(即32位,4字节),称作代理对(surrogate pair),具体方法是:

  • 码位减去0x10000,得到的值的范围为20比特长的0..0xFFFFF
  • 高位的10比特的值(值的范围为0..0x3FF)被加上0xD800得到第一个码元或称作高位代理(high surrogate),值的范围是0xD800..0xDBFF。由于高位代理比低位代理的值要小,所以为了避免混淆使用,Unicode标准现在称高位代理为前导代理(lead surrogates)
  • 低位的10比特的值(值的范围也是0..0x3FF)被加上0xDC00得到第二个码元或称作低位代理(low surrogate),现在值的范围是0xDC00..0xDFFF。由于低位代理比高位代理的值要大,所以为了避免混淆使用,Unicode标准现在称低位代理为后尾代理(trail surrogates)

上述算法可理解为:辅助平面中的码位从U+10000到U+10FFFF,共计FFFFF个,即220=1,048,576个,需要20位来表示。如果用两个16位长的整数组成的序列来表示,第一个整数(前导代理)要容纳上述20位的前10位,第二个整数(后尾代理)容纳上述20位的后10位。还要能根据16位整数的值直接判明属于前导整数代理的值的范围(210=1024),还是后尾整数代理的值的范围(210=1024)。因此,需要在基本多语言平面中保留不对应于Unicode字符的2048个码位,就足以容纳前导代理与后尾代理所需要的编码空间。这对于基本多语言平面总计65536个码位来说,仅占3.125%。

由于前导代理、后尾代理、BMP中的有效字符的码位,三者互不重叠,搜索是简单的:一个字符编码的一部分不可能与另一个字符编码的不同部分相重叠。这意味着UTF-16可以通过仅检查一个码元就可以判定给定字符的下一个字符的起始码元。 UTF-8也有类似优点,但许多早期的编码模式就不是这样,必须从头开始分析文本才能确定不同字符的码元的边界。

从U+D800到U+DFFF的码位

Unicode标准规定U+D800..U+DFFF的值不对应于任何字符,仅用于对辅助平面的字符的码位进行编码。

示例:

例如字符?,码位U+10437(辅助平面码位)编码:

  • 0x10437减去0x10000,结果为0x00437,二进制为0000 0000 0100 0011 0111。
  • 分割它的上10位值和下10位值(使用二进制):0000000001 and 0000110111。
  • 添加0xD800到上值,以形成高位:0xD800 + 0x0001 = 0xD801。
  • 添加0xDC00到下值,以形成低位:0xDC00 + 0x0037 = 0xDC37。

所以字符?的UTF-16编码位0xD801 DC37。


对于UTF-16的编码方式,可以的话,建议多看几遍,搞懂,这样能更好的理解Java中字符的处理。另外要提的是因为因为UTF-16编码的高位代理和低位代理可以同时位0,所以UTF-16是不兼容ASCII的。

2.3.3 UTF-8编码

UTF-8是一种针对Unicode的可变长度字符编码,可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的系统无须或只须做少部份修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。UTF-8使用一到四个字节为每个字符编码,编码规则如下:

  • 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
  • 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。

转换表如下:

Unicode UTF-8
0000 ~007F 0XXX XXXX
0080 ~07FF 110X XXXX 10XX XXXX
0800 ~FFFF 1110XXXX 10XX XXXX 10XX XXXX
1 0000 ~1F FFFF 1111 0XXX 10XX XXXX 10XX XXXX 10XX XXXX
20 0000 ~3FF FFFF 1111 10XX 10XX XXXX 10XX XXXX 10XX XXXX 10XX XXXX
400 0000 ~7FFF FFFF 1111 110X 10XX XXXX 10XX XXXX 10XX XXXX 10XX XXXX 10XX XXXX

根据上面的转换表,理解UTF-8的转换编码规则就变得非常简单了:第一个字节的第一位如果为0,则表示这个字节单独就是一个字符。如果为1,连续多少个1就表示该字符占有多少个字节。

对于“中”这个字,对应的unicode码是4E2D=100111000101101,在第三行对应的位置(1110XXXX 10XX XXXX 10XX XXXX),然后,从”中”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“中”的UTF-8编码是11100100 10111000 10101101=E4B8AD,所以汉字“中”的UTF-8编码是E4B8AD。

3. Java中的String

我们从接触char的时候,就知道,Java中char是使用Unicode编码存储的,另外Java中一个char占两个字节,根据上面的讲述,这里讲的Unicode编码肯定就是UTF-16了(因为Java中一个char可以存储一个汉字,但是UTF-8编码,一个汉字占24位,3个字节。UTF-32编码一个汉字为定长32位,4字节。所以Java中char肯定是通过UTF-16编码的)。

现在我们回头来看一下String类中涉及codePoint操作的方法,这里以codePointAt方法为例:

public int codePointAt(int index) {
    if ((index < 0) || (index >= value.length)) {
        throw new StringIndexOutOfBoundsException(index);
    }
    return Character.codePointAtImpl(value, index, value.length);
}

这里没什么好讲的,调用了Character类的静态方法codePointAtImpl。

static int codePointAtImpl(char[] a, int index, int limit) {
    char c1 = a[index];
    if (isHighSurrogate(c1) && ++index < limit) {
        char c2 = a[index];
        if (isLowSurrogate(c2)) {
            return toCodePoint(c1, c2);
        }
    }
    return c1;
}

isHighSurrogate方法用来判断当前字符是不是高位代理,所以这里就很明了了,如果index位置的char是高为代理,并且下一个相邻的字符是低位代理,则说明index索引位置的码位是由两个码元(32位)表示的,所以使用两个char来计算码位的值。如果index位置的char不是高为代理,则直接返回十进制值作为码位。其实这里也解释了为什么在String类那篇文章中写的例子,”a?立b”.codePointAt(2)获得的结果转成字符串是个问号,因为index 2在字符串内部持有的字符数组中是字符’?’的低位代理,codePointAtImpl方法中根本就没进if判断,直接返回了,在UTF-16编码中,低位代理无法表示任何字符,所以表现为一个问号(?)。

参考链接:

  1. 维基百科 — UTF-16
  2. 维基百科 — 码位
  3. 极客学院 — java 中文乱码解决之道
  4. 阮一峰 — 字符编码笔记:ASCII,Unicode 和 UTF-8
  5. 说说Unicode,UTF8,UTF16,BOM,Big endian,Little endian
  6. 知乎 — java中的“中文字符”和“英文字符”各占用几个字节?
  7. 聊聊java中codepoint和UTF-16相关的一些事

赞(0) 打赏
Zhuoli's Blog » Java编程拾遗『搞定编码』
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址