色翁荡息又大又硬又粗又视频软件,人人妻人人爽.,人人妻人人狠人人爽天天综合网,欧美精品亚洲精品日韩已满十八 ,欧美激情猛片xxxⅹ大3

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

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

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

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

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

下面通過三行代碼讓大家對(duì)字符串常量池建立初步認(rèn)識(shí):

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

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

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

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

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

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

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

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

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

所以s1和s2指向的是兩個(gè)完全不同的對(duì)象, 判斷s1 == s2的時(shí)候會(huì)返回false。

如果上面的知識(shí)理解起來沒有問題的話, 下面看些難點(diǎn)的.

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

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

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

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

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

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

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

第二行代碼 s1.intern();

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

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

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

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

這種直接通過雙引號(hào)""聲明字符串背后的運(yùn)行機(jī)制我們?cè)诘谝粋€(gè)案例提到過, 這里正好復(fù)習(xí)一下。

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

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

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

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

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

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

配合反編譯代碼驗(yàn)證字符串初始化操作.

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

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

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

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

如果看這篇文章是你第一次往底層探索字符串的經(jīng)歷, 那我估計(jì)你不能立即給出答案. 因?yàn)槲业谝淮我娺@幾行代碼時(shí)也卡殼了。

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

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

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

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

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

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

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

0:ldc#2 // String hello

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

2:astore_1

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

3:ldc#3 // String world

5:astore_2

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

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

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

9:dup

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

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

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

13: aload_1

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

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

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

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

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

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

24:astore_3

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

激動(dòng)人心的時(shí)刻來了, 我們之所以不知道這道題的答案是因?yàn)椴恢雷址唇雍笫且詎ew的形式還是以雙引號(hào)""的形式創(chuàng)建字符串對(duì)象.

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

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

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

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

詳解字符串操作類

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

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

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

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

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

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

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

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

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

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

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

就因?yàn)檫@點(diǎn)區(qū)別, 使得兩者在操作字符串時(shí)在不同的場景下會(huì)體現(xiàn)出不同的效率。

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

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

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

我們?cè)俅瓮ㄟ^反編譯代碼看看造成兩者性能差距的原因, 先看String類. (為了方便閱讀代碼, 我刪除了計(jì)時(shí)部分的代碼, 并重新編譯, 得到的main方法反編譯代碼如下)

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

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

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

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

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

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

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

在拼接字符串時(shí), 如果是簡單的拼接, 比如說 Strings="hello "+"world";, String類的效率會(huì)更高一點(diǎn)。

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

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