字符串拼接,很简单的一个操作,JDK给出了几种不同的拼接方法,还提供了对应封装类。早在JDK1.0的时候就提供了StringBuffer
这个类用来做字符串的拼接,为了多线程下的线程安全问题,在StringBuffer
类中的方法上都加了synchronized
锁,这种考虑是没有问题的。后续为了提高单线程下(不存在线程安全问题)字符串的拼接效率,JDK1.5提供了StringBuilder
类,这个类里面的方法是完全放开的,没有锁竞争对性能的消耗。
在公司的实际开发中,很多人知道用StringBuffer
和StringBuilder
来替代字符串的直接拼接,但是很多人却不去区分他们的区别,随意的用。(这个就体现了基础功底了,不是吗?)
1. String拼接
在说StringBuilder
和StringBuffer
之前,先说一下单纯的String拼接存在问题。看下面的代码:
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
String str = "";
for (int i = 0; i < 1000; i++) {
str += "order_id_" + i;
}
long endTime1 = System.currentTimeMillis();
System.out.println((endTime1 - startTime) + "ms");
}
这段代码运行的结果用时是30到40毫秒,如果将循环的次数由1000变为10000,扩大了10倍,运行时间远远大于原来时间的10倍,基本在1300到1500毫秒之间,可见随着拼接次数的变多,效率会变得很低。这是为什么呢?
在JVM中,String字符串的值是存储在常量池中,但是String字符串一旦创建,就不可变得,当需要拼接字符串的时候,就需要将原有的字符串复制出来和需要拼接的部分组成一个新的字符串,然后在常量池中开辟新的空间存储,在这个过程中,每次拼接都是对象的创建,方法中str的引用指针也会随之改变,另外创建过程会存在很多失去引用的值,无用的数据占用大量的空间,就会触发JVM进行频繁的GC回收,也是一个性能的开销。因此在多字符串拼接的时候,极其的不建议直接用String的拼接方式。
存在问题总结:
- 频繁的创建新的字符串
- 存在大量的垃圾,触发GC
- 引用指针一直变化,频繁的修改变量的引用
2. StringBuffer拼接
在使用StringBuffer
的时候就可以避免上面的三种问题,创建的时候只有一个对象StringBuffer
,在StringBuffer
中有对应的计数器(count)和缓存的拼接结果(value)。看源码:
//创建对象的时候,会初始化缓存的大小
public StringBuffer() {
super(16);//调用父类(AbstractStringBuilder)的构造方法
}
然后随意看一个字符串的拼接过程:
//使用synchronized加锁
@Override
public synchronized StringBuffer append(int i) {
toStringCache = null;
super.append(i);//调动父类的apppend方法
return this;//返回当前对象
}
//父类的append方法
public AbstractStringBuilder append(int i) {
if (i == Integer.MIN_VALUE) {//是int最小值
append("-2147483648");
return this;
}
//获取当前数值的长度
int appendedLength = (i < 0) ? Integer.stringSize(-i) + 1
: Integer.stringSize(i);
//计算拼接后的长度:原有长度count+新增长度
int spaceNeeded = count + appendedLength;
//判断长度是不是超过预留空间,是否要扩容
ensureCapacityInternal(spaceNeeded);
//将数值拼接到value后面,value是char[]
Integer.getChars(i, spaceNeeded, value);
//将计数器设置为最新值
count = spaceNeeded;
return this;
}
从上面的代码看的出来之前String拼接的问题并没有本质的解决,在value扩容的时候也会重新创建char[]数组,也会有引用的变化,大量垃圾的产生。也许看了如何扩容你就不是这么想了。
//扩容逻辑
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,newCapacity(minimumCapacity));
}
}
//newCapacity
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;//扩容计算
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
在扩容的时候,首先是将原来的value大小扩大两倍左右,然后和当前拼接后的最大字符串比较,如果还是没有当前字符串大,就直接采用当前的字符串的长度,如果超过了,就直接使用扩容后的长度。这里是长度两倍的扩容,在拼接次数的变多,一次扩容的容量越大,也就意味着,扩容的次数变少。自然之前String拼接的问题就得到优化,设计的是不是很巧妙。
3. StringBuilder拼接
上面的StringBuffer
说完,StringBuilder
就没有太多说的了,在实现逻辑上基本都是一样的,但是不同的在于是他没有做加锁的动作,减少锁的过程对性能的消耗,但是同时也带了线程安全问题。
话又说回来了,在拼接字符串的时候用到多线程的情况是很少的,所以在正常的拼接过程,最优的选择就是StringBuilder
。涉及到线程安全再换成StringBuffer
,岂不快哉。
4. 总结
虽然说StringBuilder
和StringBuffer
推荐使用,但是这只是在拼接频繁,次数较多的时候有优势,如果只是简短的几个字符串的拼接,就不用费那么大的力气啦。