与其做一个有价钱的人,不如做一个有价值的人;与其做一个忙碌的人,不如做一个有效率的人。
对于字符串的拼接有三种方法,加号,concat方法,StringBuilder(或者StringBuffer)的append
方法,其中加号最为常用,那么他们闪着之间有什么区别呢?我们写一个测试demo比较一下这三种方法的在执行10000次的字符串拼接操作所消耗的时间,代码如下:
public class Client {
public static void main(String[] args) throws Exception {
long str1StartTime = System.currentTimeMillis();//记录起始时间
String str1 = "a";
for (int iCount = 0; iCount < 100000; iCount++) {
str1 += "a";
}
long str1EndTime = System.currentTimeMillis();//记录结束时间
long str2StartTime = System.currentTimeMillis();
String str2 = "b";
for (int iCount = 0; iCount < 100000; iCount++) {
str2 = str2.concat("a");
}
long str2EndTime = System.currentTimeMillis();
long str3StartTime = System.currentTimeMillis();
StringBuffer str3 = new StringBuffer("c");
for (int iCount = 0; iCount < 100000; iCount++) {
str3 = str3.append("a");
}
long str3EndTime = System.currentTimeMillis();
long str4StartTime = System.currentTimeMillis();
StringBuilder str4 = new StringBuilder("d");
for (int iCount = 0; iCount < 100000; iCount++) {
str4 = str4.append("a");
}
long str4EndTime = System.currentTimeMillis();
System.out.println("使用String \"+\"操作执行100000次字符拼接操作所消耗时间:" + (str1EndTime - str1StartTime) + "毫秒");
System.out.println("使用String的concat()方法操作执行100000次字符拼接操作所消耗时间:" + (str2EndTime - str2StartTime) + "毫秒");
System.out.println("使用StringBuffer的append()方法操作执行100000次字符拼接操作所消耗时间:" + (str3EndTime - str3StartTime) + "毫秒");
System.out.println("StringBuilder的append()方法操作执行100000次字符拼接操作所消耗时间:" + (str4EndTime - str4StartTime) + "毫秒");
}
}
运行结果:
使用String "+"操作执行100000次字符拼接操作所消耗时间:7960毫秒
使用String的concat()方法操作执行100000次字符拼接操作所消耗时间:2075毫秒
使用StringBuffer的append()方法操作执行100000次字符拼接操作所消耗时间:3毫秒
StringBuilder的append()方法操作执行100000次字符拼接操作所消耗时间:1毫秒
对比结果着实让我感觉瑟瑟发抖。"+"
运算符原来是最慢的。
我们一个一个来看吧,"+"
操作是每次先得出运算结果的字面值,然后看字符串常量池里面有没有和字面值相等的对象,有的话就直接返回池中对象的引用,没有就新建一个以运算结果的字面值为初始化参数的对象。显然,我们这里要创建1000001(还有一个是str1本身初始化的时候也会被创建)个对象放入常量池,这个时间的消耗也就显而易见了。
同理,我们看一下concat()
方法的源码看看它是如何操作的:
public String concat(String str) {
int otherLen = str.length();
//如果追加的字符串参数长度为0,就返回字符串本身
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);
}
其中Arrays.copyOf(value, len + otherLen)
的内部实现是:
public static char[] copyOf(char[] original, int newLength) {
//开辟一个新的字符数组
char[] copy = new char[newLength];
//将数据拷贝到新的字符数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
而Arrays.copyOf内部调用的方法
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
是一个本地方法,它做的事情就是实现将一个数组的指定个数元素复制到另一个数组中。这个基本就不用考虑时间消耗了。
也就是说,return返回结果之前的所有操作在内存中的时间消耗其实是很小很小的,之所以会慢,其实是return的时候使用new创建了新对象,也就是说创建了一个"b"的常量放到常量池之后,在堆上有创建了100000个对象。
这里我们也可以大致知道相比于直接在对上new一个对象,其实字符串常量池中进行常量匹配,以及常量创建和投入常量池返回字符串常量引用的过程其实还是挺消耗时间的。
接着看StringBuilder的append()
方法的源码,由于StringBuffer的append()
和StringBuilder基本一样,只是StringBuffer的append()
方法加了synchronized
关键字并且每次都清空了缓存值(上一次的toString()
方法的返回值toStringCache
),我们就不再分析StringBuffer的append()
感兴趣的可以自行研究。
StringBuilder的append()
方法其实是依赖于抽象类AbstractStringBuilder提供append(...)
的实现的:
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
//申请一个更大的char数组(扩容)
ensureCapacityInternal(count + len);
//拷贝要追加的字符串到新数组(拷贝)
str.getChars(0, len, value, count);
count += len;
return this;
}
可以看出基本上就是基于内存的扩容和数据拷贝,全程并没有重新创建新对象。这种内存操作的速度是很快的,所以哪怕是100000次的append()
操作耗时也是很少的。
StringBuffer之所以比StringBuilder慢点其实是因为它每次进入都要重新获取对象监视器(有兴趣的可以去研究一下synchronized
关键字的底层实现原理),所以会慢一点,但是这样在多线程场景下线程安全就是。
总结一下:
三者的实现方法,性能都不一样,但是这并不表示我们就一定要使用StringBuilder,这是因为"+"
更加符合我们的编码习惯,更适合人类阅读。除非在性能出现临界的时候,否则还是建议使用concat
或者append
,而且很多时候系统的80%的性能消耗在20%的代码上,我们应该把更多的精力放到数据结构和算法上去。