Java | 谈谈StringBuilder的使用和细节
前言
众所周知,在Java中String对象是不可变的。不可变性会导致一系列的效率问题,例如下面几行代码,为了生成最终的结果,I
首先会和love
连接生成一个I love
String对象,然后再和java.
连接,再次生成一个新的String对象(这里先不讨论编译器会做优化)。
String str = "I ";
str += "love ";
str += "java.";
System.out.println(str);
可以发现,为了生成最终的结果,会产生一系列的需要垃圾回收的中间对象,当操作的次数增加,就会导致很严重的性能问题,而StringBuilder便是专门为解决这一问题而出现的,StringBuilder可以将我们的每次操作都只在原对象上进行操作,因此便解决了由于生成中间String对象而导致的性能问题。
基本使用
StringBuilder的基本使用方法如下,我们每次需要创建一个StringBuilder对象,当需要进行字符串拼接操作时,只需要使用append方法即可。
StringBuilder sb = new StringBuilder();
sb.append("I ");
sb.append("love ");
sb.append("java.");
System.out.println(sb);
然而其实以上两种操作,经过编译器的优化,在性能上一样的,我们可以通过javap指令来进行验证,前言中的代码我放在StringBuilderStudy这个类中,然后通过一下两步进行反编译来进行验证:
javac StringBuilderStudy.java
javap -c StringBuilderStudy
然后得到以下字节码结果,部分无关内容省去:
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String I
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:/StringBuilder;
14: ldc #6 // String love
16: invokevirtual #5 // Method java/lang/StringBuilder.append:StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: astore_1
23: new #3 // class java/lang/StringBuilder
26: dup
27: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
30: aload_1
31: invokevirtual #5 // Method java/lang/StringBuilder.append:StringBuilder;
34: ldc #8 // String java.
36: invokevirtual #5 // Method java/lang/StringBuilder.append:StringBuilder;
39: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
42: astore_1
43: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
46: aload_1
47: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
50: return
仔细查看很容易发现,尽管我们使用的是普通的字符串拼接操作,但编译器会自动帮我们改成StringBuilder进行操作,最终调用toString方法,然后进行输出。然而,尽管编译器会帮我们做底层优化,我们在某些情况下仍然需要自己显示使用,最常见的一个情况就是在for循环当中,例如以下代码:
String[] strArr = {"I ", "love ", "java."};
String res = "";
for (String str : strArr) {
res += str;
}
System.out.println(res);
我们首先先进行反编译查看生成的字节码(有部分省略):
public static void main(java.lang.String[]);
Code:
0: iconst_3
1: anewarray #2 // class java/lang/String
4: dup
5: iconst_0
6: ldc #3 // String I
8: aastore
9: dup
10: iconst_1
11: ldc #4 // String love
13: aastore
14: dup
15: iconst_2
16: ldc #5 // String java.
18: aastore
19: astore_1
20: ldc #6 // String
32: iload 5
34: iload 4
36: if_icmpge 71
39: aload_3
40: iload 5
42: aaload
43: astore 6
45: new #7 // class java/lang/StringBuilder
48: dup
49: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V
52: aload_2
53: invokevirtual #9 // Method java/lang/StringBuilder.append:StringBuilder;
56: aload 6
58: invokevirtual #9 // Method java/lang/StringBuilder.append:/StringBuilder;
61: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
64: astore_2
65: iinc 5, 1
68: goto 32
71: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
74: aload_2
75: invokevirtual #12 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
78: return
稍微读一下,可以通过68行的goto 32
知道,32行便是循环的入口点,而很容易发现在循环内部,在45行处有一个new
操作,说明在每次循环中为了进行字符串的拼接操作都会生成一个新的StringBuilder对象,最后再调用toString方法。这也导致了每次循环都会产生一个中间对象需要垃圾回收,影响了性能,那如果我们自己使用呢,又会是怎样?先自己写出如下代码:
String[] strArr = {"I ", "love ", "java."};
StringBuilder sb = new StringBuilder();
for (String str : strArr) {
sb.append(str);
}
System.out.println(sb);
然后查看反编译生成的字节码(有删减):
public static void main(java.lang.String[]);
Code:
0: iconst_3
1: anewarray #2 // class java/lang/String
4: dup
5: iconst_0
6: ldc #3 // String I
8: aastore
9: dup
10: iconst_1
11: ldc #4 // String love
13: aastore
14: dup
15: iconst_2
16: ldc #5 // String java.
18: aastore
19: astore_1
20: new #6 // class java/lang/StringBuilder
23: dup
24: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
37: iload 5
39: iload 4
41: if_icmpge 63
44: aload_3
45: iload 5
47: aaload
48: astore 6
50: aload_2
51: aload 6
53: invokevirtual #8 // Method java/lang/StringBuilder.append:/StringBuilder;
56: pop
57: iinc 5, 1
60: goto 37
63: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
66: aload_2
67: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
70: return
仔细查看便可以发现,在这里的循环入口为37行,而循环内部也没有了生成中间StringBuilder对象的代码,只有循环外20行处我们自己进行的一次new操作。因此,尽管编译器会帮助我们做底层的优化,但是当在循环中等一些地方使用字符串拼接操作时,还是需要自己亲自使用StringBuilder对象进行操作,而对于return "I " + "love " + "java.";
这种情况则可以依靠编译器的优化,而不需要自己费力去操作了。
使用细节
我们有时可能会为了方便这样使用StringBuilder进行拼接:append("(" + name + ")")
,然而这其实是一个不好的习惯,编译器并没办法识别这种情况,即自己将括号内的拼接操作转换为多次append操作,而是会生成一个中间StringBuilder对象执行拼接操作,然后再使用toString方法,因此正确的使用的方法应该是append("(").append(name).append(")")
,这里不展示反编译后的字节码了,大家感兴趣可以自己试一下。
常用方法
大家可以查看这个链接,了解一下StringBuilder的其它常用方法和具体介绍。
参考资料
- 《Java编程思想》
本文地址:https://blog.csdn.net/qq_41698074/article/details/107575273