高效拼接字符串,你會用 “+” 還是StringBuilder.append?

寫在前面

《阿里java開發手冊(泰山版)》(提取碼:hb6i)中,對於Java字符串的拼接有一條規則如下:

 

22.【推薦】循環體內,字符串的連接方式,使用 StringBuilder 的 append 方法進行擴展。

說明:下例中,反編譯出的字節碼文件顯示每次循環都會 new 出一個 StringBuilder 對象,然後進行 append 操作,最後通過 toString 方法返回 String 對象,造成內存資源浪費。

反例:

String str = "start";

for (int i = 0; i < 100; i++) {

str = str + "hello";

}

 

        關於String、StringBuilder、StringBuffer這三個類在字符串處理中的地位不言而喻,我們用的最多的就是String 的“+”號操作符(最普遍)以及StringBuilder、StringBuffer的append()方法。

        那麼他們到底有什麼優缺點,到底什麼時候該用誰?如何才能保證字符串拼接的高效率呢?下面我們一起來了解一下。

目錄

 

一、有什麼區別

 “+”號操作的字節碼

二、性能比較

1. 三者在執行速度方面的比較:

2. String <(StringBuffer,StringBuilder)的原因

一個特殊的例子:

3. StringBuffer與StringBuilder的線程安全問題 

三、使用總結:

一張照片背後的故事


一、有什麼區別

 

 

String

StringBuffer

StringBuilder

類是否可變

不可變Final)    

可變  

 可變

功能介紹

每次對String的操作都會在“常量池”中生成新的String對象

任何對它指向的字符串的操作都不會產生新的對象。每個StringBuffer對象都有一定的緩衝區容量,字符串大小沒有超過容量時,不會分配新的容量,當字符串大小超過容量時,自動擴容 

功能與StringBuffer相同相比少了同步鎖,執行速度更快

線程安全性

 線程安全    

 線程安全    

線程不安全

使用場景推薦

 單次操作或循環外操作字符串

 多線程操作字符串  

 單線程操作字符串

 

 “+”號操作的字節碼

“+”號操作符必須是字符串拼接最常用的一種了,沒有之一。使用“+”拼接字符串,其實只是Java提供的一個語法糖。那麼,我們就來解一解這個語法糖,看看他的內部原理到底是如何實現的。

還是這樣一段代碼。我們把他生成的字節碼進行反編譯,看看結果。

String str1= "唐伯虎";

String str2= "點香菸";

String endStr = str1+ "," + str2;

Dos反編譯後的內容如下(反編譯class文件的命令:javap -c 類名)。

String str1= "\u5510\u4f2f\u864e"; //唐伯虎

String str2= "\u70b9\u9999\u70df"; //點香菸

String endStr = (new StringBuilder()).append(str1).append(",").append(str2).toString();

通過查看反編譯以後的代碼,我們可以發現,原來字符串常量在拼接過程中,是將String轉成了StringBuilder後,使用其append方法進行處理的。

那麼也就是說,Java中的“+”對字符串的拼接,其實現原理是使用StringBuilder.append()方法。

語法糖:語法糖(Syntactic sugar),也譯爲糖衣語法,是由英國計算機科學家彼得·蘭丁發明的一個術語,指計算機語言中添加的某種語法,這種語法對語言的功能沒有影響,但是更方便程序員使用。語法糖讓程序更加簡潔,有更高的可讀性。

 

二、性能比較

 

1. 三者在執行速度方面的比較:

 

StringBuilder > StringBuffer > String

 

2. String <(StringBuffer,StringBuilder)的原因

  • String:字符串常量
  • StringBuffer:字符串變量(有同步鎖)
  • StringBuilder:字符串變量(無同步鎖)

從上面的名字可以看到,String是"字符串常量",也就是不可改變的對象。源碼如下:

public final class String{}

對於上面這句話的理解你可能會產生這樣一個疑問 ,比如這段代碼:

String str = "唐伯虎";

str = str + "點香菸";

System.out.print(str); // result : "唐伯虎點香菸"

我們明明改變了String型的變量str啊,爲什麼說是沒有改變呢?我們來看一下這張對String操作時內存變化的圖:

 

       我們可以看到,初始String值爲"唐伯虎",然後在這個字符串後面加上新的字符串"點香菸",這個過程是需要重新在棧堆內存中開闢內存空間的,最終得到了"唐伯虎點香菸"字符串也相應的需要開闢內存空間,這樣短短的兩個字符串,卻需要開闢三次內存空間,不得不說這是對內存空間的極大浪費

爲了應對經常性操作字符串的場景,Java提供了其他兩個操作字符串的類 —— StringBuffer、StringBuilder。

       他們倆均屬於字符串變量,是可改變的對象,每當我們用它們對字符串做操作時,實際上是在一個對象上操作的,這樣就不會像String一樣創建一些而外的對象進行操作了,速度自然就相對快了。

       我們一般在StringBuffer、StringBuild類上的主要操作是 append 和 insert 方法,這些方法允許被重載,以接受任意類型的數據。每個方法都能有效地將給定的數據轉換成字符串,然後將該字符串的字符追加或插入到字符串緩衝區中。append 方法始終將這些字符添加到緩衝區的末端;而 insert 方法則在指定的點添加字符。

1. StringBuilder一個可變的字符序列是JDK1.5新增的。此類提供一個與 StringBuffer 兼容的 API,但不保證同步。該類被設計用作 StringBuffer 的一個簡易替換,用在字符串緩衝區被單個線程使用的時候(這種情況很普遍)。

2. 如果可能,建議優先採用StringBuilder類,因爲在大多數實現中,它比 StringBuffer 要快。且兩者的方法基本相同。然而在應用程序要求線程安全的情況下,則必須使用 StringBuffer 類。

       String 類型和 StringBuffer、 StringBuild類型的主要性能區別其實在於 String 是不可變的對象(final), 因此在每次對 String 類型進行改變的時候其實都等同於在堆中生成了一個新的 String 對象,然後將指針指向新的 String 對象,這樣不僅效率低下,而且大量浪費有限的內存空間,所以經常改變內容的字符串最好不要用 String 。因爲每次生成對象都會對系統性能產生影響,特別是當內存中的無引用對象過多了以後, JVM 的 GC 開始工作,那速度是一定會相當慢的。另外當GC清理速度跟不上new String的速度時,還會導致內存溢出Error,會直接kill掉主程序!報錯如下:

Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded

Exception in thread "I/O dispatcher 3797236" java.lang.OutOfMemoryError: GC overhead limit exceeded

 

一個特殊的例子:

String str = "This is only a" + " simple" + " test";

StringBuffer builder = new StringBuilder("This is only a").append(" simple").append(" test");

在上述例子中,經測試發現,生成str對象的速度遠高於builder,而這個時候StringBuffer居然速度上根本一點都不佔優勢。爲什麼呢?

 

其實這是JVM的一個把戲,實際上:

String str = "This is" + " a " + "demo";     等同於     String str = "This is a demo";

示例:

public class demo {

public static void main(String[] args) {

String str = "This is" + " a " + "demo";

System.out.println(str);

    }
}

dos反編譯後的內容如下(反編譯class文件的命令:javap -c demo)。

 

可見,在JVM優化時,如果是多個固定字符串拼接,會將這些固定字符串進行預處理,當成一個整體的字符串,相當於僅聲明一個常量,所以並不需要太多的時間。

 

但大家這裏要注意的是,如果字符串拼接的多個元素中有其他String對象的話,速度就沒那麼快了,譬如:

String str2 = "陳哈哈";

String str3 = "不喫";

String str4 = "香菜";

String str1 = str2 +str3 + str4;  // “陳哈哈不喫香菜”

或者:

String str5 = "陳哈哈";

String str6 = str5 + "不喫香菜";

這時候JVM會規規矩矩的按照原來的方式去做。

 

3. 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對象只能讀取,無法修改。試問:還有什麼不安全的呢? 

 

三、使用總結:

1. 如果不是在循環體中進行字符串拼接的話,直接使用 String 的 “+” 就好了。

2. 單線程循環中操作大量字符串數據 → StringBuilder.append()

3. 多線程循環中操作大量字符串數據 → StringBuffer.append()

其實拼接字符串的方式還有很多種,包括String.concat()、String.join("", str1, str2)、StringUtils.join(str1, str2) 等,但在我們日常開發中最常用的就是 String 的 “+” 和 StringXXX.append()方法啦,只要掌握好這三種方式的使用場景,就基本能保證代碼的高可用性了。好了,這篇文章就到這裏,希望能夠對你有幫助!

一張照片背後的故事

庫爾斯克會戰即將打響
所有人都在準備衝鋒
只有一個人站在原地

或許他預感這一仗他很難活下去
在衝鋒前的最後一刻
他親吻了手中的十字架

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章