先讲个小故事,虽然跟主题有点不太相关哈:
唐朝诗人李绅,身为官员,脾气暴躁,瞧不起信教的,尤其鄙视装逼之僧人,动不动就对他们拳脚相加。曾扬言:“我可以接见他们,要能答出来还好,要是答不出来,我弄死他!”有一回一个和尚来跟他宣传因果报应,李绅问:“阿师从哪里来,到哪里去呢?”僧答:“贫僧从来处来,到去处去。”李绅当时就急了,撸起袖子,亮出了手腕:“我去年买了个表!”
来自知乎问答“古人是如何「装逼」的?”,略有改动。
String到哪里去?
有了前面僧人的教训,在这里就不故弄玄虚了,应该说String的去处还是蛮确定的,那就是到byte[]中去,方式就是通过getBytes这一方法。
new String与getBytes
如果说new String(byte[], encoding)是从byte[]到String的过程,那么getBytes(encoding)则正好与之相反:它是从String到byte[]的过程。
或许我们应该说,它从去处来,又到来处去。
编码的逆转
显然,我们一直在说,String也不过是一堆byte,getBytes的过程不过是UTF-16编码的byte[]再转回去其它编码的byte[]的过程。无论是new String还是getBytes,不过都是在玩编码的转换而已。
从上图中可以看出String作为桥梁,可以把一种编码的字节转换成另一种编码的字节。比如把一串的UTF-8编码的字节转换成GBK编码的字节的。
String作为转换中的一方,它的编码始终是确定的,也即是UTF-16;而encoding参数始终指的是byte[]的编码:
或者用于指明源字节的编码(new String时),
或者用于表明希望转成何种编码的目标字节(getBytes时)。
具体转换过程
以GBK为例,既然前面说,GBK转UTF-16可以通过查表实现:
那么UTF-16转GBK,我们只需要反查那张表即可。当然,考虑到效率的问题,我们可能需要另一张按UTF-16编码排序的表:
当然了,这些都不需要我们去操心的。
至于UTF-16转成UTF-8,依旧可以通过码点这一桥梁来进行。
这里就不再演示了,与前面码点转UTF-8非常类似:
剩下的如转ISO_8859_1以及ASCII之类的,那就更简单了。如果一段String表示的是ISO_8859_1或者ASCII中的字符,显然里面每个char的高位都是00,因此只要把这些没用的00掐掉就行了。
如果调用getBytes(“UTF-16”)呢?那就不存在转换了,相当于复制了一遍,不过要注意它会带上BOM,除非明确指明了端序。
如果调用getBytes(“UTF-16BE”),那么内存中就会出现两组一模一样的字节了。
但是这两者还是有本质的区别的,原因就在于指向这两者的引用所代表的类型的不同。
类型赋予了一串byte丰富的内涵,决定了我们怎么去解释它。
String是一种有趣得多的类型,它有明确的编码,还有丰富多样的方法与之绑定。
而另一方面,byte[]则要原始单调乏味得多。严格地说,byte[]只是一堆字节而已,就编码这个问题而言,它本身没有与任何编码绑定。
当然,字节间的特征也许能让你断言这不是某种编码,但你却不能肯定地说,一串字节一定是某种编码。
多种解释
我们来看个具体例子,还是拿前面说到的那串GBK编码的byte来说吧:68 69 c4 e3 ba c3。
之所以说它是GBK,那是因为我们用GBK编码保存文件得到的它。但如果内存中有一段与之一模一样的byte[],难道你能说它一定是GBK编码吗?
首先可以确定它不可能是ASCII,因为有些字节最高位是1,那么它有可能是ISO_8859_1吗?这是有可能的。而对它的不同解释会因此在内存中生成不一样的String:
具体的代码测试也可以反映这一点:
@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是通过错误的编码构建的,如下所示:
第一步不受我们控制,这时有一种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);
}
当然,我们并不鼓励这样繁琐地转来转去,正确的姿势应该是这样的:
通过修改配置,就能一步到位得到正确的String,不需要曲线救国。
有人可能说,我并没配置过iso_8859_1呀,那么这可能是某种缺省编码。
总之,如果你收到被疑似iso_8859_1错误编码的String,那肯定是某个环节使用了这一编码。
当然,如果你工作在一个遗留系统上,还是要非常慎重地去改变这些缺省的编码配置,因为可能严重冲击到那些依赖于这些缺省设置的代码。
比如上述hack方式可能就失效了,而你可能不知道系统到底有多少地方使用了这种hack。
关于String到哪里去的问题,就探讨到这里。
来源:oschina
链接:https://my.oschina.net/u/1772009/blog/471370