StringBuilder、StringBuffer、String類之間的關系
java中String、StringBuffer、StringBuilder是編程中經常使用的字符串類,在上一篇博文中我們已經熟悉String字符串的特性和使用,而StringBuffer、StringBuilder又是怎么樣的字符串類呢??他們之間的區別和關系又是什么呢??這問題經常在面試中會問到,現在總結一下,看看他們的不同與相同。
1.可變與不可變
1)String類中使用字符數組保存字符串,如下就是,因為有“final”修飾符,所以可以知道string對象是不可變的。
private final char value[];
String的值是不可變的,這就導致每次對String的操作都會生成新的String對象,不僅效率低下,而且大量浪費有限的內存空間。
1 String a = "a"; //假設a指向地址0x0001 2 a = "b";//重新賦值后a指向地址0x0002,但0x0001地址中保存的"a"依舊存在,但已經不再是a所指向的,a 已經指向了其它地址。
因此String的操作都是改變賦值地址而不是改變值操作。
2)StringBuilder與StringBuffer都繼承自AbstractStringBuilder類,在AbstractStringBuilder中也是使用字符數組保存字符串,如下就是,可知這兩種對象都是可變的。
char[] value;
StringBuffer是可變類,和線程安全的字符串操作類,任何對它指向的字符串的操作都不會產生新的對象。 每個StringBuffer對象都有一定的緩沖區容量,當字符串大小沒有超過容量時,不會分配新的容量,當字符串大小超過容量時,會自動增加容量。
1 StringBuffer buf=new StringBuffer(); //分配長16字節的字符緩沖區 2 StringBuffer buf=new StringBuffer(512); //分配長512字節的字符緩沖區 3 StringBuffer buf=new StringBuffer("this is a test")//在緩沖區中存放了字符串,並在后面預留了16字節的空緩沖區。
StringBuffer和StringBuilder類功能基本相似,主要區別在於StringBuffer類的方法是多線程、安全的,而StringBuilder不是線程安全的,相比而言,StringBuilder類會略微快一點。對於經常要改變值的字符串應該使用StringBuffer和StringBuilder類。
2.是否多線程安全
String中的對象是不可變的,也就可以理解為常量,顯然線程安全。
AbstractStringBuilder是StringBuilder與StringBuffer的公共父類,定義了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。
StringBuffer對方法加了同步鎖或者對調用的方法加了同步鎖,所以是線程安全的。看如下源碼:
1 public synchronized StringBuffer reverse() { 2 super.reverse(); 3 return this; 4 } 5 6 public int indexOf(String str) { 7 return indexOf(str, 0); //存在 public synchronized int indexOf(String str, int fromIndex) 方法 8 }
StringBuilder並沒有對方法進行加同步鎖,所以是非線程安全的。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
StringBuffer是線程安全的,這意味着它們已經同步方法來控制訪問,以便只有一個線程可以在同一時間訪問一個StringBuffer對象同步代碼。因此,StringBuffer的對象通常在多線程環境中是安全的,使用多個線程可以試圖同時訪問相同StringBuffer對象。
StringBuilder類非常相似的StringBuffer,不同之處在於它的訪問不同步的,因此,它不是線程安全的。由於不同步,StringBuilder的性能可以比StringBuffer更好。因此,如果在單線程環境中工作,使用StringBuilder,而不是StringBuffer可能會有更高的性能。這也類似其他情況,如StringBuilder的局部變量(即一個方法中的一個變量),其中只有一個線程會訪問一個StringBuilder對象。
3.StringBuffer和StringBuilder類的速度比較
一般情況下,速度從快到慢:StringBuilder>StringBuffer>String,這種比較是相對的,不是絕對的。(要考慮程序是單線程還是多線程)
接下來,我直接貼上測試過程和結果的代碼,一目了然:
1 package com.hysum.test; 2 3 public class StringTest { 4 final static int time = 50000; //循環次數 5 /* 6 * String類測試方法 7 */ 8 public void test(String s){ 9 long begin = System.currentTimeMillis();//獲取當前系統時間(毫秒數),開始 10 for(int i=0; i<time; i++){ 11 s += "add"; 12 } 13 long over = System.currentTimeMillis();//獲取當前系統時間(毫秒數),結束 14 System.out.println("操作"+s.getClass().getName()+"類型使用的時間為:"+(over-begin)+"毫秒"); 15 } 16 /* 17 * StringBuffer類測試方法 18 */ 19 public void test(StringBuffer s){ 20 long begin = System.currentTimeMillis(); 21 for(int i=0; i<time; i++){ 22 s.append("add"); 23 } 24 long over = System.currentTimeMillis(); 25 System.out.println("操作"+s.getClass().getCanonicalName()+"類型使用的時間為:"+(over-begin)+"毫秒"); 26 } 27 /* 28 * StringBuilder類測試方法 29 */ 30 public void test(StringBuilder s){ 31 long begin = System.currentTimeMillis(); 32 for(int i=0; i<time; i++){ 33 s.append("add"); 34 } 35 long over = System.currentTimeMillis(); 36 System.out.println("操作"+s.getClass().getName()+"類型使用的時間為:"+(over-begin)+"毫秒"); 37 } 38 39 /*對 String 直接進行字符串拼接的測試*/ 40 public void test2(){//操作字符串對象引用相加類型使用的時間 41 String s2 = "abcd"; 42 long begin = System.currentTimeMillis(); 43 for(int i=0; i<time; i++){ 44 String s = s2 + s2 +s2; 45 } 46 long over = System.currentTimeMillis(); 47 System.out.println("操作字符串對象引用相加類型使用的時間為:"+(over-begin)+"毫秒"); 48 } 49 public void test3(){//操作字符串相加使用的時間 50 long begin = System.currentTimeMillis(); 51 for(int i=0; i<time; i++){ 52 String s = "abcd" + "abcd" + "abcd"; 53 } 54 long over = System.currentTimeMillis(); 55 System.out.println("操作字符串相加使用的時間為:"+(over-begin)+"毫秒"); 56 } 57 public static void main(String[] args) { 58 // TODO Auto-generated method stub 59 String s1 = "abcd"; 60 StringBuffer st1 = new StringBuffer( "abcd"); 61 StringBuilder st2 = new StringBuilder( "abcd"); 62 StringTest tc = new StringTest(); 63 tc.test(s1); 64 tc.test(st1); 65 tc.test(st2); 66 tc.test2(); 67 tc.test3(); 68 } 69 70 }
運行結果:
結果分析:
從上面的結果可以看出,不考慮多線程,采用String對象時,執行時間比其他兩個都要高得多,而采用StringBuffer對象和采用StringBuilder對象的差別也比較明顯;而以String類為例,操作字符串對象引用相加類型使用的時間比直接/操作字符串相加使用的時間也多得多。由此可見,如果我們的程序是在單線程下運行,或者是不必考慮到線程同步問題,我們應該優先使用StringBuilder類;如果要保證線程安全,自然是StringBuffer;能直接操作字符串不用字符串引用就直接操作字符串。
4、StringBuilder與StringBuffer共同點
StringBuilder與StringBuffer有公共父類AbstractStringBuilder(抽象類)。
StringBuilder、StringBuffer的方法都會調用AbstractStringBuilder中的公共方法,如super.append(...)。只是StringBuffer會在方法上加synchronized關鍵字,進行同步。
那么我們接來下看一下它們的主要方法吧~
方法 | 說明 |
StringBuffer append(參數) | 追加內容到當前StringBuffer對象的末尾,類似於字符串的連接 |
StringBuffer deleteCharAt(int index) | 刪除指定位置的字符,然后將剩余的內容形成新的字符串 |
StringBuffer insert(位置, 參數) | 在StringBuffer對象中插入內容,然后形成新的字符串 |
StringBuffer reverse() | 將StringBuffer對象中的內容反轉,然后形成新的字符串 |
void setCharAt(int index, char ch) | 修改對象中索引值為index位置的字符為新的字符ch |
void trimToSize() | 將StringBuffer對象的中存儲空間縮小到和字符串長度一樣的長度,減少空間的浪費,和String的trim()是一樣的作用 |
StringBuffer delete(int start, int end) | 刪除指定區域的字符串 |
StringBuffer replace(int start, int end, String s) | 用新的字符串替換指定區域的字符串 |
void setlength(int n) | 設置字符串緩沖區大小 |
int capacity() | 獲取字符串的容量 |
void ensureCapacity(int n) | 確保容量至少等於指定的最小值。如果當前容量小於該參數,然后分配一個新的內部數組容量更大。新的容量是較大的. |
getChars(int start,int end,char chars[],int charStart); | 將字符串的子字符串復制給數組 |
以下是各個方法的代碼示例:
1 public static void main(String[] args) { 2 // TODO Auto-generated method stub 3 StringBuilder str=new StringBuilder("學習 java 編程"); 4 5 //增加字符串內容的方法 6 //append(參數),追加內容到當前對象的末尾 7 str.append("學習使我快樂"); 8 System.out.println("追加內容到當前對象的末尾:"+str); 9 // insert(位置, 參數),在對象中插入內容 10 str.insert(10,','); 11 System.out.println("在對象中插入內容:"+str); 12 13 //操作字符串內容的方法 14 //delete(int start, int end),刪除指定區域的字符串 15 str.delete(11, 17); 16 System.out.println("刪除指定區域的字符串:"+str); 17 //deleteCharAt(int index),刪除指定位置的字符 18 str.deleteCharAt(10); 19 System.out.println("刪除指定位置的字符:"+str); 20 //setCharAt(int index, char newChar),修改對象中索引值為index位置的字符為新的字符ch 21 str.setCharAt(3, 'J'); 22 System.out.println("修改對象中索引值為index位置的字符為新的字符ch:"+str); 23 //replace(int start, int end, String s), 用新的字符串替換指定區域的字符串 24 str.replace(4, 7, "AVA"); 25 System.out.println("用新的字符串替換指定區域的字符串:"+str); 26 // reverse()內容反轉 27 str.reverse(); 28 System.out.println("內容反轉:"+str); 29 //將字符串的子字符串復制給數組。 30 char[] ch = new char[5]; 31 str.getChars(0, 4, ch, 0); 32 System.out.println("將字符串的子字符串復制給數組:"+Arrays.toString(ch)); 33 34 35 36 37 StringBuilder str2=new StringBuilder(30);//創建一個長度為30的字符串 38 str2.append("JAVA"); 39 System.out.println("字符串長度為:"+str2.length());//length(),獲取字符串長度 40 System.out.println("字符串容量為:"+str2.capacity());//capacity(),獲取字符串的容量 41 //有關字符串空間的方法 42 //setLength(int newSize),設置字符串緩沖區大小 43 str2.setLength(20); 44 System.out.println("字符串長度為:"+str2.length()); 45 System.out.println("字符串容量為:"+str2.capacity()); 46 //ensureCapacity(int n),重新設置字符串容量的大小 47 str2.ensureCapacity(20); 48 System.out.println("字符串長度為:"+str2.length()); 49 System.out.println("字符串容量為:"+str2.capacity()); 50 str2.ensureCapacity(35); 51 System.out.println("字符串長度為:"+str2.length()); 52 System.out.println("字符串容量為:"+str2.capacity()); 53 //trimToSize(),存儲空間縮小到和字符串長度一樣的長度 54 str2.trimToSize(); 55 System.out.println("字符串長度為:"+str2.length()); 56 System.out.println("字符串容量為:"+str2.capacity()); 57 58 59 } 60 61 }
運行結果:
結果分析:
1、在使用有范圍的參數方法時,要注意范圍包括開頭不包括結尾!
2、insert方法的位置是你要插入的位置,不是插入前一個位置!
3、getChars方法中注意字符數組的長度一定要大於等於begin到end之間字符的長度!
4、length是字符串內容的長度,而capacity是字符串容量(包括緩存區)的長度!
5、ensureCapacity方法是確保容量至少等於指定的最小值。如果當前容量小於該參數,然后分配一個新的內部數組容量更大(不是你指定的值,系統自動分配一個空間)。如果當前容量不小於該參數,則容量不變。
6、trimToSize(),存儲空間縮小到和字符串長度一樣的長度。避免空間的浪費!
總結
(1).如果要操作少量的數據用 = String
(2).單線程操作字符串緩沖區 下操作大量數據 = StringBuilder
(3).多線程操作字符串緩沖區 下操作大量數據 = StringBuffer
參考文獻: