前言
String是Java中十分常用的類,在面試題中也是出鏡率很高的常客,本文將我自己學習中遇到的一些問題進行整理,如果有誤,歡迎指正。
String對象判等
千萬不要用 == 去判斷String對象是否相等,==比較的是地址。JVM只會共享字符串常量,因此,即使是“看起來”值相同的字符串,用==判斷也可能不相等。
舉例來說,下面這段代碼中,變量x和y都指向了常量池中共享的"a",地址相同,但是z是Java堆中的新建對象的引用,其地址與x不同,所以返回了false。
並且每次new一個String對象時,即使字符串內容相同,也會新開闢一片空間存儲對象,因此z和zCopy地址也是不用的。
這部分的細節原理在下一部分中解釋。總而言之,如果你只是想判斷兩個String對象的內容是否一樣,請使用x.equals(z)的形式。
代碼一
String x = "a";
String y = "a";
String z = new String("a");
String zCopy = new String("a");
System.out.println(x==y);//true
System.out.println(x==z);//false
System.out.println(z==zCopy);//false
String與常量池
我們在給String類型的引用賦值的時候會先看常量池中是否存在這個字符串對象的引用,若有就直接返回這個引用,若沒有,就在堆裏創建這個字符串對象並在字符串常量池中記錄下這個引用。
注意:常量池中存放的是引用,並不是實例!!!
下面結合具體代碼來理解這段話,看下面這段代碼
代碼二
String x = "a";
String y = "a" + "b";
String z = "a";
用javap -v -c對.class文件進行反編譯後,得到如下結果
可以看到,常量池中最中只保留了一份"a"的引用。因爲在String z = "a";執行時,字符串常量池中已經有"a"的引用了,不會重複創建。
同時我們注意到,對應String y = "a" + "b";這條語句,因爲"a"和"b"都是編譯器就能確定的常量,所以常量池只保留了最終計算的結果,並沒有單獨保留"b"。
我們將代碼稍作修改,然後再次反編譯。
代碼三
String witcher = "Geralt";
String sorceress = "Yennefer";
String date = witcher + sorceress;
可以看出,最終常量池只存儲了"Geralt"和"Yennefer"兩個引用,而沒有存放拼接的結果。因爲witcher和sorceress變量要運行時才能確定。但是如果將變量witcher和sorceress都聲明爲final,那編譯期就可以確定,因此拼接結果的引用信息也會放入常量池。
總結:
對於字符串表達式而言
1、對於編譯期能直接確定的值(字面量、聲明爲final的變量),會直接將表達式的結果放入常量池。
2、如果編譯期不能直接直接確定(非final的變量),那麼只將已經聲明字符串字面常量放入常量池,表達式的結果不放入常量池。
關於常量池的更多介紹歡迎查看我的另一篇博客一張圖秒懂JVM內存區域的劃分
另一個出鏡率很高的問題是如下的這段代碼創建了幾個對象?
String s = new String("xyz");
關於這個問題網上衆說紛紜,這裏放上一種比較靠譜的說法。參考自R神的博客請別再拿“String s = new String("xyz");創建了多少個String實例”來面試了吧
首先,換個問法,這段代碼在運行時涉及幾個String實例?
一種合理的解釋是:兩個,一個是字符串字面量"xyz"所對應的、駐留(intern)在一個全局共享的字符串常量池中的實例,另一個是通過new String(String)創建並初始化的、內容與"xyz"相同的實例。
StringBuilder與StringBuffer
如果你查看過源碼,就會發現String對象是被final修飾的,這意味着它是不可變的。因此,當我們拼接字符串時,會產生新的對象。爲此,設計者們提供了StringBuilder類來避免產生過多的中間對象。當我們用+拼接字符串時,編譯器會自動幫我們使用StringBuilder進行優化。
這次使用jad對代碼二進行反編譯(直接用javap -v也可以,但是使用jad產生的結果更容易看懂)
得到如下結果 可以看到編譯器自動爲我們使用了StringBuilder
String witcher = "Geralt";
String sorceress = "Yennefer";
String date = (new StringBuilder()).append(witcher).append(sorceress).toString();
有人會說,既然編譯器已經優化,我們就直接使用+拼接字符串就可以啊,爲什麼還要用StringBuilder?
來看這段代碼
代碼四
String witcher = "Geralt";
String sorceress = "Yennefer";
String res = "";
for (int i = 0; i < 8; i++) {
res += sorceress;
}
對其反編譯,可得
String witcher = "Geralt";
String sorceress = "Yennefer";
String res = "";
for(int i = 0; i < 8; i++)
res = (new StringBuilder()).append(res).append(sorceress).toString();
可以看出,每一輪的for循環都新建了一個StringBuilder,這是完全沒有必要的。因此,我們應該在for循環外部先定義一個StringBuilder對象,這樣只新建了一個對象就完成了任務,效率大增。
StringBuffer和StringBuilder基本相同,但是它保證了線程安全,如果有多線程需求,可以按需使用。
String.intern()
我們用下面這段代碼來分析intern的作用
代碼五
String witcher1 = new String("Geralt");
String witcher2 = "Geralt";
System.out.println(witcher1 == witcher2);//false
System.out.println(witcher1.intern() == witcher2);//true
第三行顯然是false,這在本文最開始已經解釋過。
但是witcher1調用intern之後,地址就與witcher2相同了,這是爲什麼?
原來,當一個對象調用intern方法時,會查看常量池是否有與當前對象內容相同的字面量,如果有,就直接返回常量池中的引用信息,如果沒有,就在常量池中補充當前對象的字面量,然後返回引用。
總結:
以上就是String類型經常引起疑惑的一些知識點。總結不易,如果有幫到你,希望可以點個贊,謝謝~