在Java语言里,char类型是一个固定的16位无符号整数,设计之初用来对应Unicode的早期版本。当时Unicode的码点总量被限制在65536以内,所以一个16位的char足以表示一个字符。我们日常用到的大部分简体、繁体汉字都落在Unicode基本多文种平面(BMP)中,码点从U+4E00到U+9FFF,这部分字符确实可以直接放进一个char变量里,不会因为位数不够而丢失信息。

Unicode码点与Java代码单元的区别
要弄清楚char能不能存中文字符,必须先区分两个概念:码点(code point)和代码单元(code unit)。码点是Unicode给每个字符分配的唯一编号,理论范围是U+0000到U+10FFFF,总共需要21位二进制来表示。代码单元则是具体编码方案里传输和存储的最小单位。Java的char就是UTF-16编码下的一个16位代码单元,而不是一个完整的码点容器。
在UTF-16中,BMP内的字符(码点不超过U+FFFF)用单个16位代码单元表示,正好对应一个char。但补充平面(如U+10000以上的字符)无法用单个16位表达,于是UTF-16引入了代理对机制:用两个char,前一个在高代理区(U+D800到U+DBFF),后一个在低代理区(U+DC00到U+DFFF),合起来编码一个码点。因此,当遇到补充平面的汉字或符号时,单个char就存不下了,必须占用两个char。
下面通过一段代码观察同一个字符在不同平面下的存储差异。我们选取一个BMP汉字“中”和一个补充平面汉字(以U+20BB7为例,实际生僻字可能依系统字体而异),用代码点转char数组的方式演示:
public class CharTest {
public static void main(String[] args) {
// BMP中的汉字“中”,码点0x4E2D
char c1 = '中';
System.out.println("字符: " + c1 + ", 码点: " + (int) c1);
// 补充平面字符 U+20BB7,使用代理对
int codePoint = 0x20BB7;
char[] surrogate = Character.toChars(codePoint);
System.out.println("代理对长度: " + surrogate.length);
for (char c : surrogate) {
System.out.println("char值: " + Integer.toHexString(c));
}
}
}
从String内部机制看中文字符的存储
Java的String内部使用一个char数组来保存数据,也就是说,一个String的长度(length()方法返回值)实际上是char代码单元的数量,而不是人类直觉里的“字符个数”。对于纯BMP汉字组成的字符串,length和字符数一致;但若字符串里混入了补充平面字符,length就会比肉眼看到的字符数多。这一机制在很多文本处理、截断、反转字符串的场景中容易引发隐藏的bug。
举例来说,如果你用substring按char位置截断,恰好切在代理对的中间,就会产生一个非法的代理char,输出时可能变成乱码或替换符。正确的做法是用codePoint相关API,比如codePointCount和offsetByCodePoints,以码点为单位操作。String也提供了codePoints()流,可以安全地遍历每一个真实字符。
以下示例展示如何安全地统计包含补充平面字符的字符串中的真实字符数:
public class StringCodePoint {
public static void main(String[] args) {
String s = "中uD83DuDDEB"; // 一个BMP汉字 + 一个补充平面表情字符(代理对)
System.out.println("char长度: " + s.length());
System.out.println("码点数量: " + s.codePointCount(0, s.length()));
s.codePoints().forEach(cp -> System.out.println("码点: " + Integer.toHexString(cp)));
}
}
实际开发中的编码建议与常见误区
很多初学者误以为Java的char一定能表示一个“字”,从而在循环里写for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); },并把c当作完整字符处理。这种写法在纯中文界面或基本汉字场景下没问题,但一旦用户输入了扩展汉字、emoji或某些古文字,程序就会出现截断错误。所以,在涉及用户输入、文件解析、网络协议时,务必以码点为最小处理单位。
另一个误区是认为把文件保存成UTF-8,Java内部char就自动变成UTF-8了。事实上,Java源码编译后,class里的字符串常量以 modified UTF-8 存储,但运行时String对象一律使用UTF-16的char数组。读写外部数据时,要通过InputStreamReader指定UTF-8等字符集,把字节流正确解码为UTF-16的char序列;写出时再编码回去。只有理解JVM内部表示和外部编码的边界,才能避免中文乱码。
如果业务需要支持全部Unicode文字且希望内存更紧凑,也可以考虑第三方库或使用Java 9之后的紧凑字符串特性(纯Latin-1时省内存,但中文仍走UTF-16)。无论如何,判断“char能不能存中文字符”的答案应当是:对于基本平面的汉字可以,对于补充平面的汉字不行,准确结论取决于字符所处的Unicode区间以及你是否以码点视角来考量。