String做爲Java開發中常用的類,弄懂它是非常有必要的,但是往往很多工作了幾年從業人員,也並沒有特別熟悉過,所以樓主總結一下String的常量池,以及intern()方法等。技術無止境,當然本文也有不足之處,歡迎大家在評論區指正。
前言
本次代碼使用 jdk 1.8版本,並且以下代碼示例除了第一個寫了main()方法,並且所有的示例分別獨立運行 ,其餘爲了簡潔做了缺省main()。在創建字符串分析的同時,都默認省略了棧中的句柄指向分析。進入正題之時,先科普幾個知識點
- String源碼裏面標註爲final修飾的類,是一個不可改變的對象,那平時用到字符串A+字符串B怎麼改變了呢,其實這裏有涉及到String的常量池,首先常量池存放在方法區。
- 在jdk1.6時,方法區是存放在永久代(java堆的一部分,例如新生代,老年代)而在jdk1.7以後將字符串常量池移動到了的堆內存中
- 在jdk1.8時,HotspotVM正式宣告了移除永久代,取而代之的是元數據區,元數據區存放在內存裏面(存放一些加載class的信息),但是常量池還是和jdk1.7存放位置一樣還是存放在堆中。
先看一波常見面試題:
首先看一道常見的面試題,問輸出的是什麼?
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" 創建 了幾個對象,那如果題目稍微改變一下成下面這樣,那輸出的又是什麼?
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是本身常量池的地址)
如果是引用,返回引用地址指向的堆空間對象地址值
如果是常量,則直接返回常量池常量的地址值,
如果不存在,
在常量池中創建該常量,並返回此常量的地址值
String s = "123";
//true,因爲s已經在常量池裏面了,s.intern()返回的也是常量池的地址,兩者地址一樣爲true
System.out.println(s == s.intern());
有看到評論區的小夥伴補明白返回的地址值(分兩種情況是啥,這個例子和下面new String()分別畫下Jvm儲存情況圖 )
2. new String創建字符串
與上面第一種方式相比,第一種方式效率高,下圖解決了本文中的最開始出的部分面試題。
首先在堆上創建對象(無論堆上是否存在相同字面量的對象),
然後判斷常量池上是否存在字符串的字面量,
如果不存在
在常量池上創建常量
如果存在
不做任何操作
String s = new String("123");
/*
嚴格來說首先肯定會在堆中創建一個123的對象,然後再去判斷常量池中是否存在123的對象,
如果不存在,則在常量池中創建一個123的常量(與堆中的123不是一個對象),
如果存在,則不做任何操作,解決了本文第一個面試題有問到創建幾個對象的問題。
因爲常量池中是有123的對象的,s指向的是堆內存中的地址值,s.intern()返回是常量池中的123的常量池地址,所以輸出false
*/
System.out.println(s == s.intern());
主要畫一下 String s = new String("123")的內存圖,看到評論區的有同學對地址不是很清楚,這裏重點畫圖解釋一下
3.兩個雙引號的字符串相加
判斷這兩個常量、相加後的常量在常量池上是否存在
如果不存在
則在常量池上創建相應的常量(並將常量地址值返回)
如果存在,則直接返回地址值(只不過地址值分爲兩種情況,1是堆中的引用,2是本身常量池的地址)
如果是引用,返回引用地址指向的堆空間對象地址值,
如果是常量,則直接返回常量池常量的地址值,
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()的字符串相加
首先會創建這兩個對象(堆中)以及相加後的對象(堆中)
然後判斷常量池中是否存在這兩個對象的字面量常量
如果存在
不做任何操作
如果不存在
則在常量池上創建對應常量
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()方法的時候再重點分析一次。
String s2 = "123";
s1.intern();
System.out.println( s1 == s2);// false
5.雙引號字符串常量與new String字符串相加
首先創建兩個對象,一個是new String的對象(堆中),一個是相加後的對象(堆中)
然後判斷雙引號字符串字面量和new String的字面量在常量池是否存在
如果存在
不做操作
如果不存在
則在常量池上創建對象的常量
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.雙引號字符串常量與一個字符串變量相加
首先創建一個對象,是相加後的結果對象(存放堆中,不會找常量池)
然後判斷雙引號字符串字面量在常量池是否存在
如果存在
不做操作
如果不存在
則在常量池上創建對象的常量
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)>相等的常量,就把該 常量池的對象返回,否則,就把當前對象加入到常量池中並且返回當前對象的引用。樓主用更加白話的方式解釋一下:
判斷這個常量是否存在於常量池。(jdk1.6以上版本)
如果存在,則直接返回地址值(只不過地址值分爲兩種情況,1是堆中的引用,2是本身常量池的地址)
如果是引用,返回引用地址指向的堆空間對象地址值,
如果是常量,則直接返回常量池常量的地址值,(但是這兩種情況不管怎麼說,都是常量池中的該常量的地址)
如果不存在, 如果該字符串對象是否存在堆中:
如果存在將當前對象引用複製到常量池,並且返回的是當前對象的引用(這個和上面最開始的字符串創建分析有點不同) 如果堆中也沒有,則在常量池中創建字符串並且返回該常量池中引用。
在jdk1.6的版本是另外的一個說法
JDK6:當調用intern方法時候,如果字符串常量池先前已經創建該字符串對象,則直接返回常量池中的該字符串的引用。如果沒有的話,就在字符串常量池中新創建一個此字符串對象的字面量對象並且返回出去。
樓主具體解釋一下:
判斷這個常量是否存在於常量池 (jdk1.6版本)
如果存在,則直接返回地址值(只不過地址值分爲兩種情況,1是堆中的引用,2是本身常量池的地址)
如果是引用,返回引用地址指向的堆空間對象地址值,
如果是常量,則直接返回常量池常量的地址值,(但是這兩種情況不管怎麼說,都是常量池中的該常量的地址)
如果不存在, 不比較堆中是否存在該常量(和jdk1.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。
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;
結尾:
由於本文都是在Jdk1.8版本(1.7由於已經把常量池放在堆中了和1.8結果應該一樣)執行,如果有讀者要探究1.6的相關問題,主要知道jdk1.6在創建String和1.8(1.7)的堆和常量池有一些不同實現,那相關問題就很清楚了,這裏樓主沒有涉及到1.6的相關問題,以免混淆讀者,如果需要探究1.6以及相關問題,歡迎在評論區留言。
樓主很多時候都說了句柄指針的概念相對抽象,如果讀者想知道兩個對象是否是指向了相同的地址,可用 System.identityHashCode(Object x) 來驗證。
最後本文也肯定有一些不足之處,歡迎大家在評論區留言,學無止境,大家加油,如果覺得博主寫的不錯的話,點個關注加 個贊,評論也是不要錢,博主會一直更新一些Java中常用的知識點和疑難點 ,對了最後( 如需轉載,請標註出處,謝謝!)