引言
周五去面試又被面試的一個問題問啞巴了
面試官:String s = new String("xxx");創(chuàng)建了幾個對象?
我:兩個?
面試官:哪兩個?
我:。。。(啞巴了)
在這之前,我只知道是兩個,至于為啥是兩個,沒有了解過。
分析
// 在常量池中
String str1 = "abc";
// 在堆上
String str2 = new String("abc");
當(dāng)直接賦值時,字符串“abc”會被存儲在常量池中,只有1份,此時的賦值操作等于是創(chuàng)建0個或1個對象。如果常量池中已經(jīng)存在了“abc”,那么不會再創(chuàng)建對象,直接將引用地址賦值給str1;如果常量池中沒有“abc”,那么創(chuàng)建一個對象,并將引用地址賦值給str1。
那么,通過new String("abc");的形式又是如何呢?答案是1個或2個。
當(dāng)JVM遇到上述代碼時,首先會檢索常量池中是否存在“abc”,如果不存在“abc”這個字符串,則會先在常量池中創(chuàng)建這個一個字符串。然后再執(zhí)行new操作,會在堆內(nèi)存中創(chuàng)建一個存儲“abc”的String對象,對象的引用地址賦值給str2。此過程創(chuàng)建了2個對象。
當(dāng)然,如果檢索常量池時發(fā)現(xiàn)已經(jīng)存在了對應(yīng)的字符串,那么只會在堆內(nèi)創(chuàng)建一個新的String對象,此過程只創(chuàng)建了1個對象。
在上述過程中檢查常量池是否有相同Unicode的字符串常量時,使用的方法便是String中的intern()方法。
public native String intern();
下面通過一個簡單的示意圖看一下String在內(nèi)存中的兩種存儲模式。

上面的示意圖我們可以看到在堆內(nèi)創(chuàng)建的String對象的char value[]屬性指向了常量池中的char value[]。
還是上面的示例,如果我們通過debug模式也能夠看到String的char value[]的引用地址。

圖中兩個String對象的value值的引用均為{char[3]@1355},也就是說,雖然是兩個對象,但它們的value值均指向常量池中的同一個地址。當(dāng)然,大家還可以拿一個復(fù)雜對象(Person)的字符串屬性(name)相同時的debug結(jié)果進行比對,結(jié)果是一樣的。
高級點的問法
如果面試官說程序的代碼只有下面一行,那么會創(chuàng)建幾個對象?
new String("abc");
答案是2個?
還真不一定。之所以單獨列出這個問題是想提醒大家一點:沒有直接的賦值操作(str="abc"),并不代表常量池中沒有“abc”這個字符串。也就是說衡量創(chuàng)建幾個對象、常量池中是否有對應(yīng)的字符串,不僅僅由你是否創(chuàng)建決定,還要看程序啟動時其他類中是否包含該字符串。
更加高級點的問法
以下實例我們暫且不考慮常量池中是否已經(jīng)存在對應(yīng)字符串的問題,假設(shè)都不存在對應(yīng)的字符串。
以下代碼會創(chuàng)建幾個對象:
String str = "abc" + "def";
上面的問題涉及到字符串常量重載“+”的問題,當(dāng)一個字符串由多個字符串常量拼接成一個字符串時,它自己也肯定是字符串常量。字符串常量的“+”號連接Java虛擬機會在程序編譯期將其優(yōu)化為連接后的值。
就上面的示例而言,在編譯時已經(jīng)被合并成“abcdef”字符串,因此,只會創(chuàng)建1個對象。并沒有創(chuàng)建臨時字符串對象abc和def,這樣減輕了垃圾收集器的壓力。
我們通過javap查看class文件可以看到如下內(nèi)容。

針對上面的問題,我們再次升級一下,下面的代碼會創(chuàng)建幾個對象?
String str = "abc" + new String("def");
創(chuàng)建了4個,5個,還是6個對象?
4個對象的說法:常量池中分別有“abc”和“def”,堆中對象new String("def")和“abcdef”。
這種說法對嗎?不完全對,如果說上述代碼創(chuàng)建了幾個字符串對象,那么可以說是正確的。但上述的代碼Java虛擬機在編譯的時候同樣會優(yōu)化,會創(chuàng)建一個StringBuilder來進行字符串的拼接,實際效果類似:
String s = new String("def");
new StringBuilder().append("abc").append(s).toString();
很顯然,多出了一個StringBuilder對象,那就應(yīng)該是5個對象。
那么創(chuàng)建6個對象是怎么回事呢?有同學(xué)可能會想了,StringBuilder最后toString()之后的“abcdef”難道不在常量池存一份嗎?
這個還真沒有存,我們來看一下這段代碼:
@Test
public void testString3() {
String s1 = "abc";
String s2 = new String("def");
String s3 = s1 + s2;
String s4 = "abcdef";
System.out.println(s3==s4); // false
}

很明顯,s3和s4的值相同,但value值的地址并不相同。即便是將s3和s4的位置調(diào)整一下,效果也一樣。s4很明確是存在于常量池中,那么s3對應(yīng)的值存儲在哪里呢?很顯然是在堆對象中。
我們來看一下StringBuilder的toString()方法是如何將拼接的結(jié)果轉(zhuǎn)化為字符串的:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
很顯然,在toString方法中又新創(chuàng)建了一個String對象,而該String對象傳遞數(shù)組的構(gòu)造方法來創(chuàng)建的:
public String(char value[], int offset, int count)
也就是說,String對象的value值直接指向了一個已經(jīng)存在的數(shù)組,而并沒有指向常量池中的字符串。
因此,上面的準確回答應(yīng)該是創(chuàng)建了4個字符串對象和1個StringBuilder對象。
拓展
面試官:StringBuilder和StringBuffer的區(qū)別在哪?
我:StringBuilder不是線程安全的,StringBuffer是線程安全的
面試官:那StringBuilder不安全的點在哪兒?
我:。。。(啞巴了)那么,你知道嗎?可以打在評論區(qū)交流