Java面試題系列:將面試題中比較經典和核心的內容寫成系列文章持續在公眾號更新,可鞏固基礎知識,可梳理底層原理,歡迎大家持續關注【程序新視界】。本篇為面試題系列第2篇。
常見面試問題
下面代碼中創建了幾個對象?
new String("abc");
答案眾說紛紜,有說創建了1個對象,也有說創建了2個對象。答案對,也不對,關鍵是要學到問題底層的原理。
底層原理分析
在上篇文章《面試題系列第1篇:說說==和equals的區別?你的回答可能是錯誤的》中我們已經提到,String的兩種初始化形式是有本質區別的。
String str1 = "abc"; // 在常量池中
String str2 = new String("abc"); // 在堆上
當直接賦值時,字符串“abc”會被存儲在常量池中,只有1份,此時的賦值操作等於是創建0個或1個對象。如果常量池中已經存在了“abc”,那么不會再創建對象,直接將引用賦值給str1;如果常量池中沒有“abc”,那么創建一個對象,並將引用賦值給str1。
那么,通過new String("abc");的形式又是如何呢?答案是1個或2個。
當JVM遇到上述代碼時,會先檢索常量池中是否存在“abc”,如果不存在“abc”這個字符串,則會先在常量池中創建這個一個字符串。然后再執行new操作,會在堆內存中創建一個存儲“abc”的String對象,對象的引用賦值給str2。此過程創建了2個對象。
當然,如果檢索常量池時發現已經存在了對應的字符串,那么只會在堆內創建一個新的String對象,此過程只創建了1個對象。
在上述過程中檢查常量池是否有相同Unicode的字符串常量時,使用的方法便是String中的intern()方法。
public native String intern();
下面通過一個簡單的示意圖看一下String在內存中的兩種存儲模式。

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

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

很明顯,字節碼中只有拼接好的abcdef。
針對上面的問題,我們再次升級一下,下面的代碼會創建幾個對象?
String str = "abc" + new String("def");
創建了4個,5個,還是6個對象?
4個對象的說法:常量池中分別有“abc”和“def”,堆中對象new String("def")和“abcdef”。
這種說法對嗎?不完全對,如果說上述代碼創建了幾個字符串對象,那么可以說是正確的。但上述的代碼Java虛擬機在編譯的時候同樣會優化,會創建一個StringBuilder來進行字符串的拼接,實際效果類似:
String s = new String("def");
new StringBuilder().append("abc").append(s).toString();
很顯然,多出了一個StringBuilder對象,那就應該是5個對象。
那么創建6個對象是怎么回事呢?有同學可能會想了,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
}
按照上面的分析,如果s1+s2的結果在常量池中存了一份,那么s3中的value引用應該和s4中value的引用是一樣的才對。下面我們看一下debug的效果。

很明顯,s3和s4的值相同,但value值的地址並不相同。即便是將s3和s4的位置調整一下,效果也一樣。s4很明確是存在於常量池中,那么s3對應的值存儲在哪里呢?很顯然是在堆對象中。
我們來看一下StringBuilder的toString()方法是如何將拼接的結果轉化為字符串的:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
很顯然,在toString方法中又新創建了一個String對象,而該String對象傳遞數組的構造方法來創建的:
public String(char value[], int offset, int count)
也就是說,String對象的value值直接指向了一個已經存在的數組,而並沒有指向常量池中的字符串。
因此,上面的准確回答應該是創建了4個字符串對象和1個StringBuilder對象。
小結
我們通過一行創建字符串的代碼逐步分析String對象的整個構建及拼接過程,了解了底層實現原理。是不是很有意思?當你掌握了這些底層基本知識,即便面試題的形式如何變化,你必定能一眼識破真相。
下篇文章,(讀者提議)我們來講講Integer的比較的底層邏輯,歡迎持續關注。

