编码方式与程序中文乱码原因分析

编码方式与中文乱码原因分析

1 基础内容

我们常见的编码方式有utf-8,utf-16,utf-32,ansi,gbk,gb2312,iso-8859-1
其中:
1)
utf-8,utf-16,utf-32,是以Unicode码表为基础的编码方式,
Unicode码表中同一个字符,使用不同的编码方式会得到不同的字节序列。
utf-32:
使用32位,即4字节来表示每个字符。
utf-16:
使用16位,即2个字节来表示每个字符,但对于一些特殊字符,会采用更多的字节表示(日常使用可以不考虑这种情况)。
utf-16就是对Unicode码表的字面量的直接使用,因为Unicode目前普遍采用的是UCS-2,它用两个字节来编码一个字符。
所以我们通常误称的“Unicode编码,UCS-2编码”指的就是UTF-16编码。

utf-8:
是一种变长的编码方式,通常采用1~3个字节来表示常用字符。
例如:普通的ascii码就使用1个字节,而中文就使用3个字节来表示。

utf系列的编码方式,通用一个Unicode码表。
2)

ansi这种说法出现在微软的操作系统中,在中国大陆,ansi就是指gbk
gbk只是对gb2312编码的扩充,不改变gb2312的编码方式。

早在80年代初,为了在计算机中显示中文,我国定义了针对中文的码表,又称“区位表”,
而gbk或gb2312就是基于区位表的一种编码方式。
gbk与区位表的关系,就相当于utf-*与Unicode码表的关系。

3)
ISO-8859-1
只能显示ASCII表中的字符,即英文、数字和一些符号,用1个字节表示,并且字节的最高位为0,编码范围为0x00~0x7F,共128个字符。不能显示中文、日语、韩语等其他国家的语言。


2 由编码方式的不同引起的乱码

在计算机领域中,乱码问题是一个我们始终绕不开的话题,
那乱码问题是如何产生的呢?
在这之前,我们要明确两个问题:

1)位,字节,字符的区别
位(bit),我们常常描述的计算机是由0和1构成的,这里的0和1就是指的位,由通电时的高低电平来表示。
在日常生活中,我们常说,我们办的光纤是200M,500M等等的,这里200M就是以位为概念描述的,200M 表示每秒能传输 200M个位,即200*1024*1024个0和1。

字节(byte),1个字节占8位,字节是计算机的存储单元,我们的内存是4G,硬盘是1T,这里指的就是4GB和1TB
(为了区分bit和byte,通常将byte缩写为B,bit缩写为b)。
一个数无论再小,哪怕就是一个0或1,那么在存储时也至少占一个字节。

所以,假如我们有个视频文件,大小为1G,那么使用200M的宽带下载(假如宽带设定的上传与下载额定速率相同),
需要花费的时间是(理想情况下): 1024/(200/8/2) 秒

字符(char),字母、数字、符号、各国语言等等,这些我们能够描述的文字和符号,统称字符。
我们的文字要在计算机中存储和显示,就需要将文字变成计算机能表示的0和1的序列。
这就是我们上一部分说明的各种编码方式的作用。

2)区分内存中的字符 与 文件(硬盘)中的字符
以Jvm为例进行说明。
在java中,数据类型char表示字符,为了兼容各种语言字符,使用Unicode码来表示字符(UCS-2),那么一个char就占两个字符。所以在各种环境中,字符在jvm中的表示形式固定的,且是定长的,这就极大的方便了运算。

例如,
汉字“经”,在内存中表示为“7E CF”(16进制)。
由于统一的表示方式,所以内存中不存在乱码这个说法。

但是,当我们将字符写入文件时,就会出现情况了。

当我们将内存中的数据写入文件时,并不是直接写入,而是需要有个编码的过程。

为什么不直接写入呢?
因为内存中所有字符均占2个字节,那么对于英文字母、数字、部分符号,我们明明一个字节(gbk和utf-8)就可以表示,如果统统采用2个字节,这样无形会浪费存储效率和空间(特别对于以英语为主的国家)。
所以普通文件常用的编码方式就是utf-8和gbk(因为能节省空间啊)。

那么在写入文件时,会将内存中的字符,按照一定的编码规则,生成字节序列 byte[],一定要注意这点——“字节序列”。
然后,将该序列数组写入硬盘文件中。

byte[] b = "经".getBytes("utf-8");
for(int i=0;i<b.length;i++){
    System.out.print(Integer.toHexString(Byte.toUnsignedInt(b[i]))+" ");
    //E7 BB 8F
}

“经”.getBytes(“gbk”) : 0xBEAD
“经”.getBytes(“utf-8”): 0xE7BB8F
于是,硬盘文件中的内容就是0xBEAD或0xE7BB8F

所以我们可以看到,jvm内存与硬盘文件对同一个汉字表示就不同了,这其中有一个“编码”的过程。

说到这里,可能有些同学及猜到了乱码的原因了,下面就是见证奇迹的时刻。

当我们使用程序读取硬盘文件内容,或接收网络传输的数据的时候,也是一个一个按字节来读取的(学过IO流,我们知道字节流是最基本的流),
假如我们得到了一个字节序列:[e6 88 91 e7 88 b1 e4 b8 ad e5 8d 8e ],

我们想看一看这个字节序列是什么内容,就需要将他们转换为内存中的字符,
我们知道将字符转换为字节序列,有个编码的过程,
那么反过来,将字节序列变为字符,就有个解码的过程,

那到底按照什么方式进行解码呢,gbk还是utf-8呢?

byte[] b = {(byte) 0xe6,(byte) 0x88,(byte) 0x91,
                (byte) 0xe7,(byte) 0x88,(byte) 0xb1,
                (byte) 0xe4,(byte) 0xb8,(byte) 0xad,
                (byte) 0xe5,(byte) 0x8d,(byte) 0x8e};
        String s1 = new String(b,"utf-8");
        System.out.println(s1);//我爱中华

        String s2 = new String(b,"gbk");
        System.out.println(s2);//鎴戠埍涓崕

我们看到,当我们使用gbk解码这个字节数组时,就会出现乱码,而utf-8就是正常显示。
而这,就是我们出现中文乱码的根本原因:
在解码字节序列时,采用的编码方式不对,或者说,与生成该字节序列时采用的编码方式不同。


有的同学会说,既然是乱码,为什么gbk解码生成的字符串,会出现“鎴戠埍涓崕”这些内容呢?
那这就需要看gbk解码的过程了:

gbk解码时会按字节进行解码,
首先读取第一个字节,0xe6,发现这个字节的最高为是1,而不是0,就认为这是一个汉字的第一个字节,而不是一个ASCII字符。

然后,继续读取第二个字节[0x88],将两个字节合并在一起,[0xe688],然后按照gbk约定的解码逻辑,解码得到一个数字。

最后一步根据这个数字去查gbk的“区位表”,发现对应的区位表的内容是“鎴”,于是就显示出了这个汉字。
以此类推,得到“鎴戠埍涓崕”这个乱码字符串。

符:
区位表(部分)这里写图片描述

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值