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

这里的关键点是:Java 使用 UTF-16 编码方案来表示 char 中的字符。
UTF-16 是一种可变长度的 Unicode 编码,这意味着:
- 对于大部分常见的字符(包括英文字母、数字、大部分中文、日文、韩文等),UTF-16 使用 1 个
char单元(16位) 来表示,这些字符的 Unicode 码点(code point)在U+0000到U+FFFF范围内。 - 对于一些不常用的字符(例如一些生僻的汉字、古文字、表情符号 emoji 等),UTF-16 使用 2 个
char单元(32位) 来表示,这被称为“代理对”(Surrogate Pair),这些字符的 Unicode 码点在U+10000到U+10FFFF范围内。
这个“代理对”的概念是 Java Unicode 编码中最需要注意的地方。
如何在 Java 中表示和处理 Unicode 字符?
字面量表示
在 Java 代码中,你可以用以下几种方式来表示一个 Unicode 字符:

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 规则。

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+0000 到 U+FFFF 的字符,而更多的字符在辅助平面,需要用代理对表示。
代理对的构成:
- 高代理区:
U+D800到U+DBFF - 低代理区:
U+DC00到U+DFFF
一个辅助平面的字符码点 P 可以通过以下公式计算代理对:
highSurrogate = (char) ((P - 0x10000) / 0x400 + 0xD800);lowSurrogate = (char) ((P - 0x10000) % 0x400 + 0xDC00);
示例:处理 emoji
让我们以笑脸 😊 (U+1F60A) 为例:
-
获取码点
int codePoint = 0x1F60A;// 十六进制表示 -
手动计算代理对
highSurrogate = ((0x1F60A - 0x10000) / 0x400 + 0xD800)= (0xF60A / 0x400) + 0xD800= 0x3D + 0xD800= 0xDE0D->char是\uD83DlowSurrogate = ((0x1F60A - 0x10000) % 0x400 + 0xDC00)= (0xF60A % 0x400) + 0xDC00= 0xA + 0xDC00= 0xDE0A->char是\uDE0A
😊 的代理对就是
\uD83D\uDE0A。 -
在代码中验证
// 直接使用代理对创建字符串 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); |
- 基础:Java 的
char是 16 位,内部使用 UTF-16 编码。 - 常见字符:大部分常用字符(BMP 内)可以用一个
char表示。 - 特殊字符:emoji、生僻字等(BMP 外)需要用 代理对(2 个
char)表示。 - 陷阱:
String.length()返回的是char数组长度,不是可见字符数,遍历字符串时,务必使用基于码点的方法(如codePointAt和codePointCount)来避免错误处理代理对。 - 实践:处理国际化、特殊符号或需要精确控制文本的场景时,一定要牢记 UTF-16 的可变长度特性,并使用正确的 API。
