「探索发现」Java反汇编字节码指令分析String和StringBuffer字符串拼接的区别

问题导出

  最近一直在复习知识点。今天在网上看到了一篇关于代码优化的文章,文章里谈到了字符串拼接的优化,通过String替代StringBuffer将多行代码优化成1行代码。代码如下:

1
2
3
4
5
6
7
8
//StringBuffer拼接
StringBuffer sb = new StringBuffer();
sb.append("abc");
sb.append("-");
sb.append("123");

//String拼接
String a = "abc" + "-" + "123";

  看到这心里满是疑惑,因为在这之前,一直认为字符串拼接的时候用StringBuffer效率会更高,而String拼接时会在内存中产生多个无用的对象,浪费内存和降低执行效率。于是抱着求知的心态,运用了Java反汇编的方式试着从字节码指令里看出什么门道来。

简单DEMO

  首先,用java写一个简单的Demo。代码如下:

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
public class Test {
public static void main(String[] args) {
string1();
string2();
}

public static void string1() {
String a = "abc" + "-" + "123";
}

public static void string2() {
StringBuffer sb = new StringBuffer();
sb.append("abc");
sb.append(t);
sb.append("123");
}

public static void string3() {
String a = "abc" + t + "123";
}

public static void string4() {
String a = "abc" + t;
a = a + "123";
}

private static String t = "-";
}

  其中:

  • string1()方法,对3个字符串常量进行了直接拼接。
  • string2()方法,通过StringBuffer拼接的方式对2个字符串常量与1个静态字符串变量进行了拼接(之所以不是3个字符串常量而是2个字符串常量和1个静态字符串变量是为了与string3()进行对比)。
  • string3()方法,对2个字符串常量和1个静态字符串变量进行直接拼接。
  • string4()方法,对2个字符串常量和1个静态字符串变量进行分段拼接。

JAVA反汇编

  然后使用javac和javap命令对代码进行编译与反汇编。

Java反汇编字节码指令分析String和StringBuffer字符串拼接的区别

字节码指令分析

  随后,得到了一个Test.txt文件,内容是反汇编后的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
72
73
74
75
76
77
78
79
80
81
82
83
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public static void main(java.lang.String[]);
Code:
0: invokestatic #2 // Method string1:()V
3: invokestatic #3 // Method string2:()V
6: return

public static void string1();
Code:
0: ldc #4 // String abc-123
2: astore_0
3: return

public static void string2();
Code:
0: new #5 // class java/lang/StringBuffer
3: dup
4: invokespecial #6 // Method java/lang/StringBuffer."<init>":()V
7: astore_0
8: aload_0
9: ldc #7 // String abc
11: invokevirtual #8 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
14: pop
15: aload_0
16: getstatic #9 // Field t:Ljava/lang/String;
19: invokevirtual #8 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
22: pop
23: aload_0
24: ldc #10 // String 123
26: invokevirtual #8 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer;
29: pop
30: return

public static void string3();
Code:
0: new #11 // class java/lang/StringBuilder
3: dup
4: invokespecial #12 // Method java/lang/StringBuilder."<init>":()V
7: ldc #7 // String abc
9: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: getstatic #9 // Field t:Ljava/lang/String;
15: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: ldc #10 // String 123
20: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: invokevirtual #14 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
26: astore_0
27: return

public static void string4();
Code:
0: new #11 // class java/lang/StringBuilder
3: dup
4: invokespecial #12 // Method java/lang/StringBuilder."<init>":()V
7: ldc #7 // String abc
9: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
12: getstatic #9 // Field t:Ljava/lang/String;
15: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: invokevirtual #14 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
21: astore_0
22: new #11 // class java/lang/StringBuilder
25: dup
26: invokespecial #12 // Method java/lang/StringBuilder."<init>":()V
29: aload_0
30: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: ldc #10 // String 123
35: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
38: invokevirtual #14 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
41: astore_0
42: return

static {};
Code:
0: ldc #15 // String -
2: putstatic #9 // Field t:Ljava/lang/String;
5: return
}

  通过以上的字节码指令可知:

  • string1()方法中,在编译器编译的时候,由于是3个常量,所以编译器将代码优化成了一个拼接之后完整的字符串直接进行赋值;
  • string2()方法中,和先前了解到的情况一样,调用了3次append()方法进行拼接;
  • string3()方法中,编译器会创建一个StringBuffer对象然后再进行字符串拼接;
  • string4()方法中,因为是进行了分段拼接,编译器这时就会创建多个StringBuffer和调用多次toString()将StringBuffer转换成String。

  因此可知,正常情况下如果是在一行代码内进行字符串的拼接,可以不使用StringBuffer,因为编译器会在字符串拼接的时候自动创建StringBuffer来进行拼接。而如果是多段拼接,这个时候使用StringBuffer则会比String更节省内存和更加高效(因为String在多段拼接的时候会产生多个StringBuffer和String对象)。