拼接字符串String、StringBuilder、StringBuffer你用对了吗


字符串拼接,很简单的一个操作,JDK给出了几种不同的拼接方法,还提供了对应封装类。早在JDK1.0的时候就提供了StringBuffer这个类用来做字符串的拼接,为了多线程下的线程安全问题,在StringBuffer类中的方法上都加了synchronized锁,这种考虑是没有问题的。后续为了提高单线程下(不存在线程安全问题)字符串的拼接效率,JDK1.5提供了StringBuilder类,这个类里面的方法是完全放开的,没有锁竞争对性能的消耗。

在公司的实际开发中,很多人知道用StringBufferStringBuilder来替代字符串的直接拼接,但是很多人却不去区分他们的区别,随意的用。(这个就体现了基础功底了,不是吗?)

1. String拼接

在说StringBuilderStringBuffer之前,先说一下单纯的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. 总结

虽然说StringBuilderStringBuffer推荐使用,但是这只是在拼接频繁,次数较多的时候有优势,如果只是简短的几个字符串的拼接,就不用费那么大的力气啦。


文章作者: 程序猿洞晓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 程序猿洞晓 !
评论
 上一篇
Java虚拟机那些事儿(九):类文件结构 Java虚拟机那些事儿(九):类文件结构
JVM 只认识 .class 文件,它不关心是何种语言生成了 .class 文件,只要 .class 文件符合 JVM 的规范就能运行。 目前已经有 JRuby、Jython、Scala 等语言能够在 JVM 上运行。它们有各自的语法规则,不过它们的编译器 都能将各自的源码编译成符合 JVM 规范的 .class 文件,从而……
2018-09-04
下一篇 
ORM框架之Mybatis(四):映射文件resultMap标签详解 ORM框架之Mybatis(四):映射文件resultMap标签详解
Mybatis的映射文件中顶级的标签并不多,之前有说过select、update、delete、insert、sql等标签,resultMap在之前的文章也有提过,但是当时也是简单的提过,其实这个标签里面的内容很多,可简单可复杂。正常开发中简单的基本都在使用,但是涉及到复杂的用的就少啦。resultMap内和关联查询有关的标签分别是……
2018-08-31
  目录