字符集与编码
约 909 字大约 3 分钟
布欧-Lewyon
2026-05-16
字符集(Charset)和编码是 IO 章节中最容易被忽视但生产环境最容易出 Bug 的主题。一个"乱码"问题往往能排查半天——根因就是字符编码不匹配。
基本概念
- 字符集(Charset):字符的集合,如 Unicode 收录了全球主流文字。
- 编码(Encoding):字符到字节的映射规则,如 UTF-8、UTF-16、GBK。
- 同一个字符在不同编码下对应不同的字节序列("中"在 UTF-8 中是 3 字节
E4 B8 AD,在 GBK 中是 2 字节D6 D0)。
Java 中的 Charset
// 获取所有可用字符集
SortedMap<String, Charset> all = Charset.availableCharsets();
// 常用 Charset(Java 7+ 提供了 StandardCharsets 常量)
Charset utf8 = StandardCharsets.UTF_8;
Charset gbk = Charset.forName("GBK");
Charset iso88591 = StandardCharsets.ISO_8859_1;
// 编码:字符 → 字节
byte[] bytes = "你好".getBytes(StandardCharsets.UTF_8);
System.out.println(bytes.length); // 6(UTF-8 下每个汉字 3 字节)
// 解码:字节 → 字符
String decoded = new String(bytes, StandardCharsets.UTF_8);乱码是如何产生的
byte[] gbkBytes = "你好".getBytes(Charset.forName("GBK")); // 4 字节
String wrong = new String(gbkBytes, StandardCharsets.UTF_8); // 用 UTF-8 解码 → 乱码核心原则:编码与解码必须使用相同的字符集。
读写文本时的编码
InputStreamReader / OutputStreamWriter
FileReader 和 FileWriter 使用操作系统默认编码,跨平台时不可预测。始终用桥接类指定编码:
// 读取 UTF-8 文件
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("input.txt"), StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
// 写入 UTF-8 文件
try (BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("output.txt"), StandardCharsets.UTF_8))) {
bw.write("你好,世界");
}Files API(Java 8+)
// 显式指定编码
List<String> lines = Files.readAllLines(Path.of("input.txt"), StandardCharsets.UTF_8);
Files.write(Path.of("output.txt"), List.of("你好"), StandardCharsets.UTF_8);
// Java 11+ 更简洁
String content = Files.readString(Path.of("input.txt"), StandardCharsets.UTF_8);
Files.writeString(Path.of("output.txt"), "你好", StandardCharsets.UTF_8);常见编码场景与陷阱
场景 1:HTTP 请求的编码
// 表单提交时,参数编码取决于页面编码和服务器配置
// 响应头 Content-Type: text/html; charset=UTF-8 指明响应编码
// 如果服务端返回 GBK 编码但客户端用 UTF-8 解析 → 乱码场景 2:String.getBytes() 无参版本
byte[] bytes = "hello".getBytes(); // 使用平台默认编码,不可移植!永远指定编码,不要依赖默认值。
场景 3:BOM(Byte Order Mark)
UTF-8 文件可能以 BOM 头(EF BB BF,3 字节)开头。某些 Windows 编辑器自动添加 BOM,导致用 Files.readString 读取后字符串开头有一个不可见字符:
String content = Files.readString(Path.of("file-with-bom.txt"), StandardCharsets.UTF_8);
if (content.charAt(0) == '\uFEFF') {
content = content.substring(1); // 移除 BOM
}小结
- 编码与解码必须使用相同字符集,否则产生乱码。
- 永远显式指定字符编码:
Files.readString(path, StandardCharsets.UTF_8),不要依赖平台默认值。 FileReader/FileWriter使用默认编码,跨平台不推荐;用InputStreamReader/OutputStreamWriter指定编码。- UTF-8 是通用标准(Web、JSON、大多数框架默认),GBK 仅在遗留系统或国内特定场景使用。
- 易错:
String.getBytes()和new String(bytes)的无参版本使用平台默认编码,在开发环境(macOS UTF-8)与生产环境(Linux UTF-8)碰巧一致,但 Windows 上可能不同。另一个常见陷阱是数据库连接 URL 中指定编码(useUnicode=true&characterEncoding=UTF-8)——如果数据库、连接、应用三者编码不一致,写入后再读出就是乱码。 - 思考任务:创建一个文本文件,用 GBK 编码写入"中文测试",然后用 UTF-8 编码读取观察乱码,再用 GBK 编码读取恢复正常。用 Java 代码实现这个编码切换对比。
上一节:IO 流
下一节:NIO.2 与文件操作
