核心概念
在开始编码之前,必须理解几个关键概念:

- 编码:将字符(如 '中')转换成字节序列(如
[0xD6, 0xD0])的规则。 - 解码:将字节序列转换回字符的过程,是编码的逆过程。
UTF-8:Unicode 字符集的一种实现方式,它是一种变长编码,可以表示全世界几乎所有字符,是国际通用的标准,英文字符占用 1 字节,中文字符通常占用 3 字节。GB2312:是中国国家标准简体中文字符集,它只能表示简体中文、英文、数字和一些符号,一个中文字符固定占用 2 字节,它是历史遗留编码,主要用于处理一些旧的系统或文件。
在 Java 代码中硬编码字符串
这是最简单的情况,Java 源代码文件(.java)本身就需要一种编码来保存,从 Java 5 开始,默认就是 UTF-8。
问题:如果你在代码中直接写了一个中文字符串,String s = "你好";,这个字符串在内存中是什么编码?
答案:在 Java 内部,所有字符串都使用 UTF-16 编码,这是一个内部表示,你不需要关心,你只需要关心在输入(读取)和输出(写出)时如何转换成你想要的编码。
示例: 这个例子展示了字符串的内存表示与编码/解码无关。

public class StringEncoding {
public static void main(String[] args) {
// Java 内部使用 UTF-16 存储字符串
String str = "你好Java";
// 获取字符串的字节数组,使用指定的编码
byte[] utf8Bytes = str.getBytes(StandardCharsets.UTF_8);
byte[] gb2312Bytes = str.getBytes(StandardCharsets.GB2312);
System.out.println("原始字符串: " + str);
System.out.println("字符串长度: " + str.length()); // 输出 5
// 打印 UTF-8 编码的字节
System.out.println("\nUTF-8 编码的字节:");
for (byte b : utf8Bytes) {
System.out.printf("%02X ", b); // E4 BD A0 E5 A5 BD 4A 61 76 61
}
// 你(E4 BD A0) 好(E5 A5 BD) J(4A) a(61) v(61) a(61)
// 打印 GB2312 编码的字节
System.out.println("\n\nGB2312 编码的字节:");
for (byte b : gb2312Bytes) {
System.out.printf("%02X ", b); // C4 E3 BA C3 4A 61 76 61
}
// 你(C4 E3) 好(BA C3) J(4A) a(61) v(61) a(61)
}
}
关键点:
str.getBytes(StandardCharsets.UTF_8):将内存中的UTF-16字符串编码成UTF-8格式的字节数组。str.getBytes(StandardCharsets.GB2312):将内存中的UTF-16字符串编码成GB2312格式的字节数组。
读写文件(File I/O)
这是最常见的编码问题场景,比如从 GB2312 编码的文本文件中读取内容,或者将内容以 GB2312 编码写入文件。
写入文件
使用 Files.writeString() 和 Files.write(),可以非常方便地指定编码。
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileWriteExample {
public static void main(String[] args) {
String content = "你好,世界!Hello, World!";
Path utf8File = Paths.get("utf8_output.txt");
Path gb2312File = Paths.get("gb2312_output.txt");
// 写入 UTF-8 编码的文件
try {
Files.writeString(utf8File, content, StandardCharsets.UTF_8);
System.out.println("UTF-8 文件写入成功: " + utf8File);
} catch (IOException e) {
e.printStackTrace();
}
// 写入 GB2312 编码的文件
try {
// 注意:StandardCharsets 没有 GB2312,需要使用 Charset.forName
Files.writeString(gb2312File, content, java.nio.charset.Charset.forName("GB2312"));
System.out.println("GB2312 文件写入成功: " + gb2312File);
} catch (IOException e) {
e.printStackTrace();
}
}
}
读取文件
同样,读取文件时也需要指定正确的编码,否则会出现乱码。

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileReadExample {
public static void main(String[] args) {
Path utf8File = Paths.get("utf8_output.txt");
Path gb2312File = Paths.get("gb2312_output.txt");
// 读取 UTF-8 编码的文件
try {
String contentFromUtf8 = Files.readString(utf8File, StandardCharsets.UTF_8);
System.out.println("从 UTF-8 文件读取的内容: " + contentFromUtf8);
} catch (IOException e) {
e.printStackTrace();
}
// 读取 GB2312 编码的文件
try {
String contentFromGb2312 = Files.readString(gb2312File, Charset.forName("GB2312"));
System.out.println("从 GB2312 文件读取的内容: " + contentFromGb2312);
} catch (IOException e) {
e.printStackTrace();
}
}
}
处理网络请求和响应(如 HTTP)
在 Web 开发中,处理请求和响应的编码至关重要。Content-Type 头部会指定字符编码。
使用 java.net.HttpURLConnection
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
public class HttpEncodingExample {
public static void main(String[] args) throws IOException {
String urlString = "http://example.com/some-gb2312-page"; // 假设这个页面是 GB2312 编码
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
// 从响应头中获取编码,这是一个好习惯
String contentType = connection.getHeaderField("Content-Type");
String charset = "UTF-8"; // 默认编码
if (contentType != null) {
// 示例: "text/html; charset=gb2312"
String[] parts = contentType.split(";");
for (String part : parts) {
if (part.trim().startsWith("charset=")) {
charset = part.trim().substring("charset=".length());
break;
}
}
}
// 读取响应流,使用我们找到的编码
try (InputStream inputStream = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, charset))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
System.out.println("获取到的页面内容 (编码: " + charset + "):");
System.out.println(response.toString());
}
connection.disconnect();
}
}
使用第三方库(如 OkHttp)
第三方库通常能更优雅地处理编码问题,OkHttp 会自动从 Content-Type 头部解析编码。
// 需要添加 OkHttp 依赖
// implementation("com.squareup.okhttp3:okhttp:4.9.3")
/*
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class OkHttpEncodingExample {
public static void main(String[] args) throws IOException {
OkHttpClient client = new OkHttpClient();
String url = "http://example.com/some-gb2312-page";
Request request = new Request.Builder()
.url(url)
.build();
try (Response response = client.newCall(request).execute()) {
// OkHttp 会自动处理响应体的编码
String responseBody = response.body().string();
System.out.println("OkHttp 获取到的页面内容:");
System.out.println(responseBody);
}
}
}
*/
控制台输入/输出
IDE(如 IntelliJ IDEA, Eclipse)和控制台都有自己的编码设置,如果源代码、文件编码和 IDE 编码不一致,就会出现乱码。
设置控制台输出编码
如果控制台显示乱码,可以在程序运行前或运行时设置其编码。
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
public class ConsoleOutputExample {
public static void main(String[] args) {
// 将标准输出流设置为 UTF-8
// 注意:这不一定对所有终端都有效,取决于终端本身的设置
System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8));
System.out.println("你好,世界!"); // 如果终端支持 UTF-8,将正确显示
}
}
读取控制台输入
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
public class ConsoleInputExample {
public static void main(String[] args) throws IOException {
System.out.println("请输入一些内容(按回车结束):");
// 使用 UTF-8 读取控制台输入
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
String userInput = reader.readLine();
System.out.println("你输入的是: " + userInput);
}
}
最佳实践和常见问题
-
优先使用
UTF-8:- 在所有新的项目中,统一使用
UTF-8作为源文件、文件、数据库、网络传输的编码。 StandardCharsets.UTF_8是 Java 内置的,性能和安全性都更好。
- 在所有新的项目中,统一使用
-
何时使用
GB2312:- 仅在与老旧系统集成时使用,读取一个由 90 年代系统生成的 GB2312 编码的日志文件,或者向一个只支持 GB2312 的旧接口发送数据。
-
如何避免乱码:
- 编码和解码必须使用同一种编码,用
UTF-8编码写入的文件,必须用UTF-8编码来读取。 - 明确指定编码:永远不要依赖平台默认编码(如
String.getBytes()或new InputStreamReader()不带编码参数的构造方法),因为不同操作系统的默认编码可能不同(Windows 早期是GBK,Linux 通常是UTF-8),这会导致程序在不同环境下表现不一致。
- 编码和解码必须使用同一种编码,用
-
处理
UnsupportedEncodingException:- 像
StandardCharsets.UTF_8和Charset.forName("GB2312")这样的方式,如果编码不存在,会抛出UnsupportedEncodingException,但对于StandardCharsets中的常量,这个异常永远不会发生,对于Charset.forName(),如果传入一个 JVM 不支持的编码名(如 "GBK",JVM 可能只支持 "GB2312"),就会抛出异常。Charset.isSupported("GB2312")可以提前检查。
- 像
| 场景 | 操作 | 推荐方法 |
|---|---|---|
| 字符串编码 | String -> byte[] |
str.getBytes(StandardCharsets.UTF_8) 或 Charset.forName("GB2312") |
| 字符串解码 | byte[] -> String |
new String(bytes, StandardCharsets.UTF_8) 或 Charset.forName("GB2312") |
| 文件读写 | Files API |
Files.readString(path, StandardCharsets.UTF_8) |
| 网络请求 | 解析响应头 | 从 Content-Type 中提取 charset,然后用它来读取流 |
| 控制台 | 输入/输出 | 使用 System.setOut() 和 new InputStreamReader(System.in, StandardCharsets.UTF_8) |
遵循“明确指定编码,优先使用 UTF-8”的原则,可以解决 99% 的 Java 编码问题。
