char 的真正含义
char 的取值范围
- 固定占用两个字节的无符号正整数
- Unicode 编码, UTF-16BE 编码格式
- 只能表示 Unicode 编号在 65536 以内的字符,超出范围的字符需要两个 char 表示
char 的赋值方式
多种赋值方式, 本质都是将 Unicode 编号赋给字符
//以下四种赋值本质都是将Unicode 27426赋值给字符c
char c = '欢'
char c = 27426
char c = 0x6B22
char c = '\u6B22'
char 的运算
- 本质是 Unicode 编号的运算
- 可以进行整数运算, 结果需要强制类型转换(默认是 int 4 字节,向下需要强制转换)
- 加减运算:
- ASCII 码字符有意义, 可用于大小写转换(+32:大写转小写)
- 可用于简单的加密解密
- 位运算:
- 与整数的位运算类似, 无符号右移和有符号右移结果相同
char 的二进制表示
可以使用 Integer.toBinaryString(c)
查看 char 的二进制表示
char 如何表示 65536 以外的字符
这个“😂”表情就是一个典型的增补平面字符。
它的 Unicode 编号是多少?
- “😂”的 Unicode 码点是
U+1F602
。 1F602
这个十六进制数明显大于FFFF
,所以一个 16 位的char
变量是无论如何也存不下的。
- “😂”的 Unicode 码点是
解决方案:UTF-16 的代理对 (Surrogate Pair)
- 为了解决这个问题,
UTF-16
编码(Java/C# 内部使用的编码)采用了一种名为“代理对”的巧妙机制。它的工作原理如下:- 腾出空间:
UTF-16
从基本平面(BMP)中预留出一段范围U+D800
到U+DFFF
,共 2048 个码点。规定任何单个字符都不能使用这个范围内的编号。这段范围被专门用作“代理”。 - 拆分字符:当遇到像
U+1F602
这样的增补平面字符时,UTF-16
会把它拆分成两个代理char
来表示:- 一个高代理项 (High Surrogate):范围在
U+D800
到U+DBFF
之间。 - 一个低代理项 (Low Surrogate):范围在
U+DC00
到U+DFFF
之间。
- 一个高代理项 (High Surrogate):范围在
- 腾出空间:
- 当程序读到一个高代理项时,它就知道这并不是一个独立的字符,而是“半个”增补字符,它会继续向后读取一个低代理项,然后将两者组合解码,还原出原始的字符。
- 为了解决这个问题,
计算“😂” (
U+1F602
) 的代理对
有一个固定的公式可以将任何一个增补平面的码点转换为代理对:
- a. 减去偏移量
0x10000
:0x1F602 - 0x10000 = 0xF602
- b. 计算高代理项 (High Surrogate) * 取上一步结果的高 10 位:
0xF602
右移 10 位是0x3D
(二进制00111101
)。 * 加上高代理项的起始地址0xD800
。 *0xD800 + 0x3D = 0xD83D
* 所以,高代理项是\uD83D
。 - c. 计算低代理项 (Low Surrogate) * 取上一步结果的低 10 位:
0xF602
和0x3FF
(二进制1111111111
) 做“与”运算,得到0x202
。 * 加上低代理项的起始地址0xDC00
。 *0xDC00 + 0x202 = 0xDE02
* 所以,低代理项是\uDE02
。
结论:“😂” (U+1F602
) 这个字符,最终被用两个 char
——\uD83D
和 \uDE02
——来表示。
在代码中如何表示?
以下是 Java 代码示例:
public class Main {
public static void main(String[] args) {
// 方法1:直接使用两个char来构建字符串
char highSurrogate = '\uD83D';
char lowSurrogate = '\uDE02';
String emoji1 = new String(new char[] {highSurrogate, lowSurrogate});
System.out.println("方法1构建的表情: " + emoji1);
// 方法2:现代编程语言可以直接在字符串字面量中使用Emoji
String emoji2 = "😂";
System.out.println("方法2直接定义的表情: " + emoji2);
// 验证:一个"😂"字符串的长度是多少?
// 因为它由两个char组成,所以长度是2!
System.out.println("“😂”字符串的长度是: " + emoji2.length()); // 输出: 2
// 验证:字符串中的两个char分别是什么?
System.out.printf("第一个char是: %s (%#X)\n", emoji2.charAt(0), (int)emoji2.charAt(0)); // 输出: ? (D83D)
System.out.printf("第二个char是: %s (%#X)\n", emoji2.charAt(1), (int)emoji2.charAt(1)); // 输出: ? (DE02)
// 如何正确获取字符的真实数量(码点数量)?
System.out.println("“😂”字符串的码点数量是: " + emoji2.codePointCount(0, emoji2.length())); // 输出: 1
}
}
代码解释与总结:
- 你可以手动用计算出的高、低代理项
char
来合成一个字符串。 - 更常见的是,你直接把 “😂” 写在代码里,编译器和运行环境会自动把它处理成一个包含两个
char
(\uD83D
和\uDE02
) 的字符串。 - 最关键的验证:当我们调用
emoji2.length()
时,返回的是 2 而不是 1。这充分证明了在语言层面,这个我们肉眼看来的“一个字符”,实际上是由两个char
组成的。 - 这也解释了为什么处理包含复杂字符(如 Emoji)的字符串时,不能简单地用
length()
来计算字符个数,而应该使用codePointCount()
这样的方法,它能正确地识别代理对并计算出真实的字符(码点)数量。