本文研讨的字符串拼接方式为以下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
- 分析用示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
| @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();
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(); }
}
|
结果及总结
1 2 3 4 5 6 7 8 9 10 11
| - 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两个方法的字节码,可明显看到两方法执行内容基本一致,但是直接使用”+”号时处理流程更短,可见编译器进行了深度优化,使用优化后的字节码理论上会有更高的性能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| private static simplePlus(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; 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
private static simpleStringBuilder(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; 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
|