引入:Unicode 家族
Unicode 家族主要是 UTF-8,UTF-16,UTF-32,它们都可以用多达 4 字节对字符进行编码
(Unicode Transfer Format)UTF 后面的数字表示单个编码所需的最小单位长度
因此 UTF-32 是定长编码
另外两个是不定长编码,UTF-8 可以是 1/2/3/4 字节,UTF-16 可以是 2/4 字节(本人愿称其为半定长编码)
这几种编码怎么来的?
Unicode 早期,推行的是定长编码(解析无需算法,性能极致)
定长编码包含 16 位和 32 位方案,即在支持生僻字符和节省内存间做一个取舍
- 16 位能表示的 65536 个字符,称为基本多语言平面(BMP,U+0000 到 U+FFFF)
由于以多字节为单位,UTF-16 和 32 引入了一个BOM机制,用于在文件开头标识内部字节是大端/小端
而后来的 UTF-8 以单字节为单位,所以也不需要BOM了
(至于编辑器中可选的 UTF-8 with BOM 编码,我的评价别管,别用。至于为什么,可以看最后)
可以看到很早支持 Unicode 的编程语言,都是支持 UTF-16
后来网络发展对数据传输的需要,使 UTF-32 这位在字符编码方面彻底入土了
当然,不考虑内存占用的话,UTF-32 依然具有信息量大、操作简单快捷的优点
主要是浪费的位太多,被互联网领域唾弃了
之后 UTF-16 接过编码全世界语言的大旗,成为了不定长编码,推出了代理对算法
再之后,算法更精妙、存储更紧凑的 UTF-8 诞生了,逐步成为如今 Unicode 的代表,是如今互联网主推的通用编码
下面是编码规则
UTF-8 的编码规则(以二进制形式表示)
1 字节编码
适用于 U+0000 至 U+007F 的码点,即原 ASCII 字符
二进制形式:
0xxxxxxx
7 位有效数据,最高位为 0
2 字节编码
适用于 U+0080 至 U+07FF 的码点
二进制形式:
110xxxxx 10xxxxxx
第一个字节以 110
开头,后续字节以 10
开头
11 位有效数据($5+6$)
3 字节编码
适用于 U+0800 至 U+FFFF 的码点
二进制形式:
1110xxxx 10xxxxxx 10xxxxxx
第一个字节以 1110
开头,后续字节以 10
开头
16 位有效数据($4+6*2$),多了 5 位
4 字节编码
适用于 U+10000 至 U+10FFFF 的码点
二进制形式:
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
第一个字节以 11110
开头,后续字节以 10
开头
21 位有效数据($3+6*3$),多了 5 位
具体编码示例(以最复杂的 4 字节为例)
U+1F600(😀,Unicode 码点 128512)
- 位数>16,超过 3 字节的表示能力,因此采用 4 字节
- 写成二进制:
00000001 11110110 00000000
- 取 21 位:
000
|00001 11110110 00000000
- 填空题:
11110___ 10______ 10______ 10______
- 填空后:
11110000 10011111 10011000 10000000
- 结果为:
0xF0 0x9F 0x98 0x80
顺便提供一个供验证的 Java 函数:
public void checkUtf8Code() {
int codePoint = 0x1F600;
String emoji = new String(Character.toChars(code));
// 字符串转换为8位字节数组
byte[] utf8Bytes = emoji.getBytes(StandardCharsets.UTF_8);
// 以16进制输出
System.out.print("UTF-8 编码的字节值: ");
for (byte b : utf8Bytes) {
System.out.printf("%02X ", b);
}
}
- 这是否应该是 Java 基础篇就讲的东西?
Java 基本类型 char 固定为16位,String 类内部也是以
char[]
存储值创建 String 对象时会进行一次 encode 转为 UTF-16 存储,输出时会再进行一次转换,包括
charAt
获取某字符但
charAt
是默认每个字符只占用 16 位,超出 BMP 的字符会以 2 个char
保存,返回的结果就歪了这就呼应了前面说 UTF-16 是半定长编码
相比之下,真正的不定长编码 UTF-8 在各大编程语言当中都是字节数组,然后外挂一系列处理算法
UTF-8 的实际编码范围?
Unicode 设计时存在一个上限
同时出于编码意义的唯一性,有些 >1 字节的编码是被跳过的
避免冗余编码
以 1 字节的 ASCII 码为例,当我在 7 位码前加 0000,便得到了值不变的 2 字节码
于是这些码被认为是冗余的,无效的
以此类推,把多字节多出的数据位设为 0 同理,都可以找到更少字节的表示
因此可以轻易得出,2 字节的前 4 位不能都为 0,3/4 字节的前 5 位不能都为 0
于是得出了一系列无效编码区间
避免与 UTF-16 混淆
UTF-16 通过代理对算法,表示超出 BMP 的字符
而代理对形式的范围是 U+D800 到 U+DFFF
于是 UTF-8 便跳过了这个范围
特别是在 3 字节编码的范围中,0xED 0xA0
到 0xED 0xBF
会被跳过,以确保不会编码无效的 U+D800 到 U+DFFF 范围的值。
避免超出有效的 Unicode 范围
Unicode 定义了码点的范围是 U+0000 到 U+10FFFF,超过这个范围的码点是无效的
因此 4 字节编码中,0xF4
后面的字节只能从 0x80
到 0x8F
前两字节示意:11110_100
|10_001111
为什么是这个上限?
值得注意的是,这个上限正好是 65536 的 16 倍,它的制定与 UTF-16 的代理对息息相关
UTF-16 的代理对
代理对是 UTF-16 的一种不定长编码的实现机制,通过指定格式标识连续两个 16 位值表示的一个 UTF-16 字符
而这两个值被称为“代理对”(Surrogate Pairs),分为高代理和低代理
算法简述
仍以 U+1F600 (😀)为例。学编程真是令人心情愉悦啊(?)
-
位数>16,因此采用代理对
-
减去 0x10000,得到
0x1F600 - 0x10000 = 0xF600
-
在高位补 0,至 20 位,得到
0000
|1111011000000000
-
分成高 10 位
00.0011.1101
和低 10 位10.0000.0000
,即0x003D
和0x0200
-
高 10 位加上
0xD800
,得到0xD83D
低 10 位加上
0xDC00
,得到0xDE00
-
于是得到编码后的值
0xD83D 0xDE00
,即代理对
代理对占用了 U+D800 到 U+DFFF,不允许直接分配字符给它们,而是用于编码 U+10000 及以上的码点
以下是一个供验证的 Java 函数:
public static void checkUtf16Code() {
int codePoint = 0x1F600;
// 获取16位字符数组
char[] surrogatePair = Character.toChars(codePoint);
// 以16进制输出
System.out.printf("高代理: %04X\n", (int) surrogatePair[0]);
System.out.printf("低代理: %04X\n", (int) surrogatePair[1]);
}
补充:BOM 的标记
UTF-16 和 UTF-32 都选择 0xFE FF
为大端标记,0xFF FE
为小端标记(当然 UTF-32 前面再加 16 位 0)
于是文件开头的第一个字符,就是 BOM 标记
一般文件处理的时候,会不约而同地忽略这个字符
关于为什么使用
FE
和FF
,其实就是业界常见的操作:拿用不到的最大字节作特殊用途而排列顺序则遵从直觉:
FE FF
顺序排列,对应高位在前的大端,而FF FE
逆序排列,对应低位在前的小端
UTF-8 的 BOM
如果一路看到这里,应该能明白 UTF-8 的 EF BB BF
形式的 BOM 是怎么回事了
不卖关子,试着用 UTF-8 编码一下大端的
FE FF
UTF-8 按理说是不需要 BOM 的
至少 UTF-8 文件处理时,不见得会辨认第一个字符是文本字符,还是 BOM 标记
所以不是特殊需求的话,请不要 [这是什么,UTF-8 with BOM?保存一下]