文本在內存中的編碼(1)——亂碼探源(4)

讓咱們從一個故事開始提及。話說北大是頗有哲學傳統的,當你準備踏進北大校門時,連門衛都會連問你三個終極哲學問題:java

你是誰?你從哪裏來?你要到哪裏去?數組

那麼這與咱們的問題又有何關係呢?我以爲理解內存中的編碼的關鍵在於理解String類型,所以咱們也來探討一下String的前世此生:String是誰(什麼)?String從哪裏來?String到哪裏去?編碼

當咱們可以清晰地回答這三個終極問題時,對文本在內存中的編碼也算理解得差很少了。spa

注:文中將用Java平臺爲例來探討這些問題。.net

String是什麼?

要回答這個問題,源碼固然是最好的參考。代理

字符序列(CharSequence)

若是看String類型的聲明:code

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
    private final char value[];
	
	// ...
}

能夠看到它實現了所謂的CharSequence接口,因此它是一個char序列,內部實質是一個char數組。blog

也即上述代碼中的」char value[]「,(也許你以爲」char[] value「的寫法更習慣一些,二者是等價的)接口

若是再看String的length方法,事實就更清楚了,實際上取的是char數組的長度:內存

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    
    private final char value[];
	
	// ...
	
	public int length() {
        return value.length;
    }
}

到如今爲止,能夠這樣看String:

image

char

如今新的問題是:什麼是char呢?Java中的char是一種基礎的數據類型,用於表示字符,長度爲16位。

能夠把char看做是無符號的16位短整型。

一個byte是8位,那麼一個char就至關於兩個byte了,因此也能夠把String視做爲byte數組。(這是毫無疑問的,事實上整個內存就是一個大大的byte數組)

image

那麼一個String在內存中老是佔據偶數個字節,具體地說是佔用length()×2個字節。

固然,單獨地拿出String中的一個byte出來是沒有意義的,你老是須要兩個兩個一塊兒地操做它們,但這並不妨礙咱們把它當作byte數組,它能夠說是有點特殊的byte數組。

容量問題

顯然,因爲char是16位固定長度,它的容量老是有限的,上限是216=65536,能表示0-65535(0x0000-0xFFFF)。

即使滿打滿算它也只能表示6萬多個不一樣字符而已,另外一方面,Unicode規劃的字符空間高達100萬以上,最新版本已經定義的字符也超過了10萬。

規劃的碼點範圍具體爲U+0000-U+10FFFF。char能表示前面的U+0000-U+FFFF,對於U+10000-U+10FFFF則無能爲力。

因此一個顯然的事實就是單個的char沒法表示全部的字符。好比下面的這個音樂符:

image

它的十進制的碼點爲119070,已經遠超過65535,確定不能放到char裏,實驗也可證明這一點:

image

蘿蔔太多,坑位不夠,怎麼辦呢?

解決方案

一種方式天然是對char進行擴展,好比弄成32位的,不過這樣會形成很大的內存浪費。

另外一種方式就是對後面的那些字符使用兩個char來表示,也便是所謂的代理對方式,須要注意的是不能跟單個char表示的字符衝突。

Java採用的就是這樣方式,其後果是使得char沒法與」抽象的字符「這一律念劃上等號,一一對應的關係被打破了。

咱們具體來看下是怎麼作的。

首先char有256×256=65536個空間:

image

經常使用的字符均可以在這個空間內表示,包括絕大多數的漢字。

好比「a」分配到的編碼是「0061」,而「你」分配到的是「4F60」。

那麼一個字符串,好比「a你」就有兩個char,內存中佔4個字節:

image

而後對於那個音樂符而言,它的碼點爲U+1D11E,有5位,固然不能簡單直接地分紅0001和D11E兩部分。

這樣會與U+0001和U+D11E衝突。

因此首先要保留一些char,它們單個而言不表明任何抽象的字符,具體地說保留了D800-DFFF共2048個位置:

image

而後橫豎弄成一張表,可以造成100多萬種組合(1K=1024):

image

在這種表示方式下,U+1D11E對應的是D834和DD1E兩個char:

image

具體的轉換方式可見:字符集與編碼(四)——Unicode

咱們就用這兩個char一塊兒來表示這個字符。這個字符沒法放到單個的char中,但它能夠放到String中,由於String是char數組。

image

綜述

以上其實就是UTF-16的編碼方式。你常常能聽到這樣的說法,好比:Java平臺在內存中使用Unicode編碼。這其實說得很籠統,讓咱們把它說得更具體一些:

Java中的String類型在內存中使用UTF-16編碼。

String以char做爲它的構成單元,這樣一個16位的char也稱爲UTF-16編碼的一個代碼單元(code unit)

一般,一個char對應一個抽象的字符,但也可能須要兩個char構成一個所謂的代理對才能表示一個抽象的字符。

因此這也致使了一些尷尬的狀況,對於一些抽象字符它的長度是2.

image

這與咱們的直覺不符,又以下面的狀況:

image

兩個抽象字符,內部爲3個char,因此長度是3,在內存中則佔據了6個字節。你可能不是很喜歡這樣的String類型,但事實就是這樣。

另可見字符集與編碼(五)——代碼單元及length方法

其它選擇

天然,你有不少的選擇。若是你本身去實現一個語言平臺,你固然也能夠選擇一個其它的編碼,好比UTF-8,甚至是UTF-32做爲String的內部編碼。

考慮到UTF-32用四字節表示一個字符,一般一個int類型也是4字節,那麼這種方式幾乎能夠認爲是用一個int數組來保存字符。

明白了String是什麼以後,在下一篇再繼續探討String從哪裏來的問題。咱們將深刻探討String的構造,字節流和字符流以及編碼間的轉換等問題。

相關文章
相關標籤/搜索