1. 程式人生 > >Java中,那些關於String和字串常量池你不得不知道的東西

Java中,那些關於String和字串常量池你不得不知道的東西

## 老套的筆試題 在一些老套的筆試題中,會要你判斷s1==s2為false還是true,s1.equals(s2)為false還是true。 ```java String s1 = new String("xyz"); String s2 = "xyz"; System.out.println(s1 == s2); System.out.println(s1.equals(s2)); ``` 對於這種題,你總能很快的給出標準答案:==比較的是物件地址,equals方法比較的是真正的字元陣列。所以輸出的是false和true。 上面的屬於最低階的題目,沒有什麼難度。 現在這種老套的題目已經慢慢消失了,取而代之的是有一些變形的新題目: ```java 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。 今天的文章就是以這幾個題目展開的。 ## String物件的建立 先簡單看一下String類的結構: 可以發現,String裡面有一個value屬性,是真正儲存字元的char陣列。 在執行`String s = "xyz";`的時候,在堆區建立了一個String物件,一個char陣列物件。 如何證明建立了一個String物件和一個char陣列物件呢?我們可以通過IDEA的Debug功能驗證: 注意看我截圖的位置,在執行完`String s = "xyz";`之後,再次點選load classes,Diff欄的String和char[]分別加了1,表示在記憶體中新增了一個char陣列物件和一個String物件。 現在,我們再來看`String s = new String("xyz");`建立了幾個物件。
從這張Debug動圖中,我們可以得出在`String s = new String("xyz");`之後,建立了兩個String物件和一個char陣列物件。 又因為`String s = new String("xyz");`的`s`引用只能指向一個物件,可以畫出記憶體分佈圖: 從圖中可以看到,在堆區,有兩個String物件,這兩個String物件的value都指向同一個char陣列物件。 那麼問題來了,下面的那個String物件根本就沒被引用,也就是說他沒有被用到,那麼它到底是幹什麼的呢? 佔了記憶體空間又不使用,難道這是JDK的設計缺陷? 很顯然不是JDK的缺陷,JDK雖然確實有設計缺陷,但不至於這麼明顯,這麼愚蠢。 那下面的那個String物件是幹什麼的呢? 答案是用於駐留到字串常量池中去的,注意,這裡我用了一個`駐留`,並不是直接把物件放到字串常量池裡面去,有什麼區別我們後面再講。 這裡出現了`字串常量池`的概念,我在[String s = new String("xyz")建立了幾個例項你真的能答對嗎?](https://mp.weixin.qq.com/s/ga9kwZYqu45a0sd_wsRalA)中也有過比較詳細的介紹,有興趣的可以去看一下,這裡不再重複了。 你只需要知道,字串常量池在JVM原始碼中對應的類是StringTable,底層實現是一個Hashtable。
我們以`String s = new String("xyz");`為例: 首先去找字串常量池找,看能不能找到“xyz”字串對應物件的引用,如果字串常量池中找不到: - 建立一個String物件和char陣列物件 - 將建立的String物件封裝成HashtableEntry,作為StringTable的value進行儲存 - new String("xyz")會在堆區又建立一個String物件,char陣列直接指向建立好的char陣列物件 如果字串常量池中能找到: - new String("xyz")會在堆區建立一個物件,char陣列直接指向已經存在的char陣列物件
而`String s = "xyz";`是怎麼樣的邏輯: 首先去找字串常量池找,看能不能找到“xyz”字串的引用,如果字串常量池中能找不到: - 建立一個String物件和char陣列物件 - 將建立的String物件封裝成HashtableEntry,作為StringTable的value進行儲存 - 返回建立的String物件 如果字串常量池中能找到: - 直接返回找到引用對應的String物件 總結而言就是: 對於`String s = new String("xyz");`這種形式建立字串物件,如果字串常量池中能找到,建立一個String物件;如果如果字串常量池中找不到,建立兩個String物件。 對於`String s = "xyz";`這種形式建立字串物件,如果字串常量池中能找到,不會建立String物件;如果如果字串常量池中找不到,建立一個String物件。 所以,在日常開發中,能用`String s = "xyz";`儘量不用`String s = new String("xyz");`,因為可以少建立一個物件,節省一部分空間。 需要強調的是,字串常量池存的不是字串也不是String物件,而是一個個HashtableEntry,HashtableEntry裡面的value指向的才是String物件,為了不讓表述變得複雜,我省略了HashtableEntry的存在,但不代表它就不存在。 上文提到的駐留就是新建HashtableEntry指向String物件,並把HashtableEntry存入字串常量池的過程。 在網上一些文章中,一些作者可能是為了讓讀者更好的理解,省略了一些這些,一定要注意辨別區分。 達成以上共識之後,我們再回顧一下那個老套的筆試題。 ```java String s1 = new String("xyz"); String s2 = "xyz"; //為什麼輸出的是false呢? System.out.println(s1 == s2); //為什麼輸出的是true呢? System.out.println(s1.equals(s2)); ``` 有了上面的基礎之後,我們畫出對應的記憶體圖,s1 == s2為什麼是false就一目瞭然了。 因為equals方法比較的真正的char資料,而s1和s2最終指向的都是同一個char陣列物件,所以s1.equals(s2)等於true。 關於他們最終指向的都是同一個char陣列物件這一觀點,也可以通過反射證明: 我修改了str1指向的String物件的value,str2指向的物件也被影響了。 ## 字串拼接 現在,我們再來看一下變式題: ```java String s1 = "aa"; String s2 = "bb"; String str1 = s1 + s2; String str2 = "aabb"; //為什麼輸出的是false System.out.println(str1 == str2); ``` 對於這個題目,我們需要先看一下這段程式碼的位元組碼。 位元組碼指令看不懂沒有關係,看我用紅色框框起來的部分就行了,可以看到居然出現了StringBuilder。 什麼意思呢,就是說`String str1 = s1 + s2;`會被編譯器會優化成`new StringBuilder().append("aa").append("bb").toString();` StringBuilder裡面的append方法就是對char陣列進行操作,那StringBuilder的toString方法做了什麼呢? 從原始碼中可以看到,StringBuilder裡面的toString方法呼叫的是String類裡面的`String(char value[], int offset, int count)`構造方法,這個方法做了什麼呢? - 根據引數複製一份char陣列物件。複製了一份! - 建立一個String物件,String物件的value指向複製的char陣列物件。 注意,並沒有駐留到字串常量池裡面去,這個很關鍵!!!畫一個圖理解一下: 也就是說str2指向的String物件並沒有駐留到字串常量池,而str1指向的物件駐留到字串常量池裡面去了,且他們並不是同一個物件。所以str1 == str2還是false 因為複製一份char陣列物件,所以如果我們改變其中一個char陣列的話,另一個也不會造成影響: 把其中String變成醜比之後,另一個還是帥比,也說明了兩個String物件用的不是同一份char陣列。 ## intern方法 上面說到,呼叫StringBuilder的toString方法建立的String物件是不會駐留到字串常量池的,那如果我偏要駐留到字串常量池呢?有沒有辦法呢? 有的,String類的intern方法就可以幫你完成這個事情。 以這段程式碼為例: ```java String s1 = "aa"; String s2 = "bb"; String str = s1 + s2; str.intern(); ``` 在執行`str.intern();`之前,記憶體圖是這樣的: 在執行`str.intern();`之後,記憶體圖是這樣的: intern方法就是建立了一個HashtableEntry物件,並把value指向String物件,然後把HashtableEntry通過hash定位存到對應的字串成常量池中。當然,前提是字串常量池中原來沒有對應的HashtableEntry。 沒了,intern方法,就是這麼簡單,一句話給你說清楚了。 關於intern方法,還有一個很有趣的故事,有興趣的可以去看一下why神的這篇文章[《深入理解Java虛擬機器》第2版挖的坑終於在第3版中被R大填平了](https://mp.weixin.qq.com/s/CrPXwNfN1LrUDDjjJ1c7rQ) ## 編譯優化 寫到這裡,好像只有一個坑沒有填。就是這個題為什麼輸出的是true。 ```Java final String s3 = "cc"; final String s4 = "dd"; String str3 = s3 + s4; String str4 = "ccdd"; //為什麼輸出的是true呢??? System.out.println(str3 == str4); ``` 這道題和上面那道題相比,有點相似,在原來的基礎上加了兩個final關鍵字。我們先看一下這段程式碼的位元組碼: 又是一段位元組碼指令,不需要看懂,你點一下#4,居然就可以看到“ccdd”字串。 原來,用final修飾後,JDK的編譯器會識別優化,會把`String str3 = s3 + s4;`優化成`String str3 = "ccdd"`。 所以原題就相當於: ```java String str3 = "ccdd"; String str4 = "ccdd"; //為什麼輸出的是true呢??? System.out.println(str3 == str4); ``` 這樣的題目還難嗎?是不是那不管str3和str4怎麼比,肯定是相等的。 ## 總結 String對於Java程式設計師來說就是“最熟悉的陌生人”,你說String簡單,它確實簡單。你說它難,深究起來確實也有難度,但這些題目,只要你腦海裡有一副記憶體圖就會很簡單。 面試題也只會越來越難,這個行業看起來也越來越內卷,但只要我學的快,內卷就卷不到我。 好了,今天就寫到了,我要去打遊戲了。 希望這篇文章,能對你有一點幫助。 ## 寫在最後(求關注) 我對每一篇發出去的文章負責,文中涉及知識理論,我都會盡量在官方文件和權威書籍找到並加以驗證。但即使這樣,我也不能保證文章中每個點都是正確的,如果你發現錯誤之處,歡迎指出,我會對其修正。 創作不易,為了更好的表達,需要畫很多圖,這些都是我自己動手用PPT畫的,畫圖也很辛苦的! 所以,不要猶豫了,給點正反饋,答應我,一鍵三連(關注、點贊、再看)好嗎? 我是CoderW,一個程式設計師。 謝謝你的閱讀,我們下期再見! > 更多精彩關注微信公眾號【C