StringBuilder做字符串拼接高效的原因
1.与String相比
通过分析源码,发现两者底层都是用一个数组来存储字符
public final class String implements java.io.Serializable,Comparable<String>,CharSequence{/** The value is used for character storage */private final char value[];
这里需要注意,StringBuilder本身并没有定义value数组,我们需进入其父类AbstractStringBuilder中,可以发现用来存储字符的数组value.
/**
* The value is used for character storage
*/
char value[];
可以发现,String底层的数组是用final修饰的,是一个数组常量,而StringBuilder底层用来接收存储字符的数组是一个变量.所以前者在创建之后是没法更改,而后者可以
这里我们看一段代码
package reason;
public class Test {public static void main(String[] args){String str = "不变";System.out.println(str);str = "变了"System.out.println(str);}
}
结果却是str从最初的"不变",到后来的"变了",字符串str发生了改变,但String底层的数组不是常量吗?为什么值会发生改变?
其实在内存中String发生了这样的变化:
在内存中,我们先创建了一个str对象,并且赋值"不变",之后其实并不是在原有创建的str对象上作更改,而是又创建了一个新的字符串对象,并且赋值变了,把之前的引用类型变量str指向新创建的对象,而之前创建的对象处于等待被回收的状态,如果没有被调用,就会被JVM提供的垃圾回收机制给回收掉.
到这里我们可以发现,之所以String本身做字符串拼接执行速度慢,是因为其本质上是一个不断创建新对象,并且回收旧对象的过程.那说到StringBuilder和StringBuffer,它们创建的对象是变量,对变量操作就是对对象操作,中间不存在对象的创建和回收,所以速度比String快
那真的是这样吗?我们进入到StringBuilder封装后的源代码,查看其append方法
@Overridepublic StringBuilder append(String str) {super.append(str);return this;}
发现StringBuilder在使用append方法时,如果传进来的参数是String类型 的,会去调用其父类的append方法,也就是AbstractStringBuilder的append方法,我们进入AbstractStringBuilder
public AbstractStringBuilder append(String str) {if (str == null)return appendNull();int len = str.length();ensureCapacityInternal(count + len);str.getChars(0, len, value, count);count += len;return this;}
发现当传入的字符串是null时,会去调用appendNull方法,找到appendNull方法的源码
private AbstractStringBuilder appendNull() {int c = count;ensureCapacityInternal(c + 4);final char[] value = this.value;value[c++] = 'n';value[c++] = 'u';value[c++] = 'l';value[c++] = 'l';count = c;return this;}
这里需要注意,count是用来记录存储字符的数组的长度首先会用一个变量c存放当前数组的长度,再去调用ensureCapacityInternal方法来进行数组扩容.(等会append方法也会调用这个方法)我们先找到ensureCapacityInternal的源码,它和append,appendNull一样在AbstractStringBuilder类里
private void ensureCapacityInternal(int minimumCapacity) {// overflow-conscious codeif (minimumCapacity - value.length > 0) {value = Arrays.copyOf(value,newCapacity(minimumCapacity));}}
发现了这个方法其实就是再做数组的扩容,利用的是Arrays类里的copyOf进行数组的扩容
回到上面appendNull方法中,会将当前数组的长度加4作为参数传入ensureCapacityInternal方法,将value数组扩容四个大小,并且把最后四个值改为null,所以我们利用append方法拼接一个值为null的字符串,得到的也是null
public class Test1 {public static void main(String[] args) throws Exception {// TODO Auto-generated method stubStringBuilder buff = new StringBuilder();System.out.println(buff);String string = null;buff.append(string);System.out.println(buff);}
}
测试发现结果的确为null
在回到append方法中,如果传的字符串不为空时,会将字符串的长度和当前value数组的长度加在一起作为参数调用ensureCapacityInternal方法,获得扩容后的数组,再去调用String类的getChars方法.进入String源码,查看getChars方法
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {if (srcBegin < 0) {throw new StringIndexOutOfBoundsException(srcBegin);}if (srcEnd > value.length) {throw new StringIndexOutOfBoundsException(srcEnd);}if (srcBegin > srcEnd) {throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);}System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);}
能够看到这个方法是利用System.arraycopy方法将value数组的值加上新传进来的String字符串的拼接到新数组dst当中
最后append方法将扩容后的本实例传回去,做到了字符串拼接的效果.可以看到整个过程中真的没有创建新的对象,一切都是在对value这个数组变量进行操控.
但是仔细考虑还是会发现一些问题,因为数组一旦创建长度没法改变,无论是利用System.arraycopy方法还是Arrays.CopyOf方法,实际上都是再new一个新的数组来存放数据.在StringBuilder扩容的过程当中,虽说没有new一个新对象,但是当拼接的字符串不为null时,会new两个新的数组.而利用"+"这种形式虽说会创建一个新的字符串对象,但是每次创对象时只要再new一个新数组就行了.所以说实际上StringBuilder也会出现不断创建新的,并且回收旧的过程,只不过从对象变成了数组.那到底为什么StringBuilder会比String快那么多呢?
这时候决定查看以下反编译的结果,发现String用"+"这种方式做字符串拼接时,竟然调用了StringBuffer的append方法.what?
翻阅Thinking in java发现,原来String在做大量拼接时,会默认调用StringBuffer的append方法.这样一切就说通过了,String比StringBuilder慢的原因是因为在大量拼接时,它会不断的new一个新的StringBuffer类,然后调用append方法,会再new两个新的数组,这样就出现了大量的浪费资源情况,效率也比StringBuilder慢很多
2.与StringBuffer相比
原因很简单,查阅源码就能发现,StringBuilder里的append方法没有synchronized关键字,所以它为了追求速度放弃了线程的安全性,如下
@Override
public synchronized StringBuffer append(String str) {toStringCache = null;super.append(str);return this;
}
@Overridepublic StringBuilder append(String str) {super.append(str);return this;}
super.append(str);
return this;
}
```java@Overridepublic StringBuilder append(String str) {super.append(str);return this;}
但是在单一线程的情况下,StringBuilder不会出现线程安全的问题,所以建议在单线程时使用StringBuilder会更快