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!
推薦閱讀:
- 詳細圖解Java中字符串的初始化
- Java中的Kotlin 內部類原理
- Java字符串的intern方法有何奧妙之處
- 分析Java中Map的遍歷性能問題
- Java 匯編JVM編寫jasmin程序的操作方法