什么是 Unicode?
要理解 Java 的 Unicode 编码,必须先明白 Unicode 是什么。

- Unicode 的目标:为世界上所有的字符(包括字母、数字、标点符号、表情符号、象形文字等)分配一个唯一的数字,这个数字被称为 码点。
- 码点:通常用
U+开头的十六进制数表示。U+0041是大写字母 'A'U+4E2D是中文字符 '中'U+1F600是笑脸表情 '😀'
- Unicode 字符集:就像一本巨大的字典,定义了字符和码点之间的映射关系,它只负责“哪个字符对应哪个码点”,不规定如何存储这个码点。
Java 中的基本单位:char
Java 语言在设计之初就内置了对 Unicode 的强大支持,其核心是 char 数据类型。
char的本质:在 Java 中,char是一个 16位无符号 的基本数据类型,这意味着它可以表示0到65535(即0xFFFF)之间的整数值。char与 Unicode 的关系:Java 选择 16 位,就是为了能够直接表示 Unicode 标准中的 基本多文种平面,BMP 包含了世界上绝大多数正在使用的文字字符,其码点范围正好是U+0000到U+FFFF。
示例:
public class CharExample {
public static void main(String[] args) {
// 字符 'A' 的码点是 U+0041
char a = 'A';
System.out.println("字符: " + a);
System.out.println("码点 (十进制): " + (int) a); // 强制转换为 int,输出 65
System.out.println("码点 (十六进制): " + Integer.toHexString(a)); // 输出 41
// 中文字符 '中' 的码点是 U+4E2D
char zhong = '中';
System.out.println("\n字符: " + zhong);
System.out.println("码点 (十进制): " + (int) zhong); // 输出 20013
System.out.println("码点 (十六进制): " + Integer.toHexString(zhong)); // 输出 4e2d
}
}
重要提示:char 的局限性
由于 char 只有 16 位,它无法直接表示 BMP 之外的字符,这些字符的码点范围在 U+10000 到 U+10FFFF 之间,被称为 辅助平面字符,emoji 表情符号。

为了解决这个问题,Java 引入了 代理对 的概念。
处理辅助平面字符:代理对
对于 BMP 之外的字符,Java 使用一对 16 位的 char 来表示它们,这被称为 代理对。
- 高位代理:范围是
\uD800到\uDBFF。 - 低位代理:范围是
\uDC00到\uDFFF。
一个辅助平面字符由一个高位代理和一个低位代理组成。
如何正确处理代理对?

手动处理代理对非常复杂且容易出错,Java 提供了 Character 类和 String 类的方法来简化操作。
示例:处理 Emoji
public class SurrogatePairExample {
public static void main(String[] args) {
// 笑脸表情 '😀' 的码点是 U+1F600
// 它是一个辅助平面字符,需要用代理对表示
String emoji = "😀";
// 1. String.length() 会返回代理对的个数,而不是字符的个数
System.out.println("字符串长度: " + emoji.length()); // 输出 2
// 2. 获取码点
int codePoint = emoji.codePointAt(0);
System.out.println("码点 (十进制): " + codePoint); // 输出 128512
System.out.println("码点 (十六进制): " + Integer.toHexString(codePoint)); // 输出 1f600
// 3. 遍历字符串时,应该使用码点迭代,而不是 char
System.out.println("\n正确遍历字符串:");
for (int i = 0; i < emoji.length(); ) {
int codePointValue = emoji.codePointAt(i);
System.out.println("字符: " + new String(Character.toChars(codePointValue)) + ", 码点: U+" + Integer.toHexString(codePointValue));
i += Character.charCount(codePointValue); // 根据码点决定移动1个或2个char位置
}
// 4. 错误的遍历方式(会得到两个无意义的代理 char)
System.out.println("\n错误的遍历方式:");
for (int i = 0; i < emoji.length(); i++) {
char c = emoji.charAt(i);
System.out.println("char值: " + c + ", 十六进制: \\u" + Integer.toHexString(c));
}
}
}
关键方法总结:
| 方法 | 描述 |
|---|---|
str.length() |
返回代理对的个数(char 的数量)。 |
str.charAt(index) |
返回指定位置的 char(可能是代理对的一部分)。 |
str.codePointAt(index) |
返回从指定 char 位置开始的码点值,这是处理辅助平面字符的关键。 |
Character.charCount(codePoint) |
判断一个码点是占用 1 个 char 还是 2 个 char(即是否为辅助平面字符)。 |
Character.toChars(codePoint) |
将一个码点转换为一个 char 数组(对于 BMP 字符是长度为1的数组,对于辅助平面字符是长度为2的代理对数组)。 |
Unicode 与编码(UTF-8, UTF-16)
现在我们来区分 字符集 和 编码。
- 字符集:Unicode,定义了“字符”和“码点”的映射。
- 编码:一套规则,规定了如何将码点存储为字节序列,常见的 Unicode 编码有 UTF-8、UTF-16 和 UTF-32。
Java 内部使用 UTF-16 编码来表示字符串。
- Java 内部表示:一个
String对象在内存中就是由char数列组成的,这个序列就是 UTF-16 编码。 - UTF-16 的特点:
- 对于 BMP 内的字符(
U+0000-U+FFFF),它使用 2个字节 存储。 - 对于辅助平面字符(
U+10000-U+10FFFF),它使用 4个字节 存储(即一个代理对)。
- 对于 BMP 内的字符(
UTF-8 vs UTF-16 (Java 中的常见问题)
| 特性 | UTF-8 | UTF-16 (Java 内部) |
|---|---|---|
| 变长/定长 | 变长 (1-4 字节) | mostly 定长 (BMP字符2字节, 辅助平面4字节) |
| ASCII 兼容性 | 完全兼容,ASCII 字符占1字节 | 不兼容,ASCII 字符也占2字节 |
| 空间效率 | 西方文本(多ASCII)效率高,中文、日文等通常占3字节。 | 中文、日文等效率高(占2字节),西方文本效率低。 |
| Java 内部 | Java 源文件、网络传输、文件 I/O 中常用。 | Java String 的内部存储格式。 |
实践应用:编码转换与乱码
乱码问题的根源在于:数据的编码格式和读取数据的编码格式不一致。
场景:从文件读取字符串
假设你有一个文件 test.txt是 "你好",并且这个文件是使用 GBK 编码保存的。
import java.io.*;
import java.nio.charset.StandardCharsets;
public class EncodingExample {
public static void main(String[] args) {
String filePath = "test.txt";
String content = "你好"; // 假设这个文件是用GBK编码保存的
// --- 错误示范:乱码 ---
// 我们错误地使用 UTF-8 去读取一个 GBK 编码的文件
try (InputStreamReader isr = new InputStreamReader(new FileInputStream(filePath), StandardCharsets.UTF_8)) {
char[] cbuf = new char[1024];
int len = isr.read(cbuf);
String wrongResult = new String(cbuf, 0, len);
System.out.println("错误读取结果 (用UTF-8读GBK文件): " + wrongResult); // 输出乱码,如 æä½½
} catch (IOException e) {
e.printStackTrace();
}
// --- 正确示范:正确解码 ---
// 我们使用正确的 GBK 编码去读取文件
try (InputStreamReader isr = new InputStreamReader(new FileInputStream(filePath), "GBK")) {
char[] cbuf = new char[1024];
int len = isr.read(cbuf);
String correctResult = new String(cbuf, 0, len);
System.out.println("正确读取结果 (用GBK读GBK文件): " + correctResult); // 输出 你好
} catch (IOException e) {
e.printStackTrace();
}
}
}
场景:向文件写入字符串
import java.io.*;
import java.nio.charset.StandardCharsets;
public class WritingEncodingExample {
public static void main(String[] args) {
String filePath = "output.txt";
String text = "Hello, 世界!";
// --- 正确示范:使用 UTF-8 写入 ---
try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(filePath), StandardCharsets.UTF_8)) {
osw.write(text);
System.out.println("已使用 UTF-8 编码写入文件。");
} catch (IOException e) {
e.printStackTrace();
}
// --- 另一个正确示范:使用 GBK 写入 ---
// 如果程序需要与只支持 GBK 的旧系统交互,就需要用 GBK 写入
try (OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("output_gbk.txt"), "GBK")) {
osw.write(text);
System.out.println("已使用 GBK 编码写入文件。");
} catch (IOException e) {
e.printStackTrace();
}
}
}
- Unicode 是基石:Java 从底层支持 Unicode,每个字符都有一个唯一的码点。
char是基本单位:Java 的char是 16 位,用于表示 BMP 中的字符,对于辅助平面字符(如 Emoji),使用 代理对(两个char)表示。String是 UTF-16 序列:Java 的String内部由 UTF-16 编码的char序列构成,处理字符串时,应优先使用codePointAt()等方法来正确处理所有字符。- 编码是桥梁:UTF-16 是 Java 内部表示,但文件、网络传输等常用 UTF-8 或其他编码。编码和解码必须使用相同的字符集,否则会产生乱码。
- 最佳实践:
- 在代码中,直接使用
String和char,让 Java 处理内部细节。 - 在进行 I/O 操作(读写文件、网络请求)时,显式指定编码(如
StandardCharsets.UTF_8),不要依赖平台默认编码,因为默认编码可能因环境而异,导致跨平台问题。
- 在代码中,直接使用
掌握这些概念,你就能在 Java 开发中游刃有余地处理各种文本和国际化问题了。
