在Java里拼接字符串,到底推荐用什么方式?


Runtime comparison

先说结论,+ >= StringBuilder > String.format

拼接的字符串较少时,直接 + 拼接和使用 StringBuilder.append 然后 toString 有近似的性能,这是由于jdk1.8编译时,已经将 +优化为使用 StringBuilder.append

甚至大多数情况下,+ 拼接的性能更好点 ,因为jdk编译成字节码后,+ 拼接的字符串已经编译成完成,而 StringBuilder.append 需要在运行时执行。

而当在循环中拼接的字符串,StringBuilder.append 的性能会比 + 好很多,因为 + 拼接在每次执行都需要创建 StringBuilder 对象。

不过这两者性能同时比String.format强出一截,这主要是由于String.format需要使用正则解析输入字符串,然后再填充参数。

接下来用一个例子验证下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StringTest {

public void testConcat() {
String a = "q" + "w" + "e" + "r";
String b = new StringBuilder().append("q").append("w").append("e").append("r").toString();

String c = "";
for (int i = 0; i < 100; i++) {
c = c + "q" + "w" + "e" + "r";
}

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
sb.append("q").append("w").append("e").append("r");
}
String d = sb.toString();
}

}

编译:

1
javac StringTest.java

查看字节码:

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
javap -c StringTest

Compiled from "StringTest.java"
public class StringTest {
public StringTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public void testConcat();
Code:
0: ldc #2 // String qwer
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: ldc #5 // String q
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: ldc #7 // String w
17: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: ldc #8 // String e
22: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: ldc #9 // String r
27: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
30: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
33: astore_2
34: ldc #11 // String
36: astore_3
37: iconst_0
38: istore 4
40: iload 4
42: bipush 100
44: if_icmpge 73
47: new #3 // class java/lang/StringBuilder
50: dup
51: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
54: aload_3
55: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
58: ldc #2 // String qwer
60: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
63: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
66: astore_3
67: iinc 4, 1
70: goto 40
73: new #3 // class java/lang/StringBuilder
76: dup
77: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
80: astore 4
82: iconst_0
83: istore 5
85: iload 5
87: bipush 100
89: if_icmpge 121
92: aload 4
94: ldc #5 // String q
96: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
99: ldc #7 // String w
101: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
104: ldc #8 // String e
106: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
109: ldc #9 // String r
111: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
114: pop
115: iinc 5, 1
118: goto 85
121: aload 4
123: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
126: astore 5
128: return
}

可以看到:

  • 13行,编译中+ 直接生成 "qwer"

  • 17行,StringBuilder 先初始化,再依次 append

  • 32行到45行,开始执行 +操作 的循环体,每次都需要初始化 StringBuilder 对象;

  • 85行到118行,开始执行 StringBuilder.append 操作的循环体,每次只用 append

从字节码来看,jdk编译后的内容和预期符合。

另外 Baeldung这篇文章 中有更详细的 Java String Performance 研究,若有兴趣可以读下。