众所周知,
Unicode
字符集;UTF-16
。char
类型用于表示 UTF-16
编码方式下 Unicode
编码的一个代码单元。从字面意思来看,字符(character)就是各种文字和符号,包括文字、标点符号、图形符号、数字等。
而字符集(character set)则是一个系统支持的所有抽象字符的集合。
乍看起来,也并没有什么难的,但是,这仅是通常意义上抽象的字符集;而我们(程序猿们)通常所说的字符集,其实是指编码字符集(coded character set)(有人也叫它字符编码集)。
那么,什么是编码字符集呢?
对于计算机来说,任何字符其实都是一张图片,计算机把这个图片内容绘制在屏幕上,我们就能看到它了。
为了便于传输与编辑,人们事先在每个计算机上存好一套字符库,记录了每个字符的样子,其实就是字体文件。每个字符对应一个唯一的 ID,这样计算机处理字符以及传输字符时,就能只处理这些 ID,实际上就是一连串的整数。
而这个唯一的 ID 就叫做码点。专业一点来说,码点(code point)就是指与一个编码表中的某个字符对应的代码值。
比如:
在 ASCII 字符集中,
- 字符 '0' 对应的码点(十进制)是 48 # 未验证是否 ASCII 的码点用十进制表示
- 字符 'A' 对应的码点是 65
- 字符 'a' 对应的码点是 97
在 Unicode 字符集中,
- 字符 '0' 对应的码点(十六进制)是 U+0030
- 字符 '™' 对应的码点是 U+2122
- 字符 '回' 对应的码点是 U+56DE
而编码字符集,则是指这个字符集里的每一个字符,都有一个对应的唯一代码值,也就是码点(可以看做是这个字符在编码字符集里的序号)。
而常见的字符集有:
要弄明白 ASCII 字符集是什么,就要从它的历史开始说起。
计算机一开始发明的时候是用来解决数字计算的问题,后来人们发现,计算机还可以做更多的事,例如文本处理。
—— Java为什么选择unicode字符集?字符编码的那些事
但是因为计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理。
最早的计算机在设计时采用 8 个比特(bit)作为一个字节(byte),所以,一个字节能表示的最大的整数就是 255(二进制 11111111 = 十进制 255)。
—— 漫画:什么是字符集和编码?ASCII、UTF-8、UTF-16、UTF-32 又是什么?
在计算机中,1 字节对应 8 位二进制数,而每位二进制数有 0、1 两种状态,因此 1 字节可以组合出 256 种状态。(有些重复,不要在意)
如果这 256 种状态每一个都对应一个符号,就能通过 1 字节的数据表示 256 个字符。美国人(美国国家标准协会 ANSI)于是就制定了一套编码(其实就是个字典),描述英语中的字符和这 8 位二进制数的对应关系,这被称为 ASCII 码。
ASCII 码一共定义了 128 个字符,例如大写的字母 A 是 65(这是十进制数,对应二进制是 0100 0001)。这 128 个字符只使用了 8 位二进制数中的后面 7 位,最前面的一位统一规定为 0。
—— 浅谈Unicode和char的关系(Java)
ASCII(American Standard Code for Information Interchange,美国信息互换标准代码)是一套基于拉丁字母的字符编码,主要用于显示英语字符。
ASCII 共收录了 128 个字符,用一个字节就可以存储,是现今最通用的单字节编码系统,它等同于国际标准 ISO/IEC 646。
ASCII 规范于 1967 年第一次发布,最后一次更新是在 1986 年,共 128 个字符,包括 33 个无法显示的控制字符和 95 个可显示字符(包括空白字符)。
—— ASCII码一览表,ASCII码对照表
—— ASCII码对照表
后来,随着计算机在世界各地的流行与发展,人们越发地发现,ASCII 字符集里那可怜的 128 个字符已经不能再满足他们的需求了。
因为这个世界,不是所有人都在用英语,有法语、有德语、有俄罗斯语等等,很多国家用的不是英文,他们的字母里有许多是 ASCII 字符集里没有的。于是这个时候,为了可以在计算机中保存他们各自国家的文字,人们就在想,一个字节能够表示的数字(编号)有 256 个,而 ASCII 字符集只用到了 0x00~0x7F,也就是只占用了前 128 个,后面 128 个数字不用白不用,因此很多人打起了后面这 128 个数字的主意。
它们用 0x7F(127) 之后的空位来表示新的字母、符号,甚至还加入了很多画表格时需要用到的横线、竖线、交叉等字符,一直将 256 个字符全部用完。
但是,同时产生这样的想法并实施行动的,并不只是一个国家,于是乎,又一个问题产生了:不同国家(似乎大部分是欧洲国家,并未验证)的字符集可能不同,即使他们很默契地没有改动前 128 位(用于兼容 ASCII 字符集),但后 128 位也会因为国家的不同、语言的不同而分别对应不同的字符。这就导致了当时世界上出现了大量各式各样的 OEM 字符集。
而在这些字符集中,同一个编码序号表示的字符可能完全不同,例如:144 在阿拉伯人的字符集中是گ
,而在俄罗斯的字符集中却是ђ
。
—— Java为什么选择unicode字符集?字符编码的那些事
—— 浅谈Unicode和char的关系(Java)
—— 常用编码字符集
Note!关于 OEM 的解释可以参考以下几篇文章:
Note!同时,随着各式各样的 OEM 字符集陆续出现,软件开发者们迎来了他们的噩梦。他们想要把软件卖到国外,就需要使用不同国家的语言。于是,他们提出了一个概念:代码页(Code Page,也叫内码表)。只要切换到相应语言对应的代码页,就可以正确显示该语言。详情请参考➡️ANSI编码与代码页(Code Page)
随着各种 OEM 字符集的出现,世界各国人民欧美?都能使用计算机表示他们本国的语言了,一切看起来似乎都皆大欢喜(即使内码表比较麻烦,但也能用)。
但是,当计算机传到中国后,我们惊骇地发现,我们已经没有可以利用的字节状态来表示汉字了,即便是创造我们自己的 OEM 字符集也根本没用,因为我们的汉字,可不仅仅是几百个那么简单,而是成千上万,就这,还只是常用字而已。
而不管是前面提到的 ASCII 字符集还是后面衍生出的各种 OEM 字符集,它们都是基于单字节编码,也就是说,一个字节翻译成一个字符。这对于拉丁语系国家来说可能没有什么问题,因为他们通过扩展第 8 个比特,就可以得到 256 个字符了,完全足够用来表示它们的语言。
但是对于亚洲国家来说,256 个字符是远远不够用的,连塞牙缝都不够,不管是中国还是日本,还是韩国,都有着成千上万的文字。
—— 关于字符编码,你所需要知道的
—— 常用编码字符集
而如果要表示这些文字,通过简单地扩充 ASCII 是绝不可能办到的。于是,一个迫切的问题出现了:需要创造一种全新结构的不同于 ASCII 的字符集!
而为了解决这个问题,也是为了可以更方便地使用电脑,亚洲国家就发明了多字节编码方式,相应的字符集也被称为多字节字符集(MBCS,Multi-Byte Chactacter Set)。而在这其中,中国使用的就是双字节字符集编码(DBCS,Double Byte Character Set),包括:GB2312
、GBK
、GB18030
。
既然我们说中国使用的是双字节字符集编码(DBCS),那么,到底是怎么使用的呢?
参考了 OEM 字符集的做法,中国也对 ASCII 字符集做了兼容,前 127 号字符保持不变,而从 127 号之后就是我们新加入的字符。
规定:一个小于 127 的字符的意义与 ASCII 相同,但两个大于 127 的字符连在一起时,就表示一个汉字,前面的一个字节(高字节)从 0xA1 用到 0xF7,后面一个字节(低字节)从 0xA1 到 0xFE,这样我们就可以组合出大约 7000 多个简体汉字了。
在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母等都统统重新编了两个字节长的编码,这就是常说的“全角”字符,而原来在 127 号以下的那些就叫"半角"字符了。于是就把这种汉字方案叫做 GB2312。
GB2312 是对 ASCII 的中文扩展。
—— 常用编码字符集
后来,随着计算机在中国的普及,一个新的问题出现了,并日渐常见:很多人的名字打不出来——一些罕见的字并未收录在 GB2312 中!
这怎么办呢?
研究者们想出了一个办法:
不再要求低字节一定是 127 号之后的内码,只要第一个字节是大于 127 就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。
—— 常用编码字符集
这个办法最终形成了一套新的编码方案,也就是 GBK(汉字内码扩展规范) 字符集。
GBK 不仅包括了 GB2312 的所有内容,同时还增加了近 20000 个新的汉字(包括繁体字)和符号。
后来,我们又在 GBK 中新加入了数千个少数民族的字符,于是,GBK 便顺势扩展成为了 GB18030。
从此,我大天朝的文化在虚拟世界开始传承!
而事实上,GB18030 也是包含两个版本的:
同时,GB18030 收录的字符事实上分别以单字节、双字节和四字节编码。
Note!具体信息这里不再讨论,可自省查找细节或参考以下给出的部分资料,这里仅用于了解字符集的发展历史。
由于 GB 系字符集采用双字节表示汉字,但同时又兼容 ASCII,因此,事实上 GB 系字符集是单字节的英文字符和至少双字节的汉字字符同时存在的。
因此,那时候的程序猿们为了使应用支持中文处理,必须要注意字串里的每一个字节的值,如果这个值是大于 127 的,那么就认为一个双字节字符集(这里忽略四字节,不进行讨论)里的字符出现了。
那时候凡是受过编程学习的程序员都要每天念下面这个咒语数百遍的折磨:
“一个汉字算两个英文字符!一个汉字算两个英文字符……”
—— 常用编码字符集
事实上,我还在大学时,也背过相似的话
说完了我大华夏的字符集发展历史,这个时候,随着历史的车轮滚滚向前,一个新的问题再次产生,那就是:
世界各地,全球各地,几乎各个国家都搞出了一套自己的编码标准,但是,互相之间谁也不懂谁的编码,谁也不支持别人的编码,连大陆和台湾这样只相隔了 150 海里,使用着同一种语言的兄弟地区,也分别采用了不同的编码方案(GB 系 VS BIG5)。
于是乎,有人意识到,需要有一种新的标准方案来展示世界上所有语言中的所有字符了,而正是出于这个目的,Unicode 诞生了。
多语言软件制造商组成的统一码联盟(The Unicode Consortium)于 1991 年发布的统一码标准(The Unicode Standard),定义了一个全球统一的通用字符集即 Unicode 字符集解决了上述的问题。
统一码标准为每个字符提供一个唯一的编号,旨在支持世界各地的交流,处理和显示现代世界各种语言和技术学科的书面文本。
此外,它支持许多书面语言的古典和历史文本,不管是什么平台,设备,应用程序或语言,都不会造成乱码问题, 它已被所有现代软件供应商采用,是目前所有主流操作系统,搜索引擎,浏览器,笔记本电脑和智能手机以及互联网和万维网(URL,HTML,XML,CSS,JSON 等)中表示语言和符号的基础。统一码标准的一个版本由核心规范、Unicode 标准、代码图、Unicode 标准附件以及 Unicode 字符数据库(Unicode Character Database 简写成 UCD)组成,同时也是开发的字符集,在不断的更加和增加新的字符。
—— Unicode 及编码方式概述
而且,在 Unicode 标准中,仅仅为每个字符分配了一个唯一的字符编号(代码点,Code Point),对于这个数字对应的二进制串如何存储并没有规定。
值得注意的是:这个数字采用 U+ 紧跟着十六进制数表示。例如:U+56DE
代表汉字 回
。
世界经济日益全球化的同时,一个应用程序需要在全球范围内使用势在必然,基于 Unicode 的应用程序能够很好地处理来自世界各地的用户文本,并适应其文化习俗。 它通过消除每种语言的构建,安装和维护更新来最大限度地降低成本。Unicode(与其并行 ISO 10646 标准)标准除了覆盖全球所有地区国家使用的字符外,它还定义了一系列文本处理的数据和算法,极大简化了 Unicode 的应用,并确保所有遵守其标准的软件产生相同的结果。在过去十年的广泛应用中,Unicode 成为互联网的基石。
Unicode 编码字符集旨在收集全球所有的字符,为每个字符分配唯一的字符编号即
代码点
(Code Point
),用 U+ 紧跟着十六进制数表示。
所有字符按照使用上的频繁度划分为 17 个平面(编号为 0-16),即:
基本的多语言平面
和增补平面(辅助平面)
。
基本的多语言平面
(英文为 Basic Multilingual Plane,简称BMP
)又称平面 0
,收集了使用最广泛的字符,代码点从U+0000
到U+FFFF
,每个平面有 216=65536 个码点;增补平面(辅助平面)
从平面 1-16,分为增补多语言平面(平面 1)
、增补象形平面(平面 2)
、保留平面(平 3-13)
、增补专用平面
等,每个增补平面也有 216=65536 个码点。所以 17 个平面总计有 17 × 65,536 = 1,114,112 个码点。
- Coded Character Set(CCS): 即编码字符集,给字符表里的抽象字符编上一个数字,也就是字符集合到一个整数集合的映射。这种映射称为编码字符集,Unicode 字符集就是属于这一层的概念;
- Character Encoding Form(CEF):即字符编码表,根据一定的算法,将编码字符集(CCS) 中字符对应的码点转换成一定长度的二进制序列,以便于计算机处理,这个对应关系被称为字符编码表,UTF-8、 UTF-16 属于这层概念;
- Code Point: 码点,简单理解就是字符的数字表示。一个字符集一般可以用一张或多张由多个行和多个列所构成的二维表来表示。二维表中行与列交叉的点称之为码点,每个码点分配一个唯一的编号,称之为码点值或码点编号,除开某些特殊区域(比如代理区、专用区)的非字符码点和保留码点,每个码点唯一对应于一个字符。
- Code Unit:代码单元,是指一个已编码的文本中具有最短的比特组合的单元。对于 UTF-8 来说,代码单元是 8 比特长;对于 UTF-16 来说,代码单元是 16 比特长。换一种说法就是 UTF-8 的是以一个字节为最小单位的,UTF-16 是以两个字节为最小单位的。
- Code Space:码点空间,字符集中所有码点的集合。
- BOM( Byte Order Mark ):字节序,出现在文件头部,表示字节的顺序,第一个字节在前,就是”大头方式”(Big-Endian),第二个字节在前就是”小头方式”(Little-Endian)。这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》,在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位。
Unicode 字符集中的字符可以有多种不同的编码方式,如 UTF-8
、UTF-16
、UTF-32
、压缩转换等。这里的 UTF
是 Unicode Transformation Format
的缩写,即统一码转换格式,将 Unicode 编码空间中每个码点和字节序列进行一一映射的算法。
Note!编码:将字母,数字,图片,符号等转换为不同的比特序列代表不同的字符;
Note!解码:将存储在计算机中的比特位序列(或者叫二进制序列)解析显示出来成对应的字母,数字,图片和符号。
UTF-8
是一种变长的编码方式,一般用 1-4 个字节序列来表示 Unicode 字符,也是目前应用最广泛的一种 Unicode 编码方式,但是它不是最早的 Unicode 编码方式,最早的 Unicode 编码方式是 UTF-16。UTF-8 编码算法有以下特点:
- 首字节码用来区分采用的编码字节数 :如果首字节以 0 开头,表示单字节编码;如果首字节以 110 开头,表示双字节编码;如果首字节以 1110 开头,表示三字节编码,以此类推;
- 除了首字节码外, 用 10 开头表示多字节编码的后续字节,下图列出 UTF-8 用 1~6 个字节所表示的编码方式和码点范围(实际上 1-4 个字节基本可以覆盖大部分 Unicode 码点);
- 与 ASCII 编码方式完全兼容:U+0000 到 U+007F 范围内(十进制为 0~127)的 Unicode 码点值所对应的字符就是 ASCII 字符集中的字符,用一个字节表示,编码方式和 ASCII 编码一致;
- 无字节序,在 UFT-8 编码格式的文本中,如果添加了 BOM,则标示该文本是由 UTF-8 编码方式编码的,而不用来说明字节序。
在实际的解码过程中:
- 情况 1:读取到一个字节的首位为 0,表示这是一个单字节编码的 ASCII 字符;
- 情况 2:读取到一个字节的首位为 1,表示这是一个多字节编码的字符,如继续读到 1,则确定这是首字节,在继续读取直到遇到 0 为止,一共读取了几个 1,就表示在字符为几个字节的编码;
- 情况 3:当读取到一个字节的首位为 1,紧接着读取到一个 0,则该字节是多字节编码的后续字节。
下图概述了 UTF-8 编码方式的特点,其中的 自同步 表示在传输过程中如果有字节序列丢失,并不会造成任何乱码现象,或者存在错误的字节序列也不会影响其他字节的正常读取。例如读取了一个 10xxxxxx 开头的字节,但是找不到首字节,就可以将这个后续字节丢弃,因为它没有意义,但是其他的编码方式,这种情况下就很可能读到一个完全不同或者错误的字符。
UTF-16
是最早的 Unicode 字符集编码方式,在概述 UTF-16 之前,需要解释一下 USC-2 编码方式,他们有源远流长的关系,UTF-16 源于 UCS-2。UCS-2 将字符编号(同 Unicode 中的码点)直接映射为字符编码,亦即字符编号就是字符编码,中间没有经过特别的编码算法转换。下图就是对 UCS-2 编码方式的概括。
由上图可知,UCS-2 编码方式只覆盖基本多语言平面的码点,因为 16 位二进制表示的最大值为 0xFFFF,而对于增补平面中的码点(范围为 0x10000-0x10FFFF,十进制为 65536-1114111),两字节的 16 位二进制是无法表示的。为了解决这个问题,The Unicode Consortium 提出了通过代理机制来扩展原来的 UCS-2 编码方式,也就是 UTF-16。
UTF-16 编码介于 UTF-8 与 UTF-32 之间,同时结合了定长和变长两种编码方法的特点。它的编码规则很简单:
- 基本多语言平面(BMP)中有效码点用固定两字节的 16 位代码单元为其编码,其数值等于相应的码点,桶 USC-2 的编码方式;
- 辅助多语言平面 1-16 中的有效码点采用代理对(surrogate pair)对其编码:用两个基本平面中未定义字符的码点合起来表示增补平面中的码点编码。
也就是说,UTF-16 的编码长度要么是 2 个字节(U+0000 到 U+FFFF),要么是 4 个字节(U+010000 到 U+10FFFF)。那么问题来了,当我们遇到两个字节时,到底是把这两个字节当作一个字符还是与后面的两个字节一起当作一个字符呢?
这里有一个很巧妙的地方,在基本平面内,从 U+D800 到 U+DFFF(十进制 55296-57343,共 2048 个码点) 是一个空段,即这些码点不对应任何字符。于是,这个空段被用来作为代理区(Surrogate Zone),用两个基本平面中的代理区(代理对,surrogate pair)代理辅助多语言平面 1-16 中的一个有效码点。
辅助平面的字符位共有 220 个,因此表示这些字符至少需要 20 个二进制位。UTF-16 将这 20 个二进制位分成两半,前 10 位映射在 U+D800 到 U+DBFF,称为高代理码点,后 10 位映射在 U+DC00 到 U+DFFF,称为低代理码点。这意味着,一个辅助平面的字符,被拆成两个基本平面的字符表示。
因此,当遇到两个字节,发现它的码点在 U+D800 到 U+DBFF 之间,就可以断定,紧跟在后面的两个字节的码点,应该在 U+DC00 到 U+DFFF 之间,这四个字节必须放在一起解读,表示一个辅助平面的字符。
UTF-32
是一个以固定四字节编码方式,所有的字符都用四个字节,特别浪费空间,所以实际上使用比较少。
UTF-16 和 UTF-32 的最小 Code Unit(代码单元)是双字节即 16 个比特位,即多字节编码方式,因此这两种编码方式都存在字节序标记(BOM)问题。
- Big-Endian( BE )即大端序: 就是高位字节(即大端字节)存放在内存的低地址,低位字节(即小端字节)存放在内存的高地址。UTF-16(BE)以 FEFF 作为开头字节,UTF-32(BE)以 00 00 FE FF 作为开头字节;
- Little-Endian (LE) 即小端序: 低位字节(即小端字节)存放在内存的低地址,而高位字节(即大端字节)存放在内存的高地址。UTF-16(LE)以 FFFE 作为开头字节,UTF-32(LE)以 FF FE 00 00 作为开头字节。
- UTF-8 不需要 BOM 来表明字节顺序: 可以用 BOM(EF BB BF 称为零宽无间断间隔)来表明编码方式,如果接收者收到以 EF BB BF 开头的字节流,就知道这是 UTF-8 编码。
Note!该节内容全部来自彻底弄懂 Unicode 编码 和 Unicode 及编码方式概述
Note!啊!终于到这部分了!我本来只想写个 Unicode 和 char 的关系的,结果——说多了都是泪啊!o(╥﹏╥)o
前面已经介绍了字符集、Unicode、编码等概念的一些情况,相信这里不用再过多地赘述了!
Java 在设计之初便选择了 Unicode 字符集,同时选择的编码方案为 UTF-16!
这样的设计在最初是非常优秀的,既满足了可以表示世界上所有语言(几乎)的需求,又有一定的可扩展性!也因此(具体详细原因未知,可自行查找,此处不深究),Java 的基础类型之一 char 类型被设计为 2 个字节,用于表示一个字符!
在当时,这样的设计可以表示世界上几乎所有的字符!
但是,随着时间的推移,Unicode 字符集不可避免地进行了一次又一次的扩充,很快便超过了 65536(216) 个字符。于是,这个时候,一个 char 表示不了所有的字符了!
于是,Java 做出了一些改变,用一个 char 表示 Unicode 的一个代码单元
(是指一个已编码的文本中具有最短的比特组合的单元;在 UTF-16 中即 16 个比特位)!
采取这样的方案后,一个字符便既有可能是用 1 个 char 来表示,也有可能是用 2 个 char 来表示
!具体一点来说,如果一个字符位于 Unicode 的基本语言平面,那么用 1 个char 就可以表示它;但如果该字符位于辅助语言平面,那么就需要用 2 个 char 才能表示它。
下面用一个简单的例子来看一下:
jshell> "a人".length()
$1 ==> 2
jshell> "a人".codePointCount(0, 2)
$2 ==> 2
jshell> "".length()
$3 ==> 2
jshell> "".codePointCount(0, 1)
$4 ==> 1
jshell> "".codePointAt(0)
$5 ==> 131813
jshell> Integer.toHexString(131813)
$6 ==> "202e5"
Note!该节关于 Java 语言设计意图及 Unicode 和 UTF 这几个东西的时间线这部分参考了一些文章,也加入了一些笔者的猜测,我实在没在网上找到关于这方面的文章!而且,这个意图,其实并不非常重要,不是吗?没找到也不用纠结,这里不做过多深究!如果有知道的朋友,欢迎留言告知,不胜感激!
Note!该节也参考了一些文章,感兴趣的朋友可以去看看!
Note!还有几篇文章中提到了关于乱码、Java 应用如何选用编码、Java 关于编码的API 等话题,感兴趣的朋友可以去看下!链接同样贴下面
Note!还需要总结吗?我实在写不动了
Note!这篇文章引用了很多朋友的博客或文章内容,也没有去一一寻找,如果有版权相关的问题,请联系我,我会及时删除相关内容