为什么要使用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文件中,这样的场景下尽管用多个“+”的方式也不是不可以,但会让代码维护性不太好。