1、字符串問題html
字符串在咱們平時的編碼工做中其實用的很是多,而且用起來也比較簡單,因此不多有人對其作特別深刻的研究。卻是面試或者筆試的時候,每每會涉及比較深刻和難度大一點的問題。我在招聘的時候也偶爾會問應聘者相關的問題,倒不是說必定要回答的特別正確和深刻,一般問這些問題的目的有兩個,第一是考察對 JAVA 基礎知識的瞭解程度,第二是考察應聘者對技術的態度。java
咱們看看如下程序會輸出什麼結果?若是你能正確的回答每一道題,而且清楚其緣由,那本文對你就沒什麼太大的意義。若是回答不正確或者不是很清楚其原理,那就仔細看看如下的分析,本文應該能幫助你清楚的理解每段程序的結果及輸出該結果的深層次緣由。面試
代碼段一:app
1
2
3
4
5
6
7
8
9
10
11
|
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
}
}
|
代碼段二:post
1
|
package
com.paddx.test.string;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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): 字體
1
2
3
4
5
6
7
8
9
10
|
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):ui
1
2
3
4
5
6
7
8
9
10
|
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
}
}
|
爲了方便描述,我對上述代碼的輸出結果由#1~#8進行了編碼,下文中藍色字體部分即爲結果。編碼
2、字符串深刻分析url
一、代碼段一分析spa
字符串不屬於基本類型,可是能夠像基本類型同樣,直接經過字面量賦值,固然也能夠經過new來生成一個字符串對象。不過經過字面量賦值的方式和new的方式生成字符串有本質的區別:
經過字面量賦值建立字符串時,會優先在常量池中查找是否已經存在相同的字符串,假若已經存在,棧中的引用直接指向該字符串;假若不存在,則在常量池中生成一個字符串,再將棧中的引用指向該字符串。而經過new的方式建立字符串時,就直接在堆中生成一個字符串的對象(備註,JDK 7 之後,HotSpot 已將常量池從永久代轉移到了堆中。詳細信息可參考《JDK8內存模型-消失的PermGen》一文),棧中的引用指向該對象。對於堆中的字符串對象,能夠經過 intern() 方法來將字符串添加的常量池中,並返回指向該常量的引用。
如今咱們應該能很清楚代碼段一的結果了:
結果 #1:由於str1指向的是字符串中的常量,str2是在堆中生成的對象,因此str1==str2返回false。
結果 #2:str2調用intern方法,會將str2中值(「string」)複製到常量池中,可是常量池中已經存在該字符串(即str1指向的字符串),因此直接返回該字符串的引用,所以str3==str2返回true。
如下運行代碼段一的代碼的結果:
二、代碼段二分析
對於代碼段二的結果,仍是經過反編譯StringTest01.class文件比較容易理解:
常量池內容(部分):
執行指令(部分,第二列#+序數對應常量池中的項):
在解釋上述執行過程以前,先了解兩條指令:
ldc:Push item from run-time constant pool,從常量池中加載指定項的引用到棧。
astore_<n>:Store reference into local variable,將引用賦值給第n個局部變量。
如今咱們開始解釋代碼段二的執行過程:
0: ldc #2:加載常量池中的第二項("baseStr")到棧中。
2: astore_1 :將1中的引用賦值給第一個局部變量,即String baseStr = "baseStr";
3: ldc #2:加載常量池中的第二項("baseStr")到棧中。
5: astore_2 :將3中的引用賦值給第二個局部變量,即 final String baseFinalStr="baseStr";
6: ldc #3:加載常量池中的第三項("baseStr01")到棧中。
8: astore_3 :將6中的引用賦值給第三個局部變量,即String str1="baseStr01";
9: ldc #3:加載常量池中的第三項("baseStr01")到棧中。
11: astore 4:將9中的引用賦值給第四個局部變量:即String str2="baseStr01";
結果#3:str1==str2 確定會返回true,由於str1和str2都指向常量池中的同一引用地址。因此其實在JAVA 1.6以後,常量字符串的「+」操做,編譯階段直接會合成爲一個字符串。
13: new #4:生成StringBuilder的實例。
16: dup :複製13生成對象的引用並壓入棧中。
17: invokespecial #5:調用常量池中的第五項,即StringBuilder.<init>方法。
以上三條指令的做用是生成一個StringBuilder的對象。
20: aload_1 :加載第一個參數的值,即"baseStr"
21: invokevirtual #6 :調用StringBuilder對象的append方法。
24: ldc #7:加載常量池中的第七項("01")到棧中。
26: invokevirtual #6:調用StringBuilder.append方法。
29: invokevirtual #8:調用StringBuilder.toString方法。
32: astore 5:將29中的結果引用賦值改第五個局部變量,即對變量str3的賦值。
結果 #4:由於str3其實是stringBuilder.append()生成的結果,因此與str1不相等,結果返回false。
34: ldc #3:加載常量池中的第三項("baseStr01")到棧中。
36: astore 6:將34中的引用賦值給第六個局部變量,即str4="baseStr01";
結果 #5 :由於str1和str4指向的都是常量池中的第三項,因此str1==str4返回true。這裏咱們還能發現一個現象,對於final字段,編譯期直接進行了常量替換,而對於非final字段則是在運行期進行賦值處理的。
38: new #9:建立String對象
41: dup :複製引用並壓如棧中。
42: ldc #3:加載常量池中的第三項("baseStr01")到棧中。
44: invokespecial #10:調用String."<init>"方法,並傳42步驟中的引用做爲參數傳入該方法。
47: invokevirtual #11:調用String.intern方法。
從38到41的對應的源碼就是new String("baseStr01").intern()。
50: astore 7:將47步返回的結果賦值給變量7,即str5指向baseStr01在常量池中的位置。
結果 #6 :由於str5和str1都指向的都是常量池中的同一個字符串,因此str1==str5返回true。
運行代碼段二,輸出結果以下:
三、代碼段三解析:
對於代碼段三,在 JDK 1.6 和 JDK 1.7中的運行結果不一樣。咱們先看一下運行結果,而後再來解釋其緣由:
JDK 1.6 下的運行結果:
JDK 1.7 下的運行結果:
根據對代碼段一的分析,應該能夠很簡單得出 JDK 1.6 的結果,由於 str2 和 str1原本就是指向不一樣的位置,理應返回false。
比較奇怪的問題在於JDK 1.7後,對於第一種狀況返回true,可是調換了一下位置返回的結果就變成了false。這個緣由主要是從JDK 1.7後,HotSpot 將常量池從永久代移到了元空間,正由於如此,JDK 1.7 後的intern方法在實現上發生了比較大的改變,JDK 1.7後,intern方法仍是會先去查詢常量池中是否有已經存在,若是存在,則返回常量池中的引用,這一點與以前沒有區別,區別在於,若是在常量池找不到對應的字符串,則不會再將字符串拷貝到常量池,而只是在常量池中生成一個對原字符串的引用。因此:
結果 #7:在第一種狀況下,由於常量池中沒有「str01」這個字符串,因此會在常量池中生成一個對堆中的「str01」的引用,而在進行字面量賦值的時候,常量池中已經存在,因此直接返回該引用便可,所以str1和str2都指向堆中的字符串,返回true。
結果 #8:調換位置之後,由於在進行字面量賦值(String str1 = "str01")的時候,常量池中不存在,因此str1指向的常量池中的位置,而str2指向的是堆中的對象,再進行intern方法時,對str1和str2已經沒有影響了,因此返回false。
3、常見面試題解答
有了對以上的知識的瞭解,咱們如今再來看常見的面試或筆試題就很簡單了:
Q:String s = new String("xyz"),建立了幾個String Object?
A:兩個,常量池中的"xyz"和堆中對象。
Q:下列程序的輸出結果:
String s1 = 「abc」;
String s2 = 「abc」;
System.out.println(s1 == s2);
A:true,均指向常量池中對象。
Q:下列程序的輸出結果:
String s1 = new String(「abc」);
String s2 = new String(「abc」);
System.out.println(s1 == s2);
A:false,兩個引用指向堆中的不一樣對象。
Q:下列程序的輸出結果:
String s1 = 「abc」;
String s2 = 「a」;
String s3 = 「bc」;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:false,由於s2+s3其實是使用StringBuilder.append來完成,會生成不一樣的對象。
Q:下列程序的輸出結果:
String s1 = 「abc」;
final String s2 = 「a」;
final String s3 = 「bc」;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:true,由於final變量在編譯後會直接替換成對應的值,因此實際上等於s4="a"+"bc",而這種狀況下,編譯器會直接合併爲s4="abc",因此最終s1==s4。
Q:下列程序的輸出結果:
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,具體緣由參考第二部份內容。