作者:每次上網(wǎng)沖杯Java時(shí),都能看到關(guān)于String無(wú)休無(wú)止的爭(zhēng)論。還是覺得有必要讓這個(gè)討厭又很可愛的String美眉,赤裸裸的站在我們這些Java色狼面前了。嘿嘿....
眾所周知,String是由字符組成的串,在程序中使用頻率很高。Java中的String是一個(gè)類,而并非基本數(shù)據(jù)類型。 不過她卻不是普通的類哦!!!
?
【鏡頭1】 String對(duì)象的創(chuàng)建
????? 1、關(guān)于類對(duì)象的創(chuàng)建,很普通的一種方式就是利用構(gòu)造器,String類也不例外:String s=new String("Hello world"); 問題是參數(shù)"Hello world"是什么東西,也是字符串對(duì)象嗎?莫非用字符串對(duì)象創(chuàng)建一個(gè)字符串對(duì)象?
????? 2、當(dāng)然,String類對(duì)象還有一種大家都很喜歡的創(chuàng)建方式:String s="Hello world"; 但是有點(diǎn)怪呀,怎么與基本數(shù)據(jù)類型的賦值操作(int i=1)很像呀?
????? 在開始解釋這些問題之前,我們先引入一些必要的知識(shí):
★ Java class文件結(jié)構(gòu)
和常量池
????? 我們都知道,Java程序要運(yùn)行,首先需要編譯器將源代碼文件編譯成字節(jié)碼文件(也就是.class文件)。然后在由JVM解釋執(zhí)行。
????? class文件是8位字節(jié)的
二進(jìn)制流
。這些二進(jìn)制流的涵義由一些
緊湊的有意義的項(xiàng)
組成。比如class字節(jié)流中最開始的4個(gè)字節(jié)組成的項(xiàng)叫做
魔數(shù)
(magic),其意義在于分辨class文件(值為0xCAFEBABE)與非class文件。class字節(jié)流大致結(jié)構(gòu)如下圖左側(cè)。
??????????????? ? ? ? ? ? ?? ??
????? 其中,在class文件中有一個(gè)非常重要的項(xiàng)—— 常量池 。這個(gè)常量池專門放置源代碼中的符號(hào)信息(并且不同的符號(hào)信息放置在不同標(biāo)志的常量表中)。如上圖右側(cè)是HelloWorld代碼中的常量表(HelloWorld代碼如下),其中有四個(gè)不同類型的常量表(四個(gè)不同的常量池入口)。 關(guān)于常量池的具體細(xì)節(jié),請(qǐng)參照我的博客《 Class文件內(nèi)容及常量池 》
public class HelloWorld{ void hello(){ System.out.println("Hello world"); } }
????? 通過上圖可見,代碼中的"Hello world"字符串字面值被編譯之后,可以清楚的看到存放在了class常量池中的字符串常量表中(上圖右側(cè)紅框區(qū)域)。
?
★ JVM運(yùn)行class文件
????? 源代碼編譯成class文件之后,JVM就要運(yùn)行這個(gè)class文件。它首先會(huì)用類裝載器加載進(jìn)class文件。然后需要?jiǎng)?chuàng)建許多內(nèi)存數(shù)據(jù)結(jié)構(gòu)來(lái)存放class文件中的字節(jié)數(shù)據(jù)。比如class文件對(duì)應(yīng)的類信息數(shù)據(jù)、常量池結(jié)構(gòu)、方法中的二進(jìn)制指令序列、類方法與字段的描述信息等等。當(dāng)然,在運(yùn)行的時(shí)候,還需要為方法創(chuàng)建棧幀等。這么多的內(nèi)存結(jié)構(gòu)當(dāng)然需要管理,JVM會(huì)把這些東西都組織到幾個(gè)“ 運(yùn)行時(shí)數(shù)據(jù)區(qū) ”中。這里面就有我們經(jīng)常說(shuō)的“ 方法區(qū) ”、“ 堆 ”、“ Java棧 ”等。 詳細(xì)請(qǐng)參見我的博客《 Java 虛擬機(jī)體系結(jié)構(gòu) 》 。
?
????? 上面我們提到了, 在Java源代碼中的每一個(gè)字面值字符串,都會(huì)在編譯成class文件階段,形成標(biāo)志號(hào) 為8(CONSTANT_String_info)的常量表 。 當(dāng)JVM加載 class文件的時(shí)候,會(huì)為對(duì)應(yīng)的常量池建立一個(gè)內(nèi)存數(shù)據(jù)結(jié)構(gòu),并存放在方法區(qū)中。同時(shí)JVM會(huì)自動(dòng)為CONSTANT_String_info常量表中 的字符串常量字面值 在堆中 創(chuàng)建 新的String對(duì)象(intern字符串 對(duì)象 ,又叫拘留字符串對(duì)象)。然后把CONSTANT_String_info常量表的入口地址轉(zhuǎn)變成這個(gè)堆中String對(duì)象的直接地址(常量池解 析)。?
?
????? 這里很關(guān)鍵的就是這個(gè) 拘留字符串對(duì)象 。 源代碼中所有相同字面值的字符串常量只可能建立唯一一個(gè)拘留字符串對(duì)象。 實(shí)際上JVM是通過一個(gè)記錄了拘留字符串引用的內(nèi)部數(shù)據(jù)結(jié)構(gòu)來(lái)維持這一特性的。在Java程序中,可以調(diào)用String的intern()方法來(lái)使得一個(gè)常規(guī)字符串對(duì)象成為拘留字符串對(duì)象。我們會(huì)在后面介紹這個(gè)方法的。
?
★
操作碼助憶符指令
? ? ? 有了上面闡述的兩個(gè)知識(shí)前提,下面我們將根據(jù)二進(jìn)制指令來(lái)區(qū)別兩種字符串對(duì)象的創(chuàng)建方式: ?
????? (1) String s=new String("Hello world");編譯成class文件后的指令(在myeclipse中查看):
0 new java.lang.String [15] //在堆中分配一個(gè)String類對(duì)象的空間,并將該對(duì)象的地址堆入操作數(shù)棧。 3 dup //復(fù)制操作數(shù)棧頂數(shù)據(jù),并壓入操作數(shù)棧。該指令使得操作數(shù)棧中有兩個(gè)String對(duì)象的引用值。 4 ldc <String "Hello world"> [17] //將常量池中的字符串常量"Hello world"指向的堆中拘留String對(duì)象的地址壓入操作數(shù)棧 6 invokespecial java.lang.String(java.lang.String) [19] //調(diào)用String的初始化方法,彈出操作數(shù)棧棧頂?shù)膬蓚€(gè)對(duì)象地址,用拘留String對(duì)象的值初始化new指令創(chuàng)建的String對(duì)象,然后將這個(gè)對(duì)象的引用壓入操作數(shù)棧 9 astore_1 [s] // 彈出操作數(shù)棧頂數(shù)據(jù)存放在局部變量區(qū)的第一個(gè)位置上。此時(shí)存放的是new指令創(chuàng)建出的,已經(jīng)被初始化的String對(duì)象的地址。
????? 事實(shí)上,在運(yùn)行這段指令之前, JVM就已經(jīng)為"Hello world"在堆中創(chuàng)建了一個(gè)拘留字符串( 值得注意的是:如果源程序中還有一個(gè)"Hello world"字符串常量,那么他們都對(duì)應(yīng)了同一個(gè)堆中的拘留字符串)。然后用這個(gè)拘留字符串的值來(lái)初始化堆中用new指令創(chuàng)建出來(lái)的新的String對(duì)象,局部變量s實(shí)際上存儲(chǔ)的是new出來(lái)的堆對(duì)象地址。 大家注意了,此時(shí)在JVM管理的堆中,有兩個(gè)相同字符串值的String對(duì)象:一個(gè)是拘留字符串對(duì)象,一個(gè)是new新建的字符串對(duì)象。如果還有一條創(chuàng)建語(yǔ)句String s1=new String("Hello world");堆中有幾個(gè)值為"Hello world"的字符串呢? 答案是3個(gè),大家好好想想為什么吧!
?
????? (2)將String s="Hello world";編譯成class文件后的指令:
0 ldc <String "Hello world"> [15]//將常量池中的字符串常量"Hello world"指向的堆中拘留String對(duì)象的地址壓入操作數(shù)棧 2 astore_1 [str] // 彈出操作數(shù)棧頂數(shù)據(jù)存放在局部變量區(qū)的第一個(gè)位置上。此時(shí)存放的是拘留字符串對(duì)象在堆中的地址
????? 和上面的創(chuàng)建指令有很大的不同, 局部變量s存儲(chǔ)的是早已創(chuàng)建好的拘留字符串的堆地址。 大家好好想想,如果還有一條穿件語(yǔ)句String s1="Hello word";此時(shí)堆中有幾個(gè)值為"Hello world"的字符串呢?答案是1個(gè)。那么局部變量s與s1存儲(chǔ)的地址是否相同呢?? 呵呵, 這個(gè)你應(yīng)該知道了吧。
?
?
★ 鏡頭總結(jié): String類型脫光了其實(shí)也很普通。真正讓她神秘的原因就在于 CONSTANT_String_info常量表 和 拘留字符串對(duì)象 的存在。現(xiàn)在我們可以解決江湖上的許多紛爭(zhēng)了。
? 【 紛爭(zhēng)1】關(guān)于字符串相等關(guān)系的爭(zhēng)論
//代碼1 String sa=new String("Hello world"); String sb=new String("Hello world"); System.out.println(sa==sb); // false //代碼2 String sc="Hello world"; String sd="Hello world"; System.out.println(sc==sd); // true
?????? 代碼1中局部變量sa,sb中存儲(chǔ)的是JVM在堆中new出來(lái)的兩個(gè)String對(duì)象的內(nèi)存地址。雖然這兩個(gè)String對(duì)象的值(char[]存放的字符序列)都是"Hello world"。 因此"=="比較的是兩個(gè)不同的堆地址。 代碼2中局部變量sc,sd中存儲(chǔ)的也是地址,但卻都是常量池中"Hello world"指向的堆的唯一的那個(gè)拘留字符串對(duì)象的地址 。自然相等了。
? 【紛爭(zhēng)2】 字符串“+”操作的內(nèi)幕
//代碼1 String sa = "ab"; String sb = "cd"; String sab=sa+sb; String s="abcd"; System.out.println(sab==s); // false //代碼2 String sc="ab"+"cd"; String sd="abcd"; System.out.println(sc==sd); //true
?????? 代碼1中局部變量sa,sb存儲(chǔ)的是堆中兩個(gè)拘留字符串對(duì)象的地址。
而當(dāng)執(zhí)行sa+sb時(shí),JVM首先會(huì)在堆中創(chuàng)建一個(gè)StringBuilder類,同時(shí)用sa指向的拘留字符串對(duì)象完成初始化,然后調(diào)用append方法完成對(duì)sb所指向的拘留字符串的合并操作,接著調(diào)用StringBuilder的toString()方法在堆中創(chuàng)建一個(gè)String對(duì)象,最后將剛生成的String對(duì)象的堆地址存放在局部變量sab中。而局部變量s存儲(chǔ)的是常量池中"abcd"所對(duì)應(yīng)的拘留字符串對(duì)象的地址。
sab與s地址當(dāng)然不一樣了。這里要注意了,代碼1的堆中實(shí)際上有五個(gè)字符串對(duì)象:三個(gè)拘留字符串對(duì)象、一個(gè)String對(duì)象和一個(gè)StringBuilder對(duì)象。
?????
代碼2中"ab"+"cd"會(huì)直接在編譯期就合并成常量"abcd",
因此相同字面值常量"abcd"所對(duì)應(yīng)的是同一個(gè)拘留字符串對(duì)象,自然地址也就相同。
?
?
?
?
【鏡頭二】? String三姐妹(String,StringBuffer,StringBuilder)
??????? String扒的差不多了。但他還有兩個(gè)妹妹StringBuffer,StringBuilder長(zhǎng)的也不錯(cuò)哦!我們也要下手了:
?
?? ?????????????????????? String(大姐,出生于JDK1.0時(shí)代)????????? 不可變字符序列
??? ?????????????????????? StringBuffer(二姐,出生于JDK1.0時(shí)代)??? 線程安全的可變字符序列
??? ?????????????????????? StringBuilder(小妹,出生于JDK1.5時(shí)代)?? 非線程安全的可變字符序列
?
★StringBuffer與String的可變性問題。
???????? 我們先看看這兩個(gè)類的部分源代碼:
//String public final class String { private final char value[]; public String(String original) { // 把原字符串original切分成字符數(shù)組并賦給value[]; } } //StringBuffer public final class StringBuffer extends AbstractStringBuilder { char value[]; //繼承了父類AbstractStringBuilder中的value[] public StringBuffer(String str) { super(str.length() + 16); //繼承父類的構(gòu)造器,并創(chuàng)建一個(gè)大小為str.length()+16的value[]數(shù)組 append(str); //將str切分成字符序列并加入到value[]中 } }
????? 很顯然,String和StringBuffer中的value[]都用于存儲(chǔ)字符序列。但是,
?????
(1) String中的是常量(final)數(shù)組,只能被賦值一次。
????? 比如:new String("abc")使得value[]={'a','b','c'},之后這個(gè)String對(duì)象中的value[]再也不能改變了。這也正是大家常說(shuō)的,
String是不可變的原因
。???
????
?
注意:這個(gè)對(duì)初學(xué)者來(lái)說(shuō)有個(gè)誤區(qū),有人說(shuō)String str1=new String("abc"); str1=new String("cba");不是改變了字符串str1嗎?那么你有必要先搞懂對(duì)象引用和對(duì)象本身的區(qū)別。這里我簡(jiǎn)單的說(shuō)明一下,對(duì)象本身指的是存放在堆空間中的該對(duì)象的實(shí)例數(shù)據(jù)(非靜態(tài)非常量字段)。而對(duì)象引用指的是堆中對(duì)象本身所存放的地址,一般方法區(qū)和Java棧中存儲(chǔ)的都是對(duì)象引用,而非對(duì)象本身的數(shù)據(jù)。
?????
(2) StringBuffer中的value[]就是一個(gè)很普通的數(shù)組,而且可以通過append()方法將新字符串加入value[]末尾。這樣也就改變了value[]的內(nèi)容和大小了。
????? 比如:new StringBuffer("abc")使得value[]={'a','b','c','',''...}(注意構(gòu)造的長(zhǎng)度是str.length()+16)。如果再將
這個(gè)對(duì)象append("abc"),那么這個(gè)對(duì)象中的value[]={'a','b','c','a','b','c',''....}。這也就是為什么大家說(shuō)
StringBuffer是可變字符串
的涵義了。從這一點(diǎn)也可以看出,StringBuffer中的value[]完全可以作為字符串的緩沖區(qū)功能。其累加性能是很不錯(cuò)的,在后面我們會(huì)進(jìn)行比較。
????
總結(jié),討論String和StringBuffer可不可變。本質(zhì)上是指對(duì)象中的value[]字符數(shù)組可不可變,而不是對(duì)象引用可不可變。
?
?
★StringBuffer與StringBuilder的線程安全性問題
????? StringBuffer和StringBuilder可以算是雙胞胎了,這兩者的方法沒有很大區(qū)別。但在線程安全性方面,StringBuffer允許多線程進(jìn)行字符操作。這是因?yàn)樵谠创a中StringBuffer的很多方法都被關(guān)鍵字
synchronized
修飾了,而StringBuilder沒有。
??? ? 有多線程編程經(jīng)驗(yàn)的程序員應(yīng)該知道synchronized。這個(gè)關(guān)鍵字是為
線程同步機(jī)制
設(shè)定的。我簡(jiǎn)要闡述一下synchronized的含義:
?????
每一個(gè)類對(duì)象都對(duì)應(yīng)一把鎖,當(dāng)某個(gè)線程A調(diào)用類對(duì)象O中的synchronized方法M時(shí),必須獲得對(duì)象O的鎖才能夠執(zhí)行M方法,否則線程A阻塞。一旦線程A開始執(zhí)行M方法,將獨(dú)占對(duì)象O的鎖。使得其它需要調(diào)用O對(duì)象的M方法的線程阻塞。只有線程A執(zhí)行完畢,釋放鎖后。那些阻塞線程才有機(jī)會(huì)重新調(diào)用M方法。這就是解決線程同步問題的鎖機(jī)制。
??? ? 了解了synchronized的含義以后,大家可能都會(huì)有這個(gè)感覺。多線程編程中
StringBuffer比StringBuilder要安全多了
,事實(shí)確實(shí)如此。如果有多個(gè)線程需要對(duì)同一個(gè)字符串緩沖區(qū)進(jìn)行操作的時(shí)候,StringBuffer應(yīng)該是不二選擇。
??? ?
注意:是不是String也不安全呢?事實(shí)上不存在這個(gè)問題,String是不可變的。線程對(duì)于堆中指定的一個(gè)String對(duì)象只能讀取,無(wú)法修改。試問:還有什么不安全的呢?
?
★String和StringBuffer的效率問題(這可是個(gè)熱門話題呀!)
????? 首先說(shuō)明一點(diǎn):StringBuffer和StringBuilder可謂雙胞胎,StringBuilder是1.5新引入的,其前身就是StringBuffer。StringBuilder的效率比StringBuffer稍高,如果不考慮線程安全,StringBuilder應(yīng)該是首選。另外,JVM運(yùn)行程序主要的時(shí)間耗費(fèi)是在創(chuàng)建對(duì)象和回收對(duì)象上。
????? 我們用下面的代碼運(yùn)行1W次字符串的連接操作,測(cè)試String,StringBuffer所運(yùn)行的時(shí)間。
//測(cè)試代碼 public class RunTime{ public static void main(String[] args){ ● 測(cè)試代碼位置1 long beginTime=System.currentTimeMillis(); for(int i=0;i<10000;i++){ ● 測(cè)試代碼位置2 } long endTime=System.currentTimeMillis(); System.out.println(endTime-beginTime); } }
(1) String常量與String變量的"+"操作比較
??????? ▲測(cè)試①代碼:???? (測(cè)試代碼位置1)? String str="";
????????????????????????????????? (測(cè)試代碼位置2)? str="Heart"+"Raid";
??? ??????? [耗時(shí):? 0ms]
??? ????????
?????? ▲測(cè)試②代碼??????? (測(cè)試代碼位置1)? String s1="Heart";
?????????????????????????????????????????????????????????? String s2="Raid";
?????????????????????????????????????????????????????????? String str="";
????????????????????????????????? (測(cè)試代碼位置2)? str=s1+s2;
??????????? [耗時(shí):? 15—16ms]
?????
結(jié)論:String常量的“+連接”? 稍優(yōu)于? String變量的“+連接”。
??? ?
原因:測(cè)試①的"Heart"+"Raid"在編譯階段就已經(jīng)連接起來(lái),形成了一個(gè)字符串常量"HeartRaid",并指向堆中的拘留字符串對(duì)象。運(yùn)行時(shí)只需要將"HeartRaid"指向的拘留字符串對(duì)象地址取出1W次,存放在局部變量str中。這確實(shí)不需要什么時(shí)間。
??? ??????????
測(cè)試②中局部變量s1和s2存放的是兩個(gè)不同的拘留字符串對(duì)象的地址。然后會(huì)通過下面三個(gè)步驟完成“+連接”:
??? ??? ??????????????????????? 1、StringBuilder temp=new StringBuilder(s1),
??? ??? ??? ??????????????????? 2、temp.append(s2);
??????????????????????????????? 3、str=temp.toString();
?????????????? 我們發(fā)現(xiàn),雖然在中間的時(shí)候也用到了append()方法,但是在開始和結(jié)束的時(shí)候分別創(chuàng)建了StringBuilder和String對(duì)象。可想而知:調(diào)用1W次,是不是就創(chuàng)建了1W次這兩種對(duì)象呢?不劃算。
???? 但是,String變量的"+連接"操作比String常量的"+連接"操作使用的更加廣泛。 這一點(diǎn)是不言而喻的。
???
?
(2)String對(duì)象的"累+"連接操作與StringBuffer對(duì)象的append()累和連接操作比較。
??? ????? ▲測(cè)試①代碼:???? (代碼位置1)? String s1="Heart";
??? ?????????????????????????????????????????????????? String s="";
??? ??????????????????????????????? (代碼位置2)? s=s+s1;
??? ???????? [耗時(shí):? 4200—4500ms]
??? ????????
??? ????? ▲測(cè)試②代碼?? ? ?? (代碼位置1)? String s1="Heart";
??? ??? ??? ??? ?????????????????????????????????????? StringBuffer sb=new StringBuffer();
??????????????????????????????????? (代碼位置2) sb.append(s1);
??? ???????? [耗時(shí):? 0ms(當(dāng)循環(huán)100000次的時(shí)候,耗時(shí)大概16—31ms)]
??? ????
結(jié)論:大量字符串累加時(shí),StringBuffer的append()效率遠(yuǎn)好于String對(duì)象的"累+"連接
??? ????
原因:測(cè)試①
中的s=s+s1,JVM會(huì)利用首先創(chuàng)建一個(gè)StringBuilder,并利用append方法完成s和s1所指向的字符串對(duì)象值的合并操作,接著調(diào)用StringBuilder的 toString()方法在堆中創(chuàng)建一個(gè)新的String對(duì)象,其值為剛才字符串的合并結(jié)果。而局部變量s指向了新創(chuàng)建的String對(duì)象。
????????????????? 因?yàn)镾tring對(duì)象中的value[]是不能改變的,每一次合并后字符串值都需要?jiǎng)?chuàng)建一個(gè)新的String對(duì)象來(lái)存放。循環(huán)1W次自然需要?jiǎng)?chuàng)建1W個(gè)String對(duì)象和1W個(gè)StringBuilder對(duì)象,效率低就可想而知了。
??? ??? ????????? 測(cè)試②中sb.append(s1);只需要將自己的value[]數(shù)組不停的擴(kuò)大來(lái)存放s1即可。循環(huán)過程中無(wú)需在堆中創(chuàng)建任何新的對(duì)象。效率高就不足為奇了。
? ????
?
★ 鏡頭總結(jié):
??? (1) 在編譯階段就能夠確定的字符串常量,完全沒有必要?jiǎng)?chuàng)建String或StringBuffer對(duì)象。直接使用字符串常量的"+"連接操作效率最高。
??? (2) StringBuffer對(duì)象的append效率要高于String對(duì)象的"+"連接操作。
??? (3) 不停的創(chuàng)建對(duì)象是程序低效的一個(gè)重要原因。那么相同的字符串值能否在堆中只創(chuàng)建一個(gè)String對(duì)象那。顯然拘留字符串能夠做到這一點(diǎn),除了程序中的字符串常量會(huì)被JVM自動(dòng)創(chuàng)建拘留字符串之外,調(diào)用String的intern()方法也能做到這一點(diǎn)。當(dāng)調(diào)用intern()時(shí),如果常量池中已經(jīng)有了當(dāng)前String的值,那么返回這個(gè)常量指向拘留對(duì)象的地址。如果沒有,則將String值加入常量池中,并創(chuàng)建一個(gè)新的拘留字符串對(duì)象。
?
?
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
