计算机是二进制的只认识 0 和 1,一个二进制位(bit)存在两种状态 0 或 1,八个二进制位可以表示 256 种状态,也称为一个字节(byte),所以一个字节能表示出 256 种字符
ASCII 码
上个世纪 60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为 ASCII 码,一直沿用至今
ASCII 码一共规定了 128 个字符的编码,比如 空格SPACE
是 32(00100000),大写的字母 A 是 65(01000001)。这 128 个符号(包括 32 个不能打印出来的控制符号),只占用了一个字节的后面 7 位,最前面的一位统一规定为 0
非 ASCII 码
英语用 128 个字符编码就够了,但是用来表示其他语言,128 个字符是不够的。比如,在法语中,字母上方有注音符号,它就无法用 ASCII 码表示。于是,一些欧洲国家就决定,利用字节中闲置的最高位编入新的字符。比如,法语中的 é
的编码为 130(10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多 256 个字符
但是这里又出现了新的问题,不同的国家有不同的字母。因此,哪怕它们都使用 256 个字符的编码方式,代表的字母却不一样。比如,130 在法语编码中代表了 é
,在希伯来语编码中却代表了字母 Gimel
「ג」,在俄语编码中又会代表另一个字符。但是不管怎样,所有这些编码方式中,0 - 127 表示的字符是一样的,不一样的只是 128 - 255 的这一段
至于亚洲国家的文字,使用的字符就更多了,汉字就多达 10 万左右。一个字节只能表示 256 种字符,肯定是不够的,就必须使用 多个字节表达一个字符。比如,简体中文常见的编码方式是 GB2312,使用 两个字节表示一个汉字,所以理论上最多可以表示 256 x 256 = 65536 个字符
Unicode
正如上一节所说,世界上存在着多种编码方式,同一个二进制数字可以被解释成不同的字符。因此,要想打开一个文本文件,就必须知道它的编码方式,否则用错误的编码方式解读,就会出现乱码。为什么电子邮件常常出现乱码?就是因为发信人和收信人使用的编码方式不一样
可以想象,如果有一种编码,将世界上所有的字符都纳入其中。每一个字符都给予一个独一无二的编码,那么乱码问题就会消失。这就是 Unicode,就像它的名字都表示的,这是一种所有符号的编码。
Unicode 当然是一个很大的集合,现在的规模可以容纳 100 多万个字符。每个符号的编码都不一样,比如,U+0639
表示阿拉伯字母 Ain
,U+0041
表示英语的大写字母 A
,U+4E25
表示汉字 严
。具体的符号对应表,可以查询 unicode.org,或者专门的 汉字对应表
Unicode 的问题
需要注意的是,Unicode 只是一个字符集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储
比如,汉字 严
的 Unicode 是十六进制数 4E25
,转换成二进制数足足有 15 位(100111000100101),也就是说,这个符号的表示至少需要 2 个字节。表示其他更大的符号,可能需要 3 个字节或者 4 个字节,甚至更多
这里就有两个严重的问题,第一个问题是,如何才能区别 Unicode 和 ASCII ?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,我们已经知道,英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是 0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的
它们造成的结果是:
- 出现了 Unicode 的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示 Unicode
- Unicode 在很长一段时间内无法推广,直到互联网的出现
UTF-8
互联网的普及,强烈要求出现一种统一的编码方式。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式。其他实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示),不过在互联网上基本不用。重复一遍,这里的关系是,UTF-8 是 Unicode 的实现方式之一
UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用 1~4 个字节表示一个符号,根据不同的符号而变化字节长度
起初,UTF-8 使用 1~6 个字节为每个字符编码,2003年11月 UTF-8 被 RFC 3629 重新规范,只能使用原来Unicode 定义的区域,U+0000 到 U+10FFFF,也就是说最多四个字节 —— UTF-8 结构
UTF-8 的编码规则很简单,只有二条:
- 对于单字节的符号,字节的第一位设为
0
,后面 7 位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的 - 对于
n
字节的符号(n > 1),第一个字节的前n
位都设为1
,第n + 1
位设为0
,后面字节的前两位一律设为10
,剩下的没有提及的二进制位,全部为这个符号的 Unicode 码
下表总结了编码规则,字母 x
表示可用编码的位
UTF-8 字节数 | UTF-8 结构 | Unicode 位数 | Unicode 范围(十六进制) |
---|---|---|---|
1 byte | 0xxxxxxx | 7 bit | 0000 0000 - 0000 007F |
2 byte | 110xxxxx 10xxxxxx | 11 bit | 0000 0080 - 0000 07FF |
3 byte | 1110xxxx 10xxxxxx 10xxxxxx | 16 bit | 0000 0800 - 0000 FFFF |
4 byte | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx | 21 bit | 0001 0000 - 001F FFFF |
以汉字 中
为例,中
的 Unicode 是 4E2D
(0100 1110 0010 1101),4E2D
在上面 Unicode 范围的第三行内,所以对应的 UTF-8 的结构是 1110xxxx 10xxxxxx 10xxxxxx
然后将 中
的 Unicode 编码从后往前依次填入 UTF-8 的结构中即可:11100100 10111000 10101101
(E4 B82D)
再以特殊的 Emoji 为例,😀 的 Unicode 是 1 F600
(0001 1111 0110 0000 0000),1 F600
对应上面的第四行,所以对应的 UTF-8 的结构是 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
,再将 Unicode 编码填入就得到了 😀 的 UTF-8 编码为:11110000 10011111 10011000 10000000
(F09F 9880)
LE 与 BE
以上面的 中
字为例,中
的 Unicode 是 4E2D
,需要用两个字节来存储,一个是 4E
,另一个是 2D
。那么在存储的时候,如果 4E
在前,2D
在后,这就是 Big-endian 的方式,反之则是 Little-endian
这两个古怪的名称来自英国作家斯威夫特的《格列佛游记》。在该书中,小人国里爆发了内战,战争起因是人们争论,吃鸡蛋时究竟是从大头(Big-endian)敲开还是从小头(Little-endian)敲开。为了这件事情,前后爆发了六次战争,一个皇帝送了命,另一个皇帝丢了王位
所以,第一个字节在前就是 BE,最后一个字节在前就是 LE
那么很自然的,就会出现一个问题:计算机怎么知道某一个文件到底采用哪一种方式编码?
Unicode 规范定义,每一个文件的最前面都加入一个表示编码顺序的字符,这个字符的名字叫做「零宽度非换行空格」(zero width no-break space),用 FE FF
的顺序来表面存储的方式
如果一个文本文件的头两个字节是 FE FF
,就表示该文件采用 BE 方式;如果头两个字节是 FF FE
,就表示该文件采用 LE 方式
ANSI
通过一个虚构的故事来理解什么是 ANSI:
- ANSI 不是一种特定的字符编码
- ANSI 能表示不同编码
- ANSI 只存在于 Windows 下
Java 对 Unicode 的编解码
利用 char 存储的是 Unicode 字符的特点,可以轻松将一个字符转为 Unicode 编码
1 |
|
解码
1 |
|
Java 对 UTF-8 的编解码
encode
1 |
|
decode
1 |
|
进制与编码
进制 | 二进制范围 |
---|---|
2 | 0 ~ 1 |
4 | 00 ~ 11 |
8 | 000 ~ 111 |
16 | 0000 ~ 1111 |
32 | 00000 ~ 11111 |
64 | 000000 ~ 111111 |
128 | 0000000 ~ 1111111 |
256 | 00000000 ~ 11111111 |
可以看出 1 个 16 进制可以表示 4 bit,所以,可以用 2 个 16 进制来表示 1 byte
简单理解Base64
Base64 是一种常用于数据编码的算法,主要用于将二进制数据转换为文本格式,以便通过文本系统(如电子邮件或网页)传输。Base64 的编码规则将 每三个字节的二进制数据编码为四个可打印的 ASCII 字符
数据块划分
- 原始数据按 3 字节(24 位)一组进行处理
- 如果数据长度不是 3 的倍数,则使用 0 补足,后续在编码中用
=
作为填充符标记
每组数据处理
- 24 位的三字节数据被分成 4 个 6 位的块(6 位 × 4 = 24 位)
- 每个 6 位块对应一个 Base64 字符表中的字符
Base64 字符表
- Base64 编码使用的字符表包含 64 个字符:
A-Z
、a-z
、0-9
、+
、/
。每个字符的索引(从 0 到 63)对应一个 6 位的二进制数 - 例如,
A
对应的索引是 0(000000
),/
对应的索引是 63(111111
)
填充规则
- 如果原始数据长度不是 3 的倍数,编码后的 Base64 字符串末尾会使用一个或两个
=
号填充,以使最终输出的长度为 4 的倍数 - 补充的
=
并不参与解码,实际上表示没有更多数据
编码示例
假设有以下三个字节的数据:Man
,其 ASCII 值分别为 77
,97
,110
,转换为二进制是:
1 | 77 -> 01001101 |
合并成 24 位的二进制数据:
1 | 01001101 01100001 01101110 |
将其分为四个 6 位的块:
1 | 010011 010110 000101 101110 |
这四个 6 位块分别对应 Base64 字符表的索引:
1 | 010011 -> 19 -> T |
因此,Man
编码为 Base64 后的结果是 TWFu
填充示例
假设编码的数据是 Ma
,只有两个字节:
1 | 77 -> 01001101 |
合并成 16 位的二进制数据:
1 | 01001101 01100001 |
再补足 8 个 0 以凑满 24 位:
1 | 01001101 01100001 00000000 |
将其分为四个 6 位的块:
1 | 010011 010110 000100 000000 |
对应的 Base64 字符是:
1 | 010011 -> 19 -> T |
由于原始数据只有两个字节,不足的部分用 =
填充,结果为 TWE=
中文编码示例
Base64 编码本身是一种针对二进制数据的编码方法,它将任意二进制数据转换为可打印的 ASCII 字符。Base64 不直接处理字符编码,而是处理字符编码后形成的字节数据。因此,处理中文等非 ASCII 字符时,需要先确定字符的编码方式(如 UTF-8、UTF-16 等),然后对编码后的字节数据进行 Base64 编码
假设要对中文字符串「你好」进行 Base64 编码
- 「你」的 Unicode 代码点是 U+4F60,UTF-8 编码为三个字节:
E4 B8 80
- 「好」的 Unicode 代码点是 U+597D,UTF-8 编码为三个字节:
E5 A5 BD
因此,「你好」在 UTF-8 编码下的字节序列为:E4 B8 80 E5 A5 BD
转为二进制:11100100 10111000 10000000 11100101 10100101 10111101
分组成每 6 位一组的二进制块:
1 | 111001 -> 36 -> e |
将这些 Base64 字符连接起来,得到最终的 Base64 编码字符串:5L2g5aW9
编码体积膨胀 33%
Base64 编码将原始数据转换成可打印的 ASCII 字符,这个过程会导致数据体积增加。具体来说,Base64 编码后的大小是原始数据的 4/3 倍(或 33% 的增长)。这是因为 Base64 编码将每 3 个字节(24 位)的二进制数据编码为 4 个可打印的字符,每个字符对应 6 位数据
原始数据以字节为单位,1 个字节等于 8 位。所以,Base64 编码处理的基本单位是 3 个字节,即 3 × 8 = 24 位
编码过程的核心就是把 3 个字节(24 位)数据变为 4 个字节(每个字节含有 Base64 字符,每个字符占用 1 个字节)的数据。因此,编码后的数据大小是原始数据的 4/3 倍
如果原始数据的长度不是 3 的倍数,则在编码过程中会用 =
字符填充。这些填充字符本身不增加有效数据的内容,但会增加编码后的总字符数,使得数据长度是 4 的倍数
虽然理论上编码后数据是原来的 4/3 倍,但由于填充字符的存在,实际增长比率可能略低于 33%。例如,对于非常小的数据块(少于 3 个字节),填充字符可能会导致总增量低于 33%