1. 程式人生 > >String、StringBuffer與StringBuilder之間區別(轉)

String、StringBuffer與StringBuilder之間區別(轉)

ons 字符 總結 當前 命令 其它 有一個 system 機制

原文地址https://www.cnblogs.com/goody9807/p/6516374.html

String 字符串常量
StringBuffer 字符串變量(線程安全)
StringBuilder 字符串變量(非線程安全)

簡要的說, String 類型和 StringBuffer 類型的主要性能區別其實在於 String 是不可變的對象, 因此在每次對 String 類型進行改變的時候其實都等同於生成了一個新的 String 對象,然後將指針指向新的 String 對象,所以經常改變內容的字符串最好不要用 String ,因為每次生成對象都會對系統性能產生影響,特別當內存中無引用對象多了以後, JVM 的 GC 就會開始工作,那速度是一定會相當慢的。

而如果是使用 StringBuffer 類則結果就不一樣了,每次結果都會對 StringBuffer 對象本身進行操作,而不是生成新的對象,再改變對象引用。所以在一般情況下我們推薦使用 StringBuffer ,特別是字符串對象經常改變的情況下。而在某些特別情況下, String 對象的字符串拼接其實是被 JVM 解釋成了 StringBuffer 對象的拼接,所以這些時候 String 對象的速度並不會比 StringBuffer 對象慢,而特別是以下的字符串對象生成中, String 效率是遠要比 StringBuffer 快的:
String S1 = “This is only a” + “ simple” + “ test”;
StringBuffer Sb = new StringBuilder(“This is only a”).append(“ simple”).append(“ test”);
你會很驚訝的發現,生成 String S1 對象的速度簡直太快了,而這個時候 StringBuffer 居然速度上根本一點都不占優勢。其實這是 JVM 的一個把戲,在 JVM 眼裏,這個
String S1 = “This is only a” + “ simple” + “test”; 其實就是:
String S1 = “This is only a simple test”; 所以當然不需要太多的時間了。但大家這裏要註意的是,如果你的字符串是來自另外的 String 對象的話,速度就沒那麽快了,譬如:
String S2 = “This is only a”;
String S3 = “ simple”;
String S4 = “ test”;
String S1 = S2 +S3 + S4;
這時候 JVM 會規規矩矩的按照原來的方式去做


在大部分情況下 StringBuffer > String
StringBuffer
Java.lang.StringBuffer線程安全的可變字符序列。一個類似於 String 的字符串緩沖區,但不能修改。雖然在任意時間點上它都包含某種特定的字符序列,但通過某些方法調用可以改變該序列的長度和內容。
可將字符串緩沖區安全地用於多個線程。可以在必要時對這些方法進行同步,因此任意特定實例上的所有操作就好像是以串行順序發生的,該順序與所涉及的每個線程進行的方法調用順序一致。
StringBuffer 上的主要操作是 append 和 insert 方法,可重載這些方法,以接受任意類型的數據。每個方法都能有效地將給定的數據轉換成字符串,然後將該字符串的字符追加或插入到字符串緩沖區中。append 方法始終將這些字符添加到緩沖區的末端;而 insert 方法則在指定的點添加字符。
例如,如果 z 引用一個當前內容是“start”的字符串緩沖區對象,則此方法調用 z.append("le") 會使字符串緩沖區包含“startle”,而 z.insert(4, "le") 將更改字符串緩沖區,使之包含“starlet”。
在大部分情況下 StringBuilder > StringBuffer java.lang.StringBuilder
java.lang.StringBuilder一個可變的字符序列是5.0新增的。此類提供一個與 StringBuffer 兼容的 API,但不保證同步。該類被設計用作 StringBuffer 的一個簡易替換,用在字符串緩沖區被單個線程使用的時候(這種情況很普遍)。如果可能,建議優先采用該類,因為在大多數實現中,它比 StringBuffer 要快。兩者的方法基本相同。

作者:每次上網沖杯Java時,都能看到關於String無休無止的爭論。還是覺得有必要讓這個討厭又很可愛的String美眉,赤裸裸的站在我們這些Java色狼面前了。嘿嘿....

眾所周知,String是由字符組成的串,在程序中使用頻率很高。Java中的String是一個類,而並非基本數據類型。 不過她卻不是普通的類哦!!!

【鏡頭1】 String對象的創建
1、關於類對象的創建,很普通的一種方式就是利用構造器,String類也不例外:String s=new String("Hello world"); 問題是參數"Hello world"是什麽東西,也是字符串對象嗎?莫非用字符串對象創建一個字符串對象?

2、當然,String類對象還有一種大家都很喜歡的創建方式:String s="Hello world"; 但是有點怪呀,怎麽與基本數據類型的賦值操作(int i=1)很像呀?

在開始解釋這些問題之前,我們先引入一些必要的知識:

★ Java class文件結構 和常量池
我們都知道,Java程序要運行,首先需要編譯器將源代碼文件編譯成字節碼文件(也就是.class文件)。然後在由JVM解釋執行。
class文件是8位字節的二進制流 。這些二進制流的涵義由一些緊湊的有意義的項 組成。比如class字節流中最開始的4個字節組成的項叫做魔數 (magic),其意義在於分辨class文件(值為0xCAFEBABE)與非class文件。class字節流大致結構如下圖左側。

技術分享圖片

其中,在class文件中有一個非常重要的項——常量池 。這個常量池專門放置源代碼中的符號信息(並且不同的符號信息放置在不同標誌的常量表中)。如上圖右側是HelloWorld代碼中的常量表(HelloWorld代碼如下),其中有四個不同類型的常量表(四個不同的常量池入口)。關於常量池的具體細節,請參照我的博客《Class文件內容及常量池 》

Java代碼 技術分享圖片
  1. public class HelloWorld{
  2. void hello(){
  3. System.out.println("Hello world");
  4. }
  5. }

通過上圖可見,代碼中的"Hello world"字符串字面值被編譯之後,可以清楚的看到存放在了class常量池中的字符串常量表中(上圖右側紅框區域)。

JVM運行class文件

源代碼編譯成class文件之後,JVM就要運行這個class文件。它首先會用類裝載器加載進class文件。然後需要創建許多內存數據結構來存放class文件中的字節數據。比如class文件對應的類信息數據、常量池結構、方法中的二進制指令序列、類方法與字段的描述信息等等。當然,在運行的時候,還需要為方法創建棧幀等。這麽多的內存結構當然需要管理,JVM會把這些東西都組織到幾個“運行時數據區 ”中。這裏面就有我們經常說的“方法區 ”、“”、“Java棧 ”等。詳細請參見我的博客《Java 虛擬機體系結構 》 。

上面我們提到了,在Java源代碼中的每一個字面值字符串,都會在編譯成class文件階段,形成標誌號 為8(CONSTANT_String_info)的常量表 。 當JVM加載 class文件的時候,會為對應的常量池建立一個內存數據結構,並存放在方法區中。同時JVM會自動為CONSTANT_String_info常量表中 的字符串常量字面值 在堆中 創建 新的String對象(intern字符串 對象,又叫拘留字符串對象)。然後把CONSTANT_String_info常量表的入口地址轉變成這個堆中String對象的直接地址(常量池解 析)。

這裏很關鍵的就是這個拘留字符串對象 。源代碼中所有相同字面值的字符串常量只可能建立唯一一個拘留字符串對象。 實際上JVM是通過一個記錄了拘留字符串引用的內部數據結構來維持這一特性的。在Java程序中,可以調用String的intern()方法來使得一個常規字符串對象成為拘留字符串對象。我們會在後面介紹這個方法的。

操作碼助憶符指令
有了上面闡述的兩個知識前提,下面我們將根據二進制指令來區別兩種字符串對象的創建方式:

(1) String s=new String("Hello world");編譯成class文件後的指令(在myeclipse中查看):

Class字節碼指令集代碼 技術分享圖片
  1. 0 new java.lang.String [15] //在堆中分配一個String類對象的空間,並將該對象的地址堆入操作數棧。
  2. 3 dup //復制操作數棧頂數據,並壓入操作數棧。該指令使得操作數棧中有兩個String對象的引用值。
  3. 4 ldc <String "Hello world"> [17] //將常量池中的字符串常量"Hello world"指向的堆中拘留String對象的地址壓入操作數棧
  4. 6 invokespecial java.lang.String(java.lang.String) [19] //調用String的初始化方法,彈出操作數棧棧頂的兩個對象地址,用拘留String對象的值初始化new指令創建的String對象,然後將這個對象的引用壓入操作數棧
  5. 9 astore_1 [s] // 彈出操作數棧頂數據存放在局部變量區的第一個位置上。此時存放的是new指令創建出的,已經被初始化的String對象的地址 (此時的棧頂值彈出存入局部變量中去)。

註意:
【這裏有個dup指令。其作用就是復制之前分配的Java.lang.String空間的引用並壓入棧頂。那麽這裏為什麽需要這樣麽做呢?因為invokespecial指令通過[15]這個常量池入口尋找到了java.lang.String()構造方法,構造方法雖然找到了。但是必須還得知道是誰的構造方法,所以要將之前分配的空間的應用壓入棧頂讓invokespecial命令應用才知道原來這個構造方法是剛才創建的那個引用的,調用完成之後將棧頂的值彈出。之後調用astore_1將此時的棧頂值彈出存入局部變量中去。】


事實上,在運行這段指令之前,JVM就已經為"Hello world"在堆中創建了一個拘留字符串( 值得註意的是:如果源程序中還有一個"Hello world"字符串常量,那麽他們都對應了同一個堆中的拘留字符串)。然後用這個拘留字符串的值來初始化堆中用new指令創建出來的新的String對象,局部變量s實際上存儲的是new出來的堆對象地址。 大家註意了,此時在JVM管理的堆中,有兩個相同字符串值的String對象:一個是拘留字符串對象,一個是new新建的字符串對象。如果還有一條創建語句String s1=new String("Hello world");堆中有幾個值為"Hello world"的字符串呢? 答案是3個,大家好好想想為什麽吧!

(2)將String s="Hello world";編譯成class文件後的指令:

Class字節碼指令集代碼 技術分享圖片
  1. 0 ldc <String "Hello world"> [15]//將常量池中的字符串常量"Hello world"指向的堆中拘留String對象的地址壓入操作數棧
  2. 2 astore_1 [str] // 彈出操作數棧頂數據存放在局部變量區的第一個位置上。此時存放的是拘留字符串對象在堆中的地址

和上面的創建指令有很大的不同,局部變量s存儲的是早已創建好的拘留字符串的堆地址(沒有new 的對象了)。 大家好好想想,如果還有一條穿件語句String s1="Hello word";此時堆中有幾個值為"Hello world"的字符串呢?答案是1個。那麽局部變量s與s1存儲的地址是否相同呢? 呵呵, 這個你應該知道了吧。

★ 鏡頭總結: String類型脫光了其實也很普通。真正讓她神秘的原因就在於CONSTANT_String_info常量表 拘留字符串對象 的存在。現在我們可以解決江湖上的許多紛爭了。

紛爭1】關於字符串相等關系的爭論

Java代碼 技術分享圖片
  1. //代碼1
  2. String sa=new String("Hello world");
  3. String sb=new String("Hello world");
  4. System.out.println(sa==sb); // false
  5. //代碼2
  6. String sc="Hello world";
  7. String sd="Hello world";
  8. System.out.println(sc==sd); // true

代碼1中局部變量sa,sb中存儲的是JVM在堆中new出來的兩個String對象的內存地址。雖然這兩個String對象的值(char[]存放的字符序列)都是"Hello world"。 因此"=="比較的是兩個不同的堆地址。代碼2中局部變量sc,sd中存儲的也是地址,但卻都是常量池中"Hello world"指向的堆的唯一的那個拘留字符串對象的地址 。自然相等了。

【紛爭2】 字符串“+”操作的內幕

Java代碼 技術分享圖片
  1. //代碼1
  2. String sa = "ab";
  3. String sb = "cd";
  4. String sab=sa+sb;
  5. String s="abcd";
  6. System.out.println(sab==s); // false
  7. //代碼2
  8. String sc="ab"+"cd";
  9. String sd="abcd";
  10. System.out.println(sc==sd); //true

代碼1中局部變量sa,sb存儲的是堆中兩個拘留字符串對象的地址。而當執行sa+sb時,JVM首先會在堆中創建一個StringBuilder類,同時用sa指向的拘留字符串對象完成初始化,然後調用append方法完成對sb所指向的拘留字符串的合並操作,接著調用StringBuilder的toString()方法在堆中創建一個String對象,最後將剛生成的String對象的堆地址存放在局部變量sab中。而局部變量s存儲的是常量池中"abcd"所對應的拘留字符串對象的地址。 sab與s地址當然不一樣了。這裏要註意了,代碼1的堆中實際上有五個字符串對象:三個拘留字符串對象、一個String對象和一個StringBuilder對象。
代碼2中"ab"+"cd"會直接在編譯期就合並成常量"abcd", 因此相同字面值常量"abcd"所對應的是同一個拘留字符串對象,自然地址也就相同。

【鏡頭二】 String三姐妹(String,StringBuffer,StringBuilder)
String扒的差不多了。但他還有兩個妹妹StringBuffer,StringBuilder長的也不錯哦!我們也要下手了:
String(大姐,出生於JDK1.0時代) 不可變字符序列
StringBuffer(二姐,出生於JDK1.0時代) 線程安全的可變字符序列
StringBuilder(小妹,出生於JDK1.5時代) 非線程安全的可變字符序列

★StringBuffer與String的可變性問題。
我們先看看這兩個類的部分源代碼:

Java代碼 技術分享圖片
  1. //String
  2. public final class String
  3. {
  4. private final char value[];
  5. public String(String original) {
  6. // 把原字符串original切分成字符數組並賦給value[];
  7. }
  8. }
  9. //StringBuffer
  10. public final class StringBuffer extends AbstractStringBuilder
  11. {
  12. char value[]; //繼承了父類AbstractStringBuilder中的value[]
  13. public StringBuffer(String str) {
  14. super(str.length() + 16); //繼承父類的構造器,並創建一個大小為str.length()+16的value[]數組
  15. append(str); //將str切分成字符序列並加入到value[]中
  16. }
  17. }

很顯然,String和StringBuffer中的value[]都用於存儲字符序列。但是,
(1) String中的是常量(final)數組,只能被賦值一次。
比如:new String("abc")使得value[]={‘a‘,‘b‘,‘c‘}(查看jdk String 就是這麽實現的),之後這個String對象中的value[]再也不能改變了。這也正是大家常說的,String是不可變的原因 。
註意:這個對初學者來說有個誤區,有人說String str1=new String("abc"); str1=new String("cba");不是改變了字符串str1嗎?那麽你有必要先搞懂對象引用和對象本身的區別。這裏我簡單的說明一下,對象本身指的是存放在堆空間中的該對象的實例數據(非靜態非常量字段)。而對象引用指的是堆中對象本身所存放的地址,一般方法區和Java棧中存儲的都是對象引用,而非對象本身的數據。


(2) StringBuffer中的value[]就是一個很普通的數組,而且可以通過append()方法將新字符串加入value[]末尾。這樣也就改變了value[]的內容和大小了。

比如:new StringBuffer("abc")使得value[]={‘a‘,‘b‘,‘c‘,‘‘,‘‘...}(註意構造的長度是str.length()+16)。如果再將這個對象append("abc"),那麽這個對象中的value[]={‘a‘,‘b‘,‘c‘,‘a‘,‘b‘,‘c‘,‘‘....}。這也就是為什麽大家說 StringBuffer是可變字符串 的涵義了。從這一點也可以看出,StringBuffer中的value[]完全可以作為字符串的緩沖區功能。其累加性能是很不錯的,在後面我們會進行比較。

總結,討論String和StringBuffer可不可變。本質上是指對象中的value[]字符數組可不可變,而不是對象引用可不可變。

★StringBuffer與StringBuilder的線程安全性問題
StringBuffer和StringBuilder可以算是雙胞胎了,這兩者的方法沒有很大區別。但在線程安全性方面,StringBuffer允許多線程進行字符操作。這是因為在源代碼中StringBuffer的很多方法都被關鍵字synchronized 修飾了,而StringBuilder沒有。
有多線程編程經驗的程序員應該知道synchronized。這個關鍵字是為線程同步機制 設定的。我簡要闡述一下synchronized的含義:
每一個類對象都對應一把鎖,當某個線程A調用類對象O中的synchronized方法M時,必須獲得對象O的鎖才能夠執行M方法,否則線程A阻塞。一旦線程A開始執行M方法,將獨占對象O的鎖。使得其它需要調用O對象的M方法的線程阻塞。只有線程A執行完畢,釋放鎖後。那些阻塞線程才有機會重新調用M方法。這就是解決線程同步問題的鎖機制。
了解了synchronized的含義以後,大家可能都會有這個感覺。多線程編程中StringBuffer比StringBuilder要安全多了 ,事實確實如此。如果有多個線程需要對同一個字符串緩沖區進行操作的時候,StringBuffer應該是不二選擇。
註意:是不是String也不安全呢?事實上不存在這個問題,String是不可變的。線程對於堆中指定的一個String對象只能讀取,無法修改。試問:還有什麽不安全的呢?

★String和StringBuffer的效率問題(這可是個熱門話題呀!)
首先說明一點:StringBuffer和StringBuilder可謂雙胞胎,StringBuilder是1.5新引入的,其前身就是StringBuffer。StringBuilder的效率比StringBuffer稍高,如果不考慮線程安全,StringBuilder應該是首選。另外,JVM運行程序主要的時間耗費是在創建對象和回收對象上。


我們用下面的代碼運行1W次字符串的連接操作,測試String,StringBuffer所運行的時間。

Java代碼 技術分享圖片
  1. //測試代碼
  2. public class RunTime{
  3. public static void main(String[] args){
  4. ● 測試代碼位置1
  5. long beginTime=System.currentTimeMillis();
  6. for(int i=0;i<10000;i++){
  7. ● 測試代碼位置2
  8. }
  9. long endTime=System.currentTimeMillis();
  10. System.out.println(endTime-beginTime);
  11. }
  12. }

(1) String常量與String變量的"+"操作比較
▲測試①代碼: (測試代碼位置1) String str="";
(測試代碼位置2) str="Heart"+"Raid";
[耗時: 0ms]

▲測試②代碼 (測試代碼位置1) String s1="Heart";
String s2="Raid";
String str="";
(測試代碼位置2) str=s1+s2;
[耗時: 15—16ms]
結論:String常量的“+連接” 稍優於 String變量的“+連接”。
原因:測試①的"Heart"+"Raid"在編譯階段就已經連接起來,形成了一個字符串常量"HeartRaid",並指向堆中的拘留字符串對象。運行時只需要將"HeartRaid"指向的拘留字符串對象地址取出1W次,存放在局部變量str中。這確實不需要什麽時間。
測試②中局部變量s1和s2存放的是兩個不同的拘留字符串對象的地址。然後會通過下面三個步驟完成“+連接”:
1、StringBuilder temp=new StringBuilder(s1),
2、temp.append(s2);
3、str=temp.toString();
我們發現,雖然在中間的時候也用到了append()方法,但是在開始和結束的時候分別創建了StringBuilder和String對象。可想而知:調用1W次,是不是就創建了1W次這兩種對象呢?不劃算。

但是,String變量的"+連接"操作比String常量的"+連接"操作使用的更加廣泛。 這一點是不言而喻的。

(2)String對象的"累+"連接操作與StringBuffer對象的append()累和連接操作比較。
▲測試①代碼: (代碼位置1) String s1="Heart";
String s="";
(代碼位置2) s=s+s1;
[耗時: 4200—4500ms]

▲測試②代碼 (代碼位置1) String s1="Heart";
StringBuffer sb=new StringBuffer();
(代碼位置2) sb.append(s1);
[耗時: 0ms(當循環100000次的時候,耗時大概16—31ms)]
結論:大量字符串累加時,StringBuffer的append()效率遠好於String對象的"累+"連接
原因:測試① 中的s=s+s1,JVM會利用首先創建一個StringBuilder,並利用append方法完成s和s1所指向的字符串對象值的合並操作,接著調用StringBuilder的 toString()方法在堆中創建一個新的String對象,其值為剛才字符串的合並結果。而局部變量s指向了新創建的String對象。

因為String對象中的value[]是不能改變的,每一次合並後字符串值都需要創建一個新的String對象來存放。循環1W次自然需要創建1W個String對象和1W個StringBuilder對象,效率低就可想而知了。


測試②中sb.append(s1);只需要將自己的value[]數組不停的擴大來存放s1即可。循環過程中無需在堆中創建任何新的對象。效率高就不足為奇了。

鏡頭總結:

(1) 在編譯階段就能夠確定的字符串常量,完全沒有必要創建String或StringBuffer對象。直接使用字符串常量的"+"連接操作效率最高。

(2) StringBuffer對象的append效率要高於String對象的"+"連接操作。

(3) 不停的創建對象是程序低效的一個重要原因。那麽相同的字符串值能否在堆中只創建一個String對象那。顯然拘留字符串能夠做到這一點,除了程序中的字符串常量會被JVM自動創建拘留字符串之外,調用String的intern()方法也能做到這一點。當調用intern()時,如果常量池中已經有了當前String的值,那麽返回這個常量指向拘留對象的地址。如果沒有,則將String值加入常量池中,並創建一個新的拘留字符串對象。

String、StringBuffer與StringBuilder之間區別(轉)