【Java】UTF-8变长编码及其3字节存储奥秘
UTF-8 是一种变长编码,一个字符可能由 1 到 4 个字节组成。
解码时(将字节数组转回 String),计算机并不需要“猜”或者去查表,因为长度信息本身就包含在字节的“头部”里。这就是 UTF-8 设计的精妙之处:它是“自同步”的。
核心机制:看字节的“高位”标志
计算机读取字节时,是按比特(Bit)一位一位看的。UTF-8 规定,利用每个字节的前几位(高位)来告诉解码器:“这个字节是独立字符,还是某个字符的一部分”。
这就好比我们看车牌号,如果第一个字母是“京”,我们就知道这车是北京的;如果第一个字母是“豫”,就知道是河南的。UTF-8 的字节也是通过“长相”来区分的。
具体的编码规则表
让我们看看一个字节(8个比特)的二进制表示。x代表存储数据的位,0和1是标志位:
| 字节数 | 格式 | 说明 | 数据位数量 | 理论最大十进制数 | 实际 UNICODE 上限 |
|---|---|---|---|---|---|
| 1 字节 | 0xxxxxxx | 0开头,这是 ASCII 字符(0-127) | 7 位 | 127 | 127 |
| 2 字节 | 110xxxxx 10xxxxxx | 110开头,后面跟 1 个字节 | 11 位 | 2,047 | 2,047 |
| 3 字节 | 1110xxxx 10xxxxxx 10xxxxxx | 1110开头,后面跟 2 个字节 | 16 位 | 65,535 | 65,535 |
| 4 字节 | 11110xxx 10.. 10.. 10.. | 11110开头,后面跟 3 个字节 | 21 位 | 2,097,151 | 1,114,111 |
⚠️特别注意:Unicode 标准的限制
虽然 UTF-8 的 4 字节编码规则允许存到 2,097,151,但实际上,Unicode 标准本身并没有用完这个空间。
Unicode 标准目前规定的最大码点是 1,114,111(十六进制0x10FFFF)。
- 原因:为了保持与 UTF-16 等其他编码的兼容性,Unicode 标准限定了范围。
- 结论:在现实世界的计算机系统中,有效的 UTF-8 4 字节字符最大只能到 1,114111。超过这个数值的二进制组合(即使符合 UTF-8 的 4 字节格式)被认为是“非法码点”,不会被标准系统使用。
解码器是如何工作的?
举个例子:汉字“中”
汉字“中”的 Unicode 码点是U+4E2D,在 UTF-8 中,它需要 3 个字节来存储:
字节数组:[228, 184, 173](十六进制0xE4 0xB8 0xAD)
解码流程:
- 读取
0xE4(11100100)- 看到
1110 - 判决:这是 3 字节头,往后读 2 个。
- 看到
- 读取
0xB8(10111000)- 看到
10 - 判决:是第 1 个后续字节,继续。
- 看到
- 读取
0xAD(10101101)- 看到
10 - 判决:是第 2 个后续字节。
- 结束:凑齐了 3 个字节,成功解析出“中”字。
- 看到
解码器看到1110...就知道往后数 2 个字节,把这三个字节里的x部分拼在一起,就还原出了“中”字。
总结
解码过程之所以准确,是因为 UTF-8 利用了二进制的高位作为标志位:
- 如果读到一个字节,首位是
0→ 1 字符,1 字节。 - 如果读到一个字节,首位是
110→ 1 字符,2 字节。 - 如果读到一个字节,首位是
1110→ 1 字符,3 字节。 - 如果读到一个字节,首位是
10→ 这不是头,这是尾巴(跟随字节)。
这种设计非常巧妙,既兼容了古老的 ASCII(英文只用 1 个字节,首位为 0),又支持了全世界所有的字符(用变长字节),而且解码速度快,不需要额外的索引表。
UTF-8:3字节存储原理
举例:汉字“中”的 Unicode 码点是U+4E2D(十进制20013),在 UTF-8 中,它需要 3 个字节来存储:
字节数组:[228, 184, 173](十六进制0xE4 0xB8 0xAD)。
在 Java 内部,char类型是使用 2 个字节(UTF-16 编码)来存储“中”这个字的。
但是,UTF-8 之所以需要 3 个字节,是因为 UTF-8 的编码规则为了兼顾兼容性和可读性,牺牲了一定的存储空间。
下面详细解释为什么明明 2 个字节够用,UTF-8 却非要用 3 个字节。
1.数值上的可行性:2 字节确实够
“中”字的 Unicode 码点是20013(十进制)。
我们把它转换成二进制:
20013 = 0100 1110 0010 1101
这串二进制一共有 15 位。
而 2 个字节等于 16 位。因为 15 < 16,所以单纯存这个数字,2 个字节绰绰有余。
- Java 内部存储(UTF-16):直接把这 15 位二进制填入 2 个字节中(高位补 0),即
0x4E2D。这就是为什么 Java 的char只需要 2 个字节。
2.为什么 UTF-8 需要 3 个字节?
UTF-8 是一种变长编码。它的设计目标之一是兼容 ASCII 码(0-127),并且不需要像 UTF-16 那样面临“字节序”(大端小端)的问题。
为了实现这一点,UTF-8 规定了非常严格的位格式。它不像 UTF-16 那样直接把数字填进去,而是把二进制位“拆分”到不同的字节容器里,每个字节都要留出几位作为“位标记”,告诉计算机这个字节是几字节序列的一部分。
UTF-8 的位规则:
- 1 字节(0 - 127):格式
0xxxxxxx(最高位是 0,剩下 7 位存数据)。 - 2 字节(128 - 2047):格式
110xxxxx 10xxxxxx。- 第一个字节的前 3 位必须是
110。 - 后续字节的前 2 位必须是
10。 - 剩下的位才是存数据的:5 + 6 = 11 位。
- 2 字节 UTF-8 能存的最大数值是 2¹¹ - 1 = 2047。
- 第一个字节的前 3 位必须是
- 3 字节(2048 - 65535):格式
1110xxxx 10xxxxxxx 10xxxxxx。- 第一个字节的前 4 位
1110。 - 后续字节的前 2 位
10。 - 剩下的位存数据:4 + 6 + 6 = 16位。
- 3 字节 UTF-8 能存的最大数值是 2¹⁶ - 1 = 65535。
- 第一个字节的前 4 位
关键点来了:
“中”字的数值是 20013。
而 2 字节 UTF-8 能存储的最大数值只有 2047。
因为20013 > 2047,所以“中”字装不进 2 字节的 UTF-8 格式里,必须升级到 3 字节的格式。
3.实际转换演示
“中”字在 UTF-8 里具体是怎么变成 3 个字节的:
原始数据(二进制):0100 1110 0010 1101(共 15 位)
目标模板(3字节 UTF-8):1110xxxx 10xxxxxx 10xxxxxx
填充过程:
从原始数据的低位开始,依次填入模板的x中(从后往前填):
- 取最后 6 位:
101101→ 填入第 3 个字节 →10101101(0xAD) - 再取中间 6 位:
111000→ 填入第 2 个字节 →10111000(0xB8) - 剩下的高位:
0100→ 填入第 1 个字节 →11100100(0xE4)
最终结果:
11100100 10111000 10101101
即十六进制的:E4 B8 AD
这就是为什么“中”字在 UTF-8 中占据了 3 个字节。
4.总结
| 编码方式 | 存储机制 | “中”字占用大小 | 原因 |
|---|---|---|---|
| UTF-16(Java char) | 定长(大部分),直接存储数值 | 2 字节 | 数值 20013 小于 65535,直接存入 16 位空间。 |
| UTF-8 | 变长,需要前缀位标记 | 3 字节 | UTF-8 的 2 字节模式最大只能存 2047。为了容纳 20013,必须用 3 字节模式。 |
一句话概括:
虽然 2 个字节的盒子(容量)足够装下 20013 这个数字,但 UTF-8 的 2 字节格式(包装方式)太小了(只能装到 2047),所以被迫换用了更大的 3 字节包装。
