Unicode 和字符集
在深入 Java 之前,必须先理解几个关键概念:

- 字符:一个抽象的符号,'A','中','€'。
- 编码:将字符映射成计算机可以处理的数字(二进制)的一套规则。
- 字符集:一个字符的集合,以及每个字符对应的唯一编号,这个编号就是 码点。
- Unicode 是目前最流行、最全面的字符集,它旨在为世界上所有的字符都分配一个唯一的码点,码点通常用
U+后跟十六进制数表示,'中' 的码点是U+4E2D。
- Unicode 是目前最流行、最全面的字符集,它旨在为世界上所有的字符都分配一个唯一的码点,码点通常用
- 编码方案:将 Unicode 码点转换为字节序列的具体实现方式。
- UTF-8:变长编码,英文字符(ASCII 范围)占用 1 字节,中文、日文等常用字符通常占用 3 字节,生僻字符可能占用 4 字节。这是目前互联网上最主流的编码方式。
- UTF-16:定长或变长编码,Java 语言内部使用 UTF-16 来表示字符串,基本多语言平面 的字符(包括绝大多数中文)固定占用 2 字节,辅助平面的字符(如某些生僻字或 emoji)占用 4 字节。
- GBK / GB2312:中国国家标准编码,一个中文通常占用 2 字节。注意:这是中文环境下的历史遗留编码,在现代新项目中应避免使用,除非必须兼容非常老的系统。
Java 中的 Unicode 支持
Java 从设计之初就内置了对 Unicode 的强大支持,这是它的一大优势。
char 类型与 UTF-16
Java 的 char 数据类型是一个 16 位无符号整数,用来表示一个 UTF-16 代码单元。
- 对于大多数中文字符(在 BMP 平面内),一个
char就可以完整表示一个字符。char chineseChar = '中'; System.out.println(chineseChar); // 输出: 中 System.out.println((int)chineseChar); // 输出该字符的码点: 20013 (十进制) 或 0x4E2D (十六进制)
- 对于需要 4 字节表示的字符(如某些 emoji 或生僻字),Java 使用 代理对 来表示,这由两个
char组成。Character类提供了处理这种情况的工具方法。
String 类
Java 的 String 类内部就是一个 char[] 数组,因此它天然就是基于 UTF-16 编码的。
String str = "Hello, 世界!"; System.out.println(str.length()); // 输出 9,因为 '世' 和 '界' 各占一个 char
转义字符
Java 源代码文件本身是使用某种编码保存的(通常是 UTF-8),但在代码中,你可以使用 Unicode 转义序列来直接表示任何字符。

格式为:\u 后跟 4 位十六进制数。
// 这两种写法是等价的 char c1 = '中'; char c2 = '\u4E2D'; System.out.println(c1 == c2); // 输出 true
你也可以在 String 中使用:
String s1 = "你好"; String s2 = "\u4F60\u597D"; // \u4F60 是 '你', \u597D 是 '好' System.out.println(s1.equals(s2)); // 输出 true
注意:\u 转义是在 Java 编译器读取源代码时进行的,与源文件本身的编码无关。
常见问题与解决方案
问题 1:乱码
这是处理中文时最常见的问题,乱码的根本原因是 编码和解码时使用的编码不一致。

场景:从文件或网络读取中文
假设一个文本文件 test.txt 是用 GBK 编码保存的,内容是 "你好世界",但你用错误的编码(如 UTF-8)去读取它,就会得到乱码。
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.charset.Charset;
public class ReadChineseFile {
public static void main(String[] args) {
String filePath = "test.txt";
// --- 错误示范:假设文件是 GBK,但用 UTF-8 读取 ---
try {
String contentWrong = new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8);
System.out.println("用 UTF-8 读取 GBK 文件: " + contentWrong); // 输出乱码
} catch (IOException e) {
e.printStackTrace();
}
// --- 正确示范:使用正确的编码 GBK 读取 ---
try {
// 需要先获取系统支持的 GBK 编码,或使用第三方库如 Apache Commons Codec
Charset gbkCharset = Charset.forName("GBK");
String contentCorrect = new String(Files.readAllBytes(Paths.get(filePath)), gbkCharset);
System.out.println("用 GBK 读取 GBK 文件: " + contentCorrect); // 输出: 你好世界
} catch (IOException e) {
e.printStackTrace();
}
}
}
解决方案:
- 明确数据源的编码:无论是文件、数据库还是网络请求,都要清楚数据最初是用什么编码保存或发送的。
- 在读取/写入时使用正确的
Charset:- 推荐:始终优先使用
StandardCharsets枚举中预定义的常量,如StandardCharsets.UTF_8。 - 对于特殊编码(如 GBK),使用
Charset.forName("GBK")。 - 避免:使用平台默认编码,如
String(byte[] bytes)或Writer w = new FileWriter("file.txt");,因为不同平台的默认编码可能不同,导致程序可移植性变差。
- 推荐:始终优先使用
问题 2:判断字符串中是否包含中文字符
一个简单的方法是判断字符的 Unicode 范围,中文字符的码点主要集中在 U+4E00 到 U+9FFF 之间(CJK 统一表意文字)。
public class ContainsChinese {
public static boolean isChinese(char c) {
Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);
// 判断是否为中日韩文字
return ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
|| ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
|| ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
|| ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B
|| ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION
|| ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS
|| ub == Character.UnicodeBlock.GENERAL_PUNCTUATION;
}
public static boolean containsChinese(String str) {
if (str == null) {
return false;
}
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (isChinese(c)) {
return true;
}
}
return false;
}
public static void main(String[] args) {
String s1 = "Hello, 世界!";
String s2 = "Hello, World!";
String s3 = "こんにちは"; // 日文平假名
System.out.println(containsChinese(s1)); // true
System.out.println(containsChinese(s2)); // false
System.out.println(containsChinese(s3)); // false (根据上面的方法)
}
}
注意:str.length() 返回的是 char 的数量(UTF-16 代码单元数),对于包含代理对的字符(如 emoji),length() 会返回 2,如果需要获取字符的真正数量(码点数量),应该使用 str.codePointCount(0, str.length())。
问题 3:遍历字符串中的字符(正确处理代理对)
如果字符串中可能包含 4 字节的字符,使用 for (int i = 0; i < str.length(); i++) 的方式遍历会有问题,因为它会把一个 4 字节的字符当成两个 char 来处理。
正确做法是使用 codePointAt 方法:
public class IterateString {
public static void main(String[] args) {
// 这是一个 emoji,由两个 char 组成的代理对
String emoji = "😊";
System.out.println("emoji.length() = " + emoji.length()); // 输出 2
// 错误遍历方式
System.out.println("--- 错误遍历 ---");
for (int i = 0; i < emoji.length(); i++) {
System.out.println(emoji.charAt(i));
}
// 输出两个无法识别的符号
// 正确遍历方式
System.out.println("--- 正确遍历 ---");
for (int i = 0; i < emoji.length(); ) {
int codePoint = emoji.codePointAt(i);
System.out.println("码点: " + codePoint + ", 字符: " + new String(Character.toChars(codePoint)));
i += Character.charCount(codePoint); // 根据是 1 个还是 2 个 char 来移动索引
}
// 输出:
// 码点: 128522, 字符: 😊
}
}
- 源代码编码:始终将你的 Java 源代码文件保存为 UTF-8 格式,现代 IDE(如 IntelliJ IDEA, Eclipse)都默认支持并推荐这样做。
- 内部处理:在 Java 程序内部,
String和char的 UTF-16 表示是透明的,你通常不需要关心,直接使用StringAPI 即可。 - I/O 操作:
- 读写文件:始终显式指定
Charset,优先使用StandardCharsets.UTF_8。 - 网络通信:确保 HTTP 请求/响应头中包含正确的
Content-Type和Charset信息,Content-Type: text/html; charset=utf-8。
- 读写文件:始终显式指定
- 数据库交互:确保数据库、数据库连接、数据库表的字符集都设置为
utf8mb4(MySQL 中推荐,能完整支持 Unicode,包括 emoji)。 - 避免使用默认编码:不要依赖平台的默认编码,始终在
String、Reader、Writer、InputStream、OutputStream的构造函数或方法中明确指定Charset。
遵循这些原则,你就可以在 Java 中游刃有余地处理包括中文在内的任何 Unicode 文本了。
