谈谈java点字符集,编码方式

本文详细阐述了字符集、编码的概念及其在数据压缩与解压缩过程中的应用,通过一个具体案例展示了字符集不一致导致的解压失败问题,并深入分析了乱码产生的原因及解决方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        经常涉及到编码,字符集,unicode, ASIIC, utf-8之类点东西,一直没太搞懂其中的奥妙,以前在搞MySQL时,被中文乱码点问题点搞点晕头转向,最后还是没搞懂,只是总结出来始终保持用同一种字符集/编码方式就ok来,一直也就没遇到什么问题,直到最近做数据压缩/解压缩时又遇到困扰来好久点问题,果然出来混,迟早时要还的,于是在搞定了问题之后花了一些时间来研究来一下字符集/编码的问题。


问题的场景

        在负责的一个项目中,访问一个服务器接口时,需要传递一个很长的字符串参数,用post方法传递该参数,为了节省传递消耗点带宽,决定压缩字符串后再传递,基本过程如下:

        原始字符串------->二进制数组------->使用GZIPOutputStream压缩------->将压缩后的二进制数组转换成字符串,进行参数传递------->服务器将收到点字符串参数转化为二进制数组-------->服务器使用GZIPInputStream解压--------->将二进制数组转换成字符串

        逻辑足够简单,但服务器无法解压,因为收到的字符串转换成二进制数组后,格式不符合GIZPInputStream的格式。奇怪。。。为什么使用同一种方式压缩后,不能解压呢,纠结来好久,发现服务器收到点压缩后的二进制数组与客户端压缩生成的一样,经过排查,发现时从二进制转换为字符串时出了问题。任何二进制数组经过GZIP压缩后,得到的二进制数组的前两个字节是0x8b1f(这里就不管什么大尾,小尾了), 转换成二进制就是10001011 00011111, 用于表示这是经过GZIP压缩的数据, 因为整个转换过程使用的都是java默认编码方式utf-8(具体的编码方式后面再讲), 结果utf-8无法将无法将10001011这个字节解码成一个字符,因为utf-8不支持这个字节,于是就采用来我们经常看到的乱码中的问号代替了这个字符,于是服务器收到来乱码,在将字符串转换为二进制数组时就得不到10001011这个字节了,于是GZIP就不认识它了,直接就报错了。这里只需要知道utf-8的二进制数组有其严格点规范,不满足这些规范的二进制流转换成utf8字符串后必然会变成乱码,无法还原成原来的二进制流(乱码中的问号是一个固定的编码)

        下面先来了解一些编码/字符集的相关知识


Unicode编码

         一说到unicode,其实很多人都非常熟悉了,比如很多书上就说了java字符串采用了unicode编码,使用两个字节表示一个字符,我们也经常用一些4位16进制数表示一个字符,如0xcfff, "/ucffff" 之类的,一直也没对这些说法产生过任何怀疑,也没仔细去思考过。直到遇到这个问题后就查询相关知识时才想到一些问题。unicode标榜可以表示全世界几乎所有的的符号,可以表示多大上百万的符号。这里就有问题了,unicode不是用两个字节表示一个字符吗?这样的话不就是最多只能表示65537个符号而已吗?我们表示字符不也就用4位16进制吗?这是怎么回事?原来unicode其实是一个变长的编码方式,可能用两个字节表示一个字符,也可能用4个字节表示一个字符,它所能表达的范围不是0x0000-0xffff, 而是0x0000-0x10ffff, 没有到0xffffffff是因为不需要用到更大的值了,所以可能一个字符的表达方式可能是0x64321,当然不能把这个值赋值给一个java char类型,因为超过范围来,char只有两个字节,这里又是一个奇怪的地方。


关于字符集/编码的理解

          经常我们会说到中文字符集,英文字符集,西欧字符集等等一堆的东西,也会说到unicode, ASIIC, ISO-8859-1(又叫Latin-1), GBK等等字符集,utf8, utf16等编码。那么字符集跟字符编码到底是什么,又有什么区别呢?

          我理解的所谓字符集,顾名思义就是收录字符的集合,如ASIIC字符集收录了127个英文符号,ISO-8859-1在ASIIC的基础上增加了128个西欧字符,GBK则主要用于处理汉字,而unicode则号称收录了几乎全世界所有的字符,多大上百万。在字符集中每个字符都可以用一个16进制数表示,如0xcb1f, 这个16进制被成为字节点,由具体点字符集来确定每个字符用什么字节点,也就是说同一个字节点在不同的字符集里面表示的可能时不同点字符,同一个字符在不同字符集中可能对应的字节点也不同。

          那字符编码又是干啥的呢?当一个字符需要在磁盘,内存中保存,或者需要传输时,就需要转换成二进制。诶,上面说到字节点不就是可以表示成二进制吗?不错,确实可以这样,实际上某些字符编码就是直接把字节点转换成二进制了,比如ASIIC字符集。但如果时unicode字符集呢,用两个字节表示一个字符,如果是"你好", 需要4个字节,无可厚非,但如果如果是"hi",也需要4个字节,而用ASIIC只需要两个字节,那些美国佬怎么可能接受unicode呢,所以就需要编码方式了。所谓点编码方式就是规定字符串如何转换成二进制以及怎么从二进制转换成字符串。每一个字符集最少都应该有一种编码方式,当然很多也只有一种编码方式,如ASIIC, ISO-8859-1,而且编码方式的名字就是字符集的名字,于是我们就经常听到ASIIC字符集,ASIIC编码,以前还一直为这两种叫法感到困惑。当然有些字符集有多种编码方式,如unicode,通常所说的unicode编码,实际指的应该是utf-16(我不太确定,网上说法太多), 也就是用两个字节或32个字节表示一个字符,另外最长用的是utf-8, 用的比较少的有utf32。

         下面来了解各种编码方式的具体细节。


各种编码方式

ASIIC编码:

        ASIIC编码用一个字节表示一个字符,而且由于其只有127个字符,所以二进制的最高位始终是0,因此字符串包含几个字符,转换得到的二进制就是多少个字节。


ISO-8859-1编码:

        ISO-8859-1编码是ASIIC的一个扩展字符集,增加了128个字符,利用了ASIIC的最高位,它也是用一个字节表示一个字符,并且完全兼容ASIIC,也就是说0x00-0x7f表示的字符与ASIIC完全相同。

        类似与ISO-8859-1的字符集还有很多,且都完全兼容ASIIC, 只是增加的128个字符各不想同而已


utf-8:

        前面的两个编码方式都是用一个字节表示一个字符,而utf-8是一种变长的编码方式。它是unicode字符集的一个编码方式,而unicode的字节点是4位或者8位的16进制,我们也说过编码实际上就是将字节点转换为字节点按某种格式转换为二进制。utf-8会用1-6个字节来编码一个unicode字节点,在转换之前首先会去掉高位的所有0,假设剩下的二进制有x位。则

1) 如果x < 8, 则用一个字节编码,跟ASIIC完全一样,最高位也必须是0

2) 如果x >= 8, 则会用多个字节编码,规则就稍微复杂一点:

    a) 第一个自己高位连续的1的个数表示需要多少个字节

    b) 从第二个字节开始,每个字节的最高两位都必须是10

    c) 每个字节没有被占用点位会被字节点对应的二进制从按顺序从最后一个字节开始从右向左填充,如不够填满,则高位补0

       例如对于字节点位0xdd的字符,其对应的直接二进制是11011101, 首先不满足条件1,所以会采用多个字节,如果采用两个字节的话,两个字节的编码位分别是11,10, 加上需  要转换的8位,还不足16位,因此两个字节足够,而且需要高位补4个0,所以得到的编码是:11000011 10011101


utf-16:

      与utf-8一样,都是unicode字符集的一种编码,对于字节点0x0000-0xffff这65537个字符,表示的是最常用的字符,包含大多数常用汉字,用两个字节编码,并且编码就是简单的将字节点直接转换为二进制而已。对于字节点0x10000-10ffff,则采用4个字节,具体的编码算法可以参见维基百科:

http://zh.wikipedia.org/wiki/UTF-16

所以说utf-16也是一种变长的编码方式,而不是很多网友说的用两个字节编码unicode。


utf-32:

      unicode字符集的又一种编码方式,终于是固定长度的编码了,对每一个字节点都使用4个字节编码,编码方式就非常简单了,直接将字节点转换为二进制就可以了。不过这种编码方式很少用,因为太浪费空间


关于乱码:

       有了众多的字符集和编码方式,就容易导致一个让人极度恶心的结果:乱码! , 那么乱码产生的原因是什么呢?我理解的主要是两个原因:

1) 编码和解码的过程使用了不同的编码方式,例如:

使用ISO-8859-1编码字节点为0xdd的字符,编码成二进制的结果是11011101, 如果用utf-8解码,由于字节开头,因此utf-8认为是用两个字节编码的,被解码的数据只有1一个字节,显然就无法正确解码,就成乱码了。反过来,如果用utf-8编码,则得到的二进制是:11000011 10011101, 然后用ISO-8859-1, 得到的结果是0xc3, 0x9d, 成了两个字节,虽然得到的不是乱码,但也不是正确的结果。

2) 使用了不适合的字符集

当使用String.getBytes(String charSet")进行编码时,我们通常只是指定了编码方式,但同时其实也指定了字符集,如utf-对应了unicode字符集, ISO-8859-1对应了ISO-8859-1字符集,ASIIC对应了ASIIC字符集。在编码一个字符时,需要从字符集中找到其对应的字节点,如'A'的在以上三种字符集中字节点都是0x41, 因此可以编码,但如果是汉字'汉‘,在ISO-8859-1和ASIIC中,没有收录这个字符,因此无法找到对应的字节点,于是就用了乱码代表符号'?'的字节点,编码成00111111,即使是字节用字节点来表示字符,如char c = 0xcfcf, 如果在对应的要编码的字符集里没有这个字节点,同样会产生乱码。


关于存储和网络传输:

      存储和网络传输中字符串和二进制的转换过程基本就是:

字符串------->二进制-------> 字符串

正确的传输和存储要求编码--->解码后能得到原始的字符串,这就基本要求使用相同的编码方式了

除了上面这种场景,还有一种就是:

二进制------->字符串------->二进制

这也是我在压缩中遇到的场景,为了能够正确的解码,从字符串转换成的二进制必须与原来的二进制完全一样,这就涉及到一个问题了:二进制转换成字符串后,怎样才能保证从字符串转换回来后得到相同的二进制?,需要满足亮点:

1) 二进制---->字符串和字符串------>二进制使用相同的编码方式,

2) 从二进制到字符串时,二进制必须满足对应编码方式的格式, 也就是不能出现乱码。

回到压缩/解压缩失败的问题,我将压缩后的二进制使用默认编码方式,也就是utf-8转换成字符串,显然压缩后的二进制是不满足utf-8的格式,因为第一个字节是用来表示gzip压缩的mask: 10001011 00011111,显然会出现乱码,于是再从字符串用utf-8转换回来时,是不能得到原始二进制的,最终导致解压缩失败。

其实如果从二进制转换为字符串,大多数编码方式都是不能保证满足其格式的,除非二进制是由对应的编码方式编码字符串得到的。但是有一种编码方式,任何的二进制都满足其格式,就是使用一个字节编码的ISO-8859-1,需要特别注意的是ASIIC虽然也是一个字节编码,但其要求最高位必须是0。

于是,对于我的问题,我只需要按ISO-8859-1编码转换成字符串,再作为参数传输,服务端就能通过ISO-8859-1转换回我原来的二进制,成功的解码。这里还有一点需要注意,在我本地是用ISO-8859-1将二进制转换为字符串,但在网络传输的时候还是却是用utf-8编/解码,这不会有问题吗?看一下具体的过程就知道了:

(1)压缩后的二进制----->(2)ISO-8859-1转换得到的字符串----------->(3)utf-8编码用于传输的二进制--------->(4)服务端utf-8解码得到的字符串--------->(5)ISO-8859-1编码用于解压缩的二进制

因为(3)和(4)使用的都是utf-8,因此字符串还原成功,也就是说(5)编码的字符串就是(2)的字符串,再使用ISO-8859-1编码,显然得到的就是原来的二进制,所以解压成功

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值