1. 程式人生 > >java四種字串拼接方式效能分析

java四種字串拼接方式效能分析

前幾天寫一個防重複提交的元件的時候,有一個操作是需要將各個欄位的字串拼接成一個requestKey。看了別人的程式碼,我發現,中介軟體這種東西,每天都要處理幾百幾千萬的請求,但是裡面很多字串拼接的時候還是很原始的“+”號拼接,如果將所有的字串拼接操作都換成更高效的實現方式,伺服器的效能會不會要好一點,於是我簡單的做了個實驗對比一下我們常見的字串拼接方法的效率。常見的四種字串拼接方法分別是
1.直接用“+”號
2.使用String的方法concat
3.使用StringBuilder的append
4.使用StringBuffer的append

字串拼接我分為了兩種拼接方式,一種是同一個字串大量迴圈拼接,第二種是大量不同的字串每次都拼接固定數量的字串。第二種更接近與伺服器上的使用場景,因為工程上是很少有一個字串不斷拼接的,基本都是大量字串按同一種模式拼接(比如構造上報引數)。程式碼如下:

public class StringConcat {
    public static void main(String[] args) {
        plus();
        concat();
        stringBuffer();
        stringBuilder();
    }

    public static void plus(){
        String initial = "";
        long start = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
initial = initial + "a"; } long end = System.currentTimeMillis(); System.out.println("plus:" + (end - start)); start = System.currentTimeMillis(); for(int i = 0; i < 100000; i++){ String a = "a"; a = a + String.valueOf(i); //a = a + String.valueOf
(i) + String.valueOf(i) + String.valueOf(i) + String.valueOf(i) + String.valueOf(i) + String.valueOf(i) + String.valueOf(i) + String.valueOf(i); } end = System.currentTimeMillis(); System.out.println("double plus:" + (end - start)); } public static void stringBuilder(){ StringBuilder initial = new StringBuilder(""); long start = System.currentTimeMillis(); for(int i = 0; i < 100000; i++){ initial = initial.append("b"); } long end = System.currentTimeMillis(); System.out.println("StringBuilder:" + (end - start)); start = System.currentTimeMillis(); for(int i = 0; i < 100000; i++){ StringBuilder b = new StringBuilder("b"); b.append(String.valueOf(i)) //b.append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)); } end = System.currentTimeMillis(); System.out.println("double StringBuilder:" + (end - start)); } public static void stringBuffer(){ StringBuffer initial = new StringBuffer(""); long start = System.currentTimeMillis(); for(int i = 0; i < 100000; i++){ initial = initial.append("c"); } long end = System.currentTimeMillis(); System.out.println("StringBuffer:" + (end - start)); start = System.currentTimeMillis(); for(int i = 0; i < 100000; i++){ StringBuffer c = new StringBuffer("c"); c.append(String.valueOf(i)); //c.append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)).append(String.valueOf(i)); } end = System.currentTimeMillis(); System.out.println("double StringBuffer:" + (end - start)); } public static void concat(){ String initial = ""; long start = System.currentTimeMillis(); for(int i = 0; i < 100000; i++){ initial = initial.concat("d"); } long end = System.currentTimeMillis(); System.out.println("concat:" + (end - start)); start = System.currentTimeMillis(); for(int i = 0; i < 100000; i++){ String d = "d"; d = d.concat(String.valueOf(i)); d = //d.concat(String.valueOf(i)).concat(String.valueOf(i)).concat(String.valueOf(i)).concat(String.valueOf(i)).concat(String.valueOf(i)).concat(String.valueOf(i)).concat(String.valueOf(i)).concat(String.valueOf(i)); } end = System.currentTimeMillis(); System.out.println("double concat:" + (end - start)); } }

結果如下:
這裡寫圖片描述

結果:從結果可以看出,在拼接10萬次的情況下,迴圈拼接同一個字串的時候,StringBuilder和StringBuffer的優勢是巨大的,僅僅花了不到10ms的時間,而StringBuilder稍快一點。而直接用“+”號和concat都花費了秒級的時間。但是在對10萬字符串都拼接一個串的時候,concat的情況發生了逆轉,速度甚至比StringBuilder和StringBuffer更快。

原理分析

1.加號拼接
開啟編譯後的位元組碼我們可以發現加號拼接字串jvm底層其實是呼叫StringBuilder來實現的,也就是說”a” + “b”等效於下面的程式碼片。

String a = "a";
StringBuilder sb = new StringBuilder();
sb.append(a).append("b");
String str = sb.toString();

但並不是說直接用“+”號拼接就可以達到StringBuilder的效率了,因為用“+”號每拼接一次都會新建一個StringBuilder物件,並且最後toString()方法還會生成一個String物件。在迴圈拼接十萬次的時候,就會生成十萬個StringBuilder物件,十萬個String物件,這簡直就是噩夢。

2.concat拼接
concat的原始碼如下,可以看到,concat其實就是申請一個char型別的buf陣列,將需要拼接的字串都放在這個數組裡,最後再轉換成String物件。

public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }

3.StringBuilder/StringBuffer
這兩個類實現append的方法都是呼叫父類AbstractStringBuilder的append方法,只不過StringBuffer是的append方法加了sychronized關鍵字,因此是執行緒安全的。append程式碼如下,他主要也是利用char陣列儲存字元,通過ensureCapacityInternal方法來保證陣列容量可用還有擴容。

public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

他擴容的方法的程式碼如下,可見,當容量不夠的時候,陣列容量右移1位(也就是翻倍)再加2,以前的jdk貌似是直接寫成int newCapacity = (value.length * 2) + 2,後來優化成了右移,可見,右移的效率還是比直接乘更高的。

private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

原因分析

1.迴圈拼接字串
通過實驗我們發現,在迴圈拼接同一個字串的時候,他們效率的按快慢排序是
StringBulider > StringBuffer >> String.concat > “+”。
StringBulider比StringBuffer更快這個容易理解,因為StringBuffer的方法是sychronized修飾的,同步的時候會損耗掉一些效能。StringBulider和String.concat的區別,主要在擴容上,String.concat是需要多少擴多少,而StringBulider是每次翻兩倍,指數級擴容。在10萬次拼接中,String.concat需要擴容10萬次,而StringBuilder只需要擴容log100000次(大約17次),除此之外,concat每次都會生成一個新的String物件,而StringBuilder則不必,那StringBuilder如此快就不難解釋了。至於直接用“+”號連線,之前已經說了,它會產生大量StringBuilder和String物件,當然就最慢了。
2.大量字串拼接
在只拼接少量字串的情況下的時候,他們效率的按快慢排序是
String.concat > StringBulider > StringBuffer > “+”。
為什麼在拼接少量字串的時候,String.concat就比StringBuilder快了呢,原因大致有兩點,一是StringBuilder的呼叫棧更深,二是StringBuilder產生的垃圾物件更多,並且它重寫的toString方法為了做到不可變性採用了“保護性拷貝”,因此效率不如concat。
詳細原因分析參考:concat和StringBuilder效能分析
保護性拷貝見:保護性拷貝
當拼接的字串少的時候,concat因為上述優勢略快,但是當一次性拼接字串多的時候,StringBuilder的擴容更少優勢便會開始展現出來,例如一次拼接8個字串,concat的效率就已經明顯不如StringBuilder了,如下圖。
一次拼接8個字串

結論

從以上分析我們可以得出以下幾點結論
1.無論如何直接用“+”號連線字串都是最慢的
2.在拼接少數字符串(不超過4個)的時候,concat效率是最高的
3.多個字串拼接的時候,StringBuilder/StringBuffer的效率是碾壓的
4.在不需要考慮執行緒安全問題的時候,使用StringBuilder的效率比StringBuffer更高