Java代碼中4種字符串拼接方式分析

本文研討的字符串拼接方式為以下4種:“+”號、StringBuilder、StringJoiner、String#join,對比分析及探討最佳實踐。

結論

後面內容比較枯燥,所以先說結論:

  • 本文研討的字符串拼接方式為以下4種:“+”號、StringBuilder、StringJoiner、String#join
  • 在簡單的字符串拼接場景中「如:"a" + "b" + "c"」,以上四種方式性能無明顯差異。
  • 在循環字符串拼接的場景下,使用“+”號性能最低,其他三種方式性能也無明顯差異,但是根據驗證結果可粗淺發現,指定初始容量的StringBuilder效率最高。當然不光考慮性能,也要考慮垃圾回收效率的問題,避免OOM。
  • 本文最後補充對比瞭StringBuffer,在無爭搶共享資源的場景下,StringBuffer性能並未明顯變差。

最佳實踐

  • 阿裡巴巴Java開發手冊-日志規約「5」可進行優化:使用占位符的形式可讀性、便捷性不佳,可考慮使用Lambda,延遲字符串的拼接,且使用更加便利。
  • 阿裡巴巴Java開發手冊-OOP 規約「23」可進行優化:循環拼接時須使用StringBuilder;在拼接大量的大容量字符串時,使用StringBuilder盡量指定初始容量。
  • 簡單的字符串拼接可用任意方式,推薦直接使用“+”號拼接,可讀性最優。
  • 盡量使用JDK等直接提供的特性「如“+”號拼接字符串,Synchronized關鍵詞等」,因為編譯器+JVM會持續對此進行優化,JDK升級即可獲得更大的收益。除非有明確的理由可以自行實現類似的功能。
  • 在需要考慮線程安全的場景可以考慮使用StringBuffer進行字符串拼接,不過一般來說沒有這種需求,故不應該使用StringBuffer,避免增加復雜性。

分析過程

環境

  • 系統: windows 10 21H1
  • JDK: OpenJDK 1.8.0_302

分析用示例代碼:

@Slf4j
public class StringConcat {

    @SneakyThrows
    public static void main(String[] args) {
        log.info("java虛擬機預熱開始");
        String[] strs = new String[6000000];
        for (int i = 0; i < strs.length; i++) {
            strs[i] = id();
        }
        loopStringJoiner(strs);
        loopStringJoin(strs);
        loopStringBuilder(strs);
        log.info("java虛擬機預熱結束");
        Thread.sleep(1000);
        log.info("開始測試:");

        Thread.sleep(1000);
        Stopwatch stopwatchLoopPlus = Stopwatch.createStarted();
//        loopPlus(strs);
        log.info("loop-plus: " + stopwatchLoopPlus.elapsed(TimeUnit.MILLISECONDS));

        Thread.sleep(1000);
        Stopwatch stopwatchLoopStringBuilderCapacity = Stopwatch.createStarted();
        loopStringBuilderCapacity(strs);
        log.info("loop-stringBuilderCapacity: " + stopwatchLoopStringBuilderCapacity.elapsed(TimeUnit.MILLISECONDS));

        Thread.sleep(1000);
        Stopwatch stopwatchLoopStringBuilder = Stopwatch.createStarted();
        loopStringBuilder(strs);
        log.info("loop-stringBuilder: " + stopwatchLoopStringBuilder.elapsed(TimeUnit.MILLISECONDS));

        Thread.sleep(1000);
        Stopwatch stopwatchLoopJoin = Stopwatch.createStarted();
        loopStringJoin(strs);
        log.info("loop-String.join: " + stopwatchLoopJoin.elapsed(TimeUnit.MILLISECONDS));

        Thread.sleep(1000);
        Stopwatch stopwatchLoopStringJoiner = Stopwatch.createStarted();
        loopStringJoiner(strs);
        log.info("loop-stringJoiner: " + stopwatchLoopStringJoiner.elapsed(TimeUnit.MILLISECONDS));

        Thread.sleep(1000);
        Stopwatch stopwatchSimplePlus = Stopwatch.createStarted();
        for (int i = 0; i < 500000; i++) {
            simplePlus(id(), id(), id());
        }
        log.info("simple-Plus: " + stopwatchSimplePlus.elapsed(TimeUnit.MILLISECONDS));

        Thread.sleep(1000);
        Stopwatch stopwatchSimpleStringBuilder = Stopwatch.createStarted();
        for (int i = 0; i < 500000; i++) {
            simpleStringBuilder(id(), id(), id());
        }
        log.info("simple-StringBuilder: " + stopwatchSimpleStringBuilder.elapsed(TimeUnit.MILLISECONDS));

        Thread.sleep(1000);
        Stopwatch stopwatchSimpleStringBuffer = Stopwatch.createStarted();
        for (int i = 0; i < 500000; i++) {
            simpleStringBuffer(id(), id(), id());
        }
        log.info("simple-StringBuffer: " + stopwatchSimpleStringBuffer.elapsed(TimeUnit.MILLISECONDS));

    }

    private static String loopPlus(String[] strs) {
        String str = "";
        for (String s : strs) {
            str = str + "+" + s;
        }
        return str;
    }

    private static String loopStringBuilder(String[] strs) {
        StringBuilder str = new StringBuilder();
        for (String s : strs) {
            str.append("+");
            str.append(s);
        }
        return str.toString();
    }

    private static String loopStringBuilderCapacity(String[] strs) {
        StringBuilder str = new StringBuilder(strs[0].length() * strs.length);
        for (String s : strs) {
            str.append("+");
            str.append(s);
        }
        return str.toString();
    }

    private static String loopStringJoin(String[] strs) {
        StringJoiner joiner = new StringJoiner("+");
        for (String str : strs) {
            joiner.add(str);
        }
        return joiner.toString();
    }

    private static String loopStringJoiner(String[] strs) {
        return String.join("+", strs);
    }

    private static String simplePlus(String a, String b, String c) {
        return a + "+" + b + "+" + c;
    }

    private static String simpleStringBuilder(String a, String b, String c) {
        StringBuilder builder = new StringBuilder();
        builder.append(a);
        builder.append("+");
        builder.append(b);
        builder.append("+");
        builder.append(c);
        return builder.toString();
    }

    private static String simpleStringBuffer(String a, String b, String c) {
        StringBuffer buffer = new StringBuffer();
        buffer.append(a);
        buffer.append("+");
        buffer.append(b);
        buffer.append("+");
        buffer.append(c);
        return buffer.toString();
    }

    private static String id() {
        return UUID.randomUUID().toString();
    }

}

結果及總結

– java虛擬機預熱開始
– java虛擬機預熱結束
– 開始測試:
– loop-plus: 執行超時
– loop-stringBuilderCapacity: 285
– loop-stringBuilder: 1968
– loop-String.join: 1313
– loop-stringJoiner: 1238
– simple-Plus: 812
– simple-StringBuilder: 840
– simple-StringBuffer: 857

  • 多次測試,可發現在字符串循環拼接場景下,直接使用“+”號性能最低,有初始容量的StringBuilder性能最高,其他方式性能均沒有太大差異。
  • 多次測試,可發現在字符串簡單拼接場景下,使用“+”號、StringBuilder、StringBuffer性能差距在5%左右,可理解為測試誤差,可認為三種方式性能一致。

代碼及結果分析

1. StringBuilder與StringBuffer對比

在無爭搶共享資源的場景下,JVM會使用偏向鎖等方法優化,甚至會進行鎖消除,使用Synchronized關鍵詞與否,性能並無明顯差異。

2. 字節碼分析

對比上述#simplePlus和#simpleStringBuilder兩個方法的字節碼,可明顯看到兩方法執行內容基本一致,但是直接使用"+"號時處理流程更短,可見編譯器進行瞭深度優化,使用優化後的字節碼理論上會有更高的性能:

  // access flags 0xA
  private static simplePlus(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    // parameter  a
    // parameter  b
    // parameter  c
   L0
    LINENUMBER 125 L0
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 0
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "+"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "+"
    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;
    ARETURN
   L1
    LOCALVARIABLE a Ljava/lang/String; L0 L1 0
    LOCALVARIABLE b Ljava/lang/String; L0 L1 1
    LOCALVARIABLE c Ljava/lang/String; L0 L1 2
    MAXSTACK = 2
    MAXLOCALS = 3

  // access flags 0xA
  private static simpleStringBuilder(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    // parameter  a
    // parameter  b
    // parameter  c
   L0
    LINENUMBER 129 L0
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ASTORE 3
   L1
    LINENUMBER 130 L1
    ALOAD 3
    ALOAD 0
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    POP
   L2
    LINENUMBER 131 L2
    ALOAD 3
    LDC "+"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    POP
   L3
    LINENUMBER 132 L3
    ALOAD 3
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    POP
   L4
    LINENUMBER 133 L4
    ALOAD 3
    LDC "+"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    POP
   L5
    LINENUMBER 134 L5
    ALOAD 3
    ALOAD 2
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    POP
   L6
    LINENUMBER 135 L6
    ALOAD 3
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ARETURN
   L7
    LOCALVARIABLE a Ljava/lang/String; L0 L7 0
    LOCALVARIABLE b Ljava/lang/String; L0 L7 1
    LOCALVARIABLE c Ljava/lang/String; L0 L7 2
    LOCALVARIABLE builder Ljava/lang/StringBuilder; L1 L7 3
    MAXSTACK = 2
    MAXLOCALS = 4

到此這篇關於Java代碼中4種字符串拼接方式分析的文章就介紹到這瞭,更多相關Java 字符串拼接內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: