引言

前一篇文章讲解了java中的String类,String类最重要的特性就是不可变性,很多需要对其字符序列进行修改的方法都会创建新的字符串对象,这样保证了字符串的线程安全,但是对于一些操作,例如大量的字符串拼接,可能会消耗性能,StringBuilder和StringBuffer作为可变字符序列的实现,在很多操作上可以替代String,在讲解StringBuilder之前,我们先讲解整个类层次结构中的几个重要的类。

StringBuilder和StringBuffer类的层次结构

StringBuilder类的定义如下:

  1. public final class StringBuilder
  2. extends AbstractStringBuilder
  3. implements java.io.Serializable, CharSequence

它继承了AbstractStringBuilder这个抽象类,实现了Serializable和CharSequence接口。再看AbstractStringBuilder这个类,这是一个抽象类,实现了Appendable和CharSequence两个接口,所以整个的层次是这样的:
其实StringBuffer也是这样的类层次结构,所以图中把StringBuffer也加上了。
StringBuffer和StringBuilder中的大部分方法都是在AbstractStringBuilder中实现的。我们重点来看一下这个抽象类。

AbstractStringBuilder

  1. /**
  2. * A mutable sequence of characters.
  3. * <p>
  4. * Implements a modifiable string. At any point in time it contains some
  5. * particular sequence of characters, but the length and content of the
  6. * sequence can be changed through certain method calls.
  7. */
  8. abstract class AbstractStringBuilder implements Appendable, CharSequence {}

AbstractStringBuilder代表一个可变的字符数组。它实现了一个可修改的字符串,在任何时间它都包含一些特定的字符序列,但是这个序列的长度和内容都可以通过特定的方法调用被修改。
分析String的文章中,我们介绍了CharSequence这个接口,它提供了对字节序列的只读访问,而Appendable就提供了对字节数组的可写访问,实现了这两个接口之后,AbstractStringBuilder就能对字节数组进行读和写操作了,我们首先来看Appendable的实现。

Appendable接口

  1. /**
  2. * An object to which <tt>char</tt> sequences and values can be appended. The
  3. * <tt>Appendable</tt> interface must be implemented by any class whose
  4. * instances are intended to receive formatted output from a {@link
  5. * java.util.Formatter}.
  6. */

Appendable代表一个可以执行追加(append)操作的字节数组对象。它只有三个方法:

  1. Appendable append(CharSequence csq) throws IOException;
  2. Appendable append(CharSequence csq, int start, int end) throws IOException;
  3. Appendable append(char c) throws IOException;

这些方法对字符数组进行添加操作。AbstractStringBuilder中就需要对这几个方法进行实现来达到修改字符序列的目的。

成员变量

AbstractStringBuilder中有三个变量:

  1. /**
  2. * The value is used for character storage.
  3. */
  4. char[] value;
  5. /**
  6. * The count is the number of characters used.
  7. */
  8. int count;
  9. /**
  10. * The maximum size of array to allocate (unless necessary).
  11. * Some VMs reserve some header words in an array.
  12. * Attempts to allocate larger arrays may result in
  13. * OutOfMemoryError: Requested array size exceeds VM limit
  14. */
  15. private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

其中,value用来存储字符序列,count表示使用的字符的数量,这里你可能会有疑问,value的length应该就可以返回字符数组的数量,为什么还要有一个count单独来记录数量。其实这两个不是一个值,在可变性字节序列中,很多操作例如append和insert都会改变value这个字节序列,这就要求value有扩容的功能,但是扩容之后数组中并不是每个位置都会被字符填充,比如我扩大到之前长度的两倍,但是添加的字符串的长度不到之前的一倍,这样字符数组中就会有位置虽然被初始化了,却没有被真正填充,所以需要记录数组中真正字符的数量,也就是,value.length返回的是整个数组的长度,count记录的是字符数组中真正被字符填充的数量。关于具体的扩容策略,我们之后会讲到。
MAX_ARRAY_SIZE限制了数组可以锁定的最大容量。如果尝试锁定更大的数组会导致OutOfMemoryError: Requested array size exceeds VM limit。注意这个长度是字符数组的长度而不是真正填充字符的长度,我们看下面的例子:

  1. public static void main(String[] args) {
  2. StringBuilder sb = new StringBuilder(Integer.MAX_VALUE);
  3. }
  4. Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit
  5. at java.lang.AbstractStringBuilder.<init>(AbstractStringBuilder.java:68)
  6. at java.lang.StringBuilder.<init>(StringBuilder.java:101)
  7. at person.andy.concurrency.string.TrimTest.main(TrimTest.java:5)

StringBuilder是AbstractStringBuilder的子类,有下面的构造方法:

  1. public StringBuilder(int capacity) {
  2. super(capacity);
  3. }

调用的就是父类AbstractStringBuilder的构造方法,我传入了一个大于MAX_ARRAY_SIZE的参数,就会抛出java.lang.OutOfMemoryError: Requested array size exceeds VM limit的错误。
实际上,如果没有给定一个较大的堆内存,即使构造方法中的参数小于MAX_ARRAY_SIZE,也会出现堆溢出的问题因为Integer.MAX_VALUE已经接近4G了,看下面的例子:

  1. public static void main(String[] args) {
  2. StringBuilder sb = new StringBuilder(Integer.MAX_VALUE-100);
  3. }
  4. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
  5. at java.lang.AbstractStringBuilder.<init>(AbstractStringBuilder.java:68)
  6. at java.lang.StringBuilder.<init>(StringBuilder.java:101)
  7. at person.andy.concurrency.string.TrimTest.main(TrimTest.java:5)

所以在使用AbstractStringBuilder或者其子类时,要注意这个初始容量值。

构造方法

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

我们主要看第二个:给定一个int类型的参数,这个构造方法会初始化value为一个大小是capacity的数组。此时我们应该能想到,count是0,value.length为capaticy。

获取容量和真实字节数量

AbstractStringBuilder提供了两个方法用来获取字符数组的容量和真实的字节数量:

  1. /**
  2. * Returns the length (character count).
  3. *
  4. * @return the length of the sequence of characters currently
  5. * represented by this object
  6. */
  7. @Override
  8. public int length() {
  9. return count;
  10. }
  11. /**
  12. * Returns the current capacity. The capacity is the amount of storage
  13. * available for newly inserted characters, beyond which an allocation
  14. * will occur.
  15. *
  16. * @return the current capacity
  17. */
  18. public int capacity() {
  19. return value.length;
  20. }

length方法返回的是count,也就是真实的字符数量。capacity返回的是value.length也就是我们所说的容量。
这里我们可以用一个StringBuilder的例子来看一下这两个数量:
StringBuilder是AbstractStringBuilder的子类,它有下面的构造方法:

  1. public StringBuilder() {
  2. super(16);
  3. }

调用了父类AbstractStringBuilder的构造方法,但是传的值是16。也就是说,value会被初始化成容量为16的数组,但是由于没有添加字符串,count的值仍然为零。
可以看下面代码的输出:

  1. public static void main(String[] args) {
  2. StringBuilder stringBuilder = new StringBuilder();
  3. System.out.println(stringBuilder.capacity());
  4. System.out.println(stringBuilder.length());
  5. }

输出结果是:16,0。
证明了我们的分析是正确的。

append方法

AbstractStringBuilder提供了很多append方法,我们这里重点分析三个:

  1. public AbstractStringBuilder append(String str) {
  2. if (str == null)
  3. return appendNull();
  4. int len = str.length();
  5. //count+len可能就会超过Integer.MAX_VALUE而发生溢出导致结果是个负数
  6. ensureCapacityInternal(count + len);
  7. str.getChars(0, len, value, count);
  8. count += len;
  9. return this;
  10. }

先不看str为null的情况,首先获取str的长度,然后调用ensureCapacityInternal(),传的参数是当前字符数组中实际的字符数量加上str的长度,也就是执行添加操作需要的最小字节容量,从这个方法开始,就是AbstractStringBuilder及其子类的扩容机制,我们来重点分析一下:

扩容策略

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

我们得从append方法调用ensureCapacityInternal的参数说起:

  1. ensureCapacityInternal(count + len);

这个参数是当前value中的实际字符数量加上要添加的字符串的长度,这个是扩容要求满足的最小容量,也就是我们的value的长度最小需要是这个值。这里有一个值得注意的地方,就是count+len这个值可能已经发生了溢出,也就是需要满足的最小容量已经超过了Integer的最大值,在分析后面的方法时,你需要一直记得这种情况。
之后,调用ensureCapacityInternal方法,首先进行了这样的校验:

  1. if (minimumCapacity - value.length > 0)

我们需要分析几种情况:
第一,minimumCapacity也就是count+len没有发生溢出,此时如果minimumCapacity - value.length > 0,说明需要满足的最小容量是比当前字符数组的长度大的,就需要扩容,否则,说明不用扩容。
第二:minimumCapacity也就是count+len发生了溢出,此时minimumCapacity肯定是负数,这个时候minimumCapacity - value.length的值肯定是大于0的,至于为什么,你需要理解计算机中补码的运算,这里不做很多解释了。
所以,minimumCapacity - value.length > 0为false只会在minimumCapacity没有发生溢出并且minimumCapacity继续我们的逻辑,如果minimumCapacity - value.length > 0这个条件成立,就会调用

  1. value = Arrays.copyOf(value,
  2. newCapacity(minimumCapacity));

,这里的重点是newCapacity方法:

  1. /**
  2. * Returns a capacity at least as large as the given minimum capacity.
  3. * Returns the current capacity increased by the same amount + 2 if
  4. * that suffices.
  5. * Will not return a capacity greater than {@code MAX_ARRAY_SIZE}
  6. * unless the given minimum capacity is greater than that.
  7. *
  8. * @param minCapacity the desired minimum capacity
  9. * @throws OutOfMemoryError if minCapacity is less than zero or
  10. * greater than Integer.MAX_VALUE
  11. */
  12. private int newCapacity(int minCapacity) {
  13. // overflow-conscious code
  14. int newCapacity = (value.length << 1) + 2;
  15. if (newCapacity - minCapacity < 0) {
  16. newCapacity = minCapacity;
  17. }
  18. return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
  19. ? hugeCapacity(minCapacity)
  20. : newCapacity;
  21. }
  22. private int hugeCapacity(int minCapacity) {
  23. if (Integer.MAX_VALUE - minCapacity < 0) { // overflow
  24. throw new OutOfMemoryError();
  25. }
  26. return (minCapacity > MAX_ARRAY_SIZE)
  27. ? minCapacity : MAX_ARRAY_SIZE;
  28. }

这里直接给出结论:
(1)如果当前的真实字节数量也就是count值+需要添加的字符串的字节数量得到的结果大于Integer.MAX_VALUE,也就是发生了溢出,直接抛出OutOfMemoryError异常,因为肯定为不能这样大小的字节序列分配容量了。
(2)如果(1)中计算的结果不大于Integer.MAX_VALUE,说明这个容量是可以满足的(不管一些VM的限制)。这个时候,将value的长度扩大为原来的两倍加2,用这个值跟前面计算出来的值进行比较,取最大值,如果这个最大值没有大于MAX_ARRAY_SIZE,新的容量就是这个值。如果这个最大值大于MAX_ARRAY_SIZE,不管这个最大值是minCapacity还是value*2+2,都取minCapacity和MAX_ARRAY_SIZE的最大值来作为新的容量。

在完成了扩容操作之后,append方法调用的是String的getChars方法将需要添加的字符序列添加到原有字符序列后面,这个逻辑比较简单。

insert方法

  1. public AbstractStringBuilder insert(int offset, String str) {
  2. if ((offset < 0) || (offset > length()))
  3. throw new StringIndexOutOfBoundsException(offset);
  4. if (str == null)
  5. str = "null";
  6. int len = str.length();
  7. ensureCapacityInternal(count + len);
  8. System.arraycopy(value, offset, value, offset + len, count - offset);
  9. str.getChars(value, offset);
  10. count += len;
  11. return this;
  12. }

insert方法将一个字节序列插入到当前字节序列的offset开始的位置,这个方法同样会调用扩容策略,这里面值得关注的是下面这两行代码:

  1. System.arraycopy(value, offset, value, offset + len, count - offset);
  2. str.getChars(value, offset);

第一段代码将当前字节序列从offset开始的字节序列复制到offset+len开始的字节序列,保留出中间len长度的字节,第二段代码将要插入的字节序列复制到中间留出的位置部分。

indexOf方法

在AbstractStringBuilder中indexOf的实现值得关注:

  1. public int indexOf(String str, int fromIndex) {
  2. return String.indexOf(value, 0, count, str, fromIndex);
  3. }

是通过String.indexOf方法实现的,这个方法我们在String源码中已经分析过了,当时就看到源码中说明这个方法会被String和StringBuffer用来做字符串查找,实际上在AbstractStringBuilder中就用到了。

StringBuilder

StringBuilder继承了AbstractStringBuilder,在AbstractStringBuilder的构造方法中,会根据传入的capacity的参数来初始化字符数组。如下:

  1. AbstractStringBuilder(int capacity) {
  2. value = new char[capacity];
  3. }

而在StringBuilder中,构造方法会有些不同:

  1. public StringBuilder() {
  2. super(16);
  3. }
  4. public StringBuilder(int capacity) {
  5. super(capacity);
  6. }
  7. public StringBuilder(String str) {
  8. super(str.length() + 16);
  9. append(str);
  10. }

在默认情况下,StringBuilder的容量会被设置成16,如果提供了初始容量,就是该容量的值,如果构造方法中给了一个字符串,那么容量会是字符串的长度+16。
StringBuidler中的大多数方法都是直接使用的AbstractStringBuilder的实现,所以明白了AbstractStringBuilder的方法逻辑之后,就能知道StringBuider的实现了:

  1. @Override
  2. public StringBuilder append(Object obj) {
  3. return append(String.valueOf(obj));
  4. }
  5. @Override
  6. public StringBuilder append(String str) {
  7. super.append(str);
  8. return this;
  9. }

StringBuffer

StringBuffer作为StringBuilder的线程安全版本,在很多方法上的实现同样是通过调用父类AbstractStringBuilder的对应方法来完成的,只不过对于需要修改状态的方法,都加上了Synchronized关键字来进行同步访问:

  1. @Override
  2. public synchronized StringBuffer append(Object obj) {
  3. toStringCache = null;
  4. super.append(String.valueOf(obj));
  5. return this;
  6. }
  7. @Override
  8. public synchronized StringBuffer append(String str) {
  9. toStringCache = null;
  10. super.append(str);
  11. return this;
  12. }

这里就不需要详细介绍了。