美文网首页
Java代码中字符串拼接方式分析

Java代码中字符串拼接方式分析

作者: 雪中亮 | 来源:发表于2021-11-22 16:27 被阅读0次

    本文研讨的字符串拼接方式为以下4种:“+”号、StringBuilder、StringJoiner、String#join,对比分析及探讨最佳实践。

    结论

    后面内容比较枯燥,所以先说结论:

    1. 本文研讨的字符串拼接方式为以下4种:“+”号、StringBuilder、StringJoiner、String#join
    2. 在简单的字符串拼接场景中「如:"a" + "b" + "c"」,以上四种方式性能无明显差异。
    3. 在循环字符串拼接的场景下,使用“+”号性能最低,其他三种方式性能也无明显差异,但是根据验证结果可粗浅发现,指定初始容量的StringBuilder效率最高。当然不光考虑性能,也要考虑垃圾回收效率的问题,避免OOM。
    4. 本文最后补充对比了StringBuffer,在无争抢共享资源的场景下,StringBuffer性能并未明显变差。

    最佳实践

    1. 阿里巴巴Java开发手册-日志规约「5」可进行优化:使用占位符的形式可读性、便捷性不佳,可考虑使用Lambda,延迟字符串的拼接,且使用更加便利。
    2. 阿里巴巴Java开发手册-OOP 规约「23」可进行优化:循环拼接时须使用StringBuilder;在拼接大量的大容量字符串时,使用StringBuilder尽量指定初始容量。
    3. 简单的字符串拼接可用任意方式,推荐直接使用“+”号拼接,可读性最优。
    4. 尽量使用JDK等直接提供的特性「如“+”号拼接字符串,Synchronized关键词等」,因为编译器+JVM会持续对此进行优化,JDK升级即可获得更大的收益。除非有明确的理由可以自行实现类似的功能。
    5. 在需要考虑线程安全的场景可以考虑使用StringBuffer进行字符串拼接,不过一般来说没有这种需求,故不应该使用StringBuffer,避免增加复杂性。

    分析过程

    环境

    1. 系统: windows 10 21H1
    2. JDK: OpenJDK 1.8.0_302
    3. 分析用示例代码:
    @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
    
    1. 多次测试,可发现在字符串循环拼接场景下,直接使用“+”号性能最低,有初始容量的StringBuilder性能最高,其他方式性能均没有太大差异。
    2. 多次测试,可发现在字符串简单拼接场景下,使用“+”号、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代码中字符串拼接方式分析

          本文链接:https://www.haomeiwen.com/subject/xnybtrtx.html