文本在内存中的编码(3)——乱码探源(6)

先讲个小故事,虽然跟主题有点不太相关哈:

唐朝诗人李绅,身为官员,脾气暴躁,瞧不起信教的,尤其鄙视装逼之僧人,动不动就对他们拳脚相加。曾扬言:“我可以接见他们,要能答出来还好,要是答不出来,我弄死他!”有一回一个和尚来跟他宣传因果报应,李绅问:“阿师从哪里来,到哪里去呢?”僧答:“贫僧从来处来,到去处去。”李绅当时就急了,撸起袖子,亮出了手腕:“我去年买了个表!”

来自知乎问答“古人是如何「装逼」的?”,略有改动。

String到哪里去?

有了前面僧人的教训,在这里就不故弄玄虚了,应该说String的去处还是蛮确定的,那就是到byte[]中去,方式就是通过getBytes这一方法。

new String与getBytes

如果说new String(byte[], encoding)是从byte[]到String的过程,那么getBytes(encoding)则正好与之相反:它是从String到byte[]的过程。

image

或许我们应该说,它从去处来,又到来处去。

编码的逆转

显然,我们一直在说,String也不过是一堆byte,getBytes的过程不过是UTF-16编码的byte[]再转回去其它编码的byte[]的过程。无论是new String还是getBytes,不过都是在玩编码的转换而已。

image

从上图中可以看出String作为桥梁,可以把一种编码的字节转换成另一种编码的字节。比如把一串的UTF-8编码的字节转换成GBK编码的字节的。

image

String作为转换中的一方,它的编码始终是确定的,也即是UTF-16;而encoding参数始终指的是byte[]的编码:

或者用于指明源字节的编码(new String时),

或者用于表明希望转成何种编码的目标字节(getBytes时)。

具体转换过程

以GBK为例,既然前面说,GBK转UTF-16可以通过查表实现:

image

那么UTF-16转GBK,我们只需要反查那张表即可。当然,考虑到效率的问题,我们可能需要另一张按UTF-16编码排序的表:

image

当然了,这些都不需要我们去操心的。

至于UTF-16转成UTF-8,依旧可以通过码点这一桥梁来进行。

这里就不再演示了,与前面码点转UTF-8非常类似:

image

可参见字符集与编码(四)——Unicode

剩下的如转ISO_8859_1以及ASCII之类的,那就更简单了。如果一段String表示的是ISO_8859_1或者ASCII中的字符,显然里面每个char的高位都是00,因此只要把这些没用的00掐掉就行了。

image

如果调用getBytes(“UTF-16”)呢?那就不存在转换了,相当于复制了一遍,不过要注意它会带上BOM,除非明确指明了端序。

image

如果调用getBytes(“UTF-16BE”),那么内存中就会出现两组一模一样的字节了。

image

但是这两者还是有本质的区别的,原因就在于指向这两者的引用所代表的类型的不同。

类型赋予了一串byte丰富的内涵,决定了我们怎么去解释它。

String是一种有趣得多的类型,它有明确的编码,还有丰富多样的方法与之绑定。

而另一方面,byte[]则要原始单调乏味得多。严格地说,byte[]只是一堆字节而已,就编码这个问题而言,它本身没有与任何编码绑定。

当然,字节间的特征也许能让你断言这不是某种编码,但你却不能肯定地说,一串字节一定是某种编码。

多种解释

我们来看个具体例子,还是拿前面说到的那串GBK编码的byte来说吧:68 69 c4 e3 ba c3。

之所以说它是GBK,那是因为我们用GBK编码保存文件得到的它。但如果内存中有一段与之一模一样的byte[],难道你能说它一定是GBK编码吗?

首先可以确定它不可能是ASCII,因为有些字节最高位是1,那么它有可能是ISO_8859_1吗?这是有可能的。而对它的不同解释会因此在内存中生成不一样的String:

image

具体的代码测试也可以反映这一点:

    @Test
    public void testReadGBKBytesAsISO_8859_1() throws Exception {
        File gbk_demo = FileUtils.toFile(getClass().getResource("/encoding/gbk_demo.txt"));
        // 当成ISO_8859_1来读取
        String content = FileUtils.readFileToString(gbk_demo, "ISO_8859_1");
        assertThat(content).isEqualTo("hiÄãºÃ");
        assertThat(content.length()).isEqualTo(6);
    }

有的时候,你没有办法拿到最底层的byte[],你直接收到的就是一个String,而这个String是通过错误的编码构建的,如下所示:

image

第一步不受我们控制,这时有一种hack的方式,也即是通过上面的第二步再度拿回原始的byte[],再通过第三步传入正确的编码再度构建出String。

代码演示如下:

    @Test
    public void testISO_8859_1Hack() throws Exception {
        File gbk_demo = FileUtils.toFile(getClass().getResource("/encoding/gbk_demo.txt"));
        // 当成ISO_8859_1来读取
        String content = FileUtils.readFileToString(gbk_demo, "ISO_8859_1");
        assertThat(content).isEqualTo("hiÄãºÃ");
        assertThat(content.length()).isEqualTo(6);
        
        // 再次拿到原始byte[],并以新的编码重新构建String
        content = new String(content.getBytes("ISO_8859_1"), "GBK");
        assertThat(content).isEqualTo("hi你好");
        assertThat(content.length()).isEqualTo(4);
    }

当然,我们并不鼓励这样繁琐地转来转去,正确的姿势应该是这样的:

image

通过修改配置,就能一步到位得到正确的String,不需要曲线救国。

有人可能说,我并没配置过iso_8859_1呀,那么这可能是某种缺省编码。

总之,如果你收到被疑似iso_8859_1错误编码的String,那肯定是某个环节使用了这一编码。

当然,如果你工作在一个遗留系统上,还是要非常慎重地去改变这些缺省的编码配置,因为可能严重冲击到那些依赖于这些缺省设置的代码。

比如上述hack方式可能就失效了,而你可能不知道系统到底有多少地方使用了这种hack。

关于String到哪里去的问题,就探讨到这里。

转载于:https://my.oschina.net/goldenshaw/blog/471370

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值