锟斤拷?UTF-8与GBK互转,为什么会乱码?-程序员宅基地

技术标签: java面试  

[置顶] 锟斤拷?UTF-8与GBK互转,为什么会乱码?

标签: UTF-8GBK编码乱码中文
  7927人阅读  评论(0)  收藏  举报
  分类:

作为一名程序员,肯定有被乱码困扰的时候,真到了百思不得其解的时候,就会觉得:英文程序员真幸福。

但其实只要明白编码之间的转换规律,其实乱码so easy~


我们知道,计算机存储数据都是2进制,就是0和1,那么这么多的字符就都需要有自己对应的0和1组成的序列,计算机将需要存储的字符转换成它们对应的01序列,然后就可以储存在电脑里了。


比如我们可以定义用8位2进制表示一个字符,“00000000”表示小写字母“a”,“00000001”表示小写字母“b”,那么计算机要存储“ab”的时候,其实在计算机里的存储的是“0000000000000001”,读取的时候先读取前8位,根据对应关系,可以解码出“a”,再读取后8位,又可以解码出“b”,这样就读出了当时写入的“ab”了。而我们定义的这种字符和二进制序列的对应关系,就可以称之为编码。我们如果需要将“ab”发送给别人,因为网络也是基于二进制,所以只要先约定好编码规则,就可以发送“0000000000000001”,然后对方根据约定的编码解码,就可以得到“ab”。现在是互联网的时代,我们经常需要和其他的计算机进行交互,一套编码系统还是比较复杂的,所以大家就需要约定统一的编码,这样的编码是大家都约定好的,就不用再去约定编码规则了~然而,为了满足各种不同的需求,人们还是制定了很多种编码,没有哪一种能全面替代其他编码,所以现在多种编码并存。通常这些编码都被大家所接受和熟知,所以现在不用再通信前商量编码的对应规则和细节,只需要告诉对方,我采用的是什么通用编码,彼此就能愉快地通信了。


所以乱码的本质就是读取二进制的时候采用的编码和最初将字符转换成二进制时的编码不一致。


ps:编码有动词含义也有名词含义,名词含义就是一套字符和二进制序列之间的转换规则,动词含义是使用这种规则将字符转换成二进制序列。


好了,废话不多,直接上一段代码:

[java]  view plain   copy
  1. import java.io.UnsupportedEncodingException;  
  2.   
  3. public class EncodingTest {  
  4.     public static void main(String[] args) throws UnsupportedEncodingException {  
  5.         String srcString = "我们是中国人";  
  6.         String utf2GbkString = new String(srcString.getBytes("UTF-8"),"GBK");  
  7.         System.out.println("UTF-8转换成GBK:"+utf2GbkString);  
  8.         String utf2Gbk2UtfString = new String(utf2GbkString.getBytes("GBK"),"UTF-8");  
  9.         System.out.println("UTF-8转换成GBK再转成UTF-8:"+utf2Gbk2UtfString);  
  10.     }  
  11. }  

因为UTF-8和GBK是两套中文支持较好的编码,所以经常会进行它们之间的转换,这里就以它们举例。

以上代码运行打印出以下内容:


UTF-8转换成GBK:鎴戜滑鏄腑鍥戒汉
UTF-8转换成GBK再转成UTF-8:我们是中国人


我们看到,将"我们是中国人"以UTF-8编码转换成byte数组(byte数组其实就相当于二进制序列了,此过程即编码),再以GBK编码和byte数组创建新的字符串(此过程即以GBK编码去解码byte数组,得到字符串),就产生乱码了。

因为编码采用的UTF-8和解码采用的GBK不是同一种编码,所以最后结果乱码了

之后再对乱码使用GBK编码,还原到解码前的byte数组,再使用和最初编码时使用的一致的编码UTF-8进行解码,就可得到最初的“我们是中国人”。

这种多余的转换有时候还是很有用的,比如ftp协议只支持ISO-8859-1编码,这个时候如果要传中文,只能先换成ISO-8859-1的乱码,ftp完成后,再转回UTF-8就又可以得到正常的中文了。


怎么样?编码转换是不是so easy?那该来点正经的了:

[java]  view plain   copy
  1. import java.io.UnsupportedEncodingException;  
  2.   
  3. public class EncodingTest {  
  4.     public static void main(String[] args) throws UnsupportedEncodingException {  
  5.         String srcString = "我们是中国人";  
  6.         String gbk2UtfString = new String(srcString.getBytes("GBK"), "UTF-8");  
  7.         System.out.println("GBK转换成UTF-8:" + gbk2UtfString);  
  8.         String gbk2Utf2GbkString = new String(gbk2UtfString.getBytes("UTF-8"), "GBK");  
  9.         System.out.println("GBK转换成UTF-8再转成GBK:" + gbk2Utf2GbkString);  
  10.     }  
  11. }  
这次我们反过来,先将字符串以GBK编码再以UTF-8解码,再以UTF-8编码,再以GBK解码。

这次的运行结果是:


GBK转换成UTF-8:�������й���
GBK转换成UTF-8再转成GBK:锟斤拷锟斤拷锟斤拷锟叫癸拷锟斤拷


WTF??万恶的“锟斤拷”,相信不少人都见过。这里GBK转成UTF-8乱码好理解,但是再转回来怎么变成了“锟斤拷锟斤拷锟斤拷锟叫癸拷锟斤拷”,这似乎不科学。

这其实和UTF-8独特的编码方式有关,由于UTF-8需要对unicode字符进行编码,unicode字符集是一个几乎支持所有字符的字符集,为了表示这么庞大的字符集,UTF-8可能需要更多的二进制位来表示一个字符,同时为了不致使UTF-8编码太占存储空间,根据二八定律,UTF-8采用了一种可变长的编码方式,即将常用的字符编码成较短的序列,而不常用的字符用较长的序列表示,这样让编码占用更少存储空间的同时也保证了对庞大字符集的支持。

正式由于UTF-8采用的这种特别的变长编码方式,这一点和其他的编码很不一样。比如GBK固定用两个字节来表示汉字,一个字节来表示英文和其他符号。

来测试一下:

[java]  view plain   copy
  1. import java.io.UnsupportedEncodingException;  
  2.   
  3. public class EncodingTest {  
  4.     public static void main(String[] args) throws UnsupportedEncodingException {  
  5.         String srcString = "我们是中国人";  
  6.         byte[] GbkBytes = srcString.getBytes("GBK");  
  7.         System.out.println("GbkBytes.length:" + GbkBytes.length);  
  8.         byte[] UtfBytes = srcString.getBytes("UTF-8");  
  9.         System.out.println("UtfBytes.length:" + UtfBytes.length);  
  10.         String s;  
  11.         for (int i = 0; i < srcString.length(); i++) {  
  12.             s = Character.valueOf(srcString.charAt(i)).toString();  
  13.             System.out.println(s + ":" + s.getBytes().length);  
  14.         }  
  15.     }  
  16. }  
运行结果为:

GbkBytes.length:12
UtfBytes.length:18
我:3
们:3
是:3
中:3
国:3
人:3


可以看到使用GBK进行编码,“我们是中国人”6个汉字占12个字节,而是用UTF-8进行编码则占了18个字节,其中每个汉字占3个字节(由于是常用汉字,只占3个字节,有的稀有汉字会占四个字节。)

UTF-8编码的读取方式也比较不同,需要先读取第一个字节,然后根据这个字节的值才能判断这个字节之后还有几个字节共同参与一个字符的表示。

对于某一个字符的UTF-8编码,如果只有一个字节则其最高二进制位为0;如果是多字节,其第一个字节从最高位开始,连续的二进制位值为1的个数决定了其编码的位数,其余各字节均以10开头。UTF-8最多可用到6个字节。 

如表: 
1字节 0xxxxxxx 
2字节 110xxxxx 10xxxxxx 
3字节 1110xxxx 10xxxxxx 10xxxxxx 
4字节 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 
5字节 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 
6字节 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 
因此UTF-8中可以用来表示字符编码的实际位数最多有31位,即上表中x所表示的位。除去那些控制位(每字节开头的10等),这些x表示的位与UNICODE编码是一一对应的,位高低顺序也相同。 
实际将UNICODE转换为UTF-8编码时应先去除高位0,然后根据所剩编码的位数决定所需最小的UTF-8编码位数。 
因此那些基本ASCII字符集中的字符(UNICODE兼容ASCII)只需要一个字节的UTF-8编码(7个二进制位)便可以表示。 

上面一随便看看就好,只要知道“由于UTF-8的特殊编码方式,所以有些序列是不可能出现在UTF-8编码中的”就可以了。


所以当我们将由GBK编码的12个字节试图用UTF-8解码时会出现错误,由于GBK编码出了不可能出现在UTF-8编码中出现的序列,所以当我们试图用UTF-8去解码时,经常会遇到这种不可能序列,对于这种不可能序列,UTF-8把它们转换成某种不可言喻的字符“�”,当这种不可言喻的字符再次以UTF-8进行编码时,他们已经无法回到最初的样子了,因为那些是UTF-8编码不可能编出的序列。然后这个神秘字符再转换成GBK编码时就变成了“锟斤拷”。当然,还有很多其他的巧合,可能正好碰到UTF-8中存在的序列,甚至原本不是一个字符的字节,可能是某个字的第二个字节和下一个字的两个字节,正好被识别成一个UTF-8序列,于是解码出一个汉字,当然这些在我们看来都是乱码了,只不过不是“锟斤拷”的样子。因为不可能序列更普遍存在,所以GBK转UTF-8再转GBK时,最常见的便是“锟斤拷”!


所以:以非UTF-8编码编码出的字节数组,一旦以UTF-8进行解码,通常这是一条不归路,再尝试将解码出的字符以UTF-8进行编码,也无法还原之前的字节数组。

相反地,其他的固定长度编码几乎都可以顺利还原。


=====================2016/11/15补充==========================

上文中其实有一个东西一直在回避,就是既然所有字符在保存时都需要转换成二进制,那么java是使用什么编码来保存字符的呢?这个问题其实我们可以不必深究,因为这对我们是透明的,我们只要假设java使用某种编码可以表示所有字符。得益于这种透明,我们可以当作java是直接保存字符本身的,就如上文所做的这样。但是今天面试的时候被问到了,我说这个是对我们透明所以没有深究。他说虽然是透明的,但是如果弄懂其中的原理还是能加深理解。我马上想到unicode,因为java要准确地表示所有字符,那么只有unicode能胜任了。这个回答也得到面试官的肯定,还说了一些更细节的。每种编码都会提供和unicode编码之间的转换规则。当我们以字符串直接量new一个String,这个String就是以unicode在内存中存储的。同样这也解决了一个让我疑惑的问题:为什么一个char中既可以存储一个字母,也可以存储一个汉字,明明很多编码如GBK、UTF-8中汉字和字母的长度不一样。如果java虚拟机使用unicode编码,那这一切就很好理解了,字母和汉字长度一样。


新增一条结论:java虚拟机中以使用unicode编码保存字符,任何编码都提供了和unicode编码的转换规则。



版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/evilcry2012/article/details/78092559

智能推荐

JavaScript之DOM操作获取元素、事件、操作元素、节点操作_元素事件-程序员宅基地

文章浏览阅读2.5k次,点赞2次,收藏15次。什么是 DOM?文档对象模型(Document Object Model,简称 DOM),是 W3C 组织推荐的处理可扩展标记语言(HTML或者XML)的标准编程接口。W3C 已经定义了一系列的 DOM 接口,通过这些 DOM 接口可以改变网页的内容、结构和样式DOM 树文档:一个页面就是一个文档,DOM 中使用 document 表示元素:页面中的所有标签都是元素,DOM 中使用 element 表示节点:网页中的所有内容都是节点(标签、属性、文本、注释等),DOM 中使用 node._元素事件

安卓基础知识(一) 服务(Service)_安卓服务-程序员宅基地

文章浏览阅读1.4k次,点赞3次,收藏7次。关于服务的一些基本知识,包括活动对接口的方法调用,接口实现,服务生命周期等知识。_安卓服务

JDBC使用数据库连接池连接数据库(DBCP,C3P0,Druid)_采用连接池方式连接数据库-程序员宅基地

文章浏览阅读7.9k次,点赞5次,收藏8次。小白式数据库连接池使用,看完拿捏数据库连接池。_采用连接池方式连接数据库

C++多线程同步_c++线程同步-程序员宅基地

文章浏览阅读4.9k次,点赞5次,收藏36次。摘要:本文介绍了C++11中如何开启新线程,并详细讲解了线程的基础同步原语:mutex, lock_guard, unique_lock, condition variable和semaphore等。如何采用async, packaged_task和promise实现future同步机制?怎样处理spurious wakeup?本文以质数判定服务为例为大家分享C++多线程同步措施!1. C++线程和基础同步原语Thread mutex, lock_guard, unique_lock con._c++线程同步

STM32 GPIOx通用输入输出I/O端口的BSRR寄存器与BRR功能简述_gpiob->brr-程序员宅基地

文章浏览阅读3.3k次,点赞4次,收藏9次。STM32 GPIOx通用输入输出I/O端口的BSRR寄存器与BRR功能简述_gpiob->brr

【小沐学C++】C++获取计算机硬件信息(Windows)_c/c++ 获取windows系统信息-程序员宅基地

文章浏览阅读3.5k次,点赞7次,收藏41次。获取计算机硬件信息:1、获取屏幕分辨率1.1 GetSystemMetrics1.2 GetDeviceCaps1.3 SystemParametersInfo1.4 GetDesktopWindow2、获取屏幕显示比例后续1、获取屏幕分辨率1.1 GetSystemMetrics整个屏幕区域。# include <windows.h>int cx = GetSystemMetrics( SM_CXSCREEN ); int cy = GetSystemMetrics( SM_C_c/c++ 获取windows系统信息

随便推点

Vuex: 实现同级组件的简单通信_vuex的组件同级通信-程序员宅基地

文章浏览阅读2.3k次。1. Vuex 是什么?Vuex 是专门为Vue 组件化思想带来的组件间通信问题提供的解决方案,主要解决以下两个问题:多个视图依赖于同一状态 来自不同视图的行为需要变更同一状态2. 核心概念State: 可以简单理解为Vue 维持的全局变量(状态)。 Getter: 获取State 中的状态的方法,可以在取出前对数据进行二次处理。 Mutation: 是改变State 中的状态..._vuex的组件同级通信

逆向中常见的Hash算法和对称加密算法的分析_findcrypt3 支持哪些算法-程序员宅基地

文章浏览阅读8.4k次,点赞31次,收藏33次。逆向中常常出现一些加密算法,如果我们能对这些加密算法进行快速识别则会大大减少我们逆向的难度,虽然IDA已有密码分析神器Findcrypt,但掌握手动分析方法能帮助我们应对更多的情况。这篇文章将介绍逆向中常见的单项散列算法和对称加密算法的识别方法。0xFF. 前言在很长一段时间里我经常发现自己面对复杂的加密算法无从下手,可能是因为还没有系统学过密码学吧orz,总之这个问题困扰了我很久。于是最近我花了一些时间来解决自己在密码学这块的薄弱点,写下这篇文章的目的之一也是为了巩固所学知识。加密算法的部分没有_findcrypt3 支持哪些算法

smplayer_Windows上的SMPlayer入门(更好地播放电影)-程序员宅基地

文章浏览阅读5.3k次。smplayerThere are lots of video players out there, but one that we think gets overlooked is SMPlayer. It can do anything other video players can do and even more – like remembering where you left off ..._sm69影视

5 python 函数用法_函数 :登录 ,余额查询,取钱,存入取钱记录到aa.txt文件中-程序员宅基地

文章浏览阅读212次。课程:函数 1目标函数的作用函数的使用步骤函数的参数作用函数的返回值作用函数的说明文档函数嵌套一. 函数的作用需求:用户到ATM机取钱:输入密码后显示"选择功能"界面查询余额后显示"选择功能"界面取2000钱后显示"选择功能"界面特点:显示“选 择功能”界面需要重复输出给用户,怎么实现?函数就是将一段具有独立功能的代码块 整合到一个整体并命名,在需要的位置调用这个名称即可完成对应的需求。函数在开发过程中,可以更高效的实现代码重用。二. 函数的使用步骤2_函数 :登录 ,余额查询,取钱,存入取钱记录到aa.txt文件中

关于“Python”的核心知识点整理大全4-程序员宅基地

文章浏览阅读5.4k次,点赞34次,收藏36次。在开发项目期间,这 种灵活性是可以接受的,但大家最终认识到,过于强调灵活性会导致大型项目难以维护:要通过 研究代码搞清楚当时解决复杂问题的人是怎么想的,既困难又麻烦,还会耗费大量的时间。第二个列表元素的索引为1。你创建的大多数列表都将是动态的,这意味着列表创建后,将随着程序的运行增删元素。为此,可在开始时将一些外星人存储在 列表中,然后每当有外星人被射杀时,都将其从列表中删除,而每次有新的外星人出现在屏幕上 时,都将其添加到列表中。要访问列表元素,可指出列表的名称,再指出元素的索引,并将其放在方括号内。

计算机操作系统重点概念整理-第五章 文件管理【期末复习|考研复习】-程序员宅基地

文章浏览阅读559次。给大家整理了一下计算机操作系统中第五章文件管理中的重点概念,以供大家期末复习和考研复习的时候使用。

推荐文章

热门文章

相关标签