对于乱码问题,其实说白了非常简单,本质就是对文本的编码和解码,使用了不同的字符编码造成的。但为什么如此让人头疼呢?因为一段文本并不一定只经过一次编码和解码,每次使用的字符编码也不一定相同,每次编码和解码的环境所使用的字符集也不一定相同。最让人崩溃的就是无论用哪一种编码都无法还原文本。为了彻底解决经常出现的几种乱码问题,下面我会重现几种乱码场景,来分析一下编码和解码的过程,掌握了字符在计算机内部的形式也就容易解决问题了。
1 HTML中的乱码问题
环境:HBuilder.
准备:项目所使用的字符编码:UTF-8

解释: 其实产生这个问题的原因非常简单,因为项目使用的UTF-8编码,所以charset.html就会以UTF-8编码文件中的所有字符:而html中设置了"charset=GBK",这就规定了浏览器要使用GBK的字符编码来解码这个html文件,如图:
编码和解码的字符编码不同,所以造成了乱码。下面我们来分析一下这些字符编码和解码的过程:
因为GBK和UTF-8都很好的兼容了ASCII字符集,所以它们编码英文字母和特殊符号并没有什么差别,也就不会造成乱码,主要是分析这段html中的一句中文:"这是一段文本"。这句话以七个中文字符组成,字符编码会对这七个字进行逐个编码。使用UTF-8编码转换工具查询后,这七个字以UTF-8编码后的产生的二进制字节流如下:
这:11101000 10111111 10011001是:11100110 10011000 10101111
一:11100100 10111000 10000000
段:11100110 10101110 10110101
文:11100110 10010110 10000111
本:11100110 10011100 10101100
我们在上一篇博客分析过UTF-8字符编码的特点,大部分汉字都会占用三个字节,并且高字节会以1110开头,后面的两个字节以10开头;而在GBK中,英文和半角特殊符号被编码后占用一个字节,汉字占用两个,特点是:若一个字节的十进制值大于128,就认为这表示是一个中文字符,会将这个字节和它的下一个字节合并起来一起解析。反之则为英文字符,取这一个字节来解析。我们将上面列出21个的字节,通过每个字节的值是否大于128(也就是模拟GBK的解码方式)来将其分一下组。分组后字节排列如下:
1组:11101000 10111111
2组:10011001 11100110
3组:10011000 10101111
4组:11100100 10111000
5组:10000000
6组:11100110 10101110
7组:10110101 11100110
8组:10010110 10000111
9组:11100110 10011100
10组:10101100
可以看到我们将字节分成了10组,其中除了第5,第10组,其他都是以两个字节一组,因为他们的首位字节的十进制都大于128,所以它们每组都表示一个汉字。第5组因为这个字节的十进制值不大于128,所以它代表一个非中文字符。而第10组的那个字节十进制值大于128,却因为在它后面不存在字节而被单独分成一组。现在就来将这几组字节通过GBK编码转换工具解码,看看产生的字符是否和浏览器上显示的乱码是否相同:
11101000 10111111 -- E8BF 杩
10011001 11100110 -- 99E6 欐
10011000 10101111 -- 98AF 槸
11100100 10111000 -- E4B8 涓
10000000 -- 80H €
11100110 10101110 -- E6AE 娈
10110101 11100110 -- B5E6 垫
10010110 10000111 -- 9687H 枃
11100110 10011100 -- E69C 鏈
10101100 -- AC ?
因为这个工具只能通过十六进制数得到字符,所以需要将分组后的二进制字节组合在一起后算出十六进制的值再进行查询,得出的字符如上所示。需要解释一下最后一个字符,并不是真正被解码成'?',而是在GBK编码表中,这个字节流的值没有对应的字符,也就是GBK“不认识”这个字节流,不认识当然会产生疑问了,所以就用'?'代替。接下来对比一下网页上显示的字符:除了最后一个字符,其他完全对应。那为什么最后一个不一样呢?因为网页上对不认识的字符使用特殊的问号表示,但代表的含义是一样的。
若想将网页正确显示原字符,只需要设置网页的字符编码为UTF-8即可。
2 JAVA中文件读取操作的乱码
环境:
JDK1.8 IDEA2016
准备:IDEA中,我们编写一个读取文件的程序,命名为TestFileInputStream:
package com.ld.test;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* Created by 123 on 2018/5/25.
*/
public class TestFileInputStream {
public static void main(String[] args) {
int b = 0;
FileInputStream in = null;
try {
in = new FileInputStream("d:\\share\\java\\io\\TestFileInputStream.java");
} catch (FileNotFoundException e) {
System.out.println("找不到指定文件");
System.exit(-1);
}
try {
long num = 0;
while((b = in.read()) != -1){
System.out.print((char) b);
num++;
}
in.close();
System.out.println();
System.out.println("共读取了 "+num+" 个字节");
} catch (IOException e1) {
System.out.println("文件读取错误");
System.exit(-1);
}
}
}
在本地磁盘(D:)中,新建一个同名的java文件,并且内容也完全一样,只不过这个文件使用GBK编码。
运行:
package com.ld.test;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* Created by 123 on 2018/5/25.
*/
public class TestFileInputStream {
public static void main(String[] args) {
int b = 0;
FileInputStream in = null;
try {
in = new FileInputStream("d:\\share\\java\\io\\TestFileInputStream.java");
} catch (FileNotFoundException e) {
System.out.println("ÕÒ²»µ½Ö¸¶¨Îļþ");
System.exit(-1);
}
try {
long num = 0;
while((b = in.read()) != -1){
System.out.print((char) b);
num++;
}
in.close();
System.out.println();
System.out.println("¹²¶ÁÈ¡ÁË "+num+" ¸ö×Ö½Ú");
} catch (IOException e1) {
System.out.println("Îļþ¶ÁÈ¡´íÎó");
System.exit(-1);
}
}
}
共读取了 975 个字节
根据输出结果可以看到,代码中所有中文都变成了乱码,其他字符都正常显示。为什么会产生这样的结果呢?因为被读取的文件采用GBK编码,所以ASCII中的字符被GBK编码后也占一个字节,而中文占两个字节。而我们编写的测试程序使用的是FileInputStream类的read()方法读取文件,这个方法是按字节来读取,每次读取一个字节,经过程序中的强制转换为char后输出。也就是说,一个中文字符会分两次读取,每次“读一半",将这“一半”强转为字符后输出,能不乱码么。
因为java中采用Unicode字符集,所以每次强转都是将整数值作为Unicode字符集中的字符编号来取出对应字符。如被读取文件中的第一个汉字“找”,它被GBK编码后的二进制字节流为11010101 11010010,每个字节的十六进制数值为:D5,D2。现在对比一下Unicode字符集中字符编号(Unicode编码表大全可以在这里下载),找一找D5和D2对应的字符:
对比程序的输出,完全一致。
3 get请求的中文参数 后台乱码
环境:
JDK1.8 IDEA2016 tomcat7(server.xml中http协议的connector未设置URIEncoding,默认ISO8859-1)
准备:
code.jsp: 含有一个跟随中文参数的超链接,使用UTF-8编码:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<!DOCTYPE>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<a href="codeServlet?name=中">点我</a>
</body>
</html>
CodeServlet: doGet方法中使用HttpServletRequest对象的getParameter方法获取前台传的参数,随后输出@WebServlet("/codeServlet/*")
public class CodeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String name = request.getParameter("name");
System.out.println(new String(name.getBytes("ISO8859-1"), "UTF-8"));
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
运行:
原因很简单,首先从这个带有中文参数的URL谈起:因为页面使用UTF-8编码,所以浏览器也会将中文参数使用UTF-8编码,所以中字被编码成三个字节,用十六进制分别表示为:E4,B8,AD.tomcat内部会获取在server.xml中配置的URIEncoding的值(默认为ISO8859-1),将这三个字节解码后存入request中,所以输出的就是上面的三个字符。可以对照ISO8859-1编码表验证。

所以要在后台恢复这个“中”字,就需要将获取的name参数使用ISO8859-1再编码,可以获取“中”字被UTF-8编码后的二进制字节流,再用UTF-8解码即可解决乱码。下次再看到“new String(name.getBytes("ISO8859-1"), "UTF-8")”这句代码,就不会感到“只会用却不知道原理”的苦恼了。
总结:
其实乱码问题说白了就是因为“断句”不同。一段中文使用UTF-8编码,明明是每三个字节代表一个字符,你GBK非得把两个字节或一个字节当成一个字符来解释,能不乱码吗? 大家在解决
乱码问题的时候,只要分析好每次编码解码使用的字符集是什么,分析清每次解码后有没有造成信息的丢失,分析好有哪些过程发生了字符的编码和解码,这样乱码问题一定会迎刃而解。