Java | 谈谈StringBuilder的使用和细节

前言

众所周知,在Java中String对象是不可变的。不可变性会导致一系列的效率问题,例如下面几行代码,为了生成最终的结果,I首先会和love连接生成一个I loveString对象,然后再和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编程思想》
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值