洞察 String字串

2020-12-28 18:00:06
欄目介紹洞察 String字串

推薦(免費):

實現原理

在 Java6 以及之前的版本中,String 物件是對 char 陣列進行了封裝實現的物件,主要有四個成員變數:char 陣列、偏移量 offset、字元數量 count、雜湊值 hash。

從 Java7 版本開始到 Java8 版本,String 類中不再有 offset 和 count 兩個變數了。這樣的好處是 String 物件佔用的記憶體稍微少了些。

從 Java9 版本開始,將 char[]欄位改為了 byte[]欄位,又維護了一個新的屬性 coder,它是一個編碼格式的標識。

一個 char 字元佔 16 位,2 個位元組。這個情況下,儲存單位元組編碼內的字元(佔一個位元組的字元)就顯得非常浪費。JDK1.9 的 String 類為了節約記憶體空間,於是使用了佔 8 位,1 個位元組的 byte 陣列來存放字串。

而新屬性 coder 的作用是,在計算字串長度或者使用 indexOf()函數時,我們需要根據這個欄位,判斷如何計算字串長度。coder 屬性預設有 0 和 1 兩個值,0 代表 Latin-1(單位元組編碼),1 代表 UTF-16。如果 String 判斷字串只包含了 Latin-1,則 coder 屬性值為 0,反之則為 1。

不可變

檢視String類的程式碼可以發現,String類被final關鍵字修飾,因此這個類不能被繼承,並且String類裡面的變數char 陣列也被 final 修飾了,因此String物件不能被修改。

String物件不可變主要有如下幾個優點:

第一,保證 String 物件的安全性。假設 String 物件是可變的,那麼 String 物件將可能被惡意修改。

第二,保證 hash 屬性值不會頻繁變更,確保了唯一性,使得類似 HashMap 容器才能實現相應的 key-value 快取功能。

第三,可以實現字串常數池。

在 Java 中,通常有兩種建立字串物件的方式:

第一種是通過字串常數的方式建立,如String str = "abc"

第二種是字串變數通過new 形式的建立,如 String str = new String("abc")

當程式碼中使用第一種方式建立字串物件時,在編譯類檔案時,」abc」常數字串將會放入到常數結構中,在類載入時,「abc」將會在常數池中建立;然後,str將參照常數池中的字串物件。這種方式可以減少同一個值的字串物件的重複建立,節約記憶體。

String str = new String("abc") 這種方式,首先在編譯類檔案時,」abc」常數字串將會放入到常數結構中,在類載入時,「abc」將會在常數池中建立;其次,在呼叫new時,JVM 命令將會呼叫 String 的建構函式,String 物件中的 char 陣列將會參照常數池中」abc」字串的char 陣列,在堆記憶體中建立一個 String 物件;最後,str 將參照 String 物件,String物件的參照跟常數池中」abc」字串的參照是不一樣的。

物件與參照:物件的內容儲存在記憶體中,作業系統通過記憶體地址來找到儲存的內容,參照就是指記憶體的地址。

比如:String str = new String("abc"),變數str指向的是String物件的儲存地址,也就是說 str 並不是物件,而只是一個物件參照。

字串拼接

常數相加

String str = "ab" + "cd" + "ef";

檢視編譯後的位元組碼

0 ldc #2 <abcdef>2 astore_13 return

可以發現編譯器將程式碼優化成如下所示

String str= "abcdef";

變數相加

String a = "ab";String b = "cd";String c = a + b;

檢視編譯後的位元組碼

 0 ldc #2 <ab>
 2 astore_1 3 ldc #3 <cd>
 5 astore_2 6 new #4 <java/lang/StringBuilder>
 9 dup10 invokespecial #5 <java/lang/StringBuilder.<init>>13 aload_114 invokevirtual #6 <java/lang/StringBuilder.append>17 aload_218 invokevirtual #6 <java/lang/StringBuilder.append>21 invokevirtual #7 <java/lang/StringBuilder.toString>24 astore_325 return

可以發現,Java在進行字串相加的時候,底層使用的是StringBuilder,程式碼被優化成如下所示:

String c = new StringBuilder().append("ab").append("cd").toString();

String.intern

String a = new String("abc").intern();String b = new String("abc").intern();System.out.print(a == b);

輸出結果:

true

在字串常數中,預設會將物件放入常數池。例如:String a = "123"

在字串變數中,物件是會建立在堆記憶體中,同時也會在常數池中建立一個字串物件,String 物件中的 char 陣列將會參照常數池中的 char 陣列,並返回堆記憶體物件參照。例如:String b = new String("abc")

如果呼叫 intern 方法,會去檢視字串常數池中是否有等於該物件的字串的參照,如果沒有,在 JDK1.6 版本中會複製堆中的字串到常數池中,並返回該字串參照,堆記憶體中原有的字串由於沒有參照指向它,將會通過垃圾回收器回收。

在 JDK1.7 版本以後,由於常數池已經合併到了堆中,所以不會再複製具體字串了,只是會把首次遇到的字串的參照新增到常數池中;如果有,就返回常數池中的字串參照。

下面開始分析上面的程式碼塊:

在一開始字串」abc」會在載入類時,在常數池中建立一個字串物件。

建立 a 變數時,呼叫 new Sting() 會在堆記憶體中建立一個 String 物件,String 物件中的 char 陣列將會參照常數池中字串。在呼叫 intern 方法之後,會去常數池中查詢是否有等於該字串物件的參照,有就返回常數池中的字串參照。

建立 b 變數時,呼叫 new Sting() 會在堆記憶體中建立一個 String 物件,String 物件中的 char 陣列將會參照常數池中字串。在呼叫 intern 方法之後,會去常數池中查詢是否有等於該字串物件的參照,有就返回常數池中的字串參照。

而在堆記憶體中的兩個String物件,由於沒有參照指向它,將會被垃圾回收。所以 a 和 b 參照的是同一個物件。

如果在執行時,建立字串物件,將會直接在堆記憶體中建立,不會在常數池中建立。所以動態建立的字串物件,呼叫 intern 方法,在 JDK1.6 版本中會去常數池中建立執行時常數以及返回字串參照,在 JDK1.7 版本之後,會將堆中的字串常數的參照放入到常數池中,當其它堆中的字串物件通過 intern 方法獲取字串物件參照時,則會去常數池中判斷是否有相同值的字串的參照,此時有,則返回該常數池中字串參照,跟之前的字串指向同一地址的字串物件。

以一張圖來總結 String 字串的建立分配記憶體地址情況:

使用 intern 方法需要注意的一點是,一定要結合實際場景。因為常數池的實現是類似於一個 HashTable 的實現方式,HashTable 儲存的資料越大,遍歷的時間複雜度就會增加。如果資料過大,會增加整個字串常數池的負擔。

判斷字串是否相等

// 執行環境 JDK1.8String str1 = "abc";String str2 = new String("abc");String str3= str2.intern();System.out.println(str1==str2); // falseSystem.out.println(str2==str3); // falseSystem.out.println(str1==str3); // true
// 執行環境 JDK1.8String s1 = new String("1") + new String("1");s1.intern();String s2 = "11";System.out.println(s1 == s2); // true , 如果不執行1.intern(),則返回false

String s1 = new String("1") + new String("1")會在堆中組合一個新的字串物件"11",在s1.intern()之後,由於常數池中沒有該字串的參照,所以常數池中生成一個堆中字串"11"的參照,此時String s2 = "11"返回的是堆字串"11"的參照,所以s1==s2

在JDK1.7版本以及之後的版本執行以下程式碼,你會發現結果為true,在JDK1.6版本執行的結果卻為false:

String s1 = new String("1") + new String("1");System.out.println( s1.intern()==s1);

StringBuilder與StringBuffer

由於String的值是不可變的,這就導致每次對String的操作都會生成新的String物件,這樣不僅效率低下,而且大量浪費有限的記憶體空間。

和 String 類不同的是,StringBuffer 和 StringBuilder 類的物件能夠被多次的修改,並且不產生新的物件

StringBuilder 類在 Java 5 中被提出,它和 StringBuffer 之間的最大不同在於 StringBuilder 的方法不是執行緒安全的(不能同步存取)。

由於 StringBuilder 相較於 StringBuffer 有速度優勢,所以多數情況下建議使用 StringBuilder 類。然而在應用程式要求執行緒安全的情況下,則必須使用 StringBuffer 類。

以上就是洞察 String字串的詳細內容,更多請關注TW511.COM其它相關文章!