杰瑞科技汇

Java字符的Unicode编码如何转换?

char 类型与 UTF-16

在 Java 中,char 类型是一个 16 位(2 字节)的无符号整数,用来表示一个 Unicode 字符,它的取值范围是 '\u0000' (0) 到 '\uffff' (65,535)。

Java字符的Unicode编码如何转换?-图1
(图片来源网络,侵删)

这里的关键点是:Java 使用 UTF-16 编码方案来表示 char 中的字符。

UTF-16 是一种可变长度的 Unicode 编码,这意味着:

  • 对于大部分常见的字符(包括英文字母、数字、大部分中文、日文、韩文等),UTF-16 使用 1 个 char 单元(16位) 来表示,这些字符的 Unicode 码点(code point)在 U+0000U+FFFF 范围内。
  • 对于一些不常用的字符(例如一些生僻的汉字、古文字、表情符号 emoji 等),UTF-16 使用 2 个 char 单元(32位) 来表示,这被称为“代理对”(Surrogate Pair),这些字符的 Unicode 码点在 U+10000U+10FFFF 范围内。

这个“代理对”的概念是 Java Unicode 编码中最需要注意的地方。


如何在 Java 中表示和处理 Unicode 字符?

字面量表示

在 Java 代码中,你可以用以下几种方式来表示一个 Unicode 字符:

Java字符的Unicode编码如何转换?-图2
(图片来源网络,侵删)

a) 转义序列 这是最常见的方式,格式为 '\u' 后跟 4 位十六进制数。

// 字母 'A' 的 Unicode 码点是 U+0041
char letterA = '\u0041'; 
System.out.println(letterA); // 输出: A
// 中文字 '中' 的 Unicode 码点是 U+4E2D
char chineseZhong = '\u4E2D';
System.out.println(chineseZhong); // 输出: 中

b) 直接使用字符 对于 ASCII 字符,可以直接使用字符本身,Java 会自动将其转换为对应的 Unicode 值。

char letterB = 'B'; // Java 内部会将其处理为 '\u0042'
System.out.println(letterB); // 输出: B

c) 从 int 强制转换 如果你知道一个字符的 Unicode 码点(十进制或十六进制),可以将其强制转换为 char

// Unicode 码点 U+0041 (十进制 65)
char fromInt = (char) 65;
System.out.println(fromInt); // 输出: A
// Unicode 码点 U+4E2D (十进制 20013)
char fromIntChinese = (char) 20013;
System.out.println(fromIntChinese); // 输出: 中

字符串中的 Unicode

字符串 String 是由 char 数组构成的,因此同样遵循 UTF-16 规则。

Java字符的Unicode编码如何转换?-图3
(图片来源网络,侵删)
String str1 = "A\u4E2DB"; // "A中B"
System.out.println(str1); // 输出: A中B
// 包含代理对的字符 (emoji)
// 笑脸表情 😊 的 Unicode 码点是 U+1F60A
// 它需要 2 个 char 来表示
String emoji = "\uD83D\uDE0A"; 
System.out.println(emoji); // 输出: 😊

查看字符的 Unicode 码点

你可以使用 Character 类的方法来获取字符的码点。

  • char ch = '中';
  • int codePoint = ch; // 直接将 char 赋值给 int,得到的就是它的码点
  • int codePoint2 = Character.getNumericValue(ch); // 不推荐,这个方法有特殊含义
  • int codePoint3 = Character.codePointAt(new char[]{ch}, 0); // 最标准的方法
char ch = 'A';
System.out.println("字符 'A' 的码点是: " + (int)ch); // 输出: 65
char zhong = '中';
System.out.println("字符 '中' 的码点是: " + (int)zhong); // 输出: 20013

代理对:处理超出 BMP 的字符

这是 Java Unicode 编码中最容易出错的地方,字符平面 0(BMP, Basic Multilingual Plane)包含了 U+0000U+FFFF 的字符,而更多的字符在辅助平面,需要用代理对表示。

代理对的构成:

  1. 高代理区U+D800U+DBFF
  2. 低代理区U+DC00U+DFFF

一个辅助平面的字符码点 P 可以通过以下公式计算代理对:

  • highSurrogate = (char) ((P - 0x10000) / 0x400 + 0xD800);
  • lowSurrogate = (char) ((P - 0x10000) % 0x400 + 0xDC00);

示例:处理 emoji

让我们以笑脸 😊 (U+1F60A) 为例:

  1. 获取码点 int codePoint = 0x1F60A; // 十六进制表示

  2. 手动计算代理对

    • highSurrogate = ((0x1F60A - 0x10000) / 0x400 + 0xD800) = (0xF60A / 0x400) + 0xD800 = 0x3D + 0xD800 = 0xDE0D -> char\uD83D
    • lowSurrogate = ((0x1F60A - 0x10000) % 0x400 + 0xDC00) = (0xF60A % 0x400) + 0xDC00 = 0xA + 0xDC00 = 0xDE0A -> char\uDE0A

    😊 的代理对就是 \uD83D\uDE0A

  3. 在代码中验证

    // 直接使用代理对创建字符串
    String emoji = "\uD83D\uDE0A";
    System.out.println("使用代理对创建的字符串: " + emoji); // 输出: 😊
    // 字符串的 length() 方法会返回 2,因为它由 2 个 char 组成
    System.out.println("字符串长度: " + emoji.length()); // 输出: 2
    // 正确获取码点的方法
    int codePoint = emoji.codePointAt(0);
    System.out.println("获取到的码点 (十进制): " + codePoint); // 输出: 128522
    System.out.println("获取到的码点 (十六进制): " + Integer.toHexString(codePoint)); // 输出: 1f60a

遍历字符串时的注意事项

如果你使用 for(char c : str.toCharArray()) 这样的循环来遍历一个包含代理对的字符串,你会错误地将一个 emoji 拆分成两个无意义的 char

String text = "A😊B"; // 字符串长度为 3
System.out.println("字符串长度: " + text.length()); // 输出: 3
// 错误的遍历方式
System.out.println("--- 错误遍历 ---");
for (int i = 0; i < text.length(); i++) {
    char c = text.charAt(i);
    System.out.println("字符 " + i + ": " + c + " (码点: " + (int)c + ")");
}
// 输出:
// 字符 0: A (码点: 65)
// 字符 1: (码点: 55357)  这是高代理
// 字符 2: (码点: 56842)  这是低代理

正确的遍历方式

你应该使用 codePointAt()offsetByCodePoints() 来遍历,这样才能正确处理代理对。

String text = "A😊B";
System.out.println("\n--- 正确遍历 ---");
for (int i = 0; i < text.length(); ) {
    int codePoint = text.codePointAt(i);
    System.out.println("字符 " + i + ": 码点 " + codePoint + " -> " + (char)codePoint);
    // 移动到下一个码点的位置
    // 如果是普通字符,+1;如果是代理对,+2
    i += Character.charCount(codePoint);
}
// 输出:
// 字符 0: 码点 65 -> A
// 字符 1: 码点 128522 -> 😊
// 字符 3: 码点 66 -> B

核心 API 总结

方法/概念 描述 示例
char 类型 Java 中表示字符的基本类型,16位,使用 UTF-16 编码。 char c = 'A';
'\uXXXX' Unicode 字符的字面量转义表示法。 char c = '\u4E2D';
String.codePointAt(index) 返回指定位置的字符的 Unicode 码点,对于代理对,返回完整的码点。 int cp = "😊".codePointAt(0); // cp = 128522
Character.charCount(codePoint) 判断一个码点是占用 1 个 char 还是 2 个 char(代理对)。 Character.charCount(128522) 返回 2
String.length() 返回 char 数组的长度,不是字符的个数,对于包含代理对的字符串,这个值会比实际字符数大。 "A😊B".length() 返回 3
String.codePointCount(beginIndex, endIndex) 返回字符串指定范围内的实际字符个数(码点个数)。 "A😊B".codePointCount(0, 3) 返回 2
Character.toChars(int codePoint) 将一个码点转换为一个 char 数组,对于辅助平面的字符,它会返回包含高代理和低代理的 2 元素数组。 char[] chars = Character.toChars(0x1F60A);
  1. 基础:Java 的 char 是 16 位,内部使用 UTF-16 编码。
  2. 常见字符:大部分常用字符(BMP 内)可以用一个 char 表示。
  3. 特殊字符:emoji、生僻字等(BMP 外)需要用 代理对(2 个 char)表示。
  4. 陷阱String.length() 返回的是 char 数组长度,不是可见字符数,遍历字符串时,务必使用基于码点的方法(如 codePointAtcodePointCount)来避免错误处理代理对。
  5. 实践:处理国际化、特殊符号或需要精确控制文本的场景时,一定要牢记 UTF-16 的可变长度特性,并使用正确的 API。
分享:
扫描分享到社交APP
上一篇
下一篇