The string splicing methods discussed in this article are the following four: "+" sign, StringBuilder, StringJoiner, String#join, comparative analysis and discussion of best practices.

in conclusion

The following content is relatively boring, so let's talk about the conclusion first:

  1. The string splicing methods discussed in this article are as follows: "+" sign, StringBuilder, StringJoiner, String#join
  2. In a simple string concatenation scenario "such as: "a" + "b" + "c"", the performance of the above four methods has no significant difference.
  3. In the scenario of cyclic string splicing, using the "+" sign has the lowest performance, and there is no significant difference in performance between the other three methods. However, according to the verification results, it can be superficially found that the StringBuilder with the specified initial capacity is the most efficient. Of course, not only performance is considered, but also the efficiency of garbage collection to avoid OOM.
  4. At the end of this article, a comparison of StringBuffer is added. In the scenario where there is no competition for shared resources, the performance of StringBuffer does not deteriorate significantly.

Best Practices

  1. Alibaba Java Development Manual - Log Protocol "5" can be optimized: the use of placeholders is not readable and convenient, and Lambda can be considered to delay string splicing, and it is more convenient to use.
  2. Alibaba Java Development Manual - OOP Protocol "23" can be optimized: StringBuilder must be used when splicing loops; when splicing a large number of large-capacity strings, use StringBuilder to specify the initial capacity as much as possible.
  3. Simple string concatenation can be done in any way. It is recommended to use the "+" sign to concatenate directly, for the best readability.
  4. Try to use the features directly provided by JDK, such as "+" splicing strings, Synchronized keywords, etc., because the compiler + JVM will continue to optimize this, and JDK upgrades can get greater benefits. Unless there is a clear reason to implement something similar on your own.
  5. In scenarios where thread safety needs to be considered, you can consider using StringBuffer for string concatenation, but generally there is no such requirement, so StringBuffer should not be used to avoid increasing complexity.

Analysis process

surroundings

  1. System: windows 10 21H1
  2. JDK: OpenJDK 1.8.0_302
  3. Sample code for analysis:
@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();
    }

}

Results and Summary

- 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
  1. After many tests, it can be found that in the scenario of cyclic splicing of strings, the direct use of the "+" sign has the lowest performance, the StringBuilder with initial capacity has the highest performance, and the performance of other methods is not much different.
  2. After many tests, it can be found that in the scenario of simple string splicing, the performance difference between using the "+" sign, StringBuilder, and StringBuffer is about 5%, which can be understood as a test error, and the performance of the three methods can be considered to be consistent.

Code and result analysis

1. StringBuilder vs StringBuffer

In the scenario where there is no competition for shared resources, the JVM will use biased locks and other methods to optimize, and even eliminate locks. There is no significant difference in performance whether the Synchronized keyword is used or not.

2. Bytecode Analysis

Comparing the bytecodes of the above two methods #simplePlus and #simpleStringBuilder, it is obvious that the execution content of the two methods is basically the same, but the processing flow is shorter when the "+" sign is directly used. It can be seen that the compiler has carried out in-depth optimization. The bytecode would theoretically have higher performance:

  // 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

bitkylin
161 声望13 粉丝

爱好技术、爱好开源、爱好分享。曾在 .NET、Android、Web 前端等领域搬砖,目前致力于 Java 后端工作与学习,靡不有初,鲜克有终。