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 以外的字符

这个“😂”表情就是一个典型的增补平面字符。

  1. 它的 Unicode 编号是多少?

    • “😂”的 Unicode 码点是 U+1F602
    • 1F602 这个十六进制数明显大于 FFFF,所以一个 16 位的 char 变量是无论如何也存不下的。
  2. 解决方案:UTF-16 的代理对 (Surrogate Pair)

    • 为了解决这个问题,UTF-16 编码(Java/C# 内部使用的编码)采用了一种名为“代理对”的巧妙机制。它的工作原理如下:
      • 腾出空间:UTF-16 从基本平面(BMP)中预留出一段范围 U+D800U+DFFF,共 2048 个码点。规定任何单个字符都不能使用这个范围内的编号。这段范围被专门用作“代理”。
      • 拆分字符:当遇到像 U+1F602 这样的增补平面字符时,UTF-16 会把它拆分成两个代理 char 来表示:
        1. 一个高代理项 (High Surrogate):范围在 U+D800U+DBFF 之间。
        2. 一个低代理项 (Low Surrogate):范围在 U+DC00U+DFFF 之间。
    • 当程序读到一个高代理项时,它就知道这并不是一个独立的字符,而是“半个”增补字符,它会继续向后读取一个低代理项,然后将两者组合解码,还原出原始的字符。
  3. 计算“😂” (U+1F602) 的代理对

有一个固定的公式可以将任何一个增补平面的码点转换为代理对:

  • a. 减去偏移量 0x100000x1F602 - 0x10000 = 0xF602
  • b. 计算高代理项 (High Surrogate) * 取上一步结果的高 10 位:0xF602 右移 10 位是 0x3D (二进制 00111101)。 * 加上高代理项的起始地址 0xD800。 * 0xD800 + 0x3D = 0xD83D * 所以,高代理项是 \uD83D
  • c. 计算低代理项 (Low Surrogate) * 取上一步结果的低 10 位:0xF6020x3FF (二进制 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
    }
}

代码解释与总结:

  1. 你可以手动用计算出的高、低代理项 char 来合成一个字符串。
  2. 更常见的是,你直接把 “😂” 写在代码里,编译器和运行环境会自动把它处理成一个包含两个 char (\uD83D\uDE02) 的字符串。
  3. 最关键的验证:当我们调用 emoji2.length() 时,返回的是 2 而不是 1。这充分证明了在语言层面,这个我们肉眼看来的“一个字符”,实际上是由两个 char 组成的。
  4. 这也解释了为什么处理包含复杂字符(如 Emoji)的字符串时,不能简单地用 length() 来计算字符个数,而应该使用 codePointCount() 这样的方法,它能正确地识别代理对并计算出真实的字符(码点)数量。

文章作者: huan
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明来源 huan !
  目录