经常看到一些论坛在谈java代码优化的时候讲到要将字符串连接操作”+”换成StringBuilder(或StringBuffer,后面为简单起见,只说StringBuilder)的append操作以提升性能,那么字符串连接使用StringBuilder#append来替代”+”真的会带来性能提升吗?
不忙回答,先看几个例子,代码如下:
public
class
StringConcat {
public
static
void
main(String... args) {
concat1();
concat2();
concat3();
}
public
static
void
concat1() {
String s =
"today is "
+
"a good day"
;
System.out.println(s);
}
public
static
void
concat2() {
int
count =
2
;
String tmp =
" on the desk"
;
String s2 =
"there are "
+ count +
" books "
+ tmp;
System.out.println(s2);
}
public
static
void
concat3() {
String s3 =
""
;
for
(
int
i=
0
; i<
100
; i++) {
s3 = s3 + i;
}
System.out.println(s3);
}
}
|
接下来分别分析下这三个操作字符串的方法,通过javap命令反编译.class文件:javap -c StringConcat ,获得字节码指令如下(只摘取concat1,concat2,concat3三个方法的):
public
static
void
concat1();
Code:
0
: ldc #
5
;
//String today is a good day
2
: astore_0
3
: getstatic #
6
;
//Field java/lang/System.out:Ljava/io/PrintStream;
6
: aload_0
7
: invokevirtual #
7
;
//Method java/io/PrintStream.println:(Ljava/lang/String;)V
10
:
return
public
static
void
concat2();
Code:
0
: iconst_2
1
: istore_0
2
: ldc #
8
;
//String on the desk
4
: astore_1
5
:
new
#
9
;
//class java/lang/StringBuilder
8
: dup
9
: invokespecial #
10
;
//Method java/lang/StringBuilder."<init>":()V
12
: ldc #
11
;
//String there are
14
: invokevirtual #
12
;
//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17
: iload_0
18
: invokevirtual #
13
;
//Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
21
: ldc #
14
;
//String books
23
: invokevirtual #
12
;
//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
26
: aload_1
27
: invokevirtual #
12
;
//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
30
: invokevirtual #
15
;
//Method java/lang/StringBuilder.toString:()Ljava/lang/String;
33
: astore_2
34
: getstatic #
6
;
//Field java/lang/System.out:Ljava/io/PrintStream;
37
: aload_2
38
: invokevirtual #
7
;
//Method java/io/PrintStream.println:(Ljava/lang/String;)V
41
:
return
public
static
void
concat3();
Code:
0
: ldc #
16
;
//String
2
: astore_0
3
: iconst_0
4
: istore_1
5
: iload_1
6
: bipush
100
8
: if_icmpge
36
11
:
new
#
9
;
//class java/lang/StringBuilder
14
: dup
15
: invokespecial #
10
;
//Method java/lang/StringBuilder."<init>":()V
18
: aload_0
19
: invokevirtual #
12
;
//Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22
: iload_1
23
: invokevirtual #
13
;
//Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
26
: invokevirtual #
15
;
//Method java/lang/StringBuilder.toString:()Ljava/lang/String;
29
: astore_0
30
: iinc
1
,
1
33
:
goto
5
36
: getstatic #
6
;
//Field java/lang/System.out:Ljava/io/PrintStream;
39
: aload_0
40
: invokevirtual #
7
;
//Method java/io/PrintStream.println:(Ljava/lang/String;)V
|
来分析下三个方法的字节码含义
在concat1中,是两个字面值(字符串常量)的连接,从concat1的字节码的第0条(0: ldc #5; //String today is a good day)可以看到,该方法直接从常量池加载”String today is a good day”,也就是说,String s = “today is ” + “a good day”;这条语句在编译后已经变成了一个字符串,等效于String s = “String today is a good day”,运行期间根本无需做连接操作了,所以对于字符串字面值的连接,使用StringBuilder是没有任何意义的。
在concat2中,是变量参与字符串的连接。从反编译的字节码中可以看出,编译期间已经转换成了StringBuilder的append操作,
String s2 =
"there are "
+ count +
" books "
+ tmp;
|
语句在编译之后已经等效于(即[5,30]之间的指令):
String s2 =
new
StringBuilder().append(
"there are "
).append(count).append(
" books"
).append(tmp).toString();
|
由此可见,在这样的字符串连接代码里显式使用new StringBuilder().append并不会带来性能的提升,因为String的“+”操作符在编译的时候已经被转换成new StringBuilder().append了。当然,StringBuilder可以传入一个int参数,作为其初始容量,这在生成的代码中是没法做到的,而只有程序可以控制。
最后看concat3,for循环中使用字符串连接,最后在for循环外使用连接后的字符串。字节码中的[11,29]之间是循环体,很容易发现,循环体中做了new StringBuilder的操作,字节码代表的代码含义如下:
String s3 =
""
;
for
(
int
i=
0
; i<
100
; i++) {
s3 =
new
StringBuilder().append(s3).append(i).toString();
}
|
在这种情况下,编译器的优化并不如我们的意,我们想要的优化代码是这样的:
String s3 =
""
;
StringBuilder tmp =
new
StringBuilder();
tmp.append(s3);
for
(
int
i=
0
; i<
100
; i++) {
tmp.append(i);
}
s3 = tmp.toString();
|
这对于编译器来说有些复杂了,我们需要手工才能做到。
综上三个方法的分析发现,用StringBuilder(StringBuffer)#append替代字符串”+”是否会带来性能提升并不是一成不变的,在不同的条件下情况也不相同,字符串字面值的连接在编译期间已经连接好了,普通的字符串连接并不需要显式的使用new StringBuilder().append来增加效率,编译器已经给我们做掉了,除非能在显式使用时能给出有效的初始容量。在这种意义下,个人觉得string的”+”可以认为是StringBuilder#append的一个语法糖;但是如果形如concat3那种循环中的字符串连接,我们就需要显式使用StringBuilder了。在jdk1.4的时候,还没有StringBuilder类,编译器生成的优化代码使用的是StringBuffer。
针对String连接操作编译器生成的StringBuidler#append肯定是单个线程在操作,因此不会有线程安全问题。