核心概念:什么是 GBK?什么是 Unicode?
GBK (Guo Biao Ku)
- 本质:它是一个字符编码集,主要用于编码简体中文、繁体中文以及日文、韩文等汉字。
- 特点:
- 变长编码:一个汉字通常占用 2 个字节,而英文字符(ASCII 范围内)占用 1 个字节。
- 向下兼容 ASCII:其前 128 个字符(0-127)与 ASCII 码完全相同。
- 区域性:GBK 是中国国家标准,主要在中国大陆使用,它不是国际标准。
- 问题:GBK 无法表示世界上所有的文字,比如古汉字、某些生僻字,或者非中日韩的字符(如西里尔字母、阿拉伯文等)。
Unicode
- 本质:它是一个字符集,而不是一种具体的编码方式,它的目标是世界上每一个字符都分配一个唯一的数字(码点, Code Point)。
- 特点:
- 统一性:无论是什么语言、什么符号,在 Unicode 中都有一个唯一的“身份证号”。
A的码点是U+0041中的码点是U+4E2D- (欧元符号) 的码点是
U+20AC
- 覆盖面广:它试图收录世界上所有的字符,是目前最通用的字符集。
- 统一性:无论是什么语言、什么符号,在 Unicode 中都有一个唯一的“身份证号”。
- 重要区别:Unicode 只定义了“字符是什么”,但没有规定“如何在计算机中存储这个字符”,这就引出了多种Unicode 转换格式。
Unicode 的实现方式:UTF-8, UTF-16, UTF-32
为了在计算机中存储和传输 Unicode 字符,人们设计了多种编码方案,其中最常见的是 UTF-8 和 UTF-16。

UTF-8 (Unicode Transformation Format - 8-bit)
- 本质:一种变长的 Unicode 编码方式。
- 特点:
- 高效灵活:它使用 1 到 4 个字节来表示一个字符。
- ASCII 字符 (U+0000 到 U+007F) 使用 1 个字节,与 ASCII 完全兼容。
- 欧洲大部分语言的字符使用 2 个字节。
- 常用的汉字(包括中文)通常使用 3 个字节。
- 不常用的字符(如 Emoji、生僻字)使用 4 个字节。
- 无 BOM (Byte Order Mark):通常不需要字节序标记。
- 互联网主流:由于其高效和对 ASCII 的兼容性,UTF-8 已经成为当今互联网上最广泛使用的编码方式。
- 高效灵活:它使用 1 到 4 个字节来表示一个字符。
UTF-16 (Unicode Transformation Format - 16-bit)
- 本质:一种定长或变长的 Unicode 编码方式。
- 特点:
- 基本多文种平面:大部分常用字符(包括中文、日文、韩文以及欧洲所有语言的字符)的码点在
U+0000到U+FFFF之间,这些字符在 UTF-16 中统一使用 2 个字节表示。 - 辅助平面:对于码点大于
U+FFFF的字符(如某些 Emoji 的码点是U+1F602),UTF-16 会将其表示为一个代理对,即由两个 16 位的代码单元组成,共 4 个字节。 - 字节序:UTF-16 需要指定字节序(大端序 Big-Endian 或小端序 Little-Endian),因此文件开头可能会有一个 BOM 标记。
- Java 内部使用:Java 的
char类型、String类在内存中的默认存储就是 UTF-16 编码。 这是 Java 与编码相关的许多问题的根源。
- 基本多文种平面:大部分常用字符(包括中文、日文、韩文以及欧洲所有语言的字符)的码点在
Java 中的关系与处理
理解了以上概念,我们来看 Java 是如何处理的。
Java 内部:UTF-16 是桥梁
- 当你在 Java 代码中写一个字符串
String s = "你好";时,这个字符串"你好"在 JVM 内存中是以 UTF-16 编码的形式存在的。 - 每个
char变量(如'你')在内存中占用 2 个字节。 - 关键点:Java 源文件(
.java文件)的编码、JVM 内部编码、以及最终输出的编码,这三者可能不同,这是乱码问题的核心。
关键类和方法:byte[] 和 String 的转换
在 Java 中,String 和 byte[] 之间的转换是编码问题最容易发生的地方。
从 String 到 byte[] (编码 - Encoding)
这是将内存中的字符序列(UTF-16)转换为可以存储或传输的字节序列的过程。
String str = "你好Hello";
// 使用 GBK 编码将 String 转换为 byte[]
byte[] gbkBytes = str.getBytes("GBK"); // 指定目标编码为 GBK
System.out.println("GBK 编码的字节数组长度: " + gbkBytes.length); // 输出 7 (2+2+1+1+1)
// 使用 UTF-8 编码将 String 转换为 byte[]
byte[] utf8Bytes = str.getBytes(StandardCharsets.UTF_8); // 推荐使用 StandardCharsets
System.out.println("UTF-8 编码的字节数组长度: " + utf8Bytes.length); // 输出 9 (3+3+1+1+1)
分析:

"你好"两个汉字,在 GBK 下各占 2 字节,共 4 字节。"Hello"五个字母,在 GBK 和 UTF-8 下各占 1 字节,共 5 字节。- 为什么 GBK 总共是 7 字节?因为 JVM 内部是 UTF-16,
"你"和"好"在 UTF-16 中各占 2 字节。getBytes("GBK")会将这些 UTF-16 字符正确地转换为 GBK 编码的字节。 - 为什么 UTF-8 总共是 9 字节?因为
"你"和"好"在 UTF-8 中各占 3 字节。
从 byte[] 到 String (解码 - Decoding)
这是将字节序列(如从文件或网络中读取的)转换回内存中的字符序列(UTF-16)的过程。
最经典的乱码场景:
// 假设我们有一个用 GBK 编码的字节数组
byte[] gbkBytes = {(byte) 0xC4, (byte) 0xE3, (byte) 0xBA, (byte) 0xC3}; // "你好" 的 GBK 编码
// --- 场景一:正确解码 ---
String correctStr = new String(gbkBytes, "GBK");
System.out.println("使用 GBK 正确解码: " + correctStr); // 输出: 你好
// --- 场景二:错误解码(最常见的乱码)---
// 错误地使用 UTF-8 去解码一个 GBK 编码的字节数组
String wrongStr = new String(gbkBytes, StandardCharsets.UTF_8);
System.out.println("使用 UTF-8 错误解码: " + wrongStr); // 输出: ä½ å¥½ (一堆乱码)
为什么会乱码?
- GBK 编码的
"你"是0xC4E3。 - 当你用 UTF-8 解码器读取
0xC4时,UTF-8 规则发现这个字节大于0x7F,它知道这是一个多字节字符的开头。 - 它期望接下来的字节是
0x??,但第二个字节是0xE3。0xE3在 UTF-8 中是一个有效的后续字节,所以解码器将0xC4E3错误地当作一个 UTF-8 字符来解析,最终得到了 。 - 同理,
0xBA和0xC3被错误地组合成了 和 。 "你好"就变成了 。
实践建议与最佳实践
-
统一使用 UTF-8
- 项目源文件编码:确保你的所有
.java源文件保存为 UTF-8 编码,现代 IDE(如 IntelliJ IDEA, Eclipse)都默认支持并推荐这样做。 - 服务器/应用编码:Web 服务器(Tomcat, Nginx)、数据库、应用配置文件等,所有涉及文本输入输出的地方,都尽可能设置为 UTF-8。
- 前端:HTML 文件头部加上
<meta charset="UTF-8">。
- 项目源文件编码:确保你的所有
-
使用
StandardCharsets- Java 7 引入了
java.nio.charset.StandardCharsets,它提供了一系列预定义的字符集常量(如UTF_8,GBK)。强烈推荐使用它,而不是手动传入字符串(如"UTF-8"),这样可以避免拼写错误。
// 推荐 byte[] bytes = str.getBytes(StandardCharsets.UTF_8); String s = new String(bytes, StandardCharsets.UTF_8); // 不推荐(容易拼错) byte[] bytes2 = str.getBytes("UFT-8"); // 拼写错误,运行时抛出 UnsupportedCharsetException - Java 7 引入了
-
明确编码,不依赖默认值
String.getBytes()和new String(byte[])这两个不指定字符集的重载方法,其行为依赖于 JVM 的默认字符集,这个默认字符集可能是平台相关的(如 Windows 上可能是GBK,Linux 上可能是UTF-8),这会导致程序在不同环境下表现不一致,是乱码的重灾区。- 永远不要使用这两个无参的重载方法! 始终明确指定你想要的编码。
-
处理 I/O 流时指定编码
- 当使用
InputStreamReader和OutputStreamWriter时,必须指定字符编码。
// 从 GBK 编码的文件中读取内容 try (FileInputStream fis = new FileInputStream("gbk_file.txt"); InputStreamReader isr = new InputStreamReader(fis, "GBK"); BufferedReader reader = new BufferedReader(isr)) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (IOException e) { e.printStackTrace(); } // 将内容以 GBK 编码写入文件 try (FileOutputStream fos = new FileOutputStream("output_gbk.txt"); OutputStreamWriter osw = new OutputStreamWriter(fos, "GBK"); BufferedWriter writer = new BufferedWriter(osw)) { writer.write("你好,世界!"); } catch (IOException e) { e.printStackTrace(); } - 当使用
| 概念 | 描述 | Java 中的体现 |
|---|---|---|
| GBK | 一种区域性的字符编码集,主要用于中文字符。 | 一种可用的字符集,用于 getBytes("GBK") 或 new String(..., "GBK")。 |
| Unicode | 一个全球统一的字符集,为每个字符分配唯一码点。 | String 和 char 在内存中基于它。'中' 的码点是 U+4E2D。 |
| UTF-8 | Unicode 的一种实现方式,变长编码(1-4字节),互联网主流。 | 推荐的编码方式,用于文件存储、网络传输。StandardCharsets.UTF_8。 |
| UTF-16 | Unicode 的另一种实现方式,Java 内部默认使用。 | JVM 内部 String 和 char 的存储格式。 |
核心思想:Java 程序处理文本时,数据在内存中是 UTF-16,当需要和外界(文件、网络、数据库)交互时,必须通过编码和解码将其转换为特定的字节流(如 GBK 或 UTF-8)。乱码的根本原因就是编码和解码时使用了不一致的字符集。 坚持使用 UTF-8 是避免绝大多数编码问题的最佳实践。
