Java 字符串编码
本文解释 Java 中的字符串编码
名词解释
名词 | 单位 | 解释 |
---|---|---|
位 | 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+DBFF
和 U+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 相加,得到 1101100000111101
即 0xD83D
低位:将后十位和 0xDC00 相加,得到 1101110010100010
即 0xDCA2
则得到 💢 U+1F4A2
的 UTF-16 的编码为 \uDB3D\uDCA2
具体的辅助平面字符的转换算法如下
1 | H = (C - 0x10000) / 0x400 + 0xD800 |
至此,将一个 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 | 00001 11101 00101 00010 |
得到 00011111010010100010(即0x1F4A2) 即💢 U+1F4A2
Java 中 String.length 返回的字符串对应的 char 的长度而不是人类认知中字符的长度
例如
1 | "💢".length() == 2 |
0001000010 1110110111
在 Java 中使用码点(Code Point)
来代表 Unicode 字符集中的每个字符,取值 0x000000(Character.MIN_CODE_POINT)
-0x10FFFF(Character.MAX_CODE_POINT)
本文参考 彻底弄懂Unicode编码 感谢分享