文本在内存中的编码(2)——乱码探源(5)

在前面咱们探讨了String是什么的问题,如今来看String从哪来的问题。java

String从哪里来?

所谓从哪里来也能够看做是String的构造问题,所以咱们会从String的构造函数提及。程序员

String的构造函数

在前面咱们知道String的内部就是char[],所以它能够根据一组char[]来构建,String中有这样的构造函数:apache

public String(char value[]) {}数组

那么char[]又从何而来呢?char的底层是byte,String从根本上讲仍是字节序列,而一个文本文件从根本上讲它也是字节序列,那是否是直接把一个文本文件按字节读取上来就成了一个String呢?安全

答案是否认的。由于咱们知道String不可是byte[],并且它是一个有特定编码的byte[],具体为UTF-16。函数

而一个文本文件的字节序列有它本身特定的编码,固然它也多是UTF-16,但更多是如UTF-8或者是GBK之类的,因此一般要涉及编码间的一个转换过程。咱们来看下经过字节序列来构造String的几种方式:工具

public String(byte bytes[]) {}      
public String(byte bytes[], String charsetName) throws UnsupportedEncodingException {}      
public String(byte bytes[], Charset charset) {}ui

第一个只有byte[]参数的构造函数实质上会使用缺省编码;而剩余的两种方式没有本质的区别。编码

后两种方式的差异在于第二个参数是用更加安全的Charset类型仍是没那么安全的String类型来指明编码。spa

实质上能够归纳为一种构造方式:也便是经过一个byte[]和一个编码来构造一个String。(没有指定则使用缺省)

因为历史的缘由,这里沿用了charset这种叫法,更加准确的说法是encoding。可参见以前的字符集与编码(一)——charset vs encoding

具体示例

录入如下内容“hi你好”,并以两种不一样编码的保存成两个不一样文件:

image_thumb[2]

那么,两种字节序列是有些不一样的,固然,两个英文字母是相同的。

image_thumb[3]

那么咱们如何把它们读取并转换成内存中的String呢?固然咱们能够用一些工具类,好比apache common中的一些:

    @Test
    public void testReadGBK() throws Exception {
        File gbk_demo = FileUtils.toFile(getClass().getResource("/encoding/gbk_demo.txt"));
        String content = FileUtils.readFileToString(gbk_demo, "GBK");
        assertThat(content).isEqualTo("hi你好");
        assertThat(content.length()).isEqualTo(4);
    }

    @Test
    public void testReadUTF8() throws Exception {
        File utf8_demo = FileUtils.toFile(getClass().getResource("/encoding/utf8_demo.txt"));
        String content = FileUtils.readFileToString(utf8_demo, "UTF-8");
        assertThat(content).isEqualTo("hi你好");
        assertThat(content.length()).isEqualTo(4);
    }

在这里,file做为byte[],加上咱们指定的编码参数,这一参数必须与保存文件时所用的参数一致,那么构造String就不成问题了,下图显示了这一过程:

image_thumb[4]

以上四个字节序列都是对四个抽象字符“hi你好”的编码,转换成string后,特定编码统一成了UTF-16的编码。

如今,若是咱们要进行比较呀,拼接呀,都方便了。若是只是把两个文件做为原始的byte[]直接读取上来,那么咱们甚至连一些很简单的问题,好比“在抽象的字符层面,这两个文件的内容是否是相同的”,都没办法去回答。

从这个角度来看,string不过就是统一了编码的byte[]。

而另外一方面,咱们看到,构造string的这一过程也就是不一样编码的byte[]转换到统一编码的byte[]的过程,咱们天然要问:它具体是怎么转换的呢?

转换的过程

让咱们一一来分析下:

1. UTF-16 BE:假如文本文件自己的编码就是UTF-16 BE,那么天然不须要任何的转换了,逐字节拷贝到内存中就是了。

2. UTF-16 LE:LE跟BE的差异在于字节序,所以只要每两个字节交换一下位置便可。

关于字节序跟BOM的话题,可见字符集与编码(七)——BOM

3. ASCII和ISO_8859_1:这两种都是单字节的编码,所以只须要前补零补成双字节便可。如上图中68,69转换成0068,0069那样。

4. UTF-8:这是变长编码,首先按照UTF-8的规则分隔编码,如把8个字节“68 69 e4 bd a0 e5 a5 bd”分红“1|1|3|3”四个部分:

68 | 69 | e4 bd a0 | e5 a5 bd

而后,编码可转换成码点,而后就能够转换成UTF-16的编码了。

关于码点及Unicode的话题,可见字符集与编码(四)——Unicode

咱们来看一个具体的转换,好比字符“你”从“e4 bd a0”转换到“4f 60”的过程:

image_thumb[5]

真正的转换代码,未必真要转换到码点,可能就是一些移位操做,移完了就算转换好了。若是涉及增补字符,这个过程还会更加复杂

5. GBK:GBK也是变长编码,首先仍是按照GBK的编码规则将字节分隔,把“68 69 c4 e3 ba c3”分红“1|1|2|2”四个部分。

68 | 69 | c4 e3 | ba c3

以后,好比对于字符“好”来讲,编码是如何从GBK的“ba c3”变成UTF-16的“59 7d”呢?

这一下,咱们无法简单地用有限的一条或几条规则就能完成转换了,由于不像Unicode的几种编码之间有码点这一桥梁。

这时候只能依靠查表这种原始的方式。首先须要创建一个对应表,要把一个GBK编码表和一个UTF-16编码表放一块,以字符为纽带,一一找到那些对应,以下图:

image_thumb[6]

很明显,因为有众多的字符,要创建这样一个对应表仍是蛮大的工做量的。天然,曾经有那么些苦逼的程序员在那里把这些关系一一创建了起来。不过,好在这些工做只须作一次就好了。若是他们藏着掖着,咱们就向他们宣扬“开源”精神,等他们拿出来共享后,咱们再发挥“拿来主义”精神。

那么,有了上图中最右边的表以后,转换就能进行了。

固然,咱们只须要扔给String的构造函数一个byte[],以及告诉它这是”GBK“编码便可,怎么去查不用咱们操心,那是JVM的事,固然它可能也没有这样的表,它也许只是转手又委托给操做系统去转换。

不支持的状况

若是咱们看前面String构造函数的声明,有一个会抛出UnsupportedEncodingException(不支持的编码)的异常。若是一个比较小众的编码,JVM没有转换表,操做系统也没有转换表,String的构建过程就无法进行下去了,这时只好抛个异常,罢工了。

固然了,不少时候抛了异常也许只是粗心把编码写错了而已。

至此,咱们基本明白了String从哪里来的问题,它从其它编码的byte[]转换而来,它自身不过也是某种编码的byte[]而已。

字节流与字符流

若是你此时认为前面的FileUtils.readFileToString(gbk_demo, "GBK")就是读取到一堆的byte[],而后调用构造函数String(byte[], encoding)来生成String,不过,实际过程并非这样的,一般的方式是使用所谓的“字符流”。

那么什么是字符流呢?它是为专门方便咱们读取(以及写入)文本文件而创建的一种抽象。

文件始终是字节流,这一点对于文本文件天然也是成立的,你始终能够按照字节流并结合编码的方式去处理文本文件。不过,另一种更方便处理文本文件的方式是把它们当作是某种”抽象的“字符流。

设想一个很大的文本文件,咱们一般不会说一下就把它所有读取上来并指定对应编码来构建出一个String,更可能的需求是要一个一个字符的读取。

好比对于前述的”hi你好“四个字符,咱们但愿说,把”h“读取上来,再把”i“读上来,再读”你“,再读”好“,如此这般,至于编码怎么分隔呀,转换呀,咱们都不关心。

Reader跟Writer是字符流的最基本抽象,分别用于读跟写,咱们先看Reader。能够用如下方式来尝试依次读取字符:

    @Test
    public void testReader() {
        File gbk_demo = FileUtils.toFile(getClass().getResource("/encoding/gbk_demo.txt"));
        Reader reader = null;
        try {
            InputStream is = new FileInputStream(gbk_demo);
            reader = new InputStreamReader(is, "GBK");
            
            char c = (char) reader.read();
            assertThat(c).isEqualTo('h');
            
            c = (char) reader.read();
            assertThat(c).isEqualTo('i');
            
            c = (char) reader.read();
            assertThat(c).isEqualTo('你');
            
            c = (char) reader.read();
            assertThat(c).isEqualTo('好');
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(reader);
        }
    }

这里,read()方法返回是一个int,把它转换成char便可,另外一种方式是read(char cbuf[]),直接把字符读取到一个char[],以后,若是有必要,能够用这个char[]去构建String,由于咱们知道String其实就是一个char[].

很显然,一个Reader不但要构建在一个字节流(InputStream)基础上,并且它与具体的编码也是息息相关的。

编码用于指导分隔底层字节数组,而后再转成UTF-16编码的字符。

那么这其实与String的构造没有本质的区别,事实上也是如此,一个字符流实质所作的工做依旧是把一种编码的byte[]转换成UTF-16编码的byte[]。

而那些须要两个char才能表示的增补字符,如前面提到的音乐符,事实上你要read两次。因此字符流这种抽象仍是要打个折扣的,准确地讲是char流,而非真正的抽象的字符。

关于String从哪里来的话题,就讲到这里,下一篇再继续探讨它到哪里去的问题

相关文章
相关标签/搜索