小瓜牛漫談 — String、StringBuffer、StringBuilder


 

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

在 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 的使用也很簡單便捷, 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, 這個從源碼的構造器中就能很容易的看出;

當往 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 差一些。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM