Java中,那些關(guān)于String和字符串常量池你不得不知道的東西

老套的筆試題

在一些老套的筆試題中,會(huì)要你判斷s1==s2為false還是true,s1.equals(s2)為false還是true。

String s1 = new String("xyz");
String s2 = "xyz";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));

對(duì)于這種題,你總能很快的給出標(biāo)準(zhǔn)答案:==比較的是對(duì)象地址,equals方法比較的是真正的字符數(shù)組。所以輸出的是false和true。

上面的屬于最低階的題目,沒有什么難度。

現(xiàn)在這種老套的題目已經(jīng)慢慢消失了,取而代之的是有一些變形的新題目:

String s1 = "aa";
String s2 = "bb";
String str1 = s1 + s2;
String str2 = "aabb";
//輸出什么呢???
System.out.println(str1 == str2);

final String s3 = "cc";
final String s4 = "dd";
String str3 = s3 + s4;
String str4 = "ccdd";
//又輸出什么呢???
System.out.println(str3 == str4);

難度提升了一些,但思考一下也不難得出答案是false和true。

今天的文章就是以這幾個(gè)題目展開的。

String對(duì)象的創(chuàng)建

先簡(jiǎn)單看一下String類的結(jié)構(gòu):

可以發(fā)現(xiàn),String里面有一個(gè)value屬性,是真正存儲(chǔ)字符的char數(shù)組。

在執(zhí)行String s = "xyz";的時(shí)候,在堆區(qū)創(chuàng)建了一個(gè)String對(duì)象,一個(gè)char數(shù)組對(duì)象。

如何證明創(chuàng)建了一個(gè)String對(duì)象和一個(gè)char數(shù)組對(duì)象呢?我們可以通過IDEA的Debug功能驗(yàn)證:

注意看我截圖的位置,在執(zhí)行完String s = "xyz";之后,再次點(diǎn)擊load classes,Diff欄的String和char[]分別加了1,表示在內(nèi)存中新增了一個(gè)char數(shù)組對(duì)象和一個(gè)String對(duì)象。

現(xiàn)在,我們?cè)賮砜?code>String s = new String("xyz");創(chuàng)建了幾個(gè)對(duì)象。

從這張Debug動(dòng)圖中,我們可以得出在String s = new String("xyz");之后,創(chuàng)建了兩個(gè)String對(duì)象和一個(gè)char數(shù)組對(duì)象。

又因?yàn)?code>String s = new String("xyz");的s引用只能指向一個(gè)對(duì)象,可以畫出內(nèi)存分布圖:

從圖中可以看到,在堆區(qū),有兩個(gè)String對(duì)象,這兩個(gè)String對(duì)象的value都指向同一個(gè)char數(shù)組對(duì)象。

那么問題來了,下面的那個(gè)String對(duì)象根本就沒被引用,也就是說他沒有被用到,那么它到底是干什么的呢?

占了內(nèi)存空間又不使用,難道這是JDK的設(shè)計(jì)缺陷?

很顯然不是JDK的缺陷,JDK雖然確實(shí)有設(shè)計(jì)缺陷,但不至于這么明顯,這么愚蠢。

那下面的那個(gè)String對(duì)象是干什么的呢?

答案是用于駐留到字符串常量池中去的,注意,這里我用了一個(gè)駐留,并不是直接把對(duì)象放到字符串常量池里面去,有什么區(qū)別我們后面再講。

這里出現(xiàn)了字符串常量池的概念,我在String s = new String("xyz")創(chuàng)建了幾個(gè)實(shí)例你真的能答對(duì)嗎?中也有過比較詳細(xì)的介紹,有興趣的可以去看一下,這里不再重復(fù)了。

你只需要知道,字符串常量池在JVM源碼中對(duì)應(yīng)的類是StringTable,底層實(shí)現(xiàn)是一個(gè)Hashtable。

那字符串到底是怎么存的呢?

我們以String s = new String("xyz");為例:

首先去找字符串常量池找,看能不能找到“xyz”字符串對(duì)應(yīng)對(duì)象的引用,如果字符串常量池中找不到:

  • 創(chuàng)建一個(gè)String對(duì)象和char數(shù)組對(duì)象
  • 將創(chuàng)建的String對(duì)象封裝成HashtableEntry,作為StringTable的value進(jìn)行存儲(chǔ)
  • new String("xyz")會(huì)在堆區(qū)又創(chuàng)建一個(gè)String對(duì)象,char數(shù)組直接指向創(chuàng)建好的char數(shù)組對(duì)象

如果字符串常量池中能找到:

  • new String("xyz")會(huì)在堆區(qū)創(chuàng)建一個(gè)對(duì)象,char數(shù)組直接指向已經(jīng)存在的char數(shù)組對(duì)象

String s = "xyz";是怎么樣的邏輯:

首先去找字符串常量池找,看能不能找到“xyz”字符串的引用,如果字符串常量池中能找不到:

  • 創(chuàng)建一個(gè)String對(duì)象和char數(shù)組對(duì)象
  • 將創(chuàng)建的String對(duì)象封裝成HashtableEntry,作為StringTable的value進(jìn)行存儲(chǔ)
  • 返回創(chuàng)建的String對(duì)象

如果字符串常量池中能找到:

  • 直接返回找到引用對(duì)應(yīng)的String對(duì)象

總結(jié)而言就是:

對(duì)于String s = new String("xyz");這種形式創(chuàng)建字符串對(duì)象,如果字符串常量池中能找到,創(chuàng)建一個(gè)String對(duì)象;如果如果字符串常量池中找不到,創(chuàng)建兩個(gè)String對(duì)象。

對(duì)于String s = "xyz";這種形式創(chuàng)建字符串對(duì)象,如果字符串常量池中能找到,不會(huì)創(chuàng)建String對(duì)象;如果如果字符串常量池中找不到,創(chuàng)建一個(gè)String對(duì)象。

所以,在日常開發(fā)中,能用String s = "xyz";盡量不用String s = new String("xyz");,因?yàn)榭梢陨賱?chuàng)建一個(gè)對(duì)象,節(jié)省一部分空間。

需要強(qiáng)調(diào)的是,字符串常量池存的不是字符串也不是String對(duì)象,而是一個(gè)個(gè)HashtableEntry,HashtableEntry里面的value指向的才是String對(duì)象,為了不讓表述變得復(fù)雜,我省略了HashtableEntry的存在,但不代表它就不存在。

上文提到的駐留就是新建HashtableEntry指向String對(duì)象,并把HashtableEntry存入字符串常量池的過程。

在網(wǎng)上一些文章中,一些作者可能是為了讓讀者更好的理解,省略了一些這些,一定要注意辨別區(qū)分。

達(dá)成以上共識(shí)之后,我們?cè)倩仡櫼幌履莻€(gè)老套的筆試題。

String s1 = new String("xyz");
String s2 = "xyz";
//為什么輸出的是false呢?
System.out.println(s1 == s2);
//為什么輸出的是true呢?
System.out.println(s1.equals(s2));

有了上面的基礎(chǔ)之后,我們畫出對(duì)應(yīng)的內(nèi)存圖,s1 == s2為什么是false就一目了然了。

因?yàn)閑quals方法比較的真正的char數(shù)據(jù),而s1和s2最終指向的都是同一個(gè)char數(shù)組對(duì)象,所以s1.equals(s2)等于true。

關(guān)于他們最終指向的都是同一個(gè)char數(shù)組對(duì)象這一觀點(diǎn),也可以通過反射證明:

我修改了str1指向的String對(duì)象的value,str2指向的對(duì)象也被影響了。

字符串拼接

現(xiàn)在,我們?cè)賮砜匆幌伦兪筋}:

String s1 = "aa";
String s2 = "bb";
String str1 = s1 + s2;
String str2 = "aabb";
//為什么輸出的是false
System.out.println(str1 == str2);

對(duì)于這個(gè)題目,我們需要先看一下這段代碼的字節(jié)碼。

字節(jié)碼指令看不懂沒有關(guān)系,看我用紅色框框起來的部分就行了,可以看到居然出現(xiàn)了StringBuilder。

什么意思呢,就是說String str1 = s1 + s2;會(huì)被編譯器會(huì)優(yōu)化成new StringBuilder().append("aa").append("bb").toString();

StringBuilder里面的append方法就是對(duì)char數(shù)組進(jìn)行操作,那StringBuilder的toString方法做了什么呢?

從源碼中可以看到,StringBuilder里面的toString方法調(diào)用的是String類里面的String(char value[], int offset, int count)構(gòu)造方法,這個(gè)方法做了什么呢?

  • 根據(jù)參數(shù)復(fù)制一份char數(shù)組對(duì)象。復(fù)制了一份!
  • 創(chuàng)建一個(gè)String對(duì)象,String對(duì)象的value指向復(fù)制的char數(shù)組對(duì)象。

注意,并沒有駐留到字符串常量池里面去,這個(gè)很關(guān)鍵!?。‘嬕粋€(gè)圖理解一下:

也就是說str2指向的String對(duì)象并沒有駐留到字符串常量池,而str1指向的對(duì)象駐留到字符串常量池里面去了,且他們并不是同一個(gè)對(duì)象。所以str1 == str2還是false

因?yàn)閺?fù)制一份char數(shù)組對(duì)象,所以如果我們改變其中一個(gè)char數(shù)組的話,另一個(gè)也不會(huì)造成影響:

把其中String變成丑比之后,另一個(gè)還是帥比,也說明了兩個(gè)String對(duì)象用的不是同一份char數(shù)組。

intern方法

上面說到,調(diào)用StringBuilder的toString方法創(chuàng)建的String對(duì)象是不會(huì)駐留到字符串常量池的,那如果我偏要駐留到字符串常量池呢?有沒有辦法呢?

有的,String類的intern方法就可以幫你完成這個(gè)事情。

以這段代碼為例:

String s1 = "aa";
String s2 = "bb";
String str = s1 + s2;
str.intern();

在執(zhí)行str.intern();之前,內(nèi)存圖是這樣的:

在執(zhí)行str.intern();之后,內(nèi)存圖是這樣的:

intern方法就是創(chuàng)建了一個(gè)HashtableEntry對(duì)象,并把value指向String對(duì)象,然后把HashtableEntry通過hash定位存到對(duì)應(yīng)的字符串成常量池中。當(dāng)然,前提是字符串常量池中原來沒有對(duì)應(yīng)的HashtableEntry。

沒了,intern方法,就是這么簡(jiǎn)單,一句話給你說清楚了。

關(guān)于intern方法,還有一個(gè)很有趣的故事,有興趣的可以去看一下why神的這篇文章《深入理解Java虛擬機(jī)》第2版挖的坑終于在第3版中被R大填平了

編譯優(yōu)化

寫到這里,好像只有一個(gè)坑沒有填。就是這個(gè)題為什么輸出的是true。

final String s3 = "cc";
final String s4 = "dd";
String str3 = s3 + s4;
String str4 = "ccdd";
//為什么輸出的是true呢???
System.out.println(str3 == str4);

這道題和上面那道題相比,有點(diǎn)相似,在原來的基礎(chǔ)上加了兩個(gè)final關(guān)鍵字。我們先看一下這段代碼的字節(jié)碼:

又是一段字節(jié)碼指令,不需要看懂,你點(diǎn)一下#4,居然就可以看到“ccdd”字符串。

原來,用final修飾后,JDK的編譯器會(huì)識(shí)別優(yōu)化,會(huì)把String str3 = s3 + s4;優(yōu)化成String str3 = "ccdd"。

所以原題就相當(dāng)于:

String str3 = "ccdd";
String str4 = "ccdd";
//為什么輸出的是true呢???
System.out.println(str3 == str4);

這樣的題目還難嗎?是不是那不管str3和str4怎么比,肯定是相等的。

總結(jié)

String對(duì)于Java程序員來說就是“最熟悉的陌生人”,你說String簡(jiǎn)單,它確實(shí)簡(jiǎn)單。你說它難,深究起來確實(shí)也有難度,但這些題目,只要你腦海里有一副內(nèi)存圖就會(huì)很簡(jiǎn)單。

面試題也只會(huì)越來越難,這個(gè)行業(yè)看起來也越來越內(nèi)卷,但只要我學(xué)的快,內(nèi)卷就卷不到我。

好了,今天就寫到了,我要去打游戲了。

希望這篇文章,能對(duì)你有一點(diǎn)幫助。

寫在最后

我對(duì)每一篇發(fā)出去的文章負(fù)責(zé),文中涉及知識(shí)理論,我都會(huì)盡量在官方文檔和權(quán)威書籍找到并加以驗(yàn)證。但即使這樣,我也不能保證文章中每個(gè)點(diǎn)都是正確的,如果你發(fā)現(xiàn)錯(cuò)誤之處,歡迎指出,我會(huì)對(duì)其修正。

創(chuàng)作不易,為了更好的表達(dá),需要畫很多圖,這些都是我自己動(dòng)手用PPT畫的,畫圖也很辛苦的!

所以,不要猶豫了,給點(diǎn)正反饋,答應(yīng)我,十分歡迎并感謝你的關(guān)注

我是CoderW,一個(gè)程序員。

謝謝你的閱讀,我們下期再見!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容