引言
前一篇文章讲解了java中的String类,String类最重要的特性就是不可变性,很多需要对其字符序列进行修改的方法都会创建新的字符串对象,这样保证了字符串的线程安全,但是对于一些操作,例如大量的字符串拼接,可能会消耗性能,StringBuilder和StringBuffer作为可变字符序列的实现,在很多操作上可以替代String,在讲解StringBuilder之前,我们先讲解整个类层次结构中的几个重要的类。
StringBuilder和StringBuffer类的层次结构
StringBuilder类的定义如下:
public final class StringBuilderextends AbstractStringBuilderimplements java.io.Serializable, CharSequence
它继承了AbstractStringBuilder这个抽象类,实现了Serializable和CharSequence接口。再看AbstractStringBuilder这个类,这是一个抽象类,实现了Appendable和CharSequence两个接口,所以整个的层次是这样的:
其实StringBuffer也是这样的类层次结构,所以图中把StringBuffer也加上了。
StringBuffer和StringBuilder中的大部分方法都是在AbstractStringBuilder中实现的。我们重点来看一下这个抽象类。
AbstractStringBuilder
/*** A mutable sequence of characters.* <p>* Implements a modifiable string. At any point in time it contains some* particular sequence of characters, but the length and content of the* sequence can be changed through certain method calls.*/abstract class AbstractStringBuilder implements Appendable, CharSequence {}
AbstractStringBuilder代表一个可变的字符数组。它实现了一个可修改的字符串,在任何时间它都包含一些特定的字符序列,但是这个序列的长度和内容都可以通过特定的方法调用被修改。
在分析String的文章中,我们介绍了CharSequence这个接口,它提供了对字节序列的只读访问,而Appendable就提供了对字节数组的可写访问,实现了这两个接口之后,AbstractStringBuilder就能对字节数组进行读和写操作了,我们首先来看Appendable的实现。
Appendable接口
/*** An object to which <tt>char</tt> sequences and values can be appended. The* <tt>Appendable</tt> interface must be implemented by any class whose* instances are intended to receive formatted output from a {@link* java.util.Formatter}.*/
Appendable代表一个可以执行追加(append)操作的字节数组对象。它只有三个方法:
Appendable append(CharSequence csq) throws IOException;Appendable append(CharSequence csq, int start, int end) throws IOException;Appendable append(char c) throws IOException;
这些方法对字符数组进行添加操作。AbstractStringBuilder中就需要对这几个方法进行实现来达到修改字符序列的目的。
成员变量
AbstractStringBuilder中有三个变量:
/*** The value is used for character storage.*/char[] value;/*** The count is the number of characters used.*/int count;/*** The maximum size of array to allocate (unless necessary).* Some VMs reserve some header words in an array.* Attempts to allocate larger arrays may result in* OutOfMemoryError: Requested array size exceeds VM limit*/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。注意这个长度是字符数组的长度而不是真正填充字符的长度,我们看下面的例子:
public static void main(String[] args) {StringBuilder sb = new StringBuilder(Integer.MAX_VALUE);}Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limitat java.lang.AbstractStringBuilder.<init>(AbstractStringBuilder.java:68)at java.lang.StringBuilder.<init>(StringBuilder.java:101)at person.andy.concurrency.string.TrimTest.main(TrimTest.java:5)
StringBuilder是AbstractStringBuilder的子类,有下面的构造方法:
public StringBuilder(int capacity) {super(capacity);}
调用的就是父类AbstractStringBuilder的构造方法,我传入了一个大于MAX_ARRAY_SIZE的参数,就会抛出java.lang.OutOfMemoryError: Requested array size exceeds VM limit的错误。
实际上,如果没有给定一个较大的堆内存,即使构造方法中的参数小于MAX_ARRAY_SIZE,也会出现堆溢出的问题因为Integer.MAX_VALUE已经接近4G了,看下面的例子:
public static void main(String[] args) {StringBuilder sb = new StringBuilder(Integer.MAX_VALUE-100);}Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat java.lang.AbstractStringBuilder.<init>(AbstractStringBuilder.java:68)at java.lang.StringBuilder.<init>(StringBuilder.java:101)at person.andy.concurrency.string.TrimTest.main(TrimTest.java:5)
所以在使用AbstractStringBuilder或者其子类时,要注意这个初始容量值。
构造方法
AbstractStringBuilder() {}/*** Creates an AbstractStringBuilder of the specified capacity.*/AbstractStringBuilder(int capacity) {value = new char[capacity];}
我们主要看第二个:给定一个int类型的参数,这个构造方法会初始化value为一个大小是capacity的数组。此时我们应该能想到,count是0,value.length为capaticy。
获取容量和真实字节数量
AbstractStringBuilder提供了两个方法用来获取字符数组的容量和真实的字节数量:
/*** Returns the length (character count).** @return the length of the sequence of characters currently* represented by this object*/@Overridepublic int length() {return count;}/*** Returns the current capacity. The capacity is the amount of storage* available for newly inserted characters, beyond which an allocation* will occur.** @return the current capacity*/public int capacity() {return value.length;}
length方法返回的是count,也就是真实的字符数量。capacity返回的是value.length也就是我们所说的容量。
这里我们可以用一个StringBuilder的例子来看一下这两个数量:
StringBuilder是AbstractStringBuilder的子类,它有下面的构造方法:
public StringBuilder() {super(16);}
调用了父类AbstractStringBuilder的构造方法,但是传的值是16。也就是说,value会被初始化成容量为16的数组,但是由于没有添加字符串,count的值仍然为零。
可以看下面代码的输出:
public static void main(String[] args) {StringBuilder stringBuilder = new StringBuilder();System.out.println(stringBuilder.capacity());System.out.println(stringBuilder.length());}
append方法
AbstractStringBuilder提供了很多append方法,我们这里重点分析三个:
public AbstractStringBuilder append(String str) {if (str == null)return appendNull();int len = str.length();//count+len可能就会超过Integer.MAX_VALUE而发生溢出导致结果是个负数ensureCapacityInternal(count + len);str.getChars(0, len, value, count);count += len;return this;}
先不看str为null的情况,首先获取str的长度,然后调用ensureCapacityInternal(),传的参数是当前字符数组中实际的字符数量加上str的长度,也就是执行添加操作需要的最小字节容量,从这个方法开始,就是AbstractStringBuilder及其子类的扩容机制,我们来重点分析一下:
扩容策略
/*** 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 codeif (minimumCapacity - value.length > 0) {value = Arrays.copyOf(value,newCapacity(minimumCapacity));}}
我们得从append方法调用ensureCapacityInternal的参数说起:
ensureCapacityInternal(count + len);
这个参数是当前value中的实际字符数量加上要添加的字符串的长度,这个是扩容要求满足的最小容量,也就是我们的value的长度最小需要是这个值。这里有一个值得注意的地方,就是count+len这个值可能已经发生了溢出,也就是需要满足的最小容量已经超过了Integer的最大值,在分析后面的方法时,你需要一直记得这种情况。
之后,调用ensureCapacityInternal方法,首先进行了这样的校验:
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
value = Arrays.copyOf(value,newCapacity(minimumCapacity));
,这里的重点是newCapacity方法:
/*** Returns a capacity at least as large as the given minimum capacity.* Returns the current capacity increased by the same amount + 2 if* that suffices.* Will not return a capacity greater than {@code MAX_ARRAY_SIZE}* unless the given minimum capacity is greater than that.** @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 codeint 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) { // overflowthrow new OutOfMemoryError();}return (minCapacity > MAX_ARRAY_SIZE)? minCapacity : MAX_ARRAY_SIZE;}
这里直接给出结论:
(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方法
public AbstractStringBuilder insert(int offset, String str) {if ((offset < 0) || (offset > length()))throw new StringIndexOutOfBoundsException(offset);if (str == null)str = "null";int len = str.length();ensureCapacityInternal(count + len);System.arraycopy(value, offset, value, offset + len, count - offset);str.getChars(value, offset);count += len;return this;}
insert方法将一个字节序列插入到当前字节序列的offset开始的位置,这个方法同样会调用扩容策略,这里面值得关注的是下面这两行代码:
System.arraycopy(value, offset, value, offset + len, count - offset);str.getChars(value, offset);
第一段代码将当前字节序列从offset开始的字节序列复制到offset+len开始的字节序列,保留出中间len长度的字节,第二段代码将要插入的字节序列复制到中间留出的位置部分。
indexOf方法
在AbstractStringBuilder中indexOf的实现值得关注:
public int indexOf(String str, int fromIndex) {return String.indexOf(value, 0, count, str, fromIndex);}
是通过String.indexOf方法实现的,这个方法我们在String源码中已经分析过了,当时就看到源码中说明这个方法会被String和StringBuffer用来做字符串查找,实际上在AbstractStringBuilder中就用到了。
StringBuilder
StringBuilder继承了AbstractStringBuilder,在AbstractStringBuilder的构造方法中,会根据传入的capacity的参数来初始化字符数组。如下:
AbstractStringBuilder(int capacity) {value = new char[capacity];}
而在StringBuilder中,构造方法会有些不同:
public StringBuilder() {super(16);}public StringBuilder(int capacity) {super(capacity);}public StringBuilder(String str) {super(str.length() + 16);append(str);}
在默认情况下,StringBuilder的容量会被设置成16,如果提供了初始容量,就是该容量的值,如果构造方法中给了一个字符串,那么容量会是字符串的长度+16。
StringBuidler中的大多数方法都是直接使用的AbstractStringBuilder的实现,所以明白了AbstractStringBuilder的方法逻辑之后,就能知道StringBuider的实现了:
@Overridepublic StringBuilder append(Object obj) {return append(String.valueOf(obj));}@Overridepublic StringBuilder append(String str) {super.append(str);return this;}
StringBuffer
StringBuffer作为StringBuilder的线程安全版本,在很多方法上的实现同样是通过调用父类AbstractStringBuilder的对应方法来完成的,只不过对于需要修改状态的方法,都加上了Synchronized关键字来进行同步访问:
@Overridepublic synchronized StringBuffer append(Object obj) {toStringCache = null;super.append(String.valueOf(obj));return this;}@Overridepublic synchronized StringBuffer append(String str) {toStringCache = null;super.append(str);return this;}
这里就不需要详细介绍了。
