Java String 字串拼接的三種方式與效率對比分析

JeremyTsai發表於2020-10-26

String 字串的拼接

+ 號拼接

通過 + 號拼接是最常見的拼接方式了。

String jeremy = "Jeremy";
String tsai = "Tsai";
String jeremytsai = jeremy + tsai;

觀察位元組碼

   L0
    LINENUMBER 12 L0
    LDC "Jeremy"
    ASTORE 1
   L1
    LINENUMBER 13 L1
    LDC "Tsai"
    ASTORE 2
   L2
    LINENUMBER 14 L2
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 2
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 3

我們不難發現, String str = a + b 被JDK編譯器在編譯位元組碼的時候幫我們優化成為了以下語句:

String jeremytsai = new StringBuilder().append(jeremy).append(tsai);

區別與C++的運算子過載,這僅僅是JDK內部優化的語法糖,Java本身沒有運算子過載之說。但是要注意這種語法糖, 只對於同一行才有效,例如:

 s = hello + world + jeremy + tsai;

注: 他們都是變數。

若將其拆分

s = hello;
s += world;
s += jeremy;
s += tsai;

檢視位元組碼:

   L5
    LINENUMBER 15 L5
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 5
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 2
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 5
   L6
    LINENUMBER 16 L6
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 5
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 3
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 5
   L7
    LINENUMBER 17 L7
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 5
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 4
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 5

可以看到,每次拼接都會建立一個StringBuilder。所以說這只是語法糖,不能算符號過載,稍加不注意就行影響效率,因為我們日常編寫不可能用+號拼接很長的串,中間還有涉及業務邏輯。

String.concat(String str) 方法

concat方法是String給我們提供的拼接字串的方法。

String jeremy = "Jeremy";
String tsai = "Tsai";
String jeremytsai = jeremy.concat(tsai);

原始碼描述如下:

將指定的字串連線到該字串的末尾。
如果引數字串的長度為0,則返回此String物件。
否則,返回一個String物件,該物件表示一個字元序列,該字元序列是此String物件表示的字元序列與引數字串表示的字元序列的串聯。

原始碼

public String concat(String str) {
    int otherLen = str.length(); // 獲取引數字串長度
    if (otherLen == 0) {
        return this; // 引數長度為0,返回自身
    }
    int len = value.length; //獲取自身長度
    char buf[] = Arrays.copyOf(value, len + otherLen); // 得到一個包含當前字元序列,長度為													// 兩者之和的字元陣列
    str.getChars(buf, len); // 從當前字元序列長度開始,將引數的字元序列寫入buf字元陣列
    return new String(buf, true); // 建立新的String物件並返回。
}

跟描述一樣。

StringBuilder 拼接字串

String jeremy = "jeremy";
String tsai = "tsai";
String jeremytsai = new StringBuilder().append(jeremy).append(tsai).toString();

檢視位元組碼

   L0
    LINENUMBER 13 L0
    LDC "jeremy"
    ASTORE 1
   L1
    LINENUMBER 14 L1
    LDC "tsai"
    ASTORE 2
   L2
    LINENUMBER 15 L2
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 2
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 3

可以看出,與 +號拼接一致。

String 字串拼接效率對比

先知道一點,String在Java中是不可變物件,因此每次拼接都是生成新的String物件,為了解決頻繁的記憶體開闢消耗資源,才有了StringBuilder類。在+拼接過程中,JDK預設優化成為StringBuilder以提高執行效率。

但是這裡又出現了一個問題,當且僅有兩個字串拼接生成一個新的字串,這個預設優化的優勢就體現不出來了。因為本來只需要三份String空間,預設優化StringBuilder的情況下,還需要一份StringBuilder的空間,多開闢了一份空間,肯定會對效能有所影響,為了驗證這一猜想,簡單寫了一個小程式測試.

// 定義要拼接的陣列
String jeremy = "Jeremy";
String tsai = "Tsai";
// 然後分別記錄各個拼接的耗時
String jeremytsai1 = jeremy + tsai;
String jeremytsai2 = jeremy.concat(tsai);

結果如我所料:

-------------------------------------------
+/StringBuilder拼接 jeremytsai 的執行時間為:100納秒	concat拼接 jeremytsai 的執行時間為:200納秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的執行時間為:200納秒	concat拼接 jeremytsai 的執行時間為:100納秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的執行時間為:100納秒	concat拼接 jeremytsai 的執行時間為:200納秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的執行時間為:100納秒	concat拼接 jeremytsai 的執行時間為:200納秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的執行時間為:200納秒	concat拼接 jeremytsai 的執行時間為:100納秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的執行時間為:0納秒	concat拼接 jeremytsai 的執行時間為:100納秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的執行時間為:100納秒	concat拼接 jeremytsai 的執行時間為:300納秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的執行時間為:100納秒	concat拼接 jeremytsai 的執行時間為:100納秒
-------------------------------------------
+/StringBuilder拼接 jeremytsai 的執行時間為:100納秒	concat拼接 jeremytsai 的執行時間為:0納秒
在100000次拼接JeremyTsai中,+/StringBuilder快的次數為: 29510,concat快的次數為:70490

迴圈拼接

迴圈拼接是一種特殊的拼接,其形式一般為:

String 結果;
for(迴圈條件) {
    String 中間量;
    // 計算
    結果 += 中間量
}

在這種情況下,JDk的預設優化就顯得很笨拙了,例如:

String jeremytsai = "";
for (int i = 0; i < 100; i++) {
    jeremytsai += "JeremyTsai\n";
}

檢視原始碼

   L0
    LINENUMBER 15 L0
    LDC ""
    ASTORE 1
   L1
    LINENUMBER 16 L1
    ICONST_0
    ISTORE 2
   L2
   FRAME APPEND [java/lang/String I]
    ILOAD 2
    BIPUSH 100
    IF_ICMPGE L3
   L4
    LINENUMBER 17 L4
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "JeremyTsai\n"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 1
   L5
    LINENUMBER 16 L5
    IINC 2 1
    GOTO L2

可以看出,+ 號的預設優化使得每個迴圈體內部都要new一個新的StringBuilder進行拼接,這會大大降低效能。同理,concat也一樣,每次拼接會生成新的String物件,會頻繁開闢空間,效率不高。

故,在迴圈體中的字串拼接推薦使用StringBuilder

StringBuilder jeremytsai = new StringBuilder();
for (int i = 0; i < 100; i++) {
    jeremytsai.append("jeremy").append("tsai\n");
}

字串拼接總結

在非迴圈體中的字串拼接,若只是兩個字串拼接,推薦使用concat

多字元或迴圈體中拼接字串優先使用StringBuilder,提高效率,還能鏈式程式設計。不要過於依賴+號拼接的語法糖,但是簡單拼接還是推薦使用的。畢竟能省很多程式碼量。

相關文章