一秤茅、字符串問題
字符串在我們平時(shí)的編碼工作中其實(shí)用的非常多璧亚,并且用起來也比較簡單砰识,所以很少有人對其做特別深入的研究。倒是面試或者筆試的時(shí)候块茁,往往會(huì)涉及比較深入和難度大一點(diǎn)的問題齿坷。我在招聘的時(shí)候也偶爾會(huì)問應(yīng)聘者相關(guān)的問題,倒不是說一定要回答的特別正確和深入数焊,通常問這些問題的目的有兩個(gè)永淌,第一是考察對 JAVA 基礎(chǔ)知識(shí)的了解程度,第二是考察應(yīng)聘者對技術(shù)的態(tài)度佩耳。
我們看看以下程序會(huì)輸出什么結(jié)果遂蛀?如果你能正確的回答每一道題,并且清楚其原因干厚,那本文對你就沒什么太大的意義李滴。如果回答不正確或者不是很清楚其原理螃宙,那就仔細(xì)看看以下的分析,本文應(yīng)該能幫助你清楚的理解每段程序的結(jié)果及輸出該結(jié)果的深層次原因悬嗓。
代碼段一:
package com.paddx.test.string;
public class StringTest {
public static void main(String[] args) {
String str1 = "string";
String str2 = new String("string");
String str3 = str2.intern();
System.out.println(str1==str2);//#1
System.out.println(str1==str3);//#2
}
}
代碼段二:
package com.paddx.test.string;
public class StringTest01 {
public static void main(String[] args) {
String baseStr = "baseStr";
final String baseFinalStr = "baseStr";
String str1 = "baseStr01";
String str2 = "baseStr"+"01";
String str3 = baseStr + "01";
String str4 = baseFinalStr+"01";
String str5 = new String("baseStr01").intern();
System.out.println(str1 == str2);//#3
System.out.println(str1 == str3);//#4
System.out.println(str1 == str4);//#5
System.out.println(str1 == str5);//#6
}
}
代碼段三(1):
package com.paddx.test.string;<br>
public class InternTest {
public static void main(String[] args) {
String str2 = new String("str")+new String("01");
str2.intern();
String str1 = "str01";
System.out.println(str2==str1);//#7
}
}
代碼段三(2):
package com.paddx.test.string;
public class InternTest01 {
public static void main(String[] args) {
String str1 = "str01";
String str2 = new String("str")+new String("01");
str2.intern();
System.out.println(str2 == str1);//#8
}
}
為了方便描述,我對上述代碼的輸出結(jié)果由#1~#8進(jìn)行了編碼裕坊,下文中藍(lán)色字體部分即為結(jié)果包竹。
二、字符串深入分析
1籍凝、代碼段一分析
字符串不屬于基本類型周瞎,但是可以像基本類型一樣,直接通過字面量賦值饵蒂,當(dāng)然也可以通過new來生成一個(gè)字符串對象声诸。不過通過字面量賦值的方式和new的方式生成字符串有本質(zhì)的區(qū)別:
通過字面量賦值創(chuàng)建字符串時(shí),會(huì)優(yōu)先在常量池中查找是否已經(jīng)存在相同的字符串退盯,倘若已經(jīng)存在彼乌,棧中的引用直接指向該字符串;倘若不存在渊迁,則在常量池中生成一個(gè)字符串慰照,再將棧中的引用指向該字符串。而通過new的方式創(chuàng)建字符串時(shí)琉朽,就直接在堆中生成一個(gè)字符串的對象(備注毒租,JDK 7 以后,HotSpot 已將常量池從永久代轉(zhuǎn)移到了堆中箱叁。詳細(xì)信息可參考《JDK8內(nèi)存模型-消失的PermGen》一文)墅垮,棧中的引用指向該對象。對于堆中的字符串對象耕漱,可以通過 intern() 方法來將字符串添加的常量池中算色,并返回指向該常量的引用。
現(xiàn)在我們應(yīng)該能很清楚代碼段一的結(jié)果了:
結(jié)果 #1:因?yàn)閟tr1指向的是字符串中的常量螟够,str2是在堆中生成的對象剃允,所以str1==str2返回false。
結(jié)果 #2:str2調(diào)用intern方法齐鲤,會(huì)將str2中值(“string”)復(fù)制到常量池中斥废,但是常量池中已經(jīng)存在該字符串(即str1指向的字符串),所以直接返回該字符串的引用给郊,因此str1==str2返回true牡肉。
以下運(yùn)行代碼段一的代碼的結(jié)果:
** 2、代碼段二分析**
對于代碼段二的結(jié)果淆九,還是通過反編譯StringTest01.class文件比較容易理解:
常量池內(nèi)容(部分):
執(zhí)行指令(部分统锤,第二列#+序數(shù)對應(yīng)常量池中的項(xiàng)):
在解釋上述執(zhí)行過程之前毛俏,先了解兩條指令:
ldc:Push item from run-time constant pool,從常量池中加載指定項(xiàng)的引用到棧饲窿。
astore_<n>:Store reference into local variable煌寇,將引用賦值給第n個(gè)局部變量。
現(xiàn)在我們開始解釋代碼段二的執(zhí)行過程:
0: ldc #2:加載常量池中的第二項(xiàng)("baseStr")到棧中逾雄。
2: astore_1 :將1中的引用賦值給第一個(gè)局部變量阀溶,即String baseStr = "baseStr";
3: ldc #2:加載常量池中的第二項(xiàng)("baseStr")到棧中鸦泳。
5: astore_2 :將3中的引用賦值給第二個(gè)局部變量银锻,即 final String baseFinalStr="baseStr";
6: ldc #3:加載常量池中的第三項(xiàng)("baseStr01")到棧中做鹰。
8: astore_3 :將6中的引用賦值給第三個(gè)局部變量击纬,即String str1="baseStr01";
9: ldc #3:加載常量池中的第三項(xiàng)("baseStr01")到棧中。
11: astore 4:將9中的引用賦值給第四個(gè)局部變量:即String str2="baseStr01"钾麸;
結(jié)果#3:str1==str2 肯定會(huì)返回true更振,因?yàn)閟tr1和str2都指向常量池中的同一引用地址。所以其實(shí)在JAVA 1.6之后饭尝,常量字符串的“+”操作殃饿,編譯階段直接會(huì)合成為一個(gè)字符串。
13: new #4:生成StringBuilder的實(shí)例芋肠。
16: dup :復(fù)制13生成對象的引用并壓入棧中乎芳。
17: invokespecial #5:調(diào)用常量池中的第五項(xiàng),即StringBuilder.<init>方法帖池。
以上三條指令的作用是生成一個(gè)StringBuilder的對象奈惑。
20: aload_1 :加載第一個(gè)參數(shù)的值睡汹,即"baseStr"
21: invokevirtual #6 :調(diào)用StringBuilder對象的append方法肴甸。
24: ldc #7:加載常量池中的第七項(xiàng)("01")到棧中。
26: invokevirtual #6:調(diào)用StringBuilder.append方法囚巴。
29: invokevirtual #8:調(diào)用StringBuilder.toString方法原在。
32: astore 5:將29中的結(jié)果引用賦值改第五個(gè)局部變量,即對變量str3的賦值彤叉。
結(jié)果 #4:因?yàn)閟tr3實(shí)際上是stringBuilder.append()生成的結(jié)果庶柿,所以與str1不相等,結(jié)果返回false秽浇。
34: ldc #3:加載常量池中的第三項(xiàng)("baseStr01")到棧中浮庐。
36: astore 6:將34中的引用賦值給第六個(gè)局部變量,即str4="baseStr01";
結(jié)果 #5 :因?yàn)閟tr1和str4指向的都是常量池中的第三項(xiàng)柬焕,所以str1==str4返回true审残。這里我們還能發(fā)現(xiàn)一個(gè)現(xiàn)象梭域,對于final字段,編譯期直接進(jìn)行了常量替換搅轿,而對于非final字段則是在運(yùn)行期進(jìn)行賦值處理的病涨。
38: new #9:創(chuàng)建String對象
41: dup :復(fù)制引用并壓入棧中。
42: ldc #3:加載常量池中的第三項(xiàng)("baseStr01")到棧中璧坟。
44: invokespecial #10:調(diào)用String."<init>"方法既穆,并傳42步驟中的引用作為參數(shù)傳入該方法。
47: invokevirtual #11:調(diào)用String.intern方法沸柔。
從38到41的對應(yīng)的源碼就是new String("baseStr01").intern()循衰。
50: astore 7:將47步返回的結(jié)果賦值給變量7铲敛,即str5指向baseStr01在常量池中的位置褐澎。
結(jié)果 #6 :因?yàn)閟tr5和str1都指向的都是常量池中的同一個(gè)字符串,所以str1==str5返回true伐蒋。
運(yùn)行代碼段二工三,輸出結(jié)果如下:
** 3、代碼段三解析:**
對于代碼段三先鱼,在 JDK 1.6 和 JDK 1.7中的運(yùn)行結(jié)果不同俭正。我們先看一下運(yùn)行結(jié)果,然后再來解釋其原因:
JDK 1.6 下的運(yùn)行結(jié)果:
JDK 1.7 下的運(yùn)行結(jié)果:
根據(jù)對代碼段一的分析焙畔,應(yīng)該可以很簡單得出 JDK 1.6 的結(jié)果掸读,因?yàn)?str2 和 str1本來就是指向不同的位置,理應(yīng)返回false宏多。
比較奇怪的問題在于JDK 1.7后儿惫,對于第一種情況返回true,但是調(diào)換了一下位置返回的結(jié)果就變成了false伸但。這個(gè)原因主要是從JDK 1.7后肾请,HotSpot 將常量池從永久代移到了元空間,正因?yàn)槿绱烁郑琂DK 1.7 后的intern方法在實(shí)現(xiàn)上發(fā)生了比較大的改變铛铁,JDK 1.7后,intern方法還是會(huì)先去查詢常量池中是否有已經(jīng)存在却妨,如果存在饵逐,則返回常量池中的引用,這一點(diǎn)與之前沒有區(qū)別彪标,區(qū)別在于梳毙,如果在常量池找不到對應(yīng)的字符串,則不會(huì)再將字符串拷貝到常量池捐下,而只是在常量池中生成一個(gè)對原字符串的引用账锹。所以:
結(jié)果 #7:在第一種情況下萌业,因?yàn)槌A砍刂袥]有“str01”這個(gè)字符串,所以會(huì)在常量池中生成一個(gè)對堆中的“str01”的引用奸柬,而在進(jìn)行字面量賦值的時(shí)候生年,常量池中已經(jīng)存在,所以直接返回該引用即可廓奕,因此str1和str2都指向堆中的字符串抱婉,返回true。
結(jié)果 #8:調(diào)換位置以后桌粉,因?yàn)樵谶M(jìn)行字面量賦值(String str1 = "str01")的時(shí)候蒸绩,常量池中不存在,所以str1指向的常量池中的位置铃肯,而str2指向的是堆中的對象患亿,再進(jìn)行intern方法時(shí),對str1和str2已經(jīng)沒有影響了押逼,所以返回false步藕。
三、常見面試題解答
有了對以上的知識(shí)的了解挑格,我們現(xiàn)在再來看常見的面試或筆試題就很簡單了:
Q:String s = new String("xyz")咙冗,創(chuàng)建了幾個(gè)String Object?
A:兩個(gè),常量池中的"xyz"和堆中對象漂彤。
Q:下列程序的輸出結(jié)果:
String s1 = “abc”;
String s2 = “abc”;
System.out.println(s1 == s2);
A:true雾消,均指向常量池中對象。
Q:下列程序的輸出結(jié)果:
String s1 = new String(“abc”);
String s2 = new String(“abc”);
System.out.println(s1 == s2);
A:false挫望,兩個(gè)引用指向堆中的不同對象立润。
Q:下列程序的輸出結(jié)果:
String s1 = “abc”;
String s2 = “a”;
String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:false,因?yàn)閟2+s3實(shí)際上是使用StringBuilder.append來完成士骤,會(huì)生成不同的對象范删。
Q:下列程序的輸出結(jié)果:
String s1 = “abc”;
final String s2 = “a”;
final String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:true,因?yàn)閒inal變量在編譯后會(huì)直接替換成對應(yīng)的值拷肌,所以實(shí)際上等于s4="a"+"bc"到旦,而這種情況下巨缘,編譯器會(huì)直接合并為s4="abc"添忘,所以最終s1==s4。
Q:下列程序的輸出結(jié)果:
String s = new String("abc");
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s == s1.intern());
System.out.println(s == s2.intern());
System.out.println(s1 == s2.intern());
A:false若锁,false搁骑,true,具體原因參考第二部分內(nèi)容。
JDK1.8關(guān)于運(yùn)行時(shí)常量池, 字符串常量池的要點(diǎn)
網(wǎng)上關(guān)于jdk 1.8的各種實(shí)驗(yàn), 結(jié)論魚龍混雜 , 很多都相矛盾,網(wǎng)上有的實(shí)驗(yàn)也被后人測試出了不同的結(jié)果
很多都分辨不了真假, 這里記錄一下網(wǎng)絡(luò)上正確的結(jié)論, 歡迎指正!
首先自行區(qū)分運(yùn)行時(shí)常量池與Class文件常量池(靜態(tài)常量池)的概念, JVM內(nèi)存模型 ,方法區(qū)與永久代的區(qū)別
在JDK1.7之前運(yùn)行時(shí)常量池邏輯包含字符串常量池存放在方法區(qū), 此時(shí)hotspot虛擬機(jī)對方法區(qū)的實(shí)現(xiàn)為永久代
在JDK1.7 字符串常量池被從方法區(qū)拿到了堆中, 這里沒有提到運(yùn)行時(shí)常量池,也就是說字符串常量池被單獨(dú)拿到堆,運(yùn)行時(shí)常量池剩下的東西還在方法區(qū), 也就是hotspot中的永久代
在JDK1.8 hotspot移除了永久代用元空間(****Metaspace)取而代之, 這時(shí)候字符串常量池還在堆, 運(yùn)行時(shí)常量池還在方法區(qū), 只不過方法區(qū)的實(shí)現(xiàn)從永久代變成了元空間(Metaspace)
對于直接做+運(yùn)算的兩個(gè)字符串(字面量)常量仲器,并不會(huì)放入字符串常量池中煤率,而是直接把運(yùn)算后的結(jié)果放入字符串常量池中
(String s = "abc"+ "def", 會(huì)直接生成“abcdef"字符串常量 而不把 "abc" "def"放進(jìn)常量池)對于先聲明的字符串字面量常量,會(huì)放入字符串常量池乏冀,但是若使用字面量的引用進(jìn)行運(yùn)算就不會(huì)把運(yùn)算后的結(jié)果放入字符串常量池中了
(String s = new String("abc") + new String("def"),在構(gòu)造過程中不會(huì)生成“abcdef"字符串常量)總結(jié)一下就是JVM會(huì)對字符串常量的運(yùn)算進(jìn)行優(yōu)化蝶糯,未聲明的,只放結(jié)果辆沦;已經(jīng)聲明的昼捍,只放聲明
常量池中同時(shí)存在字符串常量和字符串引用。直接賦值和用字符串調(diào)用String構(gòu)造函數(shù)都可能導(dǎo)致常量池中生成字符串常量;而intern()方法會(huì)嘗試將堆中對象的引用放入常量池
String str1 = "a";
String str2 = "b";
String str4 = str1 + str2; //該語句只在堆中生成一個(gè)對象(str4)
這句被Java編譯器做了優(yōu)化, 實(shí)際上使用StringBuilder實(shí)現(xiàn)的(不在堆里生成str1和str2對象)String str5 = new String("ab");(字符串常量池中不存在"ab"時(shí))在字符換常量池中創(chuàng)建"ab"對象,在堆中生成了一個(gè)對象str5,
str5
指向堆上new的對象肢扯,而str5
內(nèi)部的char value[]
則指向常量池中的char value[]
關(guān)于這個(gè)問題可以參考這篇博客:new String()究竟創(chuàng)建幾個(gè)對象?