根本原因:字符编码不匹配
乱码的唯一根本原因在编码(byte[] -> String)和解码(String -> byte[])过程中使用了不一致的字符集(Character Set)。

让我们把这个过程拆解一下:
-
编码:当你在 Java 中将一个
String转换成byte[]时,JVM 会使用一个指定的字符集(如 UTF-8, GBK, ISO-8859-1)将字符串中的每个字符转换成一个或多个字节,这个过程叫编码。// String -> byte[] String str = "你好"; byte[] bytes = str.getBytes("UTF-8"); // 使用 UTF-8 编码 -
解码:当你从
byte[]创建一个String时,JVM 需要知道这些字节原本是用哪个字符集编码的,才能正确地将其“翻译”回字符,这个过程叫解码。// byte[] -> String String newStr = new String(bytes, "UTF-8"); // 必须使用和编码时相同的 UTF-8 来解码
乱码就发生在解码这一步:如果你用 A 字符集编码,却用 B 字符集去解码,解码器就会“读错”字节的含义,从而产生一堆看不懂的符号,也就是乱码。

一个生动的比喻:
- 编码:你把一段中文(
String)用密码本 A(如 UTF-8)翻译成了一串摩斯电码(byte[])。 - 解码:接收方必须用同一本密码本 A 来翻译这串摩斯电码,才能得到正确的中文,如果他用了密码本 B(如 ISO-8859-1),翻译出来的结果必然是乱码。
常见乱码场景及解决方案
场景 1:最经典的“默认编码”陷阱
这是最常见的原因,Java 提供了一些不指定字符集的重载方法,它们会使用 JVM 的平台默认字符集。
问题代码示例:
假设你的操作系统默认字符集是 GBK,但你想处理一个用 UTF-8 编码的文件。
import java.nio.charset.StandardCharsets;
public class EncodingTrap {
public static void main(String[] args) {
String originalStr = "Hello, 世界";
// 1. 使用 UTF-8 将字符串编码成字节
byte[] utf8Bytes = originalStr.getBytes(StandardCharsets.UTF_8);
System.out.println("UTF-8 编码后的字节: " + java.util.Arrays.toString(utf8Bytes));
// 2. 【错误】使用默认字符集(可能是GBK)去解码
// 如果你的系统默认是GBK,这里就会乱码
String wrongStr = new String(utf8Bytes); // 没有指定字符集!
System.out.println("使用默认字符集解码后的结果: " + wrongStr); // 可能输出 "Hello, ???" 或乱码
// 3. 【正确】使用正确的 UTF-8 字符集去解码
String correctStr = new String(utf8Bytes, StandardCharsets.UTF_8);
System.out.println("使用 UTF-8 解码后的结果: " + correctStr); // 输出 "Hello, 世界"
}
}
解决方案:
永远、永远、永远不要依赖默认字符集! 在任何进行 String 和 byte[] 转换的地方,都显式地指定字符集。

- 推荐使用
java.nio.charset.StandardCharsets枚举,它提供了预定义的、标准字符集的常量,避免了拼写错误。StandardCharsets.UTF_8StandardCharsets.ISO_8859_1StandardCharsets.US_ASCII
- 如果必须使用字符集名称,请确保其拼写正确,并考虑处理
UnsupportedCharsetException。
场景 2:HTTP 请求/响应中的乱码
这在 Web 开发中非常普遍。
问题代码示例(Servlet):
前端用 UTF-8 发送数据,但后端用默认的 ISO-8859-1 去读取。
// 前端用 UTF-8 提交了一个参数:name=张三
// 后端 Servlet 代码
request.setCharacterEncoding("ISO-8859-1"); // 错误的设置
String name = request.getParameter("name"); // 得到的是乱码,"???"
// 或者,在获取参数后进行转换
byte[] bytes = name.getBytes("ISO-8859-1"); // 拿到的是错误的字节
String correctName = new String(bytes, "UTF-8"); // 试图用UTF-8修复,但已经晚了,字节是错的
解决方案:
统一整个请求链路的字符集为 UTF-8。
-
后端设置:
- POST 请求体:在获取任何请求参数之前,调用
request.setCharacterEncoding("UTF-8");。 - GET 请求参数:GET 请求的参数在 URL 中,服务器(如 Tomcat)需要配置
URIEncoding="UTF-8"来正确解析 URL 中的编码字符。 - 响应:在向客户端返回数据前,设置响应的字符集。
// 在 Servlet 的 doGet 或 doPost 方法开头 request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); // 同时设置 Content-Type
- POST 请求体:在获取任何请求参数之前,调用
-
前端设置:
- HTML 表单:
<meta charset="UTF-8">和<form accept-charset="UTF-8">。 - AJAX 请求:明确指定
contentType: "application/x-www-form-urlencoded; charset=UTF-8"。
- HTML 表单:
场景 3:读取文件时的乱码
问题代码示例:
用 UTF-8 编码保存了一个文本文件,但用 GBK 去读取它。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class FileEncoding {
public static void main(String[] args) throws IOException {
// 假设文件 "test.txt" 是用 UTF-8 编码的,内容为 "你好"
byte[] fileContent = Files.readAllBytes(Paths.get("test.txt"));
// 【错误】使用 GBK 字符集去解码 UTF-8 的字节
String wrongContent = new String(fileContent, "GBK");
System.out.println("用GBK读取UTF-8文件: " + wrongContent); // 输出乱码,如 "浣犲ソ"
}
}
解决方案: 使用与文件保存时完全相同的字符集去读取文件,最好在文件中或通过其他方式(如文件扩展名、元数据)记录文件的编码。
// 【正确】使用 UTF-8 字符集去读取 UTF-8 文件
String correctContent = new String(fileContent, StandardCharsets.UTF_8);
System.out.println("用UTF-8读取UTF-8文件: " + correctContent); // 输出 "你好"
特殊但重要的字符集:ISO-8859-1
ISO-8859-1(又称 Latin-1)有一个非常特殊的属性:它是一个单字节字符集,并且它定义的字节范围(0-255)与 byte 类型的取值范围完全一致。
这意味着:
byte[] -> new String(bytes, "ISO-8859-1") -> String -> getBytes("ISO-8859-1") -> byte[]
这个转换过程是无损的,不会丢失任何信息,每个字节都会被原封不动地映射到一个字符(即使那个字符是不可见的)。
应用场景:
当你在两个不同的编码系统之间“中转”字节数据时,ISO-8855-1 是一个完美的“无损通道”。
示例:
假设你有一个从 GBK 编码的数据库中读出的 byte[],你想把它转换成 UTF-8 编码的 String,你不能直接 new String(bytes, "GBK"),因为如果这个 byte[] 中间被其他地方用 UTF-8 处理过,它可能已经损坏了。
正确的中转步骤:
GBK bytes -> new String(gbkBytes, "GBK") -> GBK String -> getBytes("ISO-8859-1") -> ISO-8859-1 bytes -> `new String(iso
