Fork me on GitHub

三.StringBuffer和StringBuilder可变的源码分析

目标

本次源码分析的目标是深入了解 StringBuffer类中 append 方法的实现机制。

分析方法

测试代码

1
2
3
4
StringBuffer stringBuffer = new StringBuffer(); //断点

stringBuffer.append("hello");
stringBuffer.append("hello11");

构造函数分析

源码
1
2
3
4
5
6
7
/**
* Constructs a string buffer with no characters in it and an
* initial capacity of 16 characters.
*/
public StringBuffer() {
super(16);
}

image

以上代码调用父类的有参构造,并传入16作为参数

我们再次跟进 super(16) 进入父类的有参构造

源码
1
2
3
4
5
6
/**
* Creates an AbstractStringBuilder of the specified capacity.
*/
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}

image

我们发现这里给value初始化了一个大小为16(就是我们刚刚传进来的)的一个char数组,这个value是什么呢?我们再次跟进value

value

image


我们发现这个value是StringBuffer和StringBuilder的父类AbstractStringBuilder的一个值,其实我们的StringBuffer和StringBuilder就是将字符串保存在这个char数组中的(类比你可以去看看String的源码,也是讲字符串保存在char数组中的,而且是final修饰的,这也就是String为什么不可变的原因),而这一步就是给这个数组初始化容量为16,我们接着往下走




通过上述代码我们知道,StringBuffer 的父类是 AbstractStringBuilder,这是一个抽象类。其构造函数初始化了一个默认大小的字符数组,而这个字符数组的大小正是传进来的参数。

通过字符数组来保存字符串信息,为什么默认大小为16,如果字符串超过16,超过了字符数组的大小了怎么办?

append方法分析

源码
1
2
3
4
5
6
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}

image

从上述代码可以看到,直接调用了父类 AbstractStringBuilder 类的 append 方法。

我们来看看父类的append(String s)方法

源码
1
2
3
4
5
6
7
8
9
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}

image

  1. 首先判断追加的字符串是否为 null,如果为 null 则执行 appendNull()方法。首先不看这个方法
  2. 根据上面测试代码编写的:stringBuffer.append(“hello”);
  3. 我们追加的是一个字符串”hello”,并非 null 值。
  4. 获取追加字符串的长度 len值为5。
  5. 下面将进入 ensureCapacityInternal ()方法,该方法的参数为 count+len = 0 + 5 = 5.

这个 count属性是什么意思呢?我们来看看

image


从代码的注释我们可以看到,count 表示的是已经使用的字符数量。


从前面分析构造函数我们知道这些字符串都是存储在一个字符数组中,而 count 指的就是这个字符数组已经使用了多少个

由于我们是第一次执行 append 方法,此前没有追加任何的字符,因此此时 count 为0,当我们追加完成后,此时 count 的值就要更新为5,表示此时的字符数组中已经有5个字符了。

所以 ensureCapacityInternal()方法的参数指的是已经使用的字符数量+将要使用的字符数量,即字符数组的最小容量大小。

该方法确定了新字符数组的容量,并初始化新字符数组,将原有字符数组内容复制到新字符数组中。

ensureCapacityInternal()方法

源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* For positive values of {@code minimumCapacity}, this method
* behaves like {@code ensureCapacity}, however it is never
* synchronized.
* If {@code minimumCapacity} is non positive due to numeric
* overflow, this method throws {@code OutOfMemoryError}.
*/
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}

image



通过前面构造函数的分析,我们了解到 StringBuffer 的底层是使用字符数组来存储这些字符串的,而且默认的大小是16,一旦这个字符数组用完了,就得重新分配新的字符数组,并将以前的字符数组内容复制到新的字符数组中。

minimumCapacity指的就是当前字符串的最小容量,如果这个容量比当前字符数组的容量要大,则需要重新申请新的字符数组,并将以前字符数组的内容复制到新的字符数组中。

那么新的字符数组容量是多少呢?

答案就在 newCapacity(minimumCapacity)方法中

### newCapacity()方法

源码
1
2
3
4
5
6
7
8
9
10
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;
}

image


默认的 newCapacity 大小是原有的字符数组大小左移一位加上2,即2*oldCapacity+2,将原有的字符数组扩大一倍再加上2。为什么是这样的一种算法呢?直接左移一位不就可以了吗?为什么还要加2?

这里面的newCapacity minCapacity 两个变量容易产生混淆,其中 newCapacity 指的是字符数组新的容量大小,而 minCapacity 指的是当前要存储字符串而需要的最小容量。因此要想能够存储当前字符串,就必须保证 newCapacity >= minCapacity。

所以上述源码中加了一个判断newCapacity 是否大于 minCapacity,如果不是则 newCapacity 的大小直接设置为 minCapacity。

最后返回的时候,还加上了相关判断信息,当 newCapacity 超过了当前数组的最大值的时候,执行 hugeCapacity()方法。

Arrays.copyOf方法

image

str.getChars()方法

源码
1
2
3
4
5
6
7
8
9
10
11
12
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);
}

image


getChars()为 String 类的方法,通过调用 System.arraycopy()系统方法完成将当前字符串的 scrBegin ~ srcEnd 复制到字符数组的 dstBegin 位置。

总结

本文分析了 StringBuffer 类的 append 方法,通过分析我们知道append方法的所有工作都是由父类 AbstractStringBuilder 完成的。基本的思路是检查当前字符数组的容量是否足够,如果不够,则申请新的字符数组,然后将原有字符数组的内容复制到新的字符数组。最后将追加的字符串复制到新的字符数组后面,从而完成追加操作。


如果让你来设计一个类来完成字符串的不断追加操作,你会怎么设计呢?


可能大部分同学想到的都是每次申请一个新的字符数组,将原有数组的内容复制到新数组,最后将追加的字符串复制到新数组后面。

这种实现方式最大的问题就是效率。不停的进行数组的复制操作导致效率非常低下,因此StringBuffer 提出的思路是每次我多申请一些字符数组,当容量不够的时候,申请原有容量2倍+2的容量,而不仅仅是满足 minCapacity 最小容量的大小。这就是提升效率的一种方式,这种设计方式在很多场景都有应用。