String s = new String(“xxx“);創(chuàng)建了幾個對象?

引言

周五去面試又被面試的一個問題問啞巴了

面試官: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)存中的兩種存儲模式。


image

上面的示意圖我們可以看到在堆內(nèi)創(chuàng)建的String對象的char value[]屬性指向了常量池中的char value[]。

還是上面的示例,如果我們通過debug模式也能夠看到String的char value[]的引用地址。

image

圖中兩個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)容。

da2e53e424750afd4ec63dd5acbc6770.jpeg

針對上面的問題,我們再次升級一下,下面的代碼會創(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
}
adc020264de49d273648164d40fed2d0.jpeg

很明顯,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ū)交流

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容