StringBuilder作为JDK提供的可变字符串操作类类型,在String字符串类不可变的特性基础上,提供了字符串可变操作的能力。StringBuilder类和StringBuffer两个作为可变类类型,都统一继承至AbstractStringBuilder类,实现CharSequence字符序列统一接口类型。
两个可变字符串操作类类型区别在于是否支持线程安全操作上,其中StringBuffer类类型支持线程并发安全性操作(支持同步控制),StringBuilder类类型线程非安全。
理解StringBuilder类类型,首要理解其父类AbstractStringBuilder提供的统一字符串的数据存储结构以及其结构之上封装基本操作。
1.字符串承载的数据结构(统一定义在AbstractStringBuilder类中)
AbstractStringBuilder:
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
1)字符数组char[] value
对于StringBuilder类类型来讲,其存放字符串的数据存储结构为字符数组char[] value,该value中以单个字符为元素存放整个字符串,这一点存储结构思路和C++语言相似。
2)int count
另外,该结构还有一个count计数的变量,用于记录字符数组空间,已经被存放的字符的数量,即已经被使用存放的字符数量。
2.可变字符串对象构造(统一定义在AbstractStringBuilder类中)
AbstractStringBuilder:两个构造可变字符串对象方法
/**
* This no-arg constructor is necessary for serialization of subclasses.
*/
AbstractStringBuilder() {
}
/**
* Creates an AbstractStringBuilder of the specified capacity.
*/
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
1)默认构造方法,空的实现。
2)指定容量capacity构造可变字符串对象,内部在数据存储结构上为字符数组value申请分配空间,默认情况下为16。
3.可变字符串操作
具体认识StringBuilder类类型,主要从如下几个方面:
a. 该类的基本存储结构如何;
b. 该类的数据结构存储如何实现扩容;
c. 该类基础上操作封装思路。
1)基本存储结构
构建StringBuilder类对象,实际上是初始化创建了字符数组value;对于父类AbstractStringBuilder构造方法支持指定容量构建字符串存储结构,对于子实例类StringBuilder提供了多种构造字符串对象的构造方法如下:
a. 默认构造StringBuilder类对象
public StringBuilder() {
super(16);
}
默认构造该对象,字符数组存储容量申请为16个,后续基于可变字符数组value可支持扩容空间操作。如默认构造方式定义StringBuilder的对象stringBuilder:
在其内部实际上代码逻辑如下:
value = new char[capacity];
为父类的存放字符串的字符数组通过new方式分配字符存储内存空间,这里默认情况下是16个字符空间。
b. 指定容量构造StringBuilder类对象
public StringBuilder(int capacity) {
super(capacity);
}
内部使用和默认构造相同的分配字符数组内存空间的方式。可以按照指定字符数组大小的方式来创建字符串存放的数组。
c. 初始化字符串构造对象
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
采用初始化字符串方式构造StringBuilder对象方式,该构造方法中传入需要初始化的字符串,内部分为两个步骤构造该对象。与前面构造对象不同,初始化构造步骤如下:
i. 初始化指定字符数组容量空间:
初始化构造的时候,其字符数组value的容量为初始化的字符串str的长度+16个字符空间;
ii. 初始化字符串值:
字符串初始化和String字符串类不同的是,StringBuilder采用的是append方法来初始化指定的字符串值,其步骤如下:
1.append方法在父类封装中定义了具体的逻辑:
append方法的主要思路是在现有的字符数组基础上,拷贝新追加的字符串,其中需要考虑字符数组初始化空间是否足够新追加的字符串存放的问题?
public AbstractStringBuilder append(String str) {
if (str == null) str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
append方法具体思路如下:
1)首先会判断append的字符串是否为空,为空的话,给输入的字符串存放“null”字符串;
2)获取输入str字符串的长度,通过ensureCapacityInternal方法检查并或者扩容确保新append的字符串(这里可以看到判断的长度是count+本次追加的字符串长度len),现有的字符数组有足够的空间可以存放;(如果判断append的字符串长度在现有的value字符数组中不够空间存放,那么会进行扩容处理,后续会详细介绍扩容操作处理过程)
3)使用字符串String类的getChars方法,实现字符串到当前的value字符数组的拷贝。
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);
}
该方法中最终是要System类对应的arraycopy,数组拷贝的方法来实现新增加的字符串到现有字符串追加的处理的。计数已使用空间的变量count+len新append字符串的长度。
总结:
StringBuilder字符串是存储在一个字符数组这样一个连续内存空间数据结构中,所有关于字符串的操作,都是围绕这个字符数组空间而来的。
2)字符串存储扩容
对一个字符串操作类,存储空间的扩容是我们需要重要关注点之一。所有的字符串操作都围绕存储字符串的数据结构,那么存储这个字符串的结构本身因为数组的缘故,存放的空间需要指定。
数组的缺点是指定内存空间后,一旦存储的数据超出这个空间就需要进行扩容操作,否则就会出现溢出错误。
StringBuilder的父类AbstractStringBuilder统一定义了字符串存储的数据结构基础上,也定义了围绕该结构的一系列扩容操作。
扩容相关的几个方法总结如下:
第一通常扩容操作是因为存储字符数组结构存放不下新增加的字符串,这类append就是字符串追加的基本操作方法,由该方法调用触发扩容操作;
public AbstractStringBuilder append(String str) {
if (str == null) str = "null";
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
该方法中在获取了追加的字符串长度len之后,调用了ensureCapacityInternal方法来判断其长度是否需要进行字符数组的扩容操作,确保追加的操作有足够的字符数组存放空间应对处理。
ensureCapacityInternal方法将当前字符串已经使用字符数组的空间+追加的字符串的长度len作为参数,即总字符串长度传入。
确保容量方法ensureCapacityInternal,该方法参数是输入校验的长度。
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0)
expandCapacity(minimumCapacity);
}
该方法内部,将输入的新拼凑的字符串长度跟当前字符数组value进行比较:
如果新拼凑的字符串长度比value的总容量还大,那么通过expandCapacity方法进行扩容,否则不需要做任何处理。
2.字符数组value扩容方法expandCapacity,该方法实现思路如下:
void expandCapacity(int minimumCapacity) {
int newCapacity = value.length * 2 + 2;
if (newCapacity - minimumCapacity < 0)
newCapacity = minimumCapacity;
if (newCapacity < 0) {
if (minimumCapacity < 0) // overflow
throw new OutOfMemoryError();
newCapacity = Integer.MAX_VALUE;
}
value = Arrays.copyOf(value, newCapacity);
}
根据输入的最新拼凑的字符串的长度为参数,内部进行扩容处理:
第一步:定义新的扩容容量长度,新的容量是原先value字符数组的总容量的2备+2个字符长度,容量存放临时变量newCapacity。
第二步:拿最新扩容容量的长度newCapacity和输入的新拼凑字符串长度变量minimumCapacity 进行比较:
如果新扩容的容量还没有新拼凑的字符串长度长,那么新扩容的长度就按照新拼凑的字符串的长度minimumCapacity 来定;
如果字符数组value本身异常出现内存溢出,那么newCapacity长度可能为负数,同时输入的minimumCapacity也可能为负,抛出OutOfMemoryError()内存溢出错误。此时,新扩容的容量直接定义为Integer整型最大值。
第三步:通过Arrays数组类的拷贝方法,将按照newCapacity最新容量拷贝一个新的容量的value字符数组出来,同时存放原始已有的字符串值,这样确保了追加字符串存储的字符数组value空间足够。
使用String字符串类的str.getChars(0, len, value, count)方法实现,新追加的字符串,到最新的value的字符数组中保存。
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);
}
String类中的getChars方法分析如下:
该方法前面判断的输入参数,作出了一些异常判断处理,这些异常处理主要包括对追加字符串的校验。
最后通过System类的arraycopy数组拷贝的方式来实现新追加的字符串到目标字符数组中的拷贝的功能。
总结:
对于可变字符串StringBuilder类的扩容机制,需要理解的重点和关注点:
1.当前StringBuilder类的字符数组因为新追加的字符串长度不够存放,引发扩容,其中扩容的部分涉及新的字符数组value的创建和原先存放字符串内容的拷贝;
2.另外,新追加的字符串,通过字符数组拷贝的方式来实现最终的追加拼接。
3.所有的可变字符串方面的操作都围绕字符数组value展开。
所以,在我们大规模的字符串处理操作的时候,设计思路上要考虑字符数组扩容和追加带来的代价,尽可能的朝着优化的方向去设计。