本文主要梳理了 JDK 8 中 StringBuilder 的无参构造方法、append(String str) 方法、setLength(int newLength) 方法的执行流程。
知识点总结在最底下。
1 类图
StringBuilder 的 UML 类图如下,只记录了主要的方法、变量。
StringBuilder 继承了 AbstractStringBuilder 抽象类,并实现了 java.io.Serializable、CharSequence 接口。StringBuilder 的大部分方法都是直接调用了父类 AbstractStringBuilder 中的方法,然后通过 return this 返回当前 StringBuilder 的实例。
AbstractStringBuilder 实现了 Appendable、CharSequence 接口,类中是一些核心的代码。成员变量有 value、count,value 是一个char[],用来保存拼接进来的字符;count 是 int 类型,用来记录“已使用的字符数量”。
2 构造方法
public StringBuilder() {
super(16);
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
StringBuilder 的无参构造方法中会调用 AbstractStringBuilder 的 AbstractStringBuilder(int capacity) 构造方法,并且传参是 16,而 AbstractStringBuilder(int capacity) 构造方法中,会根据参数 capacity 给 成员变量 value 这个char[] 创建一个指定容量的字符数组。
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
String(char value[], int offset, int count) 这个构造方法中,最终会执行数组的复制,创建一个长度为 count 的字符数组,并将原数组的元素复制到新数组中。
3 方法
StringBuilder 的方法介绍按照方法所属的类分成 2 节介绍。
3.1 StringBuilder
3.1.1 append(String str)
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
直接调用父类的 append(String str) 方法,并返回 this。
3.1.2 toString()
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
执行 String(char value[], int offset, int count) 构造方法,创建一个 String 对象。也就是将字符数组中已使用的这部分字符生成一个字符串。
3.2 AbstractStringBuilder
3.2.1 append(String str)
public AbstractStringBuilder append(String str) {
if (str == null)
// 字符串为null,执行特定的 拼接null的方法
return appendNull();
int len = str.length();
// 确保字符数组的容量足够放下新的字符串
ensureCapacityInternal(count + len);
// 将要拼接的字符串复制到字符数组中,从count位置开始往后拼len个
str.getChars(0, len, value, count);
// 更新 使用字符数量
count += len;
return this;
}
字符串为 null 会执行特殊的拼接 null 字符串的方法。在确保内部的字符数组容量充足后,会将字符串拼接到字符数组中,str.getChars(0, len, value, count) 涉及两个 char[] 的复制,注意复制的目标位置是从 count 下标开始,也就是从已使用的字符后面拼接。
3.2.2 appendNull()
private AbstractStringBuilder appendNull() {
int c = count;
// 确保字符数组的容量足够放下新的字符串
ensureCapacityInternal(c + 4);
// 拼上 n u l l 四个字符
final char[] value = this.value;
value[c++] = 'n';
value[c++] = 'u';
value[c++] = 'l';
value[c++] = 'l';
// 更新 使用字符数量
count = c;
return this;
}
首先记录方法开始时的“使用字符数量”为 c,确保字符数组容量足够后,在下标 c 位置开始,往后偏移4个元素,分别赋值 'n' 'u' 'l' 'l' 四个字符。这里用 c 记录当前“使用字符数量”的快照,即使后续 count 发生变化,赋值操作也是基于开始时的“使用字符数量”来执行的。
3.2.3 ensureCapacityInternal(int minimumCapacity)
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
当所需要的容量大于字符数组的长度时,计算出一个新的容量,将原字符数组的元素复制到一个新的长度为新容量的数组中,并将 value 指向新的数组,也就是将字符数组扩容。Arrays.copyOf(char[] original, int newLength) 方法会创建一个新的char[],且数组长度为 newLength,而复制的偏移量只会取两数组中小的那个长度,防止索引越界。
为什么用 minimumCapacity - value.length > 0 来判断,而不直接用 minimumCapacity > value.length 判断?因为 minimumCapacity 是 int 类型,当数组长度特别大,超过 int 类型的最大值(0x7fffffff)时,会变成负数,直接用 minimumCapacity > value.length 判断布尔值会变成 false。
3.2.4 newCapacity(int minCapacity)
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;
}
这里可以看出,默认扩容机制是 增加1倍的容量再加2,使用位运算计算效率更高。当默认扩容的容量依然不满足要求的话,就按照所需要的容量扩容。然后还会判断新容量是否小于等于0 或者 大于 MAX_ARRAY_SIZE,是的话还会执行大容量处理的方法,注意这里传的参数是minCapacity,不是newCapacity。
这里的判断使用 newCapacity - minCapacity < 0 、newCapacity <= 0 、MAX_ARRAY_SIZE - newCapacity < 0 也是防止容量过大变成负数。MAX_ARRAY_SIZE 是 JVM 建议的数组可分配的最大长度。
当 value.length = 2147483647,拼接一个长度为2 ~ 2147483647的字符串时,minCapacity = 2147483647 + 2 ~ 2147483647,默认扩容后 newCapacity = 0,此时 newLength - minCapacity < 0 都为 false,newCapacity 就是0, 所以才有 newCapacity <= 0 的判断。
3.2.5 hugeCapacity(int minCapacity)
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
当容量大于 int 类型最大值时,会抛内存不足错误。否则,如果所需容量大于 JVM 建议的数组可分配的最大长度,就返回需要的容量,不大于就返回可分配的最大数组长度。
这里要结合 newCapacity(int minCapacity) 方法中调 hugeCapacity(int minCapacity) 方法传的参数为 minCapacity 理解,意思就是如果是用户主动拼接字符串导致的容量超过 MAX_ARRAY_SIZE,那么就按照用户的选择来;如果是程序默认扩容导致的容量超过MAX_ARRAY_SIZE,那么就最多分配 MAX_ARRAY_SIZE 的容量。
换句话说就是,如果你们报了OutOfMemoryError,这锅我们不背!
3.2.6 setLength(int newLength)
public void setLength(int newLength) {
if (newLength < 0)
// 使用字符数量不能设置为负数
throw new StringIndexOutOfBoundsException(newLength);
// 确保容量足够
ensureCapacityInternal(newLength);
if (count < newLength) {
// value 数组的 count 到 newLength - 1 的元素全部赋值为 '\0'
Arrays.fill(value, count, newLength, '\0');
}
// 更新使用字符数量
count = newLength;
}
当设置的新的“使用字符数量”为负数时,会抛字符串索引越界异常。在确保字符数组能容纳下新的字符数量后,如果新数量比原数量大,会用 '\0' 填充跳过去的这部分元素,再更新使用字符数量。
在 Arrays.fill(value, count, newLength, '\0') 方法中,会先校验开始索引、偏移量在不在数组的范围内,在的话才会开始赋值。因为数组的长度是固定的,在调用 fill 方法的这一时刻,所要校验的数组就确定了,只要校验通过,赋值时不会索引越界。
4 总结
经过上述的分析,整理一下知识点:
1、new StringBuilder 若不指定容量,默认容量为16,本质上是 value = new char[16]。
2、如果 append() 拼接的字符串为 null,会拼接字符串 "null", 不是空字符串("")。
3、当所需容量大于 value 的长度,会扩容。默认扩容后的容量 = (原容量 × 2) + 2,若默认扩容不满足所需容量,按照所需容量扩容。
4、巨大容量的处理:若所需容量大于 0x7fffffff,抛 OutOfMemoryError;若是程序默认扩容导致容量超 MAX_ARRAY_SIZE ,最大默认扩容容量限制为 MAX_ARRAY_SIZE。
5、如果 setLength() 参数是负数,抛 StringIndexOutOfBoundsException。若参数比 count 大,将中间跳过的部分填充 '\0' 。若参数小于等于 count,只会更新 count。
6、append() 拼接非 null 字符串,若不扩容,会导致 1次数组复制(偏移量为字符串长度);若扩容,则导致 1次 new char[newLength]、2 次数组复制(扩容复制的偏移量为原数组长度)。
7、StringBuilder 的 toString() 方法,会创建 String 对象。只要 StringBuilder 的 count 大于 0,本质上是 1次 new char[count]、1次数组复制(偏移量为 count)。
(这里的数组复制指调用了 System.arraycopy() 本地方法,复制效率与偏移量有关)