StringBuffer的append()源码分析以及适用场景

在牛客网上做了一道题:

答案是StringBuffer以及String。String不用说,是字符串常量类。但是StringBuffer是常量类,用了这么多年我还是才知道,今天研究一下StringBuffer类。首先,我是带着以下几个问题研究的:

(1)StringBuffer 使用final修饰,为什么可以拼接字符串,改变内容?

(2)为什么打印StringBuffer对象打印的是拼接后的值,而不是StringBuffer对象?

(3)使用StringBuffer 的优势在哪里?什么情况下使用最合适?


第一个问题:StringBuffer 使用final修饰,为什么可以拼接字符串,改变内容?

首先总结一下final的作用:

(1)修饰变量,则该变量无法修改

(2)修饰类,则类不可继承且引用地址不可变,但是内容可变。

(3)修饰方法,方法不可继承。

举个例子:创建了一个buffer对象,使用append拼接了一个char类型的值和String类型的值,之后打印buffer对象的地址,拼接前后地址不变。

地址未变,说明引用未变,但是值改变了,通过源码分析一下:

StringBuffer是个常量类,继承AbstractStringBuilder,对值进行插入或者修改操作的方法都加了synchronized,保证线程安全。

拼接char类型源码:buffer.append(char),继承父类AbstractStringBuilder的append(char),并且使用父类的append(char)方法

 public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
    @Override
    public synchronized StringBuffer append(char c) {
        toStringCache = null;
        //调用父类AbstractStringBuilder的append方法
        super.append(c);
        //返回当前对象
        return this;
    }
}


abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;
    @Override
    public AbstractStringBuilder append(char c) {
        //调用扩容方法,因为char类型占一个字节,因此长度+1即可
        ensureCapacityInternal(count + 1);
        //将char值放入数组中
        value[count++] = c;
        return this;
    }

    //扩容方法,调用Arrays.copyOf扩容
    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            //调用Arrays.copyOf方法扩容value数组
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }

}


public class Arrays {
    public static char[] copyOf(char[] original, int newLength) {
        //创建一个新的长度数组
        char[] copy = new char[newLength];
        //将原来的数组复制到新数组中,这里是浅拷贝,copy新数组的地址不变
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        //返回新数组
        return copy;
    }
}
AbstractStringBuilder类中有数组value,用于存储拼接后的数组值,当调用append方法时,会先计算需要的数组长度(旧数组的长度+参数的长度),之后创建新数组,并且将原来数组拷贝到新数组中,这里是浅拷贝,因此返回新数组的地址和原来数组地址已经不一样,在jvm中又重新开辟了一块内存用于存放新数组。地址这里可以打debugger看value数组前后的地址即可验证:

字符串拼接:buffer.append(s);StringBuffer实现这个方法仍然是继承自AbstractStringBuffer的append(String s)方法,直接看父类的append的源码:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;

    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        //获取字符串长度
        int len = str.length();
        //扩容
        ensureCapacityInternal(count + len);
        //将str的值放到value数组后面
        str.getChars(0, len, value, count);
        //数组长度变长
        count += len;
        //返回当前对象
        return this;
    }

    //扩容方法,调用Arrays.copyOf扩容
    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            //调用Arrays.copyOf方法扩容value数组
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }

    //拼接空值数组后拼接"null"
    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;
    }
}


public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    //将当前值依次放到对应的数组中,放在数组的后面,浅拷贝
    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);
    }
}
看完了上述源码,大致明白了为何可以实现字符串拼接了,StringBuffer使用了父类AbstractStringBuilder中的数组变量value用于拼接数据,支持int、char、String、double、long、Object以及其他类型的数据,拼接完成之后返回当前对象this,因此对象一直没变,但是value数组中的数据已经改变。

第二个问题:为什么打印StringBuffer对象打印的是拼接后的值,而不是StringBuffer对象

我们发现,打印StringBuffer的值,并不是调用其中的方法,而是直接打印StringBuffer的对象:

我们平时打印对象时,会打印当前对象的class对象信息,比如这种:com.example.demo.A@3578436e。为什么StringBuffer却是打印的拼接后的值呢?因为StringBuffer重写了toString()方法,toString方法里面将当前最新的数组转化为字符串常量返回,因此打印buffer时为当前拼接后的数值信息。

 public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{

    /**
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
        使用transient反序列化修饰,每次StringBuffer的值被修改时,都置为null
     */
    private transient char[] toStringCache;

    @Override
    public synchronized String toString() {
        if (toStringCache == null) {
            //将当前拼接后的最新数组放到toStringCache中
            toStringCache = Arrays.copyOfRange(value, 0, count);
        }
        //转化为String类型
        return new String(toStringCache, true);
    }

}

第三个问题:使用StringBuffer 的优势在哪里?

那么使用优势在哪里呢?首先,StringBuffer修改数组的方法都是被synchronized修饰,因此在多线程环境下是安全的。在性能方面,我们将StringBuffer与StringBuilder/String三个做字符拼接的所用时间进行比较,我将拼接次数分别设置为100次和10000次,比较各自耗费时间:

 @Test
    public void test04(){
        String s = "a";
        long before;
        long after;
        before = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
            s = s + "a";
        }
        after = System.currentTimeMillis();
        System.out.println("String : " + (after - before));

        StringBuilder builder = new StringBuilder();
        before = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
            builder.append("a");
        }
        after = System.currentTimeMillis();
        System.out.println("builder : " + (after - before));

        StringBuffer buffer = new StringBuffer();
        before = System.currentTimeMillis();
        for(int i = 0; i < 100000; i++){
            buffer.append("a");
        }
        after = System.currentTimeMillis();
        System.out.println("buffer : " + (after - before));
    }

运行结果:

很明显,在大量字符串拼接方面,性能比较结果为StringBuilder>StringBuffer>String。但是StringBuffer和StringBuilder略有差异,但是差异不大,后面我又运行了几次,发现两者所用时间都是0,可以忽略不计。因此在多线程环境,并且需要大量拼接字符串的时候,可以使用StringBuffer。单线程环境也可以使用,毕竟和StringBuilder所用时间差不大。

StringBuffer的优势:

(1)线程安全,多线程环境下保证数据安全。

(2)拼接时间比String所用时间少,和StringBuilder所用时间相差不大,因此性能也不错。

(3)提供了各种append类型的方法,兼容性强。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值