国产精品成人一区二区三区,7777色鬼xxxx欧美色妇,国产精品久久久久久人妻精品,欧美精品中文字幕亚洲专区,欧美精品xxxxbbbb

搞懂String,StringBuilder,StringBuffer的實現(xiàn)怎么回事

在深入學(xué)習(xí)字符串類之前, 我們先搞懂JVM是怎樣處理新生字符串的. 當(dāng)你知道字符串的初始化細節(jié)后, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數(shù)。

首先得搞懂字符串常量池的概念。

常量池是Java的一項技術(shù), 八種基礎(chǔ)數(shù)據(jù)類型除了float和double都實現(xiàn)了常量池技術(shù). 這項技術(shù)從字面上是很好理解的: 把經(jīng)常用到的數(shù)據(jù)存放在某塊內(nèi)存中, 避免頻繁的數(shù)據(jù)創(chuàng)建與銷毀, 實現(xiàn)數(shù)據(jù)共享, 提高系統(tǒng)性能。

字符串常量池是Java常量池技術(shù)的一種實現(xiàn), 在近代的JDK版本中(1.7后), 字符串常量池被實現(xiàn)在Java堆內(nèi)存中。

下面通過三行代碼讓大家對字符串常量池建立初步認識:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

我們先來看看第一行代碼 Strings1="hello";干了什么.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

對于這種直接通過雙引號""聲明字符串的方式, 虛擬機首先會到字符串常量池中查找該字符串是否已經(jīng)存在. 如果存在會直接返回該引用, 如果不存在則會在堆內(nèi)存中創(chuàng)建該字符串對象, 然后到字符串常量池中注冊該字符串。

在本案例中虛擬機首先會到字符串常量池中查找是否有存在"hello"字符串對應(yīng)的引用. 發(fā)現(xiàn)沒有后會在堆內(nèi)存創(chuàng)建"hello"字符串對象(內(nèi)存地址0x0001), 然后到字符串常量池中注冊地址為0x0001的"hello"對象, 也就是添加指向0x0001的引用. 最后把字符串對象返回給s1。

溫馨提示: 圖中的字符串常量池中的數(shù)據(jù)是虛構(gòu)的, 由于字符串常量池底層是用HashTable實現(xiàn)的, 存儲的是鍵值對, 為了方便大家理解, 示意圖簡化了字符串常量池對照表, 并采用了一些虛擬的數(shù)值。

下面看 Strings2=newString("hello");的示意圖:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

當(dāng)我們使用new關(guān)鍵字創(chuàng)建字符串對象的時候, JVM將不會查詢字符串常量池, 它將會直接在堆內(nèi)存中創(chuàng)建一個字符串對象, 并返回給所屬變量。

所以s1和s2指向的是兩個完全不同的對象, 判斷s1 == s2的時候會返回false。

如果上面的知識理解起來沒有問題的話, 下面看些難點的.

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

第一行代碼 Strings1=newString("hello ")+newString("world");的執(zhí)行過程是這樣子的:

1.依次在堆內(nèi)存中創(chuàng)建"hello "和"world"兩個字符串對象

2.然后把它們拼接起來 (底層使用StringBuilder實現(xiàn), 后面會帶大家讀反編譯代碼)

3.在拼接完成后會產(chǎn)生新的"hello world"對象, 這時變量s1指向新對象"hello world"

執(zhí)行完第一行代碼后, 內(nèi)存是這樣子的:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

第二行代碼 s1.intern();

String類的源碼中有對 intern()方法的詳細介紹, 翻譯過來的意思是: 當(dāng)調(diào)用 intern()方法時, 首先會去常量池中查找是否有該字符串對應(yīng)的引用, 如果有就直接返回該字符串; 如果沒有, 就會在常量池中注冊該字符串的引用, 然后返回該字符串。

由于第一行代碼采用的是new的方式創(chuàng)建字符串, 所以在字符串常量池中沒有保存"hello world"對應(yīng)的引用, 虛擬機會在常量池中進行注冊, 注冊完后的內(nèi)存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

第三行代碼 Strings2="hello world";

這種直接通過雙引號""聲明字符串背后的運行機制我們在第一個案例提到過, 這里正好復(fù)習(xí)一下。

首先虛擬機會去檢查字符串常量池, 發(fā)現(xiàn)有指向"hello world"的引用. 然后把該引用所指向的字符串直接返回給所屬變量。

執(zhí)行完第三行代碼后, 內(nèi)存示意圖如下:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

如圖所示, s1和s2指向的是相同的對象, 所以當(dāng)判斷s1 == s2時返回true。

最后我們對字符串常量池進行總結(jié):

當(dāng)用new關(guān)鍵字創(chuàng)建字符串對象時, 不會查詢字符串常量池; 當(dāng)用雙引號直接聲明字符串對象時, 虛擬機將會查詢字符串常量池. 說白了就是: 字符串常量池提供了字符串的復(fù)用功能, 除非我們要顯式創(chuàng)建新的字符串對象, 否則對同一個字符串虛擬機只會維護一份拷貝。

配合反編譯代碼驗證字符串初始化操作.

相信看到這里, 再見到有關(guān)的面試題, 你已經(jīng)無所畏懼了, 因為你已經(jīng)懂得了背后原理。

在結(jié)束之前我們不妨再做一道壓軸題

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

這道壓軸題是經(jīng)過精心設(shè)計的, 它不但照應(yīng)上面所講的字符串常量池知識, 也引出了后面的話題.

如果看這篇文章是你第一次往底層探索字符串的經(jīng)歷, 那我估計你不能立即給出答案. 因為我第一次見這幾行代碼時也卡殼了。

首先第一行和第二行是常規(guī)的字符串對象聲明, 我們已經(jīng)很熟悉了, 它們分別會在堆內(nèi)存創(chuàng)建字符串對象, 并會在字符串常量池中進行注冊。

影響我們做出判斷的是第三行代碼 Strings3=s1+s2;, 我們不知道 s1+s2在創(chuàng)建完新字符串"hello world"后是否會在字符串常量池進行注冊。

說白了就是我們不知道這行代碼是以雙引號""形式聲明字符串, 還是用new關(guān)鍵字創(chuàng)建字符串。

這時, 我們應(yīng)該去讀一讀這段代碼的反編譯代碼. 如果你沒有讀過反編譯代碼, 不妨借此機會入門。

在命令行中輸入 javap-c對應(yīng).class文件的絕對路徑, 按回車后即可看到反編譯文件的代碼段。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

首先調(diào)用構(gòu)造器完成Main類的初始化

0:ldc#2 // String hello

從常量池中獲取"hello "字符串并推送至棧頂, 此時拿到了"hello "的引用

2:astore_1

將棧頂?shù)淖址么嫒氲诙€本地變量s1, 也就是s1已經(jīng)指向了"hello "

3:ldc#3 // String world

5:astore_2

重復(fù)開始的步驟, 此時變量s2指向"word"

6:new#4 // class java/lang/StringBuilder

刺激的東西來了: 這時創(chuàng)建了一個StringBuilder, 并把其引用值壓到棧頂

9:dup

復(fù)制棧頂?shù)闹? 并繼續(xù)壓入棧定, 也就意味著棧從上到下有兩份StringBuilder的引用, 將來要操作兩次StringBuilder.

10:invokespecial#5 // Method java/lang/StringBuilder."":()V

調(diào)用StringBuilder的一些初始化方法, 靜態(tài)方法或父類方法, 完成初始化.

13: aload_1

把第二個本地變量也就是s1壓入棧頂, 現(xiàn)在棧頂從上往下數(shù)兩個數(shù)據(jù)依次是:s1變量和StringBuilder的引用

14:invokevirtual#6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;

調(diào)用StringBuilder的append方法, 棧頂?shù)膬蓚€數(shù)據(jù)在這里調(diào)用方法時就用上了.

接下來又調(diào)用了一次append方法(之前StringBuilder的引用拷貝兩份就用途在此)

完成后, StringBuilder中已經(jīng)拼接好了"hello world", 看到這里相信大家已經(jīng)明白虛擬機是如何拼接字符串的了. 接下來就是關(guān)鍵環(huán)節(jié)

21:invokevirtual#7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

24:astore_3

拼接完字符串后, 虛擬機調(diào)用StringBuilder的 toString()方法獲得字符串 hello world, 并存放至s3.

激動人心的時刻來了, 我們之所以不知道這道題的答案是因為不知道字符串拼接后是以new的形式還是以雙引號""的形式創(chuàng)建字符串對象.

下面是我們追蹤StringBuilder的 toString()方法源碼:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

ok, 這道題解了, s3是通過new關(guān)鍵字獲得字符串對象的。

回到題目, 也就是說字符串常量表中沒有存儲"hello world"的引用, 當(dāng)s4以引號的形式聲明字符串時, 由于在字符串常量池中查不到相應(yīng)的引用, 所以會在堆內(nèi)存中新創(chuàng)建一個字符串對象. 所以s3和s4指向的不是同一個字符串對象, 結(jié)果為false。

詳解字符串操作類

明白了字符串常量池, 我相信關(guān)于字符串的創(chuàng)建你已經(jīng)有十足的把握了. 但是這還不夠, 作為一名合格的Java工程師, 我們還必須對字符串的操作做到了如指掌. 注意! 不是說你不用查api能熟練操作字符串就了如指掌了, 而是說對String, StringBuilder, StringBuffer三大字符串操作類背后的實現(xiàn)了然于胸, 這樣才能在開發(fā)的過程中做出正確, 高效的選擇。

String, StringBuilder, StringBuffer的底層實現(xiàn)

點進String的源碼, 我們可以看見String類是通過char類型數(shù)組實現(xiàn)的。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

接著查看StringBuilder和StringBuffer的源碼, 我們發(fā)現(xiàn)這兩者都繼承自AbstractStringBuilder類, 通過查看該類的源碼, 得知StringBuilder和StringBuffer兩個類也是通過char類型數(shù)組實現(xiàn)的。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

而且通過StringBuilder和StringBuffer繼承自同一個父類這點, 我們可以推斷出它倆的方法都是差不多的. 通過查看源碼也發(fā)現(xiàn)確實如此, 只不過StringBuffer在方法上添加了 synchronized關(guān)鍵字, 證明它的方法絕大多數(shù)方法都是線程同步方法. 也就是說在多線程的環(huán)境下我們應(yīng)該使用StringBuffer以保證線程安全, 在單線程環(huán)境下我們應(yīng)使用StringBuilder以獲得更高的效率。

既然如此, 我們的比較也就落到了StringBuilder和String身上了。

關(guān)于StringBuilder和String之間的討論

通過查看StringBuilder和String的源碼我們會發(fā)現(xiàn)兩者之間一個關(guān)鍵的區(qū)別: 對于String, 凡是涉及到返回參數(shù)類型為String類型的方法, 在返回的時候都會通過new關(guān)鍵字創(chuàng)建一個新的字符串對象; 而對于StringBuilder, 大多數(shù)方法都會返回StringBuilder對象自身。

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

就因為這點區(qū)別, 使得兩者在操作字符串時在不同的場景下會體現(xiàn)出不同的效率。

下面還是以拼接字符串為例比較一下兩者的性能:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

就拼接5萬次字符串而言, StringBuilder的效率是String類的956倍。

我們再次通過反編譯代碼看看造成兩者性能差距的原因, 先看String類. (為了方便閱讀代碼, 我刪除了計時部分的代碼, 并重新編譯, 得到的main方法反編譯代碼如下)

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

從反匯編代碼中可以看到, 當(dāng)用String類拼接字符串時, 每次都會生成一個StringBuilder對象, 然后調(diào)用兩次append()方法把字符串拼接好, 最后通過StringBuilder的toString()方法new出一個新的字符串對象。

也就是說每次拼接都會new出兩個對象, 并進行兩次方法調(diào)用, 如果拼接的次數(shù)過多, 創(chuàng)建對象所帶來的時延會降低系統(tǒng)效率, 同時會造成巨大的內(nèi)存浪費. 而且當(dāng)內(nèi)存不夠用時, 虛擬機會進行垃圾回收, 這也是一項相當(dāng)耗時的操作, 會大大降低系統(tǒng)性能。

下面是使用StringBuilder拼接字符串得到的反編譯代碼:

從底層徹底搞懂String,StringBuilder,StringBuffer的實現(xiàn)

可以看到StringBuilder拼接字符串就簡單多了, 直接把要拼接的字符串放到棧頂進行append就完事了, 除了開始時創(chuàng)建了StringBuilder對象, 運行時期沒有創(chuàng)建過其他任何對象, 每次循環(huán)只調(diào)用一次append方法. 所以從效率上看, 拼接大量字符串時, StringBuilder要比String類給力得多。

當(dāng)然String類也不是沒有優(yōu)勢的, 從操作字符串a(chǎn)pi的豐富度上來講, String是要多于StringBuilder的, 在日常操作中很多業(yè)務(wù)都需要用到String類的api。

在拼接字符串時, 如果是簡單的拼接, 比如說 Strings="hello "+"world";, String類的效率會更高一點。

但如果需要拼接大量字符串, StringBuilder無疑是更合適的選擇。

講到這里, Java中的字符串背后的原理就講得差不多, 相信在了解虛擬機操作字符串的細節(jié)后, 你在使用字符串時會更加得心應(yīng)手. 字符串是編程中一個重要的話題, 本文圍繞Java體系講解的字符串知識只是字符串知識的冰山一角. 字符串操作的背后是數(shù)據(jù)結(jié)構(gòu)和算法的應(yīng)用, 如何能夠以盡可能低的時間復(fù)雜度去操作字符串, 又是一門大學(xué)問。