为什么要使用StringBuilder和StringBuffer拼接字符串?

 

大家在开发中一定有一个原则是”利用StringBuilder和StringBuffer拼接字符串”,但是为什么呢?用一段代码来分析一下:

 

public class StringTest {
 
    @Test
    public void testStringPlus() {
        String str = "111";
        str += "222";
        str += "333";
        System.out.println(str);
    }
     
}

 

这段代码,我们找到编译后的StringTest.class文件,使用”javap -verbose StringTest”或者”javap -c StringTest”都可以,反编译一下class获取到对应的字节码:

 

public void testStringPlus();
    Code:
       0: ldc           #17                 // String 111
       2: astore_1
       3: new           #19                 // class java/lang/StringBuilder
       6: dup
       7: aload_1
       8: invokestatic  #21                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)L
java/lang/String;
      11: invokespecial #27                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/S
tring;)V
      14: ldc           #30                 // String 222
      16: invokevirtual #32                 // Method java/lang/StringBuilder.append:(Ljava/lang/Str
ing;)Ljava/lang/StringBuilder;
      19: invokevirtual #36                 // Method java/lang/StringBuilder.toString:()Ljava/lang/
String;
      22: astore_1
      23: new           #19                 // class java/lang/StringBuilder
      26: dup
      27: aload_1
      28: invokestatic  #21                 // Method java/lang/String.valueOf:(Ljava/lang/Object;)L
java/lang/String;
      31: invokespecial #27                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/S
tring;)V
      34: ldc           #40                 // String 333
      36: invokevirtual #32                 // Method java/lang/StringBuilder.append:(Ljava/lang/Str
ing;)Ljava/lang/StringBuilder;
      39: invokevirtual #36                 // Method java/lang/StringBuilder.toString:()Ljava/lang/
String;
      42: astore_1
      43: getstatic     #42                 // Field java/lang/System.out:Ljava/io/PrintStream;
      46: aload_1
      47: invokevirtual #48                 // Method java/io/PrintStream.println:(Ljava/lang/String
;)V
      50: return
}

 

这段字节码不用看得很懂,大致上能明白就好,意思很明显:编译器每次碰到”+”的时候,会new一个StringBuilder出来,接着调用append方法,在调用toString方法,生成新字符串

 

那么,这意味着,如果代码中有很多的”+”,就会每个”+”生成一次StringBuilder,这种方式对内存是一种浪费,效率很不好。

 

在Java中还有一种拼接字符串的方式,就是String的concat方法,其实这种方式拼接字符串也不是很好,具体原因看一下concat方法的实现:

 

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

 

意思就是通过两次字符串的拷贝,产生一个新的字符数组buf[],再根据字符数组buf[],new一个新的String对象出来,这意味着concat方法调用N次,将发生N*2次数组拷贝以及new出N个String对象,无论对于时间还是空间都是一种浪费。

 

根据上面的解读,由于”+”拼接字符串与String的concat方法拼接字符串的低效,我们才需要使用StringBuilder和StringBuffer来拼接字符串。以StringBuilder为例:

 

public class TestMain
{
    public static void main(String[] args)
    {
        StringBuilder sb = new StringBuilder("111");
        sb.append("222");
        sb.append("111");
        sb.append("111");
        sb.append("444");
        System.out.println(sb.toString());
  }
}

 

StringBuffer和StringBuilder原理一样,无非是在底层维护了一个char数组,每次append的时候就往char数组里面放字符而已,在最终sb.toString()的时候,用一个new String()方法把char数组里面的内容都转成String,这样,整个过程中只产生了一个StringBuilder对象与一个String对象,非常节省空间。StringBuilder唯一的性能损耗点在于char数组不够的时候需要进行扩容,扩容需要进行数组拷贝,一定程度上降低了效率。

 

StringBuffer和StringBuilder用法一模一样,唯一的区别只是StringBuffer是线程安全的,它对所有方法都做了同步,StringBuilder是线程非安全的,所以在不涉及线程安全的场景,比如方法内部,尽量使用StringBuilder,避免同步带来的消耗。

 

另外,StringBuffer和StringBuilder还有一个优化点,上面说了,扩容的时候有性能上的损耗,那么如果可以估计到要拼接的字符串的长度的话,尽量利用构造函数指定他们的长度

 

真的不能用”+”拼接字符串?

 

虽然说不要用”+”拼接字符串,因为会产生大量的无用StringBuilder对象,但也不是不可以,比如可以使用以下的方式:

 

public class TestMain
{
    public static void main(String[] args)
    {
        String str = "111" + "222" + "333" + "444";
        System.out.println(str);
    }
}

 

就这种连续+的情况,实际上编译的时候JVM会只产生一个StringBuilder并连续append等号后面的字符串。

 

不过上面的例子要注意一点,因为”111″、”222″、”333″、”444″都是编译期间即可得知的常量,因为第5行的代码JVM在编译的时候并不会生成一个StringBuilder而是直接生成字符串”111222333444″。

 

但是这么写得很少,主要原因有两点:

 

1、例子比较简单,但实际上大量的“+”会导致代码的可读性非常差;

 

2、待拼接的内容可能从各种地方获取,比如调用接口、从.properties文件中、从.xml文件中,这样的场景下尽管用多个“+”的方式也不是不可以,但会让代码维护性不太好。