在 Java 中,char 类型使用 UTF-16 编码来表示字符,这意味着:

- 大部分常用汉字(基本多文种平面,BMP)可以用一个
char(16位)表示。 - 一些生僻汉字或特殊符号(辅助平面,Supplementary Planes)需要两个
char(一个“代理对”,Surrogate Pair)来表示。
Java 中的 char 和 Unicode
Java 语言设计之初,Unicode 标准还停留在 BMP(基本多文种平面,U+0000 到 U+FFFF)范围内,BMP 包含了世界上绝大多数语言的常用字符,其中就包括了几乎所有的常用汉字。
Java 的 char 类型被定义为 16 位无符号整数,正好可以存储 BMP 中的一个字符。
char c = '中'; // '中' 字的 Unicode 码点是 U+4E2D System.out.println(c); // 输出: 中 System.out.println((int)c); // 输出: 20013 (这是 U+4E2D 的十进制表示)
在上面的例子中,'中' 这个字在 BMP 内部,所以它完美地适配一个 char。
代理对:处理 BMP 之外的字符
随着 Unicode 标准的发展,字符数量大大增加,超出了 BMP 的范围,这些新增的字符被放置在“辅助平面”(从 U+10000 到 U+10FFFF),为了在仍然使用 16 位 char 的 Java 中表示这些字符,Unicode 标准引入了代理对(Surrogate Pair)机制。

代理对由两个特殊的 char 组成:
- 高代理(High Surrogate / Leading Surrogate): 范围在
\uD800到\uDBFF。 - 低代理(Low Surrogate / Trailing Surrogate): 范围在
\uDC00到\uDFFF。
一个辅助平面的字符码点 codePoint 可以通过以下公式转换为代理对:
highSurrogate = (char) ((codePoint - 0x10000) / 0x400 + 0xD800)lowSurrogate = (char) ((codePoint - 0x10000) % 0x400 + 0xDC00)
例子:一个生僻字 "𠮷"
这个字(古同“吉”)的 Unicode 码点是 U+20BB7,它在辅助平面,无法用一个 char 表示。
// 这个字的 Unicode 码点
int codePoint = 0x20BB7;
// 在 Java 中,你必须用 char 数组或 String 来表示它
// 它实际上由两个 char 组成
String s = new String(new int[]{codePoint}, 0, 1);
// 获取这个字符串的长度
System.out.println(s.length()); // 输出: 2
// 获取构成它的两个 char
char[] chars = s.toCharArray();
System.out.println("高代理: " + Integer.toHexString(chars[0])); // 输出: d842
System.out.println("低代理: " + Integer.toHexString(chars[1])); // 输出: dfb7
// 正确地打印这个字
System.out.println(s); // 输出: 𠮷
关键点:
s.length()返回的是 2,因为它由两个char组成。- 如果错误地遍历
char数组,你会得到两个无意义的“代理字符”,而不是正确的汉字。
正确处理汉字(推荐使用 int codePoint)
由于代理对的存在,在处理字符串时,永远不要直接遍历 char 数组,你应该使用 codePointAt() 方法来获取正确的字符码点。
String text = "Hello 你好 𠮷";
// 错误的遍历方式:会错误地拆分生僻字
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
System.out.println("Index " + i + ": " + c + " (code point: " + (int)c + ")");
}
// 输出:
// Index 0: H (code point: 72)
// Index 1: e (code point: 101)
// ...
// Index 8: 你 (code point: 20320)
// Index 9: 好 (code point: 22909)
// Index 10: (code point: 55362) <- 这是高代理,不是完整的字
// Index 11: (code point: 57223) <- 这是低代理,不是完整的字
// 正确的遍历方式:使用 codePointAt
System.out.println("\n--- 正确遍历 ---");
for (int i = 0; i < text.length(); ) {
int codePoint = text.codePointAt(i);
// 判断这个码点是代表一个字符还是代理对
int charCount = Character.charCount(codePoint);
System.out.println("Code Point U+" + Integer.toHexString(codePoint) +
" -> Character: " + (char)codePoint +
", Length in chars: " + charCount);
// 移动索引,如果是代理对就移动2位,否则移动1位
i += charCount;
}
// 输出:
// --- 正确遍历 ---
// Code Point U+48 -> Character: H, Length in chars: 1
// Code Point U+65 -> Character: e, Length in chars: 1
// ...
// Code Point U+4f60 -> Character: 你, Length in chars: 1
// Code Point U+597d -> Character: 好, Length in chars: 1
// Code Point U+20bb7 -> Character: 𠮷, Length in chars: 2
String、char[] 与字节数组的转换
在实际应用中,我们经常需要将 Java 的 String 与网络传输或文件存储的字节数组进行转换,这时就需要指定字符编码。
最重要的编码:UTF-8
UTF-8 是目前互联网上最通用的编码方式,它是一种变长编码:
- ASCII 字符(0-127)占用 1 个字节。
- 汉字等常用字符(BMP 内)通常占用 3 个字节。
- 辅助平面的字符(如 )占用 4 个字节。
String -> byte[] (编码)
String str = "你好,世界!𠮷";
// 使用 UTF-8 编码
byte[] utf8Bytes = str.getBytes(StandardCharsets.UTF_8);
System.out.println("UTF-8 字节数组长度: " + utf8Bytes.length);
// "你好,世界!" 是 6个汉字 * 3字节 = 18字节
// "𠮷" 是 4字节
// 总长度是 22 字节
// 输出: UTF-8 字节数组长度: 22
// 如果不指定编码,会使用 JVM 默认字符集,这可能导致问题
byte[] defaultBytes = str.getBytes(); // 不推荐!
byte[] -> String (解码)
// 从字节数组解码回 String
String decodedStr = new String(utf8Bytes, StandardCharsets.UTF_8);
System.out.println("解码后的字符串: " + decodedStr);
// 输出: 解码后的字符串: 你好,世界!𠮷
其他编码(GBK/GB2312)
在中国大陆,有时还会遇到 GBK 或 GB2312 编码,这些是双字节编码,主要用于表示简体中文。
- 特点:一个汉字通常固定占用 2 个字节。
- 问题:无法表示 这样的生僻字,也无法表示日文、韩文等其他语言的字符。
String -> byte[] (GBK 编码)
String gbkStr = "你好";
// 使用 GBK 编码
byte[] gbkBytes = gbkStr.getBytes("GBK");
System.out.println("GBK 字节数组长度: " + gbkBytes.length);
// 输出: GBK 字节数组长度: 4 (每个汉字 2 字节)
byte[] -> String (GBK 解码)
String decodedGbkStr = new String(gbkBytes, "GBK");
System.out.println("GBK 解码后的字符串: " + decodedGbkStr);
// 输出: GBK 解码后的字符串: 你好
总结与最佳实践
- 理解
char的局限性:知道char只能表示 BMP 字符,生僻字需要代理对。 - 永远不要用
for(char c : str):遍历字符串时,使用codePointAt()和Character.charCount()来确保正确处理所有字符。 - 显式指定编码:在进行
String和byte[]转换时,永远显式地指定字符编码(如StandardCharsets.UTF_8),不要依赖 JVM 的默认编码,这能避免绝大多数乱码问题。 - 优先使用 UTF-8:在 Web 开发、文件存储、网络通信等所有场景下,都优先使用 UTF-8 作为标准编码。
- 使用
String.codePointCount():如果你想获取一个字符串中有多少个字符(而不是多少个char),使用这个方法。String s = "𠮷"; System.out.println(s.length()); // 2 System.out.println(s.codePointCount(0, s.length())); // 1
