名词解释

名词 单位 解释
bit 计算机中最小的单位 用0/1标识
字节 byte 可表示常用英文字符8位二进制称为一字节,一字节可以存储2^8(=256)种状态

在 Java 中, char 占两个字节(2 byte),即 16 bits

Unicode

Unicode 是一个字符集,包含了全世界的几乎所有字符,现有的字符大约有百万多个,详见 https://home.unicode.org/

编码

码点(code point)代表 Unicode 字符集中的一个值,代表一个符号

代码单元(code unit)代表具体编码形式中的最小单位

在 Java 中,使用 UTF-16 作为内存中字符存储格式,即 16位,两个字节,只能存储 2^16 - 1 = 65535 个字符。 UTF-16 编码的规则很简单:基本平面的字符占用2个字节,辅助平面的字节占4个字节

则其 代码单元(code unit) 就是一个字节,在 Java 中,一个码点(code point)可能由两个代码单元(code unit)或者四个代码单元(code unit)组成

例如字的码点为 U+4E2D,UTF-16 的编码为 \u4e2d,占用两个代码单元(code unit),一共占用两个字节(1个 char)

但是,在 Unicode 字符集已经远远超过 65535 个字符了,难道 Java 中不能表示 Unicode 中超过 65535 的字符了吗,当然不是

在 Java 中,char 为两个字节,即16位;在代码中可以使用 \uxxxx 表示某个字符,但是只能表示 0x0000~0xFFFF 之间的字符,如果需要表示超过 0xFFFF以后的字符,则需要用两个 char 来表示,例如 💢 U+1F4A2 则使用 \uD83D\uDCA2 表示

很显然,这个 U+1F4A2 已经超过了基本面的范围0x0000-0xFFFF

这个\uD83D\uDCA2是如何计算出来的呢

UTF-16 编码中,基本面的字符使用两个字节表示,如上的,辅助平面的字符使用四个字节表示。

也就是说在 UTF-16 中,一个字符要么为两个字节(位于[U+0000,U+FFFF]间),要么为四个字节(位于[U+010000,U+10FFFF]间)

那么问题来了,当遇到两个字节时,是把它当做单独的字符,还是与后面的两个字节一起当成一个四字节的字符呢?

在 UTF-16 中,辅助平面的字符使用 20个 bit 进行表示,分为两部分,高10位和低10位

0000000000 0000000000

UTF-16编码将超过 U+FFFF 的字符,会将其拆成两个字符表示,分别为H(高位 high) L (低位 low)
并将其映射到 U+D800-U+DBFFU+DC00-U+DFFF 之间(基本平面中[U+D800,U+DFFF] 为空,不对应任何字符)

即一个四个字节的辅助平面字符会使用两个基本平面的字符来表示

因此,当遇到一个字符的码点超过 U+FFFF 时候,例如 💢 U+1F4A2 已经超过了 U+FFFF,则在 UTF-16 中使用四个字节表示,则需要进行计算出低位和高位的数值

先计算高位。
减去超过的部分,得到数值 A
A = 0x1F4A2 - 0x10000 = 0xF4A2 , 即 1 11101 00101 00010

将 A 补齐到20位二进制,得到 00001 11101 00101 00010
将前十位映射到 U+D800-U+DBFF之间,后十位映射到 U+DC00-U+DFFF 之间

0xD800 的二进制为 110110 00000 00000
0xDC00 的二进制为 110111 00000 00000

高位:将前十位和 0xD800 相加,得到 11011000001111010xD83D
低位:将后十位和 0xDC00 相加,得到 11011100101000100xDCA2

则得到 💢 U+1F4A2 的 UTF-16 的编码为 \uDB3D\uDCA2

具体的辅助平面字符的转换算法如下

1
2
3
4
H = (C - 0x10000) / 0x400 + 0xD800
L = (C - 0x10000) % 0x400 + 0xDC00

PS. 0x400 即 二进制的 1 00000 00000

至此,将一个 Unicode 字符转为 UTF16 的编码的方法已经探究完了,现在反过来看一下在遇到 UTF16 编码时,如何将其转为 Unicode 字符

还是以💢 U+1F4A2为例,其 UTF-16 编码为 \uDB3D\uDCA2,可见第一个字符 0xDB3D 位于 U+D800-U+DBFF之间 那么可以确定 \uDB3D\uDCA2 是一个占四个字节的字符

接下来再进行翻译

将前一个字符减去 0xDB00 ,后一个字符减去 0xDC00


0xDB3D - 0xDB00 = 0x003D, 即 00111101
0xDCA2 - 0xDC00 = 0x00A2, 即 10100010

将高位H 和 低位L补齐到10位,即

0000111101
0010100010

再拼接一起,得到 00001 11101 00101 00010,再加上 0x10000(二进制为10000000000000000),等于

1
2
3
4
00001 11101 00101 00010
00010 00000 00000 00000
-----------------------
00011 11101 00101 00010

得到 00011111010010100010(即0x1F4A2) 即💢 U+1F4A2

Java 中 String.length 返回的字符串对应的 char 的长度而不是人类认知中字符的长度

例如

1
2
3
"💢".length() == 2
"中文".length() == 2
"中".length() == 1

0001000010 1110110111

在 Java 中使用码点(Code Point) 来代表 Unicode 字符集中的每个字符,取值 0x000000(Character.MIN_CODE_POINT)-0x10FFFF(Character.MAX_CODE_POINT)

本文参考 彻底弄懂Unicode编码 感谢分享