本次代碼使用 jdk 1.8版本,並且以下代碼示例除了第一個寫了main()方法,並且所有的示例分別獨立運行 ,其余為了簡潔做了缺省main()。在創建字符串分析的同時,都默認省略了棧中的句柄指向分析。進入正題之時,先科普幾個知識點
String源碼里面標注為final修飾的類,是一個不可改變的對象,那平時用到字符串A+字符串B怎么改變了呢,其實這里有涉及到String的常量池,首先常量池存放在方法區。
在jdk1.6時,方法區是存放在永久代(java堆的一部分,例如新生代,老年代)而在jdk1.7以后將字符串常量池移動到了的堆內存中
在jdk1.8時,HotspotVM正式宣告了移除永久代,取而代之的是元數據區,元數據區存放在內存里面(存放一些加載class的信息),但是常量池還是和jdk1.7存放位置一樣還是存放在堆中。
先看一波常見面試題:
首先看一道常見的面試題,問輸出的是什么?
1
2
3
4
5
|
public
static
void
main(String[] args){
String s1 =
new
String(
"123"
);
String s2 =
"123"
;
System.out.println(s1 == s2);
}
|
基本上大家都能知道是false,但是再這么深究一次,問 String s1 = new String("123") 創建了幾個對象,String s2 = "123" 創建 了幾個對象,那如果題目稍微改變一下成下面這樣,那輸出的又是什么?
1
2
3
4
5
6
7
8
9
|
String s1 =
new
String(
"123"
).intern();
String s2 =
"123"
;
System.out.println(s1 == s2);
// true
// 如果這樣再改一下
String s1 =
new
String(
"123"
);
s1.intern();
String s2 =
"123"
;
System.out.println(s1 == s2);
// false
|
如果對輸出結果不是很明白的,本文都會一一解答並且進行拓展。
創建字符串分析:
首先要分析String,一定要知道String幾種常見的創建字符串的方式,以及每一種不同的方式常量池和堆分別是什么儲存情況。
1.直接寫雙引號常量來創建
判斷這個常量是否存在於常量池,
如果存在,則直接返回地址值(只不過地址值分為兩種情況,1是堆中的引用,2是本身常量池的地址)
如果是引用,返回引用地址指向的堆空間對象地址值
如果是常量,則直接返回常量池常量的地址值,
如果不存在,
在常量池中創建該常量,並返回此常量的地址值、
1
2
3
|
String s =
"123"
;
//true,因為s已經在常量池里面了,s.intern()返回的也是常量池的地址,兩者地址一樣為true
System.out.println(s == s.intern());
|
2. new String創建字符串
與上面第一種方式相比,第一種方式效率高,下圖解決了本文中的最開始出的部分面試題。
首先在堆上創建對象(無論堆上是否存在相同字面量的對象),
然后判斷常量池上是否存在字符串的字面量,
如果不存在
在常量池上創建常量(並將常量地址值返回)
如果存在
不做任何操作
1
2
3
4
5
6
7
8
|
String s =
new
String(
"123"
);
/*
嚴格來說首先肯定會在堆中創建一個123的對象,然后再去判斷常量池中是否存在123的對象,
如果不存在,則在常量池中創建一個123的常量(與堆中的123不是一個對象),
如果存在,則不做任何操作,解決了本文第一個面試題有問到創建幾個對象的問題。
因為常量池中是有123的對象的,s指向的是堆內存中的地址值,s.intern()返回是常量池中的123的常量池地址,所以輸出false
*/
System.out.println(s == s.intern());
|
3.兩個雙引號的字符串相加
判斷這兩個常量、相加后的常量在常量池上是否存在
如果不存在
則在常量池上創建相應的常量(並將常量地址值返回)
如果存在,則直接返回地址值(只不過地址值分為兩種情況,1是堆中的引用,2是本身常量池的地址)
如果是引用,返回引用地址指向的堆空間對象地址值,
如果是常量,則直接返回常量池常量的地址值,
1
2
3
4
5
6
7
8
9
|
String s1 =
new
String(
"123"
).intern();
String s2 =
"1"
+
"23"
;
/*
* 首先第一句話 String s1 = new String("123") 以上分析過創建了兩個對象(一個堆中,一個常量池 中)此時s1指向堆中
* 當s1調用.intern()方法之后,發現常量池中已經有了字面量是123的常量,則直接把常量池的地址返回給s1
* 在執行s2等於123時候,去常量池查看,同上常量池已經存在了,則此時s2不創建對象,直接拿常量池123的地址值使用
* 所以此時s1 和 s2 都代表是常量池的地址值,則輸出為true
*/
System.out.println(s1 == s2);
|
如果這里看不懂 intern()方法時,可以快速滑動到文章尾部,先看intern()方法的分析。
4.兩個new String()的字符串相加
首先會創建這兩個對象(堆中)以及相加后的對象(堆中)
然后判斷常量池中是否存在這兩個對象的字面量常量
如果存在
不做任何操作
如果不存在
則在常量池上創建對應常量
1
2
3
4
5
6
7
8
|
String s1 =
new
String(
"1"
)+
new
String(
"23"
);
/*
* 首先堆中會有 1 ,23 ,以及相加之后的123 這三個對象。如果 1,23 這兩個對象在常量池中沒有相等的字面量
* 那么還會在常量池中創建2個對象 最大創建了5個對象。最小創建了3個對象都在堆中。
*/
s1.intern();
String s2 =
"123"
;
System.out.println( s1 == s2);
// true
|
這個地方比較復雜 ,如果我把String s2 = "123" 代碼放在s1.intern()前面先執行,其余代碼不變,那么輸出結果又為false,這里等會樓主會在分析 intern()方法的時候再重點分析一次。
1
2
3
|
String s2 =
"123"
;
s1.intern();
System.out.println( s1 == s2);
// false
|
5.雙引號字符串常量與new String字符串相加
首先創建兩個對象,一個是new String的對象(堆中),一個是相加后的對象(堆中)
然后判斷雙引號字符串字面量和new String的字面量在常量池是否存在
如果存在
不做操作
如果不存在
則在常量池上創建對象的常量
1
2
3
4
5
6
7
|
String s1 =
"1"
+
new
String(
"23"
);
/*
*首先堆中會有 23 ,以及相加之后的123 這2個對象。如果23,1 這兩個對象在常量池中沒有相等的字面量
*那么還會在常量池中創建2個對象最大創建了4個對象(2個堆中,2個在常量池中)。最小創建了2個對象都堆中。
*/
String s2 =
"123"
;
System.out.println( s1.intern() == s2);
// true
|
6.雙引號字符串常量與一個字符串變量相加
首先創建一個對象,是相加后的結果對象(存放堆中,不會找常量池)
然后判斷雙引號字符串字面量在常量池是否存在
如果存在
不做操作
如果不存在
則在常量池上創建對象的常量
1
2
3
4
5
6
7
8
9
|
String s1 =
"23"
;
/*
* 這里執行時,常量“1” 會首先到字符串常量池里面去找,如果沒有就創建一個,並且加入字符串常量池。
* 得到的123結果對象,不會存入到常量池。這里特別注意和兩個常量字符串相加不同 “1”+“23” 參考上面第三點
* 由於不會進入常量池,所以s2 和 s3 常量池地址值不同,所以輸出為false
*/
String s2 =
"1"
+s1;
String s3 =
"123"
;
System.out.println( s2 == s3.intern());
|
Q: 有人會問為什么兩個常量字符串相加得到的對象就會入常量池(參考上面第3點),而加上一個變量就不會???
A: 這是由於Jvm優化機制決定的,Jvm會有編譯時的優化,如果是兩個常量,Jvm會認定這已經是不可變的,就會直接在編譯
時和常量池進行判斷比對等,但是如果是加上一個變量,說明最后運行得出的結果是可變的,Jvm無法在編譯時就確定執行之后的結果是多少,所以不會把該結果和常量池比對。
String.intern()方法分析:
在分析intern()方法時候,首先去官網查看api的相關解釋
樓主大概翻譯一下,意思就是:當調用這個方法時候,如果常量池包含了一個<調用 code equals(Object)>相等的常量,就把該 常量池的對象返回,否則,就把當前對象加入到常量池中並且返回當前對象的引用。樓主用更加白話的方式解釋一下:
判斷這個常量是否存在於常量池。
如果存在,則直接返回地址值(只不過地址值分為兩種情況,1是堆中的引用,2是本身常量池的地址)
如果是引用,返回引用地址指向的堆空間對象地址值
如果是常量,則直接返回常量池常量的地址值,
如果不存在,
將當前對象引用復制到常量池,並且返回的是當前對象的引用(這個和上面最開始的字符串創建分析有點不同)
實戰分析問題:
基本上讀者看到這里就可以嘗試着去回過頭文章一些示例代碼,看看輸出結果,這里分析一下上文存在的一個例子
1
2
3
4
5
6
|
public
static
void
main(String[] args){
String s1 =
new
String(
"1"
)+
new
String(
"23"
);
s1.intern();
String s2 =
"123"
;
System.out.println( s1 == s2);
}
|
分析: 1 首先看第一行是兩個new String類型的字符串相加(詳見上文第4點)可知道,這里創建了堆中有3個對象 一個是1,一個是23,還有一個是結果 123,由於程序剛啟動常量池也沒有 1,23 所以會在常量池創建2個對象 (1 , 23)
2 當s1執行intern()方法之后,首先去常量池判斷有沒有123,此時發現沒有,所以會把對象加入到常量池,並且返回當前對象的引用(堆中的地址)
3 當創建s2時候(詳見上文第1點),並且找到常量池中123,並且把常量池的地址值返回給s2
4 由於常量池的地址值就是s1調用intern()方法之后得到的堆中的引用,所以此時s1和s2的地址值一樣,輸出true。
1
2
3
4
5
6
|
public
static
void
main(String[] args){
String s1 =
new
String(
"1"
)+
new
String(
"23"
);
String s2 =
"123"
;
s1.intern();
System.out.println( s1 == s2);
}
|
如果把中間兩行換一個位置,那輸出就是false了,下面在分析一下不同點,上面分析過的不再贅述。
1.在執行到第二行的時候String s2 = "123"時,發現常量池沒有123,所以會先創建一個常量
2.在當s1調用intern()方法時,會發現常量池已經有了123對象,就會直接把123的常量給返回出去,但是由於返回值並沒有接收,所以此時s1還是堆中地址,則輸入false;如果代碼換成 s1 = s1.intern();那s1就會重新指向常量池了,那輸出就為true。
更多學習資料可關注gzitcast獲取(weiixn)