在 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_1
3 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 dup
10 invokespecial #5 <java/lang/StringBuilder.<init>>
13 aload_1
14 invokevirtual #6 <java/lang/StringBuilder.append>
17 aload_2
18 invokevirtual #6 <java/lang/StringBuilder.append>
21 invokevirtual #7 <java/lang/StringBuilder.toString>
24 astore_3
25 return
可以發現,Java在進行字串相加的時候,底層使用的是StringBuilder,程式碼被最佳化成如下所示:
String c = new StringBuilder().append("ab").append("cd").toString();
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.8
String str1 = "abc";
String str2 = new String("abc");
String str3= str2.intern();
System.out.println(str1==str2); // false
System.out.println(str2==str3); // false
System.out.println(str1==str3); // true
// 執行環境 JDK1.8
String 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);
由於String的值是不可變的,這就導致每次對String的操作都會生成新的String物件,這樣不僅效率低下,而且大量浪費有限的記憶體空間。
和 String 類不同的是,StringBuffer 和 StringBuilder 類的物件能夠被多次的修改,並且不產生新的物件。
StringBuilder 類在 Java 5 中被提出,它和 StringBuffer 之間的最大不同在於 StringBuilder 的方法不是執行緒安全的(不能同步訪問)。
由於 StringBuilder 相較於 StringBuffer 有速度優勢,所以多數情況下建議使用 StringBuilder 類。然而在應用程式要求執行緒安全的情況下,則必須使用 StringBuffer 類。
本作品採用《CC 協議》,轉載必須註明作者和本文連結