文本在內存中的編碼(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從哪裏來的話題,就講到這裏,下一篇再繼續探討它到哪裏去的問題

相關文章
相關標籤/搜索