1 - 引言
在 Java 中,String 被设置为不可变类,JDK 开发人员很小心地保证 String 的底层存储结构 private final char[] value
不被修改。所有对字符串的直接赋值操作,实际上都将字符串变量指向了新的 String 对象。
对于需要对字符串进行大量修改的程序来说,例如对字符串进行拼接,会产生很多无用的 String 实例,所以我们常常会使用 StringBuffer 或者 StringBuilder 来取代 String。
2 - StringBuilder 和 StringBuffer
这两个类其实在实现上是大同小异的,主要的区别就是 StringBuffer 在绝大部分必要的方法上添加了 synchronized 关键字,所以 StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的。在单线程应用中,使用哪一个问题都不大,但 StringBuffer 使用同步锁会损失一部分性能,这一点也是需要知道的。(有点像 HashMap 和 HashTable)
先观察这两个类的声明,我们发现它们继承相同的父类和实现相同的接口,同时两个类几乎没有自己定义的成员变量。
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.
*/
private transient char[] toStringCache; // 这个cache在本文不讨论
static final long serialVersionUID = 3388685877147921107L;
...
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
...
}
public final class StringBuilder extends AbstractStringBuilder
implements java.io.Serializable, CharSequence {
static final long serialVersionUID = 4383685877147921099L;
...
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
...
}
观察两个类的其他方法,我们发现大部分的方法都是直接调用父类已经定义好的方法,所以StringBuilder 和 StringBuffer 的研究我们先放下,将注意力放在它们的父类 AbstractStringBuilder 上。
3 - AbstractStringBuilder 的值修改
首先我们比较关心 AbstractStringBuilder 的成员变量,或者说关心他底层实际的存储结构,可以猜想,肯定是一个 char 数组,情况也确实如此,如下:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
...
}
我们看到只有两个成员变量(还有一些常量没有列出来),其中 value 就是存储字符串的底层数据结构,count 是记录字符数的。
3.1 value 成员变量
value 没有使用 final 修饰,意味着我们可以改变 value 的指向,指向一个新的数组。但是如果只是单纯替换 char 数组的某个字符我们自然不需要改变整个指向,比如下面的代码。
public void setCharAt(int index, char ch) {
if ((index < 0) || (index >= count))
throw new StringIndexOutOfBoundsException(index);
value[index] = ch; // 直接修改value[i]的值
}
在 String 中为了保持不变性,是不允许直接这样修改 value 的某个字符,所以除了构造方法外,我们看不到 value[i] = ch
这种赋值语句。
3.2 append()
看看 append()
中是怎么使用这个 value 的:
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;
}
我们在上面的代码中,似乎没有看到将 str 的值保存到 value,怎么回事呢?不急,我们留意 getChars()
这个方法,打开源码看到:
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);
}
// 第一个参数 value 是 String 的 char 数组
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
一切恍然大悟,原来调用了 System 下的数组拷贝方法,这个方法的函数原型长下面这样:
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
这个是一个 native 方法,不是 Java 的实现。我们翻译一下注释就知道这个方法会把 src 的 srcPos ~ srcPos + (length - 1) 之间的元素复制到 dest 的 destPos ~ destPos + (length - 1) 上,简单来说就是将 src 的某一段元素全部复制到 dest 上。
append()
直接将 str 的字符数组拷贝到 value,本质上也是将 char 一个一个赋值,并没有改变 value 的指向。那么什么时候 value 可以改变指向呢?
3.3 ensureCapacity()
其实很容易想到,当 value 的空间放不下所有字符的时候,value 应该重新申请一块更大内存,我们观察一下 AbstractStringBuilder 中关于容量的方法即可。最终修改容量的方法是如下:
private void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0) { // 当前需求的容量大于数组实际容量
value = Arrays.copyOf(value,
newCapacity(minimumCapacity)); // 确认新数组的容量
}
}
终于我们在 Arrays.copyOf()
中找到新数组分配的代码:
public static char[] copyOf(char[] original, int newLength) {
char[] copy = new char[newLength];
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
在寻找 value 的使用过程中,我们已经基本了解了 AbstractStringBuilder 的一些基本方法,通过类比我们也能猜测到其他诸如 insert、replace等修改 value 的方法是怎样实现的了,无非就是确认容量再进行拷贝。
4 - AbstractStringBuilder 的扩容
上一节已经给出了判断是否扩容的方法 ensureCapacityInternal()
,这一节我们研究扩容后的新容量是怎么确定的。
下面将这一系列方法截取并翻译:
/**
* 确保当前容量至少和指定容量相等。
* 如果当前容量比 {@code minimumCapacity} 指定的容量小,那么就分配一个
* 更大的容量,新容量是下面两者中的较大者:
* <ul>
* <li>The {@code minimumCapacity} argument.
* <li>Twice the old capacity, plus {@code 2}.
* </ul>
* 如果参数是非正数,这个方法直接返回。
* Note that subsequent operations on this object can reduce the
* actual capacity below that requested here.
*
* @param minimumCapacity the minimum desired capacity.
*/
public void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > 0)
ensureCapacityInternal(minimumCapacity);
}
/**
* 如果参数是正数,这个方法的效果就如 {@code ensureCapacity} 所说的那样,
* 但这个方法不是一个同步方法。
* 如果由于数值溢出导致 {@code minimumCapacity} 是非正数,这个方法会抛出
* {@code OutOfMemoryError}.
*/
private void ensureCapacityInternal(int minimumCapacity) {
if (minimumCapacity - value.length > 0) { // 当前需求的容量大于数组实际容量
value = Arrays.copyOf(value,
newCapacity(minimumCapacity)); // 确认新数组的容量
}
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // (2^31-1)-8
/**
* 返回一个至少和 {@code minCapacity} 相等的容量。
* 如果可以,会返回一个当前容量加倍后 +2 的容量。
* 不会返回一个容量大于 {@code MAX_ARRAY_SIZE}, 除非指定的最小容量更大。
*
* @param minCapacity the desired minimum capacity
* @throws OutOfMemoryError if minCapacity is less than zero or
* greater than Integer.MAX_VALUE
*/
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;
}
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError(); // 实际请求的 size 超过 2^31-1
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;
}
简单介绍一下 newCapacity()
方法流程:
- 首先尝试将 newCapacity 设置为现有容量的 2 倍加 2。
- 但是扩充后有几种可能,第一种是不满足申请的空间,此时
0 < newCapacity < minCapacity
;第二种是扩充后有可能超过 int 类型正数的最大值(231−1)(2^{31}-1)(231−1),造成溢出,此时newCapacity < 0
。 这意味着这种扩充方法不适用,我们尝试将 newCapacity 设置为 minCapacity,也就是要多少给多少。 - 我们理想中的 newCapacity 应该是
0 < newCapacity <= MAX_ARRAY_SIZE
。但现实很骨感,假如 newCapacity 超过了 MAX_ARRAY_SIZE 怎么处理?我们会调用hugeCapacity()
,申请最多不超过 (231−1)(2^{31}-1)(231−1) 的容量。
考虑一个问题:假如 minCapacity 是负数怎么办?负数什么时候会出现呢?
很简单,负数会在hugeCapacity()
中抛出 Error。minCapacity 溢出就是负数,这个负数其实也代表我们申请的空间已经超过 Integer.MAX_VALUE 了,所以 JDK 开发者也在注释中用 overflow 表示这种情况 。
关于源码的部分还有很多没有讲解到的地方,也有可能讲的不对的地方,欢迎大家留言指出。
正文结束,欢迎留言。