String
類如何存儲文本,內部存儲和常量池如何工作。 這里要理解的要點是String
Java對象與其內容– private value
字段下的char[]
之間的區別。 String
基本上是char[]
數組的包裝器,將其封裝并使其無法修改,因此String
可以保持不變。 另外, String
類還記住該數組的實際部分(請參閱下文)。 這一切都意味著您可以擁有兩個指向相同char[]
不同String
對象(相當輕量)。
我會告訴你一些例子,連同hashCode()
的每個String
和hashCode()
內部的char[] value
字段(我將其稱之為文本字符串從區分)。 最后,我將顯示javap -c -verbose
輸出以及測試類的常量池。 請不要將類常量池與字符串文字池混淆。 它們并不完全相同。 另請參見了解常量池的javap輸出 。
先決條件
為了進行測試,我創建了一個實用程序方法來破壞String
封裝:
private int showInternalCharArrayHashCode(String s) {final Field value = String.class.getDeclaredField("value");value.setAccessible(true);return value.get(s).hashCode();
}
它將打印char[] value
hashCode()
,有效地幫助我們了解此特定String
是否指向相同的char[]
文本。
一個類中的兩個字符串文字
讓我們從最簡單的示例開始。
Java代碼
String one = "abc";
String two = "abc";
順便說一句,如果您只寫"ab" + "c"
,則Java編譯器將在編譯時執行串聯,并且生成的代碼將完全相同。 僅當在編譯時知道所有字符串時,此方法才有效。
類常量池
每個類都有自己的常量池 -常量值列表,如果它們在源代碼中多次出現,則可以重用。 它包括常見的字符串,數字,方法名稱等。 這是上面示例中常量池的內容:
const #2 = String #38; // abc
//...
const #38 = Asciz abc;
需要注意的重要事項是String
常量對象( #2
)和字符串指向的Unicode編碼文本"abc"
( #38
)之間的區別。
字節碼
這是生成的字節碼。 請注意, one
引用和two
引用都分配有指向"abc"
字符串的相同#2
常量:
ldc #2; //String abc
astore_1 //one
ldc #2; //String abc
astore_2 //two
輸出量
對于每個示例,我將打印以下值:
System.out.println("one.value: " + showInternalCharArrayHashCode(one));
System.out.println("two.value: " + showInternalCharArrayHashCode(two));
System.out.println("one" + System.identityHashCode(one));
System.out.println("two" + System.identityHashCode(two));
這兩對相等并不奇怪:
one.value: 23583040
two.value: 23583040
one: 8918249
two: 8918249
這意味著不僅兩個對象都指向相同的char[]
(下面的相同文本),所以equals()
測試將通過。 但更重要的是, one
和two
是完全相同的引用! 因此, one == two
也是正確的。 顯然,如果one
和two
指向同一個對象,則one.value
和two.value
必須相等。
文字和new String()
?
Java代碼
現在,我們都在等待該示例–一個字符串文字和一個使用相同文字的新String
。 這將如何運作?
String one = "abc";
String two = new String("abc");
在源代碼中兩次使用了"abc"
常量這一事實應該給您一些提示……
類常量池與上面相同。
字節碼
ldc #2; //String abc
astore_1 //onenew #3; //class java/lang/String
dup
ldc #2; //String abc
invokespecial #4; //Method java/lang/String."<init>":(Ljava/lang/String;)V
astore_2 //two
仔細地看! 第一個對象的創建方法與上面相同,不足為奇。 它只需要從常量池中常量引用已經創建的String
( #2
)。 但是,第二個對象是通過常規構造函數調用創建的。 但! 第一個String
作為參數傳遞。 可以將其反編譯為:
String two = new String(one);
輸出量
輸出有點令人驚訝。 第二對表示對String
對象的引用是可以理解的-我們創建了兩個String
對象-一個在常量池中為我們創建,第二個是為two
手動創建的。 但是,為什么第一對建議兩個String
對象都指向同一個char[] value
數組呢?
one.value: 41771
two.value: 41771
one: 8388097
two: 16585653
當您查看String(String)
構造函數的工作原理時,這一點變得很清楚(此處已大大簡化):
public String(String original) {this.offset = original.offset;this.count = original.count;this.value = original.value;
}
看到? 在基于現有對象創建新的String
對象時,它會重用 char[] value
。 String
是不可變的,不需要復制已知永遠不會修改的數據結構。 而且,由于new String(someString)
創建了現有字符串的精確副本,并且字符串是不可變的,因此顯然沒有理由同時存在兩者。
我認為這是一些誤解的線索:即使您有兩個String
對象,它們仍可能指向相同的內容。 如您所見, String
對象本身很小。
運行時修改和intern()
?
Java代碼
假設您最初使用了兩個不同的字符串,但是在進行一些修改之后,它們都是相同的:
String one = "abc";
String two = "?abc".substring(1); //also two = "abc"
Java編譯器(至少是我的)不夠聰明,無法在編譯時執行此類操作,請看一下:
類常量池
突然我們以指向兩個不同常量文本的兩個常量字符串結尾:
const #2 = String #44; // abc
const #3 = String #45; // ?abc
const #44 = Asciz abc;
const #45 = Asciz ?abc;
字節碼
ldc #2; //String abc
astore_1 //oneldc #3; //String ?abc
iconst_1
invokevirtual #4; //Method String.substring:(I)Ljava/lang/String;
astore_2 //two
拳頭弦照常構造。 通過首先加載常量"?abc"
字符串,然后在其上調用substring(1)
來創建第二個。
輸出量
這里不足為奇–我們有兩個不同的字符串,指向內存中兩個不同的char[]
文本:
one.value: 27379847
two.value: 7615385
one: 8388097
two: 16585653
好吧,文本并沒有真正的不同 , equals()
方法仍然會產生true
。 我們有兩個不必要的相同文本副本。
現在我們應該進行兩次練習。 首先,嘗試運行:
two = two.intern();
在打印哈希碼之前。 one
和two
不僅指向同一文本,而且它們是相同的參考!
one.value: 11108810
two.value: 11108810
one: 15184449
two: 15184449
這意味著one.equals(two)
和one == two
測試都將通過。 我們還節省了一些內存,因為"abc"
文本在內存中僅出現一次(第二個副本將被垃圾回收)。
第二個練習略有不同,請查看以下內容:
String one = "abc";
String two = "abc".substring(1);
顯然one
和two
是兩個不同的對象,指向兩個不同的文本。 但是輸出如何表明它們都指向同一個char[]
數組?!
one.value: 11108810
two.value: 8918249
one: 23583040
two: 23583040
我將答案留給你。 它會教您substring()
工作原理,這種方法的優點是什么以及何時會導致大麻煩 。
得到教訓
-
String
對象本身相當便宜。 它指向的文本占用了大部分內存 -
String
只是char[]
的薄包裝,以保持不變性 -
new String("abc")
作為內部文本表示被重用是不是真的那么貴。 但是還是要避免這樣的構造。 - 從編譯時已知的常量值連接
String
時,連接由編譯器而不是由JVM完成 -
substring()
有點棘手,但最重要的是,就使用的內存和運行時間而言,它都很便宜(在兩種情況下均保持不變)
參考:來自Java和社區博客的JCG合作伙伴 Tomasz Nurkiewicz的字符串內存內部結構 。
翻譯自: https://www.javacodegeeks.com/2012/07/string-memory-internals.html