字符、编码与 Java
在深入代码之前,必须理解几个基本概念:

-
字符:这是文本的最小单元,'A', '中', '€',字符是抽象的,与具体的存储或传输方式无关。
-
编码:这是将字符映射成二进制数据(字节)的规则,同一个字符可以用不同的编码规则表示成不同的字节序列。
- Unicode:这是一个字符集,它为世界上几乎所有的字符都分配了一个唯一的数字(称为“码点”,Code Point),'中' 的码点是
U+4E2D,Unicode 本身只定义了字符和码点的映射,不规定如何存储。 - UTF-8:这是目前最主流的 Unicode 实现方式,它是一种变长编码,英文字符(如 'A')占用 1 个字节,中文等大部分字符占用 3 个字节,生僻字符可能占用 4 个字节。UTF-8 是兼容 ASCII 的,这是它的一大优势。
- GBK:这是由中国制定的、在中文 Windows 系统上曾经非常流行的编码标准,它收录了汉字、符号等,一个中文字符通常固定占用 2 个字节,它 不是 Unicode 的标准实现,而是针对中文环境的扩展。
- Unicode:这是一个字符集,它为世界上几乎所有的字符都分配了一个唯一的数字(称为“码点”,Code Point),'中' 的码点是
-
Java 内部的处理方式:
- Java 使用 UTF-16 编码作为其内部字符表示,这意味着
char类型(2字节)和String类都使用 UTF-16 来存储字符。 - 这意味着,在 Java 程序的内存中,
'中'这个字符已经被表示成了一个 UTF-16 编码的字节序列(通常是\u4e2d)。 - 关键点:Java 的
String对象本身是“编码无关”的,它是一串抽象的字符序列,只有在需要将字符串写入到文件、网络或从文件、网络读取时,才需要考虑具体的编码格式(如 GBK, UTF-8)。
- Java 使用 UTF-16 编码作为其内部字符表示,这意味着
Java 与 GBK 文件的读写(最常见的痛点)
当你的程序需要读取一个用 GBK 编码保存的文本文件,或者需要将一个字符串以 GBK 编码写入文件时,就必须进行显式的编码转换。

读取 GBK 编码的文件
如果你直接使用 Files.readString() 而不指定编码,它会使用 JVM 的默认字符集(可能不是 GBK),导致读取出的字符串是乱码。
正确做法:显式指定 StandardCharsets.GBK
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class GbkFileReader {
public static void main(String[] args) {
String filePath = "gbk_encoded.txt";
try {
// 关键:显式指定使用 GBK 字符集来读取字节流,并将其解码成 String
String content = Files.readString(Paths.get(filePath), StandardCharsets.GBK);
System.out.println("成功读取 GBK 文件:");
System.out.println(content);
System.out.println("字符串的内部表示 (UTF-16): " + content);
} catch (IOException e) {
System.err.println("读取文件时出错,请确保文件存在且编码为 GBK。");
e.printStackTrace();
}
}
}
写入 GBK 编码的文件
同样,如果你直接使用 Files.writeString() 而不指定编码,它会使用 JVM 的默认字符集,写入的文件可能不是 GBK 格式。
正确做法:显式指定 StandardCharsets.GBK
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class GbkFileWriter {
public static void main(String[] args) {
String filePath = "output_gbk.txt";
String contentToWrite = "你好,世界!Hello, GBK!";
try {
// 关键:显式指定使用 GBK 字符集将 String 编码成字节流,然后写入文件
// StandardOpenOption.CREATE_NEW 表示如果文件不存在则创建,存在则抛出异常
Files.writeString(Paths.get(filePath), contentToWrite, StandardCharsets.GBK, StandardOpenOption.CREATE_NEW);
System.out.println("成功以 GBK 编码写入文件: " + filePath);
} catch (IOException e) {
System.err.println("写入文件时出错。");
e.printStackTrace();
}
}
}
控制台输入输出与 GBK
在中文版的 Windows 系统中,命令行的默认编码通常是 GBK,这意味着你从控制台读取的输入和打印到控制台的输出,都需要考虑 GBK 编码。
从控制台读取 GBK 输入
System.in 是一个字节流,为了正确读取中文字符,我们需要将其包装成一个使用 GBK 编码的 Reader。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
public class GbkConsoleReader {
public static void main(String[] args) {
// 将 System.in (字节流) 包装成 InputStreamReader,并指定使用 GBK 解码
// 然后再包装成 BufferedReader 以便按行读取
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.GBK))) {
System.out.println("请输入一些中文(GBK编码),然后按回车:");
String userInput = reader.readLine();
System.out.println("你输入的内容是: " + userInput);
} catch (IOException e) {
e.printStackTrace();
}
}
}
向控制台打印 GBK 输出
System.out 是一个 PrintStream,它也依赖于平台的默认编码,为了确保中文能正确显示,可以显式指定编码。
Java 11+ 的做法:
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
public class GbkConsoleWriter {
public static void main(String[] args) {
// 重定向 System.out,使其使用 GBK 编码进行输出
// 注意:这会影响整个 JVM 的标准输出流
System.setOut(new PrintStream(System.out, true, StandardCharsets.GBK));
System.out.println("这条消息会以 GBK 编码输出到控制台。");
System.out.println("This message will also be encoded correctly for GBK console.");
}
}
字符串编码转换(不涉及文件/IO)
有时候你有一个 String 对象,需要把它转换成 GBK 编码的字节数组,或者反之。
import java.nio.charset.StandardCharsets;
public class StringEncodingConversion {
public static void main(String[] args) {
String originalString = "这是一个测试字符串,This is a test string.";
// 1. 将 String (内部是 UTF-16) 转换为 GBK 编码的字节数组
byte[] gbkBytes = originalString.getBytes(StandardCharsets.GBK);
System.out.println("GBK 编码的字节数组长度: " + gbkBytes.length);
// System.out.println(Arrays.toString(gbkBytes)); // 可以打印出来看
// 2. 将 GBK 编码的字节数组转换回 String
// 注意:这里必须使用与编码时相同的字符集(GBK)进行解码
String fromGbkBytes = new String(gbkBytes, StandardCharsets.GBK);
System.out.println("从 GBK 字节数组解码后的字符串: " + fromGbkBytes);
// 3. 错误示例:用错误的字符集解码
String wrongDecoded = new String(gbkBytes, StandardCharsets.UTF_8);
System.out.println("--- 错误解码示例 (用UTF-8解码GBK字节) ---");
System.out.println("结果将是乱码: " + wrongDecoded);
}
}
最佳实践与总结
-
优先使用 UTF-8:
- 在所有可能的情况下,都应优先使用 UTF-8 作为项目的默认编码,这包括:
- 源代码文件(
.java)保存为 UTF-8。 - 项目构建文件(如
pom.xml,build.gradle)使用 UTF-8。 - Web 项目的 JSP/HTML 文件使用 UTF-8。
- 数据库连接和表使用 UTF-8。
- 文件存储使用 UTF-8。
- 源代码文件(
- 这样可以最大程度地避免乱码问题,尤其是在国际化应用中。
- 在所有可能的情况下,都应优先使用 UTF-8 作为项目的默认编码,这包括:
-
明确指定编码:
- 永远不要依赖 JVM 的默认字符集,因为它会因操作系统、JVM 版本甚至环境变量的不同而改变。
- 在进行任何 I/O 操作(文件、网络、控制台)时,都应显式地指定编码,如
StandardCharsets.UTF_8或StandardCharsets.GBK。 - Java 7+ 引入的
java.nio.file包(Files类)是进行文件 I/O 的首选,因为它可以方便地指定字符集。
-
处理遗留系统:
当你不得不与使用 GBK 或其他旧编码的遗留系统(如某些 Windows 服务器、旧版数据库)交互时,遵循上述“显式指定编码”的原则,在读取时用对方的编码解码,在写入时用对方的编码编码。
-
IDE 配置:
确保你的 IDE(如 IntelliJ IDEA, Eclipse)的文件编码和项目编码都设置为 UTF-8,这可以防止你的源代码文件本身出现编码问题。
常见问题排查
-
问题:读取 GBK 文件时出现乱码(如 )。
- 原因:代码中未指定编码,或错误地指定了
UTF-8。 - 解决:检查
Files.readString()或InputStreamReader的构造函数,确保第二个参数是StandardCharsets.GBK。
- 原因:代码中未指定编码,或错误地指定了
-
问题:写入 GBK 文件后,用记事本打开是乱码。
- 原因:代码中未指定编码,或错误地指定了
UTF-8。 - 解决:检查
Files.writeString()的构造函数,确保指定了StandardCharsets.GBK。
- 原因:代码中未指定编码,或错误地指定了
-
问题:控制台输出中文时显示乱码。
- 原因:控制台编码与程序输出编码不匹配,在中文 Windows 上,控制台是 GBK,而程序可能默认输出为 UTF-8。
- 解决:如上所示,使用
System.setOut(new PrintStream(System.out, true, StandardCharsets.GBK))重定向输出流。
希望这份详细的指南能帮助你彻底理解并解决 Java 中关于 Unicode 和 GBK 的编码问题!
