其餘更多java基礎文章: java基礎學習(目錄)html
學習資料:
String類API中文
深刻解析String#intern
Java 中new String("字面量") 中 "字面量" 是什麼時候進入字符串常量池的?
new一個String對象的時候,若是常量池沒有相應的字面量真的會去它那裏建立一個嗎?我表示懷疑。java
經過上一篇的學習,咱們已經瞭解了String源碼的方法,這一章,咱們就經過Stirng.intern()方法來延伸,講一下String的其餘方面。git
字符串字面量是在 Java™語言規範的3.10.5. String 字面量中定義的 關於字面量通俗點解釋就是,使用雙引號""
建立的字符串,在堆中建立了對象後其引用插入到字符串常量池中(jdk1.7後),能夠全局使用,遇到相同內容的字面量,就不須要再次建立。舉個例子:github
//這就是建立了一個aaa字符串字面量
String a = "aaa";
//簡單來講,這就是建立了一個Stirng對象和一個aaa字符串字面量,後面會詳細討論
String a = new String("aaa")
複製代碼
java中常量池的概念主要有三個:全局字符串常量池
,class文件常量池
,運行時常量池
。咱們如今所說的就是全局字符串常量池
,在下文中可能會簡稱常量池。對這個想弄明白的同窗能夠看這篇Java中幾種常量池的區分。bash
字符串常量池裏面存的究竟是對象,仍是引用呢?我查了不少資料,最後根據本身的測試和查到的各類說法,認爲在jdk1.7後字符串常量池中存的是引用。在new一個String對象的時候,若是常量池沒有相應的字面量真的會去它那裏建立一個嗎?我表示懷疑。問題中,R大的回答解答了我:oracle
至於說: 以前一直有個結論就是:當建立一個string對象的時候,去字符串常量池看是否有相應的字面量,若是沒有就建立一個。 這個說法歷來都不正確。 對象在堆裏。常量池存引用。app
這個字符串常量池的位置也是隨着jdk版本的不一樣而位置不一樣。在jdk6中,常量池的位置在永久代(方法區)中,此時常量池中存儲的是對象。在jdk7中,常量池的位置在堆中,此時,常量池存儲的就是引用了。在jdk8中,永久代(方法區)被元空間取代了。這裏就引出了一個很常見很經典的問題,看下面這段代碼。函數
@Test
public void test(){
String s = new String("2");
s.intern();
String s2 = "2";
System.out.println(s == s2);
String s3 = new String("3") + new String("3");
s3.intern();
String s4 = "33";
System.out.println(s3 == s4);
}
jdk6
false
false
jdk7
false
true
複製代碼
這段代碼在jdk6中輸出是false false
,可是在jdk7中輸出的是false true
。咱們經過圖來一行行解釋。post
JDK1.6 學習
String s = new String("2");
建立了兩個對象,一個在堆中的StringObject對象,一個是在常量池中的「2」對象。
s.intern();
在常量池中尋找與s變量內容相同的對象,發現已經存在內容相同對象「2」,返回對象2的地址。
String s2 = "2";
使用字面量建立,在常量池尋找是否有相同內容的對象,發現有,返回對象"2"的地址。
System.out.println(s == s2);
從上面能夠分析出,s變量和s2變量地址指向的是不一樣的對象,因此返回false
String s3 = new String("3") + new String("3");
建立了兩個對象,一個在堆中的StringObject對象,一個是在常量池中的「3」對象。中間還有2個匿名的new String("3")咱們不去討論它們。
s3.intern();
在常量池中尋找與s3變量內容相同的對象,沒有發現「33」對象,在常量池中建立「33」對象,返回「33」對象的地址。
String s4 = "33";
使用字面量建立,在常量池尋找是否有相同內容的對象,發現有,返回對象"33"的地址。
System.out.println(s3 == s4);
從上面能夠分析出,s3變量和s4變量地址指向的是不一樣的對象,因此返回false
JDK1.7
String s = new String("2");
建立了兩個對象,一個在堆中的StringObject對象,一個是在堆中的「2」對象,並在常量池中保存「2」對象的引用地址。
s.intern();
在常量池中尋找與s變量內容相同的對象,發現已經存在內容相同對象「2」,返回對象「2」的引用地址。
String s2 = "2";
使用字面量建立,在常量池尋找是否有相同內容的對象,發現有,返回對象「2」的引用地址。
System.out.println(s == s2);
從上面能夠分析出,s變量和s2變量地址指向的是不一樣的對象,因此返回false
String s3 = new String("3") + new String("3");
建立了兩個對象,一個在堆中的StringObject對象,一個是在堆中的「3」對象,並在常量池中保存「3」對象的引用地址。中間還有2個匿名的new String("3")咱們不去討論它們。
s3.intern();
在常量池中尋找與s3變量內容相同的對象,沒有發現「33」對象,將s3對應的StringObject對象的地址保存到常量池中,返回StringObject對象的地址。
String s4 = "33";
使用字面量建立,在常量池尋找是否有相同內容的對象,發現有,返回其地址,也就是StringObject對象的引用地址。
System.out.println(s3 == s4);
從上面能夠分析出,s3變量和s4變量地址指向的是相同的對象,因此返回true。
再來一段變種代碼
經過上面的逐句分析,應該都瞭解了爲何兩個版本的jdk返回值會不同了。那咱們稍稍改變一下上面代碼中的語句順序,將intern方法與字面量賦值語句調換順序:
String s = new String("2");
String s2 = "2";
s.intern();
System.out.println(s == s2);
String s3 = new String("3") + new String("3");
String s4 = "33";
s3.intern();
System.out.println(s3 == s4);
複製代碼
答案是多少呢,你們能夠稍微思考一下再往下看:
jdk6
false
false
jdk7
false
false
複製代碼
原理很簡單,由於在調用intern方法前,先使用了字面量賦值語句,因此在常量池中都存在了與變量相同內容的對象(jdk1.6)或對象的引用(jdk1.7+),此時再調用intern方法,就會發現常量池裏的對象地址和變量的地址不是指向同一個對象,天然就false了。對於這段不懂的同窗能夠評論,我看需不須要再畫一次結構圖和逐句解釋。
經過上面兩段代碼,咱們發現調用intern方法和字面量賦值的順序是很重要的。咱們將上面兩段代碼都經過javap命令查看其字節碼,發如今class類常量池中都有「33」。這說明在運行時,class常量池裏的常量並不會直接所有加入到全局常量池中,那這是在何時加入的呢?我搜到了下面大神的回答 new String(「字面量」) 中 「字面量」 是什麼時候進入字符串常量池的?
簡單來講:
HotSpot VM的實現來講,加載類的時候,那些字符串字面量會進入到當前類的運行時常量池,不會進入全局的字符串常量池 ;
在字面量賦值的時候,會翻譯成字節碼ldc指令,ldc指令觸發lazy resolution動做
- 到當前類的運行時常量池(runtime constant pool,HotSpot VM裏是ConstantPool + ConstantPoolCache)去查找該index對應的項
- 若是該項還沒有resolve則resolve之,並返回resolve後的內容。
- 在遇到String類型常量時,resolve的過程若是發現StringTable已經有了內容匹配的java.lang.String的引用,則直接返回這個引用;
- 若是StringTable裏還沒有有內容匹配的String實例的引用,則會在Java堆裏建立一個對應內容的String對象,而後在StringTable記錄下這個引用,並返回這個引用出去。
在咱們使用中常常會用到+符號來拼接字符串,可是這個+符號在String中的實現仍是有講究的。若是是相加含有String對象,則底部是使用StringBuilder實現的拼接的
String str1 ="str1";
String str2 ="str2";
String str3 = str1 + str2;
複製代碼
若是相加的參數只有字面量或者常量或基礎類型變量,則會直接編譯爲拼接後的字符串。
String str1 =1+"str2"+"str3";
複製代碼
這裏有個小細節
若是使用字面量拼接的話,java常量池裏是不會保存拼接的參數的,而是直接編譯成拼接後的字符串保存,咱們看看這段代碼:
String str1 = new String("aa"+"bb");
//String str3 = "aa";
String str2 = new StringBuilder("a").append("a").toString();
System.out.println(str2==str2.intern());
複製代碼
這段代碼的輸出是true
。能夠得知,在str1變量的建立中,雖然咱們用了字面量「aa」,可是咱們常量池裏並無aa,因此str2==str.intern()
纔會返回true
。若是咱們去掉str3的註釋,從新運行,就會輸出false
。
我在學習的過程當中,遇到了一個疑問,怎麼都查不到是爲何,你們若是看到這裏,能夠順手寫一下這段代碼,看是否是也會遇到這樣的問題。
public static void main(String[] args){
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
複製代碼
@Test
public void test7(){
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
複製代碼
如上所示,分別在test環境和main方法裏運行相同代碼,此時main函數裏返回true
,test環境下倒是返回false
。按邏輯這裏應該是返回true纔對。可是我測試了將參數「1」改成「2「」或者「3」,二者返回的都是true。