拼接字符串String、StringBuilder、StringBuffer比较

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

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

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");
}

这段代码运行的结果用时是3040毫秒,如果将循环的次数由1000变为10000,扩大了10倍,运行时间远远大于原来时间的10倍,基本在13001500毫秒之间,可见随着拼接次数的变多,效率会变得很低。这是为什么呢?

在JVM中,String字符串的值是存储在常量池中,但是String字符串一旦创建,就不可变得,当需要拼接字符串的时候,就需要将原有的字符串复制出来和需要拼接的部分组成一个新的字符串,然后在常量池中开辟新的空间存储,在这个过程中,每次拼接都是对象的创建,方法中str的引用指针也会随之改变,另外创建过程会存在很多失去引用的值,无用的数据占用大量的空间,就会触发JVM进行频繁的GC回收,也是一个性能的开销。因此在多字符串拼接的时候,极其的不建议直接用String的拼接方式。

存在问题总结:

  • 频繁的创建新的字符串
  • 存在大量的垃圾,触发GC
  • 引用指针一直变化,频繁的修改变量的引用

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拼接的问题就得到优化,设计的是不是很巧妙。

StringBuilder拼接

上面的StringBuffer说完,StringBuilder就没有太多说的了,在实现逻辑上基本都是一样的,但是不同的在于是他没有做加锁的动作,减少锁的过程对性能的消耗,但是同时也带了线程安全问题。

话又说回来了,在拼接字符串的时候用到多线程的情况是很少的,所以在正常的拼接过程,最优的选择就是StringBuilder。涉及到线程安全再换成StringBuffer,岂不快哉。

总结

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

福利

上面在说String的时候,是直接用String str = "123",说str的引用是直接指向常量池,但是String str = new String("123"),他的引用还是指向常量池吗?这个可以思考一下,一些公司的笔试题或面试题经常会问这个东西。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿洞晓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值