之前我们在另一篇博客讲过,String是被final修饰的,是一个不可变对象。我们如果要修改String的内容,就只能通过重新new一个对象来实现。毫无疑问,这是非常耗内存的,当我们需要不断的更改String对象的内容时,我们的内存空间很容易溢出。因此,我们有了StringBuffer和StringBuilder这两个替代品。

StringBuffer

StringBuffer类继承自AbstractStringBuilder抽象类,实现了Serializable序列化接口和CharSequence接口。

StringBuffer的构造方法

构造方法

Constructor and Description

StringBuffer()

构造一个没有字符的字符串缓冲区,初始容量为16个字符。

StringBuffer(CharSequence

构造一个包含与指定的相同字符的字符串缓冲区 CharSequence 。

StringBuffer(int capacity)

构造一个没有字符的字符串缓冲区和指定的初始容量。

StringBuffer(String

构造一个初始化为指定字符串内容的字符串缓冲区。

StringBuffer的常用方法

StringBuffer

append(String

将指定的字符串附加到此字符序列。

char

charAt(int index)

返回 char在指定索引在这个序列值。

StringBuffer

delete(int start, int end)

删除此序列的子字符串中的字符。

int

indexOf(String

返回指定子字符串第一次出现的字符串内的索引。

StringBuffer

insert(int offset, String

将字符串插入到此字符序列中。

int

length()

返回长度(字符数)。

String

substring(int start)

返回一个新的 String ,其中包含此字符序列中当前包含的字符的子序列。

String

substring(int start, int end)

返回一个新的 String ,其中包含此序列中当前包含的字符的子序列

StringBuffer初始化及扩容机制

StringBuffer与String最大的不同在于StringBuffer是可变的对象,它可以根据要存储的字符串长度来改变自己的容量,这离不开它本身的扩容机制。首先我们通过StringBuffer的构造函数来了解它不同的初始化方式。

  • StringBuffer():StringBuffer的初始容量可以容纳16个字符,当该对象的实体存放的字符长度大于16时,实体容量就自动增加。StringBuffer对象可以通过length()方法获取实体中存放的字符序列长度,通过capacity()方法来获取当前实体的实际容量。
  • StringBuffer(int size):可以指定分配给该对象的实体的初始容量参数为参数size指定的字符个数。当该对象的实体存放的字符序列的长度大于size个字符时,实体的容量就自动的增加。以便存放所增加的字符。
  • StringBuffer(String s):可以指定给对象的实体的初始容量为参数字符串s的长度额外再加16个字符。当该对象的实体存放的字符序列长度大于size个字符时,实体的容量自动的增加,以便存放所增加的字符。

接下来我们来通过append()方法来了解它的扩容方法。

使用append()方法在字符后面追加东西的时候,如果长度超过了该字符串存储空间大小了就需要进行扩容:构建新的存储空间,将旧的复制过去。我们可以从它的源码很容易看出来这个过程:

//AbstractStringBuilder的方法
public AbstractStringBuilder append(String str) {
        //判空
        if (str == null) str = "null";
        int len = str.length();

        //判断是否足够存储str字符串,若不够则调用expandCapacity方法进行扩容
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
}



//判断是否需要扩容
private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }



//扩容方法
void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }



//将字符串复制过去 String类的方法
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);
        }


        /*  srcBegin - 以此偏移开始复制。
            srcEnd - 在此偏移处停止复制。
            dst - 将数据复制到的数组。
            dstBegin - 偏移到 dst 。*/


        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}

从源码我们可以更加清晰的了解StringBuffer的扩容过程。当我们进行字符串append时,会先计算添加后字符串大小,传入一个方法ensureCapacityInternal()进行扩容判断,需要扩容则调用expandCapacity方法进行扩容。尝试将新容量扩为原大小的2倍+2,然后再进行一次if判断,容量如果不够,直接扩充到需要的容量大小,即更新后字符串的大小。

StringBuilder和StringBuffer

StringBuilder所继承的抽象类和实现的接口都与StringBuffer一模一样,他们两者的方法和功能完全是等价的。只是StringBuffer中的方法大都采用synchronized关键字进行修饰,因此是线程安全的,而StringBuilder没有,可以被认为是线程不安全的。由于使用了synchronized关键字,所以在单线程程序下,StringBuilder效率更快。

总结

  1. 从字符串底层存储对象来看,String是不可变对象,而StringBuilder和StringBuffer是可变长度对象。
  2. 从线程安全来看,StringBuffer是线程安全对象,因为很多方法被synchronized方法修饰,而String和StringBuilder是线程不安全的。
  3. 从效率来看,大部分情况下StringBuilder>StringBuffer>String.