核心概念
在深入代码之前,必须先理解几个核心概念:

- 字符集:这是一个字符的集合,ASCII、GB2312、GBK、Big5、Unicode,它规定了“哪些字符是有效的”,GB2312 包含了约 6763 个汉字和符号,而 Unicode 则包含了世界上几乎所有的字符。
- 编码:这是将字符集中的字符转换为计算机可以存储和传输的二进制数据(字节序列)的规则。同一个字符集可以有多种编码方式。
- 编码与字符集的关系:
- GB2312:它既是字符集,也是最常见的编码方式,我们通常说的 "GB2312 编码" 就是指使用 GB2312 字符集的编码规则。
- UTF-8:它是 Unicode 字符集的一种编码方式,Unicode 字符集还有 UTF-16、UTF-32 等编码方式,UTF-8 是目前互联网上最广泛使用的编码,因为它对英文是单字节,对中文是 3 字节,兼容性好且节省空间。
简单比喻:
- 字符集 好比一本字典,里面收录了所有可用的汉字(如“你”、“好”、“Hello”)。
- 编码 好比字典后面的“电报码”,每个汉字都有一个唯一的数字或电报码(字节序列)来代表它。
- Java 内部处理:Java 的
String、char等类型,在内存中统一使用 UTF-16 这种编码来表示字符,这意味着,无论你输入的是什么编码的字节,只要被正确解码成String,它在内存中的形式都是一样的。
GB2312 和 UTF-8 的主要区别
| 特性 | GB2312 | UTF-8 |
|---|---|---|
| 字符范围 | 简体中文为主,包含 6763 个汉字和 919 个其他符号。 | 几乎涵盖全世界的所有字符(包括中文、英文、日文、emoji 等)。 |
| 字节/字符 | 英文 1 字节,中文 2 字节。 | 英文 1 字节,中文 3 字节,生僻字可能 4 字节。 |
| 兼容性 | 仅适用于简体中文环境,无法处理其他语言或繁体中文。 | 国际通用,是 Web 标准和跨平台开发的推荐编码。 |
| 历史 | 较早的编码,在老系统、老数据库中常见。 | 现代、通用的编码,是目前的主流。 |
Java 中的编码转换关键类
Java 提供了强大的 java.nio.charset 包来处理编码问题,最核心的类是 Charset。
java.nio.charset.StandardCharsets:一个包含标准字符集常量的便捷类。StandardCharsets.UTF_8StandardCharsets.ISO_8859_1(一个单字节编码,常用于“中间人”转换)
java.lang.String:String(byte[] bytes, Charset charset):使用指定的charset将字节数组解码成字符串。byte[] getBytes(Charset charset):将字符串使用指定的charset编码成字节数组。
java.io.InputStreamReader/java.io.OutputStreamWriter:在进行文件读写或网络 I/O 时,用它们来包装字节流,并指定编码,可以避免乱码。
场景与实践
字符串与字节数组的相互转换
这是最基础的转换,理解了这个,其他问题就迎刃而解。

import java.nio.charset.StandardCharsets;
public class EncodingConversion {
public static void main(String[] args) {
String originalString = "你好,世界!Hello, World!";
// 1. String -> Bytes (编码)
// 使用 GB2312 编码
byte[] gb2312Bytes = originalString.getBytes(StandardCharsets.GBK); // 注意:GBK 是 GB2312 的超集,更常用
System.out.println("GB2312 编码后的字节数组长度: " + gb2312Bytes.length);
// 使用 UTF-8 编码
byte[] utf8Bytes = originalString.getBytes(StandardCharsets.UTF_8);
System.out.println("UTF-8 编码后的字节数组长度: " + utf8Bytes.length);
// 2. Bytes -> String (解码)
// 从 GB2312 字节数组解码回字符串
String decodedFromGb2312 = new String(gb2312Bytes, StandardCharsets.GBK);
System.out.println("从 GB2312 解码后的字符串: " + decodedFromGb2312);
// 从 UTF-8 字节数组解码回字符串
String decodedFromUtf8 = new String(utf8Bytes, StandardCharsets.UTF_8);
System.out.println("从 UTF-8 解码后的字符串: " + decodedFromUtf8);
// 3. 常见的乱码问题:用错误的编码解码
// 假设我们有一个用 GB2312 编码的字节数组,但我们错误地用 UTF-8 去解码
String wrongDecoded = new String(gb2312Bytes, StandardCharsets.UTF_8);
System.out.println("【乱码】用 UTF-8 解码 GB2312 字节: " + wrongDecoded);
}
}
输出分析:
- GB2312 编码下,中文字符占 2 字节,英文字符占 1 字节。
- UTF-8 编码下,中文字符占 3 字节,英文字符占 1 字节。
- 最后一个
wrongDecoded输出会是乱码,因为解码规则和编码规则不一致。
文件的读写(解决乱码)
这是最常见的需求。核心思想是:在打开文件流时,就指定好正确的编码。
import java.io.*;
import java.nio.charset.StandardCharsets;
public class FileEncodingExample {
public static void main(String[] args) throws IOException {
String content = "这是一个使用 GB2312 编码的文件,This is a GB2312 encoded file.";
String filePath = "gb2312.txt";
// 1. 以 GB2312 编码写入文件
// 使用 OutputStreamWriter 来指定编码
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream(filePath), StandardCharsets.GBK)) {
osw.write(content);
System.out.println("已用 GB2312 编码写入文件: " + filePath);
}
// 2. 以 GB2312 编码读取文件
// 使用 InputStreamReader 来指定编码
System.out.println("\n--- 正确读取 ---");
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream(filePath), StandardCharsets.GBK)) {
char[] cbuf = new char[1024];
int len = isr.read(cbuf);
String readContent = new String(cbuf, 0, len);
System.out.println("正确读取到的内容: " + readContent);
}
// 3. 演示错误的读取方式(会乱码)
System.out.println("\n--- 错误读取(会乱码) ---");
try (FileReader fr = new FileReader(filePath)) { // FileReader 默认使用系统编码,很可能是 UTF-8
char[] cbuf = new char[1024];
int len = fr.read(cbuf);
String wrongContent = new String(cbuf, 0, len);
System.out.println("错误读取到的内容(乱码): " + wrongContent);
}
}
}
关键点:
- 不要直接使用
FileReader和FileWriter,因为它们的编码依赖于系统的默认编码(file.encoding),这会导致代码在不同环境下表现不一致,是乱码的重灾区。 - 始终使用
InputStreamReader和OutputStreamWriter,并在构造函数中明确指定Charset。
处理网络请求(如 HTTP)
在处理 HTTP 请求和响应时,编码信息通常在 Content-Type 头中指定,Content-Type: text/html; charset=gb2312。

使用像 HttpClient 或 OkHttp 这样的现代 HTTP 客户端库时,它们通常会自动根据响应头中的 charset 来解码响应体,如果你需要手动处理,可以这样做:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
public class NetworkEncodingExample {
public static void main(String[] args) throws Exception {
// 假设这个 URL 返回的是 GB2312 编码的页面(实际中非常少见)
String urlString = "http://example.com/some-gb2312-page.html"; // 请替换为一个真实的 GB2312 编码页面
URL url = new URL(urlString);
URLConnection conn = url.openConnection();
// 1. 从响应头中获取编码信息(如果有的话)
String contentType = conn.getContentType();
String charset = "UTF-8"; // 默认使用 UTF-8
if (contentType != null) {
// 示例: "text/html; charset=gb2312"
for (String param : contentType.replace(" ", "").split(";")) {
if (param.startsWith("charset=")) {
charset = param.split("=")[1];
break;
}
}
}
System.out.println("检测到或使用的编码: " + charset);
// 2. 使用检测到的编码来读取流
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), charset))) {
String line;
StringBuilder response = new StringBuilder();
while ((line = reader.readLine()) != null) {
response.append(line);
}
System.out.println("\n获取到的页面内容(前100字符):");
System.out.println(response.substring(0, Math.min(100, response.length())));
}
}
}
总结与最佳实践
- 优先使用 UTF-8:在所有新项目中,统一使用 UTF-8 作为编码标准,包括文件编码、数据库编码、网络传输编码等,这可以避免 90% 的乱码问题。
- 明确指定编码:在 Java 中,所有涉及 I/O 的地方(文件、网络、数据库连接等),都不要依赖系统默认编码。显式地传入
StandardCharsets.UTF_8或其他需要的Charset对象。 - 理解
String的内部表示:Java 的String在内存中是 UTF-16 编码的,编码和解码只发生在String和byte[]之间,或者通过Reader/Writer与外部 I/O 流之间。 - 处理遗留系统:当你必须与使用 GB2312、GBK 等旧编码的系统交互时(例如读取一个旧数据库导出的 CSV 文件),只在数据进入你系统的边界处进行一次解码,将数据解码成
String后,在你的整个应用内部都使用 UTF-8 进行处理,当需要将数据返回给旧系统时,再进行一次编码。 - IDE 和工具配置:确保你的 IDE(如 IntelliJ IDEA, Eclipse)的文件编码设置为 UTF-8,并且你的构建工具(Maven, Gradle)的源文件和资源文件编码也配置为 UTF-8。
遵循这些原则,你就能在 Java 中游刃有余地处理各种编码问题。
