本文研討的字符串拼接方式為以下4種:“+”號(hào)、StringBuilder、StringJoiner、String#join,對(duì)比分析及探討最佳實(shí)踐。
結(jié)論
后面內(nèi)容比較枯燥,所以先說(shuō)結(jié)論:
- 本文研討的字符串拼接方式為以下4種:“+”號(hào)、StringBuilder、StringJoiner、String#join
- 在簡(jiǎn)單的字符串拼接場(chǎng)景中「如:"a" + "b" + "c"」,以上四種方式性能無(wú)明顯差異。
- 在循環(huán)字符串拼接的場(chǎng)景下,使用“+”號(hào)性能最低,其他三種方式性能也無(wú)明顯差異,但是根據(jù)驗(yàn)證結(jié)果可粗淺發(fā)現(xiàn),指定初始容量的StringBuilder效率最高。當(dāng)然不光考慮性能,也要考慮垃圾回收效率的問(wèn)題,避免OOM。
- 本文最后補(bǔ)充對(duì)比了StringBuffer,在無(wú)爭(zhēng)搶共享資源的場(chǎng)景下,StringBuffer性能并未明顯變差。
最佳實(shí)踐
- 阿里巴巴Java開(kāi)發(fā)手冊(cè)-日志規(guī)約「5」可進(jìn)行優(yōu)化:使用占位符的形式可讀性、便捷性不佳,可考慮使用Lambda,延遲字符串的拼接,且使用更加便利。
- 阿里巴巴Java開(kāi)發(fā)手冊(cè)-OOP 規(guī)約「23」可進(jìn)行優(yōu)化:循環(huán)拼接時(shí)須使用StringBuilder;在拼接大量的大容量字符串時(shí),使用StringBuilder盡量指定初始容量。
- 簡(jiǎn)單的字符串拼接可用任意方式,推薦直接使用“+”號(hào)拼接,可讀性最優(yōu)。
- 盡量使用JDK等直接提供的特性「如“+”號(hào)拼接字符串,Synchronized關(guān)鍵詞等」,因?yàn)榫幾g器+JVM會(huì)持續(xù)對(duì)此進(jìn)行優(yōu)化,JDK升級(jí)即可獲得更大的收益。除非有明確的理由可以自行實(shí)現(xiàn)類似的功能。
- 在需要考慮線程安全的場(chǎng)景可以考慮使用StringBuffer進(jìn)行字符串拼接,不過(guò)一般來(lái)說(shuō)沒(méi)有這種需求,故不應(yīng)該使用StringBuffer,避免增加復(fù)雜性。
分析過(guò)程
環(huán)境
- 系統(tǒng): windows 10 21H1
- JDK: OpenJDK 1.8.0_302
- 分析用示例代碼:
@Slf4j
public class StringConcat {
@SneakyThrows
public static void main(String[] args) {
log.info("java虛擬機(jī)預(yù)熱開(kāi)始");
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虛擬機(jī)預(yù)熱結(jié)束");
Thread.sleep(1000);
log.info("開(kāi)始測(cè)試:");
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();
}
}
結(jié)果及總結(jié)
- java虛擬機(jī)預(yù)熱開(kāi)始
- java虛擬機(jī)預(yù)熱結(jié)束
- 開(kāi)始測(cè)試:
- loop-plus: 執(zhí)行超時(shí)
- loop-stringBuilderCapacity: 285
- loop-stringBuilder: 1968
- loop-String.join: 1313
- loop-stringJoiner: 1238
- simple-Plus: 812
- simple-StringBuilder: 840
- simple-StringBuffer: 857
- 多次測(cè)試,可發(fā)現(xiàn)在字符串循環(huán)拼接場(chǎng)景下,直接使用“+”號(hào)性能最低,有初始容量的StringBuilder性能最高,其他方式性能均沒(méi)有太大差異。
- 多次測(cè)試,可發(fā)現(xiàn)在字符串簡(jiǎn)單拼接場(chǎng)景下,使用“+”號(hào)、StringBuilder、StringBuffer性能差距在5%左右,可理解為測(cè)試誤差,可認(rèn)為三種方式性能一致。
代碼及結(jié)果分析
1. StringBuilder與StringBuffer對(duì)比
在無(wú)爭(zhēng)搶共享資源的場(chǎng)景下,JVM會(huì)使用偏向鎖等方法優(yōu)化,甚至?xí)M(jìn)行鎖消除,使用Synchronized關(guān)鍵詞與否,性能并無(wú)明顯差異。
2. 字節(jié)碼分析
對(duì)比上述#simplePlus和#simpleStringBuilder兩個(gè)方法的字節(jié)碼,可明顯看到兩方法執(zhí)行內(nèi)容基本一致,但是直接使用"+"號(hào)時(shí)處理流程更短,可見(jiàn)編譯器進(jìn)行了深度優(yōu)化,使用優(yōu)化后的字節(jié)碼理論上會(huì)有更高的性能:
// 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