之前我们在另一篇博客讲过,String是被final修饰的,是一个不可变对象。我们如果要修改String的内容,就只能通过重新new一个对象来实现。毫无疑问,这是非常耗内存的,当我们需要不断的更改String对象的内容时,我们的内存空间很容易溢出。因此,我们有了StringBuffer和StringBuilder这两个替代品。
StringBuffer
StringBuffer类继承自AbstractStringBuilder抽象类,实现了Serializable序列化接口和CharSequence接口。
StringBuffer的构造方法
构造方法
Constructor and Description |
构造一个没有字符的字符串缓冲区,初始容量为16个字符。 |
构造一个包含与指定的相同字符的字符串缓冲区 |
构造一个没有字符的字符串缓冲区和指定的初始容量。 |
构造一个初始化为指定字符串内容的字符串缓冲区。 |
StringBuffer的常用方法
|
将指定的字符串附加到此字符序列。 |
|
返回 |
|
删除此序列的子字符串中的字符。 |
|
返回指定子字符串第一次出现的字符串内的索引。 |
|
将字符串插入到此字符序列中。 |
|
返回长度(字符数)。 |
|
返回一个新的 |
|
返回一个新的 |
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效率更快。
总结
- 从字符串底层存储对象来看,String是不可变对象,而StringBuilder和StringBuffer是可变长度对象。
- 从线程安全来看,StringBuffer是线程安全对象,因为很多方法被synchronized方法修饰,而String和StringBuilder是线程不安全的。
- 从效率来看,大部分情况下StringBuilder>StringBuffer>String.