重溫java中的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 的使用也非常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類