老套的筆試題
在一些老套的筆試題中,會(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è)程序員。
謝謝你的閱讀,我們下期再見!