前言

想必大家编写代码时肯定和我一样,也遇到过汉字乱码的问题。特别是,有时候和上下游对接接口,不能统一编码格式的话,一堆乱码问题,让人头皮发麻。

那么为什么会有这么多的乱码问题?

什么是字符编码?什么是字符集?他们之间有什么区别和联系?

什么是 Unicode ?Unicode 和我们常说的 UTF-8 又有什么关系?

 

字符编码和解码

要想搞清楚上面的问题,首先我们要知道,在计算机中,不管是一段文字、一张图片还是一段视频,最终都是以二进制的方式来存储。也就是最终都会转化为 0001 1011 0010 0110  这样的格式。

换句话说,计算机只认识 0 和 1 这样的数字,并不能直接存储字符。所以我们需要告诉它什么样的字符对应的是什么数字。

例如,我们的业务中有记录客户端的客户行为日志,然后导出文件来分析,字段间会以 ESC 来分隔。

我在编写代码的时候,就需要定义一下这个ESC 字符应该对应什么数字,这样计算机才能识别并存储。

比如我把它定为 0001 1011,这样计算机就把 ESC 这个字符存了下来。等我下次需要查看的时候,根据对应关系把它解出来就可以了。

上边的两个过程就对应字符的编码和解码过程。

字符编码就是把字符按一定的规则,转换成数字。字符解码是编码的逆过程,即把数字按规则转换成字符。

这样看来,貌似没有什么问题。

但是,这是我自己定义的编码规则,我同桌阿霄就不乐意了。他非要认为 ESC 应该定义为 1101 1000,好家伙正好和我定义的二进制数字顺序相反。

那结果肯定不用说了,我把 0001 1011 这串数字给他之后,按照他的编码规则来解,肯定是 &$#!这样的东西。

所以,乱码问题说到底,就是编码和解码的规则对应不上导致的。

 

ASCII 码

为了避免我和阿霄因为编码问题打起来,美国国家标准学会(AMERICAN NATIONAL STANDARDS INSTITUTE) ANSI 组织发话了。

停、停、停。不就是个编码问题吗,这种小事犯不着动手,我定义一个统一的规则,大家都按照我的规则来编码和解码不就好了嘛。

于是,ASCII 码出现了,它定义了一个常用字符集,用来表示字符和数字的对应关系,如下表。

ASCII 码全称:美国信息交换标准代码 (American Standard Code for Information Interchange)

图片来自百度图片

我一查表,ESC 字符不就对应 27 吗,对应的二进制就是 0001 1011 。我去,没想到我定义的规则竟和 ANSI 不谋而合。

同桌阿霄把抡在空中的拳头收了起来,默默地回去敲代码了。

 

ASCII 码扩展码

在使用英语的国家,ASCII 码就足够用了。但是,在其他欧洲发达国家比如法国,使用的语言是法语,有类似于这样的 á 符号,ASCII 码就不能表示了。那怎么办呢?

我们看上表就会发现,ASCII 码表的表示范围是十进制 0~127,也就是二进制 0000 00000111 1111 。其实只是用了后边的 7 位,第一位都是 0 。

而计算机二进制中一个字节是 8 个位,现在只用了 7 位。不行啊太浪费了,要充分利用第一个高位,扩展一下,这样多了一位,能表示的字符范围就多了一倍。(2的8次方=256)

这样一些欧洲其他国家,也能在计算机中表示自己的文字了。

后来,随着计算机的普及,中国的用户也多了起来。却发现,一个字节只能表示 256 个字符,远远不能满足我们的要求。

于是,就出现了 GB2312 编码,它使用了两个字节来表示一个汉字。但是,并没有把所有的位都用完,前面一个字节范围 0xA1 ~ 0xF7 (即 10110001 ~ 11110111),后面一个字节范围 0xA1 ~ 0xFE (即 10110001 ~ 11111110) 。这样就能表示简体汉字 6763 个。

GB2312 是国家标准总局发布的《信息交换用汉字编码字符集》,也可以说是简体中文的字符集。

但是,台湾和香港等使用繁体字的地区怎么办。于是,就有了大五码 Big5 编码来存储繁体。高字节(第一个字节)表示范围 0x81~0xFE,低字节(第二个字节)表示范围  0x40 ~ 0x7E,以及0xA1 ~ 0xFE 。

需要注意的是,GB2312 是简体中文,Big5 是繁体中文。如果用其中一种编码文字去读另外一种编码文字就会乱码。所以,就出来了 GBK 编码,把简体中文和繁体中文,以及一些 GB2312 不支持的人名(如历代总理有的名字用 GB2312 打不出来),还有一些我们不认识的古汉语都包含进去,共 2 万多个字符。

再然后,我们发现少数民族像藏文,蒙古文这些少数民族的语言,GBK 也支持不了,就再进行扩展,出现了 GB18030 。又多了几千个少数民族的文字。

所以,我们使用的 GB 国标系列文字都是在 ASCII 码之上扩展的,它们是依次向下兼容的。表示文字范围从小到大为 GB2312 = Big5 < GBK < GB18030 。

 

Unicode 字符集

我们在打开一个文档之前,就必须要知道它的编码格式,否则用错误的方式解码就会出现乱码情况。

设想,如果一个文本中,有多种类型文字,包括中文,韩语,德语,日语,应该用哪种编码方式?貌似怎么处理都会有乱码问题,那怎么办呢?

ISO(国际标准化组织)说:这好办啊,我把地球上,只要是人们使用的,所有语言和符号都囊括其中,为每个字符都指定一个唯一的字符码,这样就没有乱码问题了。于是 Unicode 出现了,又叫统一码,万国码。

如上图表,汉字“一”对应的 unicode 码是 \u4e00。我们通常在字符码前加个 \u代表这是 unicode 码。4e00 是十六进制表示。

也有很多在线转码工具供我们使用,如:http://tool.chinaz.com/tools/unicode.aspx

 

Unicode 编码方案

首先强调一下以下几个概念的区别:

  1. 字符:就是我们看到的一个字母或一个汉字、一个标点符号都叫字符。如上边的汉字“一”就是一个字符。

  2. 字符码:在指定的字符集中,一个字符对应唯一一个数字,这个数字就叫字符码。如上边的字符“一”,在 Unicode 字符集中,对应的字符码为 \u4e00

  3. 字符集:规定了字符和字符码之间的对应关系。

  4. 字符编码:规定了一个字符码在计算机中如何存储。

需要注意的是,Unicode 只是一个字符集,它规定了每个字符对应的唯一字符码,却没有规定这个字符码在计算机中怎样存储(也就是它的字符编码格式)。

例如,上边的汉字“一”,它的 Unicode 字符码为 \u4e00,转换成二进制就是 100 1110 0000 0000 。可以看到,它有 15 位二进制数,至少需要两个字节来存储。

这只是简单的汉字,如果其他复杂的字符有可能会需要 三、四 个字节或者更多字节来存储。

那么到底应该用几个字节来存储呢?

于是 UTF-32 编码 制定了标准,一个字符就用四个字节来表示。这样编码和解码都方便,固定取 32 位二进制就行了。

但是这样又引来一个问题。比如 A 字符其实只需要一个字节就可以存储了。如果必须要用四个字节来存储,那么前边三个字节都要补 0 ,这样势必会造成空间的浪费。

于是 UTF-16 编码(一个字符用两个字节或者四个字节)和我们熟悉的 UTF-8 编码格式就出现了。

这里我们重点介绍 UTF-8 。它使用一种变长的编码方式,可以使用 1~4 个字节来表示一个字符。根据不同的字符变换长度。

变长听起来很美好,但是它的不固定性,就让计算机懵逼了。比如,计算机怎么知道这四个字节代表的是一个字符,还是四个字符,亦或是两个字符呢?

于是,UTF-8 规定了以下编码规则,来避免以上问题。

  • 对于单字节的符号,第一位设为0,后边 7 位对应这个字符的ASCII码值。因此,像“A"这样的英文字母,UTF-8 编码和 ASCII 编码是相同的。

  • 对于大于一个字节的符号,假设为 n 字节,那么第一个字节的前 n 位都设为 1,这样有几个 1 就说明有几个字节。然后,第 n+1 位设为0 。后边的字节,前两位都设为10 ,剩余的其他二进制位都用这个字符的 Unicode 码填充(从后向前填充,不够补0)。

刚开始看上表,可能比较懵逼。其实,Unicode 符号表示的范围最大为四个字节,因此二进制为 4*8=32 位。我们知道,二进制转换十六进制时,以四位为一个单位转换,因此,对应的十六进制为 32/4=8 位。

上表中的 Unicode 符号范围是以 16 进制表示,可以看到就是 8 位的。

我们还是以汉字 “一” 为例,16进制表示为 4e00,补全所有位,其实就是 0000 4E00 (不区分大小写)。因此,查上表发现,它处在三个字节的 Unicode 范围内(0000 0800 < 0000 4e00 < 0000 FFFF)。

所以,它用 UTF-8 来编码,就是三个字节的,即格式是这样的 1110xxxx 10xxxxxx 10xxxxxx

4e00 转换为二进制为 100 1110 0000 0000,二进制位从后向前依次填充到上述格式中的x位置(也是从后向前填充)。

于是,就得出汉字 “一” 的 UTF-8 编码后的二进制表示为:1110 0100 1011 1000 1000 0000

其实,可以发现,汉字的二进制为 15 位,前边补零一位即为 16 位 0100 1110 0000 0000。而三个字节的 UTF-8 编码格式中的 x 个数也为 3*8 - (4+2+2) = 16 位,正好一一对应。

那么,我们这一通推算,是否正确呢。可以在程序中打印这个字符的二进制格式,以及UTF-8编码后的二进制。程序如下,

public class Test {public static void main(String[] args) throws UnsupportedEncodingException {System.out.println("字符'一'的二进制为:" + Integer.toBinaryString('一'));System.out.println("========");String str = "一";System.out.println("转换为UTF-8编码格式的二进制为:"+ toBinary(str,"utf-8"));}public static String toBinary(String str, String encode) throws UnsupportedEncodingException {StringBuilder sb = new StringBuilder();byte[] bytes = str.getBytes(encode);for (int i = 0; i < bytes.length; i++) {byte b = bytes[i];sb.append(Integer.toBinaryString(b & 0xFF));}return sb.toString();}}

打印结果为:

字符'一'的二进制为:100111000000000
========
转换为UTF-8编码格式的二进制为:111001001011100010000000

PS:通常的,我们发现常用的汉字以 16 进制表示都在 0000 0800 ~ 0000 FFFF 范围内。因此,汉字在 UTF-8 编码下通常占用三个字节。

细心的同学可能发现了,我上边转换的汉字可以用 char 类型来存储,这是为什么呢?

这是因为,在 Java 中,默认使用的字符集就是 Unicode,可以容纳 100 多万个字符,其中就包括汉字。

我们使用的绝对大多数汉字,都在0000 0800 ~ 0000 FFFF 这个范围内,可以看出来前边的四位十六进制都用不到(都是0000),因此,只需要后边的四位十六进制位,转换为二进制就是 4*4=16 位,只占用了两个字节(16/8=2)。而 char 在 Java 中占用两个字节,完全可以用来存储汉字。

 

总结

最后,来解答下文章开头的问题。

乱码的问题,究其根本原因,其实是编码和解码时的规则不一样导致的。

字符编码和字符集是两个不同的概念。一句话表示:字符集定义了字符到数字的映射关系,字符编码定义了这个数字如何在计算机中表达(存储)。

对于 ASCII 和 GB 系列,他们既是字符集也是字符编码。GB 兼容 ASCII 码。

而对于 Unicode 来说,字符集是 Unicode,而字符编码可以是 UTF-8,UTF-16 和 UTF-32 。所以,我们平时常用的 UTF-8 编码其实只是 Unicode 的一种编码实现方式而已。

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

我都服了,为啥上游接口返回的汉字总是乱码?相关推荐

  1. springmvc提供RestController方法接口返回json数据中文乱码

    web应用使用@RestController @RequestMapping 注解提供http接口 项目中遇到被调用接口后返回的json数据中出现中文乱码的问题 @RequestMapping(met ...

  2. (转)C#进阶系列——WebApi 接口返回值不困惑:返回值类型详解

    原文链接:https://www.cnblogs.com/landeanfen/p/5501487.html 阅读目录 一.void无返回值 二.IHttpActionResult 1.Json(T ...

  3. php app接口id参数类型过滤,PHP开发APP接口---返回数据的封装类

    /** * app返回数据类 * 1.接受多维,缺少键名的数组, * 2.可由输入的format参数决定返回数据格式 * 例子:Response::show(200, 'success', $data ...

  4. 接口返回html转换josn,接口返回数据Json格式处理

    有这样一个页面 , 用来显示用户的账户记录数据,并且需要显示每个月的 收入 支出合计 ,在分页的时候涉及到一些问题,需要对返回的Json格式做处理,处理起来比较麻烦,后端返回的Json数据格式形式如下 ...

  5. 佳信客服接口文档 REST API(第二部分)包含用户、聊天室、群聊、消息管理,通用接口数据结构、通用接口返回码

    6.聊天室管理 6.1.添加聊天室 接口定义: 请求URI https://api.jiaxincloud.com/rest/{orgName}/{appName}/chatrooms 访问角色 de ...

  6. 蚂蚁mPaaS框架控制台打印 原生与h5交互数据 和 RPC接口返回数据 都是Unicode乱码如何处理

    1.原生与h5交互数据 具体的参考之前博客:https://blog.csdn.net/qq_15509071/article/details/113876172 这里修改为: - (void)use ...

  7. 2021最新某某文书列表参数pageId、ciphertext、__RequestVerificationToken以及接口返回数据result逆向分析(二)

    文章目录 前言 一.抓包分析 二.参数解析 1.参数ciphertext 2.参数__RequestVerificationToken 3.参数pageId 三. result 解析 总结 前言 哦嚯 ...

  8. 调用后台接口返回报错前端隐藏提示_前端异常监控解决方案研究(转)

    前端监控包括行为监控.异常监控.性能监控等,本文主要讨论异常监控.对于前端而言,和后端处于同一个监控系统中,前端有自己的监控方案,后端也有自己等监控方案,但两者并不分离,因为一个用户在操作应用过程中如 ...

  9. java接口返回xml格式_Java xml数据格式返回实现操作

    前言:对于服务器后端开发,接口返回的数据格式一般要求都是json,但是也有使用xml格式 RequestBody注解 对于SpringMVC,很多人会认为接口方法使用@Controller搭配@Res ...

最新文章

  1. wps临时文件不自动删除_win10系统下wps残留文件无法删除如何解决
  2. 中国互联网哪来的所谓“所谓”的创新?“狗日”的腾讯究竟动了谁的蛋糕?...
  3. minGW64安装和使用 极简教程
  4. 不属于python数据类型的是_Python不支持的数据类型有( )。
  5. UC伯克利超酷研究:舞痴和舞王之间,只差一个神经网络
  6. Python——元组Tuple
  7. 关于axios中'$router' of undefined问题
  8. $.type 怎么精确判断对象类型的 --(源码学习2)
  9. 3dsmax动画六、骨骼调整及蒙皮。
  10. 基础LSB算法的matlab实现
  11. 通过ip查找域名的网站
  12. Android新手常见问题(一)
  13. 日期,手机号码正则表达式校验,身份证校验等常用工具
  14. 51单片机汇编入门基础代码-流水灯
  15. python中import文件夹下面py文件,报错
  16. find 搜索关键字并显示文件名
  17. SaaS是什么?企业为什么要有SaaS系统?
  18. 与 Oh My Zsh 不可错过的邂逅:如何离线安装 Oh My Zsh
  19. EtherNet/IP
  20. getnameinfo使用

热门文章

  1. ajax加载对应的json,jQuery:多个AJAX/JSON请求对应单个回调并行加载
  2. mysql并发_MySQL并发更新数据时的处理方法
  3. python生成范围内随机数_python在一个范围内取随机数的方法是什么
  4. linux 批量替换文件内容及查找某目录下所有包含某字符串的文件(批量修改文件内容)
  5. (王道408考研数据结构)第六章图-第四节6:拓扑排序(AOV网、代码、排序规则)
  6. 2021-07-06-Intellij IDEA新建项目时JDK以及模块语言等级(language level)默认为1.8或1.5,每次创建新项目都需要重新更改
  7. Java编写的统计字符代码
  8. golang flag包(命令行参数解析)
  9. docker ubuntu16.04镜像下安装cowrie蜜罐记录
  10. 33. 脱壳篇-重建输入表