1. 程式人生 > >重溫java中的String,StringBuffer,StringBuilder類

重溫java中的String,StringBuffer,StringBuilder類

nbsp times 優勢 不變 () 網絡 rgb final 線程不安全

不論什麽一個系統在開發的過程中, 相信都不會缺少對字符串的處理。

在 java 語言中, 用來處理字符串的的類經常使用的有 3 個: String、StringBuffer、StringBuilder。

它們的異同點:

1) 都是 final 類, 都不同意被繼承;

2) String 長度是不可變的, StringBuffer、StringBuilder 長度是可變的;

3) StringBuffer 是線程安全的, StringBuilder 不是線程安全的。

String 類已在上一篇隨筆 小瓜牛漫談 — String 中敘述過, 這裏就不再贅述。

本篇隨筆意在漫遊 StringBuffer 與 StringBuilder。

事實上如今網絡上談論 String、StringBuffer、StringBuilder 的文章已經多到不可勝數了。小瓜牛不才, 蝸行牛步, 慢了半個世紀。

。。

StringBuilder 與 StringBuffer 支持的全部操作基本上是一致的, 不同的是, StringBuilder 不須要運行同步。

同步操作意味著

要耗費系統的一些額外的開銷, 或時間, 或空間, 或資源等, 甚至可能會造成死鎖。從理論上來講,StringBuilder 的速度要更快一些。

串聯字符串的性能小測:

技術分享
 1 public class Application {
 2 
 3     private final int LOOP_TIMES = 200000;
 4     private final String CONSTANT_STRING = "min-snail";
 5     
 6     public static void main(String[] args) {
 7         
 8         new Application().startup();
 9     }
10     
11     public void testString(){
12 String string = ""; 13 long beginTime = System.currentTimeMillis(); 14 for(int i = 0; i < LOOP_TIMES; i++){ 15 string += CONSTANT_STRING; 16 } 17 long endTime = System.currentTimeMillis(); 18 System.out.print("String : " + (endTime - beginTime) + "\t"); 19 } 20 21 public void testStringBuffer(){ 22 StringBuffer buffer = new StringBuffer(); 23 long beginTime = System.currentTimeMillis(); 24 for(int i = 0; i < LOOP_TIMES; i++){ 25 buffer.append(CONSTANT_STRING); 26 } 27 buffer.toString(); 28 long endTime = System.currentTimeMillis(); 29 System.out.print("StringBuffer : " + (endTime - beginTime) + "\t"); 30 } 31 32 public void testStringBuilder(){ 33 StringBuilder builder = new StringBuilder(); 34 long beginTime = System.currentTimeMillis(); 35 for(int i = 0; i < LOOP_TIMES; i++){ 36 builder.append(CONSTANT_STRING); 37 } 38 builder.toString(); 39 long endTime = System.currentTimeMillis(); 40 System.out.print("StringBuilder : " + (endTime - beginTime) + "\t"); 41 } 42 43 public void startup(){ 44 for(int i = 0; i < 6; i++){ 45 System.out.print("The " + i + " [\t "); 46 testString(); 47 testStringBuffer(); 48 testStringBuilder(); 49 System.out.println("]"); 50 } 51 } 52 }
技術分享

上面演示樣例是頻繁的去串聯一個比較短的字符串, 然後重復調 6 次。

測試是一個非常漫長的過程, 在本人的筆記本電腦上總共花去了 23 分鐘之多, 以下附上詳細數據:

Number String StringBuffer StringBuilder
0 231232 17 14
1 233207 6 6
2 231294 8 6
3 235481 7 6
4 231987 9 6
5 230132 8 7
平均 3‘52‘‘ 9.2 7.5


從表格數據能夠看出, 使用 String 的 "+" 符號串聯字符串的性能差的驚人, 大概會維持在 3分40秒 的時候能夠看到一次打印結果;

其次是 StringBuffer, 平均花時 9.2 毫秒; 然後是 StringBuilder, 平均花時 7.5 毫秒。

1) 耗時大的驚人的 String 究竟是幹嘛去了呢? 調出 cmd 窗體, 敲 jconsole 調出 java 虛擬機監控工具, 查看堆內存的使用情況例如以下:

技術分享

實際上這個已經在上一篇 小瓜牛漫談 — String 中提到過, 底層實際上是將循環體內的 string += CONSTANT_STRING; 語句轉成了:

string = (new StringBuilder(String.valueOf(string))).append("min-snail").toString();

所以在二十萬次的串聯字符串中, 每一次都先去創建 StringBuilder 對象, 然後再調 append() 方法來完畢 String 類的 "+" 操作。

這裏的大部分時間都花在了對象的創建上, 並且每一個創建出來的對象的生命都不能長久, 朝生夕滅, 由於這些對象創建出來之後沒有引用變量來引用它們,

那麽它們在使用完畢時候就處於一種不可到達狀態, java 虛擬機的垃圾回收器(GC)就會不定期的來回收這些垃圾對象。

因此會看到上圖堆內存中的曲線起伏變化非常大。

但假設是遇到例如以下情況:

1 String concat1 = "I" + " am " + "min-snail";
2 
3 String concat2 = "I";
4 concat2 += " am ";
5 concat2 += "min-snail";

java 對 concat1 的處理速度也是快的驚人。本人在自己的筆記本上測試多次, 耗時基本上都是 0 毫秒。這是由於 concat1 在編譯期就能夠被確定是一個字符常量。

當編譯完畢之後 concat1 的值事實上就是 "I am min-snail", 因此, 在執行期間自然就不須要花費太多的時間來處理 concat1 了。假設是站在這個角度來看, 使用

StringBuilder 全然不占優勢, 在這樣的情況下, 假設是使用 StringBuilder 反而會使得程序執行須要耗費很多其它的時間。

可是 concat2 不一樣, 因為 concat2 在編譯期間不可以被確定, 因此, 在執行期間 JVM 會按老一套的做法, 將其轉換成使用 StringBuilder 來實現。

2) 從表格數據能夠看出, StringBuilder 與 StringBuffer 在耗時上並不相差多少, 僅僅是 StringBuilder 略微快一些, 可是 StringBuilder 是

冒著多線程不安全的潛在風險。

這也是 StringBuilder 為賺取表格數據中的 1.7 毫秒( 若按表格的數據來算, 性能已經提升 20% 多 )所須要付出的代價。

3) 綜合來說:

StringBuilder 是 java 為 StringBuffer 提供的一個等價類, 但不保證同步。

在不涉及多線程的操作情況下能夠簡易的替換 StringBuffer 來提升

系統性能; StringBuffer 在性能上稍略於 StringBuilder, 但能夠不用考慮線程安全問題; String 的 "+"符號操作起來簡單方便,

String 的使用也非常easy便捷, java 底層會轉換成 StringBuilder 來實現, 特別假設是要在循環體內使用, 建議選擇其余兩個。

使用 StringBuffer、StringBuilder 的無參構造器產生的對象默認擁有 16 個字符長度大小的字符串緩沖區, 假設是調參數為 String 的構造器,

默認的字符串緩沖區容量是 String 對象的長度 + 16 個長度的大小(留 16 個長度大小的空緩沖區)。具體信息可見 StringBuilder 源代碼:

技術分享

當使用 append 或 insert 方法向源字符串追加內容的時候, 假設內部緩沖區的大小不夠, 就會自己主動擴張容量, 詳細信息看 AbstractStringBuilder 源代碼:

技術分享

StringBuffer 與 StringBuilder 是相類似的, 這裏就不貼 StringBuffer 的源代碼了。

不同構造器間的差異:

技術分享
 1 public static void main(String[] args) {
 2     
 3     StringBuilder builder1 = new StringBuilder("");
 4     StringBuilder builder2 = new StringBuilder(10);
 5     StringBuilder builder3 = new StringBuilder("min-snail"); // [ 9個字符  ]
 6 
 7     System.out.println(builder1.length());    // 0
 8     System.out.println(builder2.length());    // 0
 9     System.out.println(builder3.length());    // 9
10     
11     System.out.println(builder1.capacity());  // 16
12     System.out.println(builder2.capacity());  // 10
13     System.out.println(builder3.capacity());  // 25 [ 25 = 9 + 16 ]
14     
15     builder2.append("I am min-snail");        // [ 14個字符  ]
16     
17     System.out.println(builder2.length());    // 14
18     System.out.println(builder2.capacity());  // 22 [ 22 = (10 + 1) * 2 ]
19 }
技術分享

從上面的演示樣例代碼能夠看出, length() 方法計算的是字符串的實際長度, 空字符串的長度為 0 (這個和 String 是一樣的: "".length() == 0)。

capacity() 方法是用來計算對象字符串緩沖區的總容量大小:

builder1 為: length + 16 = 0 + 16 = 16;

builder3 為: length + 16 = 9 + 16 = 25;

builder2 因為是直接指定字符串緩沖區的大小, 因此容量就是指定的值 10, 這個從源代碼的構造器中就能非常easy的看出;

當往 builder2 追加 14 個字符長度大小的字符串時, 這時候原有的緩沖區容量不夠用, 那麽就會自己主動的擴容: (10 + 1) * 2 = 22

這個從源代碼的 expandCapacity(int) 方法的第一行就行看的出。

不同構造器的性能小測:

技術分享
 1 public class Application {
 2 
 3     private final int LOOP_TIMES = 1000000;
 4     private final String CONSTANT_STRING = "min-snail";
 5     
 6     public static void main(String[] args) {
 7         
 8         new Application().startup();
 9     }
10     
11     public void testStringBuilder(){
12         StringBuilder builder = new StringBuilder();
13         long beginTime = System.currentTimeMillis();
14         for(int i = 0; i < LOOP_TIMES; i++){
15             builder.append(CONSTANT_STRING);
16         }
17         builder.toString();
18         long endTime = System.currentTimeMillis();
19         System.out.print("StringBuilder : " + (endTime - beginTime) + "\t");
20     }
21     
22     public void testCapacityStringBuilder(){
23         StringBuilder builder = new StringBuilder(LOOP_TIMES * CONSTANT_STRING.length());
24         long beginTime = System.currentTimeMillis();
25         for(int i = 0; i < LOOP_TIMES; i++){
26             builder.append(CONSTANT_STRING);
27         }
28         builder.toString();
29         long endTime = System.currentTimeMillis();
30         System.out.print("StringBuilder : " + (endTime - beginTime) + "\t");
31     }
32     
33     public void startup(){
34         for(int i = 0; i < 10; i++){
35             System.out.print("The " + i + " [\t    ");
36             testStringBuilder();
37             testCapacityStringBuilder();
38             System.out.println("]");
39         }
40     }
41 }
技術分享

演示樣例中是頻繁的去調 StringBuilder 的 append() 方法往源字符串中追加內容, 總共測試 10 次, 以下附上測試的結果的數據:

Number StringBuilder() StringBuilder(int)
0 60 33
1 43 26
2 41 25
3 42 24
4 51 30
5 92 24
6 55 24
7 40 24
8 55 21
9 44 21
平均 52.3 25.2


從表格數據能夠看出, 合理的指定字符串緩沖區的容量能夠大大的提高系統的性能(若按表格的數據來算, 性能約提升了 108%), 這是由於 StringBuilder 在

緩沖區容量不足的時候會自己主動擴容, 而擴容就會涉及到數組的拷貝(StringBuilder 和 StringBuffer 底層都是使用 char 數組來實現的), 這個也能夠在源代碼

的 expandCapacity(int) 方法中看的出。

這些額外的開銷都是須要花費掉一定量的時間的。

在上示代碼中, 假設將 StringBuilder 換成 StringBuffer, 其余保持不變, 測試的結果的數據例如以下:

Number SstingBuffer() StringBuffer(int)
0 85 58
1 70 56
2 73 56
3 71 55
4 73 58
5 117 55
6 84 55
7 69 55
8 70 52
9 73 52
平均 78.5 55.2

與 StringBuilder 相類似的, 指定容量的構造器在性能上也得到了較大的提升(若按表格數據來算, 性能約提升了 42%), 但因為 StringBuffer 須要

運行同步, 因此性能上會比 StringBuilder 差一些。


重溫java中的String,StringBuffer,StringBuilder類