JDK9為何要將String的底層實現由char[]改成了byte[]

語言: CN / TW / HK

關注公眾號:IT老哥,每天閲讀一篇乾貨技術文章,一年後你會發現一個不一樣的自己。

如果你不是 Java8 的釘子户,你應該早就發現了:String 類的源碼已經由 char[] 優化為了 byte[] 來存儲字符串內容,為什麼要這樣做呢?

開門見山地説,從 char[] 到 byte[],最主要的目的是為了節省字符串佔用的內存。內存佔用減少帶來的另外一個好處,就是 GC 次數也會減少。

一、為什麼要優化 String 節省內存空間

我們使用 jmap -histo:live pid | head -n 10 命令就可以查看到堆內對象示例的統計信息、查看 ClassLoader 的信息以及 finalizer 隊列。

以我正在運行着的編程喵喵項目實例(基於 Java 8)來説,結果是這樣的。

圖片

其中 String 對象有 17638 個,佔用了 423312 個字節的內存,排在第三位。

由於 Java 8 的 String 內部實現仍然是 char[],所以我們可以看到內存佔用排在第 1 位的就是 char 數組。

char[] 對象有 17673 個,佔用了 1621352 個字節的內存,排在第一位。

那也就是説優化 String 節省內存空間是非常有必要的,如果是去優化一個使用頻率沒有 String 這麼高的類庫,就顯得非常的雞肋。

二、byte[] 為什麼就能節省內存空間呢?

眾所周知,char 類型的數據在 JVM 中是佔用兩個字節的,並且使用的是 UTF-8 編碼,其值範圍在 '\u0000'(0)和 '\uffff'(65,535)(包含)之間。

也就是説,使用 char[] 來表示 String 就導致了即使 String 中的字符只用一個字節就能表示,也得佔用兩個字節。

而實際開發中,單字節的字符使用頻率仍然要高於雙字節的。

當然了,僅僅將 char[] 優化為 byte[] 是不夠的,還要配合 Latin-1 的編碼方式,該編碼方式是用單個字節來表示字符的,這樣就比 UTF-8 編碼節省了更多的空間。

換句話説,對於:

``` String name = "jack";  

```

這樣的,使用 Latin-1 編碼,佔用 4 個字節就夠了。

但對於:

``` String name = "小二";  

```

這種,木的辦法,只能使用 UTF16 來編碼。

針對 JDK 9 的 String 源碼裏,為了區別編碼方式,追加了一個 coder 字段來區分。

``` /*    * The identifier of the encoding used to encode the bytes in    * {@code value}. The supported values in this implementation are    *    * LATIN1    * UTF16    *    * @implNote This field is trusted by the VM, and is a subject to    * constant folding if String instance is constant. Overwriting this    * field after construction will cause problems.    /   private final byte coder;  

```

Java 會根據字符串的內容自動設置為相應的編碼,要麼 Latin-1 要麼 UTF16。

也就是説,從 char[] 到 byte[]中文是兩個字節,純英文是一個字節,在此之前呢,中文是兩個字節,英文也是兩個字節

三、為什麼用UTF-16而不用UTF-8呢?

在 UTF-8 中,0-127 號的字符用 1 個字節來表示,使用和 ASCII 相同的編碼。只有 128 號及以上的字符才用 2 個、3 個或者 4 個字節來表示。

  • 如果只有一個字節,那麼最高的比特位為 0;

  • 如果有多個字節,那麼第一個字節從最高位開始,連續有幾個比特位的值為 1,就使用幾個字節編碼,剩下的字節均以 10 開頭。

具體的表現形式為:

  • 0xxxxxxx:一個字節;

  • 110xxxxx 10xxxxxx:兩個字節編碼形式(開始兩個 1);- 1110xxxx 10xxxxxx 10xxxxxx:三字節編碼形式(開始三個 1);

  • 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:四字節編碼形式(開始四個 1)。

也就是説,UTF-8 是變長的,那對於 String 這種有隨機訪問方法的類來説,就很不方便。所謂的隨機訪問,就是charAt、subString這種方法,隨便指定一個數字,String要能給出結果。如果字符串中的每個字符佔用的內存是不定長的,那麼進行隨機訪問的時候,就需要從頭開始數每個字符的長度,才能找到你想要的字符。

那有小夥伴可能會問,UTF-16也是變長的呢?一個字符還可能佔用 4 個字節呢?

的確,UTF-16 使用 2 個或者 4 個字節來存儲字符。

  • 對於 Unicode 編號範圍在 0 ~ FFFF 之間的字符,UTF-16 使用兩個字節存儲。

  • 對於 Unicode 編號範圍在 10000 ~ 10FFFF 之間的字符,UTF-16 使用四個字節存儲,具體來説就是:將字符編號的所有比特位分成兩部分,較高的一些比特位用一個值介於 D800~DBFF 之間的雙字節存儲,較低的一些比特位(剩下的比特位)用一個值介於 DC00~DFFF 之間的雙字節存儲。

但是在 Java 中,一個字符(char)就是 2 個字節,佔 4 個字節的字符,在 Java 裏也是用兩個 char 來存儲的,而String的各種操作,都是以Java的字符(char)為單位的,charAt是取得第幾個char,subString取的也是第幾個到第幾個char組成的子串,甚至length返回的都是char的個數。

所以UTF-16在Java的世界裏,就可以視為一個定長的編碼。

關注公眾號:IT老哥,每天閲讀一篇乾貨技術文章,一年後你會發現一個不一樣的自己。