一文弄懂字符串编码

2020-12-03 00:00:00 字符串 一文 弄懂

1. 基本概念

字符(character)

在计算机和电信领域中,字符(Character)是一个信息单位。对使用字母系统或音节文字等自然语言,它大约对应为一个音位、类音位的单位或符号。简单来讲就是一个汉字、假名、韩文字……,或是一个英文、其他西方语言的字母。
字符的例子有:字母、数字系统或标点符号。比如‘a’,‘人’,‘の’,‘*’等都是字符;

抽象字符(abstract character)

抽象字符是字符的抽象,它不仅包括了通常意义上的字符,还包含了计算机中的一些特殊字符。在计算机中,有许多的字符是空白的,甚至是不可打印的。比如ASCII字符集中的0,就是NULL,它就是一个抽象字符。另外控制字符也是一类抽象字符,它是指:对应到语言中一些用来处理文句的概念(类似排版)。例子为打印机或其它显示设备的命令,如Enter或Tab。

抽象字符集(abstract character set)

抽象字符集是多个抽象字符的集合,字符集种类较多,每个字符集包含的字符个数不同,常见字符集名称:ASCII字符集、GB2312字符集、BIG5字符集、 GB18030字符集、Unicode字符集等。

编码(encode)

编码是信息从一种形式或格式转换为另一种形式的过程,在计算机领域语境下的编码,主要指的是将各类信息,如文字、图像、音频等转换为计算机可处理和分析的信息。

解码(decode)

编码的逆过程。

字符编码(Character encoding)

字符编码是把字符集中的字符编码为指定集合中某一对象(例如:比特模式、自然数序列、8位组或者电脉冲),以便文本在计算机中存储和通过通信网络的传递。在计算机中,字符编码就是将字符映射为一个二进制序列;

码点/码位(code point)

码点是在字符集给指定字符分配的编号,是1个非负整数N;在同一个字符集中,码点和字符是一一对应的;比如在Unicode字符集中,字母“A”的编号就是0x41,即U+0041,而汉字“黄”的编号就是0x6731,即U+6731;
在表示一个Unicode的字符时,通常会用“U+”然后紧接着一组十六进制的数字来表示这一个字符。例如“U+0041”代表字符“A”;

码元(code unit)

因为码点实际上只代表了其在字符集中的编号,但是其在计算机中的二进制表示还需要做一次映射,这是码元的作用。码元是在字符编码方案中表示一个码点的最小固定二进制长度,如UTF-8的码元是1byte(8bit),UTF-16是2byte(16bit),UTF-32是4byte(32bit),这也是UTF后的数字的含义,即码元的比特数;
在定长字符编码中(例如UTF-32),一个码点总是由一个码元表示,此时不需做映射,码点的值就等于码元的值;但对于变长编码(例如UTF-8和UTF-16)来说,该映射比较复杂,把一些码点映射到一个码元,把另外一些码点映射到由多个码元组成的序列。

编码空间(code space)

编码空间指的是一个编码集中码点的范围, 例如 Unicode 编码空间就是 0x000000 – 0x10FFFF。

字符编码方案(character encoding scheme)

字符编码方案指的是将字符用一个或多个码元表示的方案,即将字符映射为一个二进制序列的方案。如UTF-8就是一个字符编码方案,其它的还有UTF-16,UTF-32,GBK等等,需要说明的是,一个字符集一般对应着一个编码方案,各个国家和地区在制定编码标准的时候,字符集和编码方案一般都是同时制定的。因此,平常我们所说的字符集,比如:GB2312, GBK, JIS 等,除了有“字符的集合”这层含义外,同时也包含了“编码方案”的含义。
注意:Unicode字符集有多种编码方式,如UTF-8、UTF-16等;ASCII只有一种;大多数MBCS(包括GB2312)也只有一种。

2. 字符集及编码方案

ASCII(美国)

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套计算机编码系统。它主要用于显示现代英语,而其扩展版本EASCII则可以部分支持其他西欧语言,并等同于国际标准ISO/IEC 646。
ASCII 由电报码发展而来。第一版标准发布于1963年,1967年经历了一次主要修订,最后一次更新则是在1986年, 至今为止共定义了128个字符;其中33个字符无法显示(一些终端提供了扩展,使得这些字符可显示为诸如笑脸、扑克牌花式等8-bit符号),且这33个字符多数都已是陈废的控制字符。控制字符的用途主要是用来操控已经处理过的文字。在33个字符之外的是95个可显示的字符。用键盘敲下空白键所产生的空白字符也算1个可显示字符(显示为空白)。

《一文弄懂字符串编码》

缺点

ASCII的局限在于只能显示26个基本拉丁字母、阿拉伯数字和英式标点符号,因此只能用于显示现代美国英语(且处理naïve、café、élite等外来语时,必须去除附加符号)。虽然EASCII解决了部分西欧语言的显示问题,但对更多其他语言依然无能为力。因此,现在的软件系统大多采用Unicode。
最早的英文DOS操作系统的系统内码是:ASCII。计算机这时候只支持英语,其他语言不能够在计算机存储和显示。
在该阶段,单字节字符串使用一个字节存放一个字符(SBCS,Single Byte Character System)。如:”Bob123″占6个字节。
详见维基百科:https://zh.wikipedia.org/wiki/ASCII

EASCII

EASCII码是将ASCII中闲置的最高位(即首位)用来编码新的字符(这些ASCII字符之外的新字符,其最高位总是为1)。换言之,也就是将一个字节中的全部8个比特位用来表示一个字符。比如,法语中的é的编码为130(二进制1000 0010)。
显然,EASCII码虽与ASCII码一样使用单字节编码,但却可以表示最多256个字符(2^8 = 256),比ASCII的128个字符(2^7=128)多了一倍。
因此,在EASCII码中,当第一个比特位(即字节的最高位)为0时,仍表示之前那些常用的ASCII字符(实际的二进制编码为0000 0000 ~ 0111 1111,对应的十进制就是0~127),而为1时就表示补充扩展的其他衍生字符(实际的二进制编码为1000 0000 ~ 1111 1111,对应的十进制就是128~255)。
这样就在ASCII码的基础上,既保证了对ASCII码的兼容性,又补充扩展了新的字符,于是就称之为Extended ASCII(扩展ASCII)码,简称EASCII码。
EASCII码比ASCII码扩充出来的符号包括表格符号、计算符号、希腊字母和特殊的拉丁符号。
详见维基百科:https://zh.wikipedia.org/wiki/EASCII

ISO 8859(欧洲)

不过,EASCII码目前已经很少使用,常用的是ISO/IEC 8859字符编码方案。该方案与EASCII码类似,也同样是在ASCII码的基础上,利用了ASCII的7位编码所没有用到的最高位(首位),将编码范围从原先ASCII码的0x000x7F(十进制为0127),扩展到了0x800xFF(十进制为128255)。
ISO/IEC 8859字符编码方案所扩展的这128个编码中,实际上只有0xA00xFF(十进制为160255)被实际使用。也就是说,只有0xA00xFF(十进制为160255)这96个编码定义了字符,而0x80~0x9F (十进制为128~159)这32个编码并未定义字符。
显然,ISO/IEC 8859字符编码方案同样是单字节编码方案,也同样完全兼容ASCII。
注意,与ASCII、EASCII属于单个独立的字符集不同,ISO/IEC 8859是一组字符集的总称,其下共包含了15个字符集,即ISO/IEC 8859-n,其中n=1,2,3,…,15,16(其中12未定义,所以共15个)。
这15个字符集大致上包括了欧洲各国所使用到的字符(甚至还包括一些外来语字符),而且每一个字符集的补充扩展部分(即除了兼容ASCII字符之外的部分)都只实际使用了0xA00xFF(十进制为160255)这96个编码。
其中,ISO/IEC 8859-1收录了西欧常用字符(包括德法两国的字母),目前使用得最为普遍。ISO/IEC 8859-1往往简称为ISO 8859-1,而且还有一个称之为Latin-1(也写作Latin1)的别名。
详见维基百科:https://zh.wikipedia.org/wiki/ISO/IEC_8859

GB2312、GBK和GB18030(中国)

GB2312

当中国人们得到计算机时,已经没有可以利用的字节状态来表示汉字,况且有6000多个常用汉字需要保存,于是想到把那些ASCII码中127号之后的奇异符号们直接取消掉, 规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(称之为高字节)从0xA1用到0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的”全角”字符,而原来在127号以下的那些就叫”半角”字符了。这种汉字方案叫做 “GB2312”。GB2312 是对 ASCII 的中文扩展。兼容ASCII。
详见维基百科:https://zh.wikipedia.org/wiki/GB_2312

GBK

但是中国的汉字太多了,我们很快就就发现有许多人的人名没有办法在这里打出来,不得不继续把 GB2312 没有用到的码位找出来用上。后来还是不够用,于是干脆不再要求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。结果扩展之后的编码方案被称为 “GBK” 标准,GBK 包括了 GB2312 的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号。
GBK向下完全兼容GB2312编码。支持GB2312编码不支持的部分中文姓,中文繁体,日文假名,还包括希腊字母以及俄语字母等字母。不过这种编码不支持韩国字,也是其在实际使用中与unicode编码相比欠缺的部分。
详见维基百科:https://zh.wikipedia.org/wiki/%E6%B1%89%E5%AD%97%E5%86%85%E7%A0%81%E6%89%A9%E5%B1%95%E8%A7%84%E8%8C%83

GB18030

后来少数民族也要用电脑了,于是我们再扩展,又加了几千个新的少数民族的字,GBK 扩成了 GB18030。从此之后,中华民族的文化就可以在计算机时代中传承了。
GB 18030,全称《信息技术 中文编码字符集》,是中华人民共和国国家标准所规定的变长多字节字符集。其对GB 2312完全向后兼容,与GBK基本向后兼容,并支持Unicode(GB 13000)的所有码位。GB 18030共收录汉字70,244个。
GB 18030主要有以下特点:

  • 采用变长多字节编码,每个字可以由1个、2个或4个字节组成。
  • 编码空间庞大,最多可定义161万个字符。
  • 完全支持Unicode,无需动用造字区即可支持中国国内少数民族文字、中日韩和繁体汉字以及emoji等字符。

详见维基百科:https://zh.wikipedia.org/wiki/GB_18030

Unicode(国际)

Unicode(中文:万国码、国际码、统一码、单一码)是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得计算机可以用更为简单的方式来呈现和处理文字。
Unicode 伴随着通用字符集的标准而发展,同时也以书本的形式对外发表。Unicode 至今仍在不断增修,每个新版本都加入更多新的字符。当前最新的版本为2019年5月公布的12.1.0,已经收录超过13万个字符(第十万个字符在2005年获采纳)。Unicode涵盖的数据除了视觉上的字形、编码方法、标准的字符编码外,还包含了字符特性,如大小写字母。
Unicode发展由非营利机构统一码联盟负责,该机构致力于让 Unicode 方案取代既有的字符编码方案。因为既有的方案往往空间非常有限,亦不适用于多语环境。
Unicode备受认可,并广泛地应用于计算机软件的国际化与本地化过程。有很多新科技,如可扩展置标语言(Extensible Markup Language,简称:XML)、Java编程语言以及现代的操作系统,都采用Unicode编码。

UCS

通用字符集(Universal Character Set)是由ISO制定的ISO 10646(或称ISO/IEC 10646)标准所定义的标准字符集。
通用字符集包括了其他所有字符集。它保证了与其他字符集的双向兼容,即,如果你将任何文本字符串翻译到UCS格式,然后再翻译回原编码,你不会丢失任何信息。
UCS包含了已知语言的所有字符。除了拉丁语、希腊语、斯拉夫语、希伯来语、阿拉伯语、亚美尼亚语、格鲁吉亚语,还包括中文、日文、韩文这样的方块文字,UCS还包括大量的图形、印刷、数学、科学符号。
ISO/IEC 10646定义了一个31位的字符集。
并不是所有的系统都需要支持像组合字符这样的的先进机制。因此ISO 10646指定了如下三种实现级别:

  • 级别1:不支持组合字符和谚文字母字符。
  • 级别2:类似于级别1,但在某些文字中,允许一列固定的组合字符,因为如果没有最起码的几个组合字符,UCS就不能完整地表达这些语言。
  • 级别3:支持所有的通用字符集字符,如,可以在任意一个字符上加上一个箭头或一个鼻音化符号.

历史的进程

历史上存在两个独立的尝试创立单一字符集的组织,即:
1、国际标准化组织(ISO)于1984年创建的ISO/IEC
2、统一码(Unicode)联盟
1991年前后,两个项目的参与者都认识到,世界不需要两个不兼容的字符集。于是,它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。1991年,不包含CJK统一汉字集的Unicode 1.0发布。随后,CJK统一汉字集的制定于1993年完成,发布了ISO 10646-1:1993,即Unicode 1.1。
从Unicode 2.0开始,Unicode采用了与ISO 10646-1相同的字库和字码,即Unicode使用的字符集和相应的码点就是UCS;ISO也承诺,ISO 10646将不会替超出U+10FFFF的UCS-4编码赋值,以使得两者保持一致。两个项目仍都独立存在,并独立地公布各自的标准。但统一码联盟和ISO/IEC JTC1/SC2都同意保持两者标准的码表兼容,并紧密地共同调整任何未来的扩展。

表示方法

在表示一个 Unicode 的字符时,通常会用“U+”然后紧接着一组十六进制的数字来表示这一个字符。在基本多文种平面(英语:Basic Multilingual Plane,简写 BMP。又称为“零号平面”、plane 0)里的所有字符,要用四个数字(即两个byte,共16 bits,例如 U+4AE0,共支持六万多个字符);在零号平面以外的字符则需要使用五个或六个数字。旧版的 Unicode 标准使用相近的标记方法,但却有些微小差异:在 Unicode 3.0 里使用“U-”然后紧接着八个数字,而“U+”则必须随后紧接着四个数字。

编码空间

Unicode的编码空间从U+000000到U+10FFFF,共有1,112,064个码位(code point)可用来映射字符. Unicode的编码空间可以划分为17个平面(plane),每个平面包含65,536个码位。17个平面的码位可表示为从U+xx0000到U+xxFFFF,其中xx表示十六进制值从00到10,共计17个平面。第一个平面称为基本多语言平面(Basic Multilingual Plane, BMP),或称第零平面(Plane 0)。其他平面称为辅助平面(Supplementary Planes,SMP)。
《一文弄懂字符串编码》

编码方式

Unicode的编码方式与 ISO 10646 的通用字符集概念相对应。当前实际应用的统一码版本对应于 UCS-2,使用 16 位的编码空间。也就是每个字符占用 2 个字节。这样理论上一共最多可以表示 216(即 65536)个字符。基本满足各种语言的使用。实际上当前版本的统一码并未完全使用这 16 位编码,而是保留了大量空间以作为特殊使用或将来扩展。
上述 16 位统一码字符构成基本多文种平面。最新(但未实际广泛使用)的统一码版本定义了 16 个辅助平面,两者合起来至少需要占据 21 位的编码空间,比 3 字节略少。但事实上辅助平面字符仍然占用 4 字节编码空间,与 UCS-4 保持一致。未来版本会扩充到 ISO 10646-1 实现级别 3,即涵盖 UCS-4 的所有字符。UCS-4 是一个更大的尚未填充完全的 31 位字符集,加上恒为 0 的首位,共需占据 32 位,即 4 字节。理论上最多能表示 231个字符,完全可以涵盖一切语言所用的符号。
基本多文种平面的字符的编码为 U+hhhh,其中每个 h 代表一个十六进制数字,与 UCS-2 编码完全相同。而其对应的 4 字节 UCS-4 编码后两个字节一致,前两个字节则所有位均为 0。

实现方式

在Unicode里,所有的字符被一视同仁。汉字不再使用“两个扩展ASCII”,而是使用“1个Unicode”,注意,现在的汉字是“一个字符”了,于是,拆字、统计字数这些问题也就自然而然的解决了。
但是,这个世界不是理想的,不可能在一夜之间所有的系统都使用Unicode来处理字符,所以Unicode在诞生之日,就必须考虑一个严峻的问题:和ASCII字符集之间的不兼容问题。
我们知道,ASCII字符是单个字节的,比如“A”的ASCII是65。而Unicode是双字节的,比如“A”的Unicode是0065,这就造成了一个非常大的问题:以前处理ASCII的那套机制不能被用来处理Unicode了。
另一个更加严重的问题是,C语言使用’\0’作为字符串结尾,而Unicode里恰恰有很多字符都有一个字节为0,这样一来,C语言的字符串函数将无法正常处理Unicode,除非把世界上所有用C写的程序以及他们所用的函数库全部换掉。
于是,比Unicode更伟大的东东诞生了,之所以说它更伟大是因为它让Unicode不再存在于纸上,而是真实的存在于我们大家的电脑中。那就是:UTF。
Unicode 的实现方式称为 Unicode转换格式(Unicode Transformation Format,简称为 UTF)。在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对 Unicode 编码的实现方式有所不同换句话说,Unicode的字符编码方案有多种;
《一文弄懂字符串编码》
详见维基百科:https://zh.wikipedia.org/wiki/Unicode#Unicode_%E7%9A%84%E7%BC%96%E7%A0%81%E5%92%8C%E5%AE%9E%E7%8E%B0

UTF-8

UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部分修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或发送文字优先采用的编码。

编码规则

UTF-8使用一至六个字节为每个字符编码(尽管如此,2003年11月UTF-8被RFC 3629重新规范,只能使用原来Unicode定义的区域,U+0000到U+10FFFF,也就是说最多四个字节):

  1. 128个US-ASCII字符只需一个字节编码(Unicode范围由U+0000至U+007F)。
  2. 带有附加符号的拉丁文、希腊文、西里尔字母、亚美尼亚语、希伯来文、阿拉伯文、叙利亚文及它拿字母则需要两个字节编码(Unicode范围由U+0080至U+07FF)。
  3. 其他基本多文种平面(BMP)中的字符(这包含了大部分常用字,如大部分的汉字)使用三个字节编码(Unicode范围由U+0800至U+FFFF)。
  4. 其他极少使用的Unicode 辅助平面的字符使用四至六字节编码(Unicode范围由U+10000至U+1FFFFF使用四字节,Unicode范围由U+200000至U+3FFFFFF使用五字节,Unicode范围由U+4000000至U+7FFFFFFF使用六字节)。
编码空间
十六进制
码点
二进制
UTF-8
二进制/十六机制
注释
000000 – 00007F
128个代码
00000000 00000000 0zzzzzzz0zzzzzzz(00-7F)ASCII字符范围,字节由零开始
七个z七个z
000080 – 0007FF
1920个代码
00000000 00000yyy yyzzzzzz110yyyyy(C0-DF) 10zzzzzz(80-BF)第一个字节由110开始,接着的字节由10开始
三个y;二个y;六个z五个y;六个z
000800 – 00D7FF
00E000 – 00FFFF
61440个代码
00D800-00DFFF除外
00000000 xxxxyyyy yyzzzzzz1110xxxx(E0-EF) 10yyyyyy 10zzzzzz第一个字节由1110开始,接着的字节由10开始
四个x;四个y;二个y;六个z四个x;六个y;六个z
010000 – 10FFFF
1048576个代码
000wwwxx xxxxyyyy yyzzzzzz11110www(F0-F7) 10xxxxxx 10yyyyyy 10zzzzzz将由11110开始,接着的字节由10开始

可以看到UTF-8的编码还是较为复杂的,为什么要这么设计呢?先说结论,因为这样设计可以使得UTF-8编码后的字节序列,天然包含了每个字节是几个字节字符编码的组成部分的信息,同时保证了一个字符的字节序列不会包含在另一个字符的字节序列中。
我们可以看到对于UTF-8的编码规则,Unicode在范围D800-DFFF中不存在任何字符,基本多文种平面中约定了这个范围用于UTF-16扩展标识辅助平面(两个UTF-16表示一个辅助平面字符)。当然,任何编码都是可以被转换到这个范围,但在unicode中他们并不代表任何合法的值。
对上述提及的第四种字符而言,UTF-8使用四至六个字节来编码似乎太耗费资源了。但UTF-8对所有常用的字符都可以用三个字节表示,而且它的另一种选择,UTF-16编码,对前述的第四种字符同样需要四个字节来编码,所以要决定UTF-8或UTF-16哪种编码比较有效率,还要视所使用的字符的分布范围而定。不过,如果使用一些传统的压缩系统,比如DEFLATE,则这些不同编码系统间的的差异就变得微不足道了。若顾及传统压缩算法在压缩较短文字上的效果不大,可以考虑使用Unicode标准压缩格式(SCSU)。
互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。互联网邮件联盟(IMC)建议所有电子邮件软件都支持UTF-8编码。

示例

例如,希伯来语字母aleph(א)的Unicode代码是U+05D0,按照以下方法改成UTF-8:

  • 它属于U+0080到U+07FF区域,这个表说明它使用双字节,_110_yyyyy _10_zzzzzz.
  • 十六进制的0x05D0换算成二进制就是101-1101-0000.
  • 这11位数按顺序放入”y”部分和”z”部分:11010111 10010000.
  • 最后结果就是双字节,用十六进制写起来就是0xD7 0x90,这就是这个字符aleph(א)的UTF-8编码。

UTF-8编码字节含义

  • 对于UTF-8编码中的任意字节B,如果B的第一位为0,则B独立的表示一个字符(ASCII码);
  • 如果B的第一位为1,第二位为0,则B为一个多字节字符中的一个字节(非ASCII字符);
  • 如果B的前两位为1,第三位为0,则B为两个字节表示的字符中的第一个字节;
  • 如果B的前三位为1,第四位为0,则B为三个字节表示的字符中的第一个字节;
  • 如果B的前四位为1,第五位为0,则B为四个字节表示的字符中的第一个字节;

因此,对UTF-8编码中的任意字节,根据第一位,可判断是否为ASCII字符;根据前二位,可判断该字节是否为一个字符编码的第一个字节;根据前四位(如果前两位均为1),可确定该字节为字符编码的第一个字节,并且可判断对应的字符由几个字节表示;根据前五位(如果前四位为1),可判断编码是否有错误或数据传输过程中是否有错误。

设计UTF-8的理由

UTF-8的设计有以下的多字符组序列的特质:

  • 单字节字符的最高有效比特永远为0。
  • 多字节序列中的首个字符组的几个最高有效比特决定了序列的长度。最高有效位为110的是2字节序列,而1110的是三字节序列,如此类推。
  • 多字节序列中其余的字节中的首两个最高有效比特为10。

UTF-8的这些特质,保证了一个字符的字节序列不会包含在另一个字符的字节序列中。这确保了以字节为基础的部分字符串比对(sub-string match)方法可以适用于在文字中搜索字或词。有些比较旧的可变长度8位编码(如Shift JIS)没有这个特质,故字符串比对的算法变得相当复杂。虽然这增加了UTF-8编码的字符串的信息冗余,但是利多于弊。另外,数据压缩并非Unicode的目的,所以不可混为一谈。即使在发送过程中有部分字节因错误或干扰而完全丢失,还是有可能在下一个字符的起点重新同步,令受损范围受到限制。
另一方面,由于其字节序列设计,如果一个疑似为字符串的序列被验证为UTF-8编码,那么我们可以有把握地说它是UTF-8字符串。一段两字节随机序列碰巧为合法的UTF-8而非ASCII的几率为32分1。对于三字节序列的几率为256分1,对更长的序列的几率就更低了。
详见维基百科:https://zh.wikipedia.org/wiki/UTF-8

UTF-16

UTF-16把Unicode字符集的抽象码位映射为16位长的整数(即码元)的序列,用于数据存储或传递。Unicode字符的码位,需要1个或者2个16位长的码元来表示,因此这是一个变长表示。

编码规则

编码空间
十六进制
UTF-16
十六进制
UTF-16BE
十六进制
UTF-16LE
十六进制
000000-00D7FF
00E000-00FFFF
63488个代码
使用一个码元(16bit)表示,其值等于码点:
0000-D7FF
E000-FFFF
0000-D7FF
E000-FFFF
对于UTF-16BE的每一个码元,颠倒其高位字节和低位字节,比如1234变为3412
010000-10FFFF
1048576个代码
使用2个码元(32bit)表示,其转换规则是:
1. 码位减去**0x10000,**得到的值的范围为20比特长的0~0xFFFFF
1. 高位的10比特的值(值的范围为0~0x3FF)被加上0xD800得到第一个码元或称作高位代理(high surrogate),值的范围是0xD800…0xDBFF
1. 低位的10比特的值(值的范围也是0~0x3FF)被加上0xDC00得到第二个码元或称作低位代理(low surrogate),现在值的范围是0xDC00…0xDFFF
等同于UTF-16对于UTF-16BE的每一个码元,颠倒其高位字节和低位字节,比如d83dde02变为3dd802de
00D800-00DFFF不对应于任何字符不对应于任何字符不对应于任何字符

示例

例如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的字节序问题

UTF-16不同于UTF-8,其一个码元是2个字节(16 Bit),所以存在字节序的问题;常见的字节序分为大端序(Big-Endian,简写为BE)和小端序(Little-Endian,简写为LE)。
UTF-16的大端序和小端序存储形式都在用。一般来说,以Macintosh制作或存储的文字使用大端序格式,以Microsoft或Linux制作或存储的文字使用小端序格式。
为了弄清楚UTF-16文件的大小端序,在UTF-16文件的开首,都会放置一个U+FEFF字符作为BOM(Byte Order Mark,UTF-16LE以FF FE代表,UTF-16BE以FE FF代表),以显示这个文本文件是以UTF-16编码,其中U+FEFF字符在UNICODE中代表的意义是ZERO WIDTH NO-BREAK SPACE,顾名思义,它是个没有宽度也没有断字的空白。
以下的例子有四个字符:“朱”(U+6731)、半角逗号(U+002C)、“聿”(U+807F)、“?”(U+2A6A5)。

使用UTF-16编码的例子
编码名称编码次序编码
BOM,?
UTF-16LE小端序,不含BOM31 672C 007F 8069 D8 A5 DE
UTF-16BE大端序,不含BOM67 3100 2C80 7FD8 69 DE A5
UTF-16LE小端序
,包含BOM
FF FE31 672C 007F 8069 D8 A5 DE
UTF-16BE大端序,包含BOMFE FF67 3100 2C80 7FD8 69 DE A5

详见维基百科:https://zh.wikipedia.org/wiki/UTF-16

UTF-32

UTF-32是32位Unicode转换格式的缩写。UTF-32是一种用于编码Unicode的协定,该协定使用32位比特对每个Unicode码位进行编码(但前导比特数必须为零,故仅能表示221个Unicode码位)。与其他可变长度的Unicode转换格式(UTF)相比,UTF-32编码长度是固定的,UTF-32中的每个32位值代表一个Unicode码位,并且与该码位的数值完全一致。
详见维基百科:https://zh.wikipedia.org/wiki/UTF-32

UTF-n总结

字符的最终的二进制编码由基本单位码元组成,即由一个或多个码元这样的最小基本单元构成的。三种不同的UTF编码方式对应着不同长度的码元,其中,UTF-8码元长度是1个字节,UTF-16的码元长度是2个字节,UTF-32的码元长度是4个字节。
《一文弄懂字符串编码》
上图中展示了4个字符在UTF-8、UTF-16和UTF-32三种编码方式中所对应的编码,其中每个方框中都有一个字符,字符的下方是在相应的编码方式中所对应的十六进制的编码,用小竖线隔开的是不同的码元;

编码方式码元类别编码字节数字节序备注
UTF-81字节码元单字节(ASCII字符)或多字节(非ASCII字符)无字节序问题扩展性好,理论上支持的字节数无上限
UTF-162字节码元双字节(BMP字符)或四字节(非BMP字符)有字节序问题扩展性差,目前最多支持四个字节
UTF-324字节码元四字节有字节序问题扩展性差,目前最多支持四个字节

3. Java字符串

概念

java内存中使用的编码

java内存中使用的编码是Unicode,具体编码方式是UTF-16

由来

最初Unicode的编码数量并没有超过65,535 (0xFFFF),早期Java版本中使用16bit的char表示当时全部的Unicode字符。后来Unicode字符集扩展到了1,114,111 (0x10FFFF)(在Unicode标准2.0用引入了辅助编码平面SMP,在3.1首次为SMP的部分编码分配了字符), JAVA中的char已经不足以表示Unicode的全部编码(需要32bit),JSR-204的专家讨论了很多方法想要解决这个问题,其中包括:

  • 设计一种新的字符类型char32来替换原有的char
  • 用int来表示code point,同时保留,并为String和StringBuffer等增加兼容char和int表示的api

  • 最后处于内存占用和兼容性等方面的考虑,采用了如下方法:
  • 在底层api中用int来表示code point,比如在Character类中
  • 所有的字符串都char表示,并采用utf16的格式来表示,并提倡在高层api中使用这种方式
  • 提供便于在int(code point)和char之间转换的方法,用于必要时候两者的转换

前文提到了UTF-16用两个编码单元来表示超过U+FFFF的1,048,576 (1024*1024)个字符,即一个高位代理一个低位代理,Java中与之对应的概念就是”代理对(surrogate pair)“。

java.lang.String

可以看到在JDK1.8中的String的内部是一个char的数组;

//JDK版本:jdk1.8.0_151
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence { 
    /** The value is used for character storage. */
    private final char value[];
    
    ...
}

JVM平台默认编码

Java中获取JVM的默认编码可以通过方法java.nio.charset.Charset#defaultCharset来获取,

public static Charset defaultCharset() { 
    if (defaultCharset == null) { 
        synchronized (Charset.class) { 
            String csn = AccessController.doPrivileged(
                new GetPropertyAction("file.encoding"));
            Charset cs = lookup(csn);
            if (cs != null)
                defaultCharset = cs;
            else
                defaultCharset = forName("UTF-8");
        }
    }
    return defaultCharset;
}

上面的代码可以看出, 在JVM中defaultCharset()是在初始化阶段被调用, 且只会初始化一次, 首先会取file.encoding指定的字符集, 如果取不到则使用系统默认字符集(如: windows下为GBK,mac下为UTF-8), 然后通过取到的字符集名称(csn)去获取Charset对象, 如果能获取到则将其设为defaultCharset, 如果取不到则将defaultCharset设置为UTF-8字符集, defaultCharset一旦被初始化后, 在JVM之后的运行过程中
就无法再进行更改, 比如在JVM启动后在程序中使用properties.setProperty(“file.encoding”,“UTF-8”);也不会改变defaultCharset的值
如果想指定defaultCharset的值, 则可以通过JVM启动参数(-Dfile.encoding=“UTF-8”)来显示的指定此JVM的字符集。

外部资源编码

假如我们需要从磁盘文件、数据库记录、网络传输一些字符,那么这些字符串内容对于JVM来说其实都是外部资源,对于外部资源,可能有很多种不同格式的编码,例如:GBK、UTF-8、GB18030等等。因此就需要处理各种各样的编码问题,在处理之前,必须明确“源”的编码,然后用指定的编码方式正确读取到内存中。如果是一个方法的参数,实际上必须明确该字符串参数的编码,因为这个参数可能是另外一个日文系统传递过来的。当明确了字符串编码时候,就可以按照要求正确处理字符串,以避免乱码。
在对字符串进行解码编码的时候,应该调用下面的方法:
**getBytes(String charsetName) **
String(byte[] bytes, String charsetName)
而不要使用那些不带字符集名称的方法签名,通过上面两个方法,可以对内存中的字符进行重新编码。

Java源文件编码

Java源文件实际上可看作是一类特殊的外部资源,它本质上是一些文本文件,文本文件的编码格式是多种多样的,那么在编译时,Java编译器会自动将这些编码按照Java文件的编码格式正确读取后产生class文件,这里的class文件编码是Unicode编码(具体说是UTF-16编码)。
因此,在Java代码中定义一个字符串:
String s=“汉字”;
不管在编译前java文件使用何种编码,在编译后成class后,他们都是一样的—-Unicode编码表示。
那么java编译器在编译源文件时使用的编码是什么呢?

javac -encoding utf-8

在编译时可以以以上命令指定编码格式,如果不指定那么java编译器会使用系统默认的编码来编译;

字符串编码相关函数

  • Character.toCodePoint(char high, char low),return int,将两个UTF16的char(两个UTF16码元)转换为code point
  • Character.toChars(int codePoint), return char[],将code point转换为一个或两个UTF16码元
  • isSupplementaryCodePoint(int codePoint), 判断一个code point是否SMP(Unicode中超过U+FFFF)的字符
  • Character.isSurrogate(char ch), 判断一个char是否为UTF16超过U+FFFF的两代码单元的字符的一个代码单元
  • Character.isHighSurrogate(char ch), 判断是否UTF16中两单元字符的高位单元
  • Character.isLowSurrogate(char ch), 判断是否UTF16中两单元字符的低位单元
  • Stirng提供的length(), 这是一个比较常用的方法,但是它的实际含义是UTF16码元的个数,也就是说如果字符串中包含了两代码单元的字符,那么length的值比实际的字符个数要多
  • String提供的codePointCount(), 这个是返回的代码点的个数,对于不包含两代码单元的字符时,其值等于length的值,包含时,其值为字符的个数,小于length的值
  • String.codePoints()可以返回一个字符串中对应的所有码点的流(IntStream)()
  • StringBuilder和StringBuffer主要提供的都是string和char的append方法,但是也提供了一个可以通过codePoint添加字符的方法 appendCodePoint(int codePoint)
String str = "666?";
System.out.println("字符串长度:" + str.length());
System.out.println("码点个数:" + str.codePointCount(0, str.length()));
//输出所有码点
IntStream codePoints = str.codePoints();
codePoints.forEach(s-> System.out.println("码点:" + Integer.toHexString(s)));

参考

https://www.cnblogs.com/williamjie/p/9268151.html
https://blog.csdn.net/huningjun/article/details/79037309
https://blog.csdn.net/liangwanmian/article/details/78386014
https://www.jianshu.com/p/ad4bff4d9fa3
https://www.cnblogs.com/chiguozi/p/5860364.html
https://www.cnblogs.com/benbenalin/p/6897591.html
https://www.cnblogs.com/benbenalin/p/6921553.html
https://www.cnblogs.com/fuyoucaoyu/articles/5707911.html
https://www.cnblogs.com/chrischennx/p/6623610.html
https://www.bbsmax.com/A/gGdXGORG54/
https://blog.csdn.net/u010234516/article/details/52842170

    原文作者:文如王勃三生慧
    原文地址: https://blog.csdn.net/hf19931101/article/details/97669886
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。

相关文章