一、介绍
1、StringBuilder和StringBuffer的相同点
StringBuilder和StringBuffer都代表可变字符序列的类,上节讲的String是不可变字符序列,StringBulder和StringBuffer都继承了AbstractStringBuilder,除了序列化相关的其他操作都是调用父类AbstractStringBuilder的方法实现的,两个类代码基本上相同。
2、StringBuilder和StringBuffer的不同点
StriingBuilder不是线程安全的,用在单线程环境,而StringBuffer是线程安全的,可以用在多线程环境,StringBuffer的方法都添加了synchronized关键字,synchronized就是java中的内置锁,多个线程对同一个StringBuffer操作时需要获取锁,StringBuffer为了提升性能,toString方法使用了缓存,我们看以下代码。
private transient char[] toStringCache;
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
从代码中我们看出,如果toStringCache为空,则调用Arrays.copyOfRange(value, 0, count)拷贝当前StringBuffer内部数组的数据并赋值给toStringCache,如果StringBuffer对象没有改变,则下次调用就省去了拷贝数据的开销,直接使用toStringCache。如果StringBuffer对象有修改操作就会将toStringCache置空,下次调用toString还要重新拷贝数据,读取操作不会置空toStringCache。置空就是将toStringCache引用赋值nul,我们也看到了toString方法添加了synchronized关键字。
二、代码讲解
因为StringBuilder和StringBuffer的大部分操作都是调用父类AbstractStringBuilder的方法,因此我们除了构造函数以外只讲解AbstractStringBuilder的代码。
StringBuilder构造函数(StringBuffer类似)
public StringBuilder() {
super(16);
}
无参构造函数,调用父类构造函数,创建大小为16的字符数组。
public StringBuilder(int capacity) {
super(capacity);
}
创建指定大小的字符数组
public StringBuilder(String str) {
super(str.length() + 16);
append(str);
}
public StringBuilder(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}
字符串或者字符序列为参数的构造函数,先调用父类的构造函数创建字符串或者字符序列的长度加上16,为什么加上16呢,因为StringBuilder和StringBuffer是可变字符序列,因此继续添加字符串的可能性很大,因此原长度加上16可以避免内部char数组的扩容,前提添加的字符串小于16.
AbstractStringBuilder
1、属性
char[] value;存储数据的数组
int count; 当前数量大小
2、构造方法
//子类序列化需要
AbstractStringBuilder() {
}
//创建指定大小的char数组
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
3、常用方法
public void ensureCapacity(int minimumCapacity) 确保char数组容量至少等于minimumCapacity,如果当前char数组容量小于minimumCapacity,将进行扩容,扩容是新建一个当前char数组容量乘以2加2的新数组,并把当前char数组的数据拷贝到新建数组中去。如果minimumCapacity为负数将不进行任何操作。
public void ensureCapacity(int minimumCapacity) {
if (minimumCapacity > 0) //1
ensureCapacityInternal(minimumCapacity);
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
if (minimumCapacity - value.length > 0) { //2
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2; //3
if (newCapacity - minCapacity < 0) { //4
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) //5
? hugeCapacity(minCapacity)
: newCapacity;
}
private int hugeCapacity(int minCapacity) {
if (Integer.MAX_VALUE - minCapacity < 0) { // overflow //6
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE) //7
? minCapacity : MAX_ARRAY_SIZE;
}
1、指定的minimumCapacity如果小于0则直接返回
2、指定的minimumCapacity大于当前char数组容量才进行扩容,先调用newCapacity计算新容量,然后调用Arrays.copyOf拷贝一个指定大小的新数组并赋值给当前对象的char数组,新数组中包含原来数组的所有值。Arrays.copyOf使用了System.arraycopy方法,上节已经介绍过,不再赘述。
3、value.length左移一位,等于乘以2,然后加2,计算扩容的数组大小,为什么乘以2呢?因为如果扩容后的数组过大会造成存储空间的浪费,如果扩容后的数组过小会造成数组的频繁扩容影响性能,因此为了权衡选择了两倍扩容,两倍扩容也是计算机领域常用方法。为什么后面还要加2呢?在value的大小为0的情况下,如果不加2newCapacity的大小是0,会造成扩容失败,当然有人会有疑问了,什么情况下当前的char数组大小为0呢,子类的构造函数不是创建了大小为16的数组吗?关键在trimToSize方法,如果当前对象的count为0,调用此方法当前数组会缩减为大小为0的数组。
4、如果根据当前char数组的大小扩容后的newCapacity小于指定的minCapacity,则使用minCapacity当作扩容后的数组大小。
5、如果newCapacity小于0(溢出)或者newCapacity大于Integer.MAX_VALUE - 8,说明指数扩容后的大小或者指定的minCapacity都过大,则调用hugeCapacity计算容量,否则返回指数扩容后的容量。
6、指定的minCapacity大于Integer.MAX_VALUE,直接抛出OOM异常
7、minCapacity大于Integer.MAX_VALUE - 8则返回minCapacity,否则返回Integer.MAX_VALUE - 8
public AbstractStringBuilder append(String str) 添加指定的字符串到当前字符序列
public AbstractStringBuilder append(String str) {
if (str == null) //1
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len); //3
str.getChars(0, len, value, count); //4
count += len;
return this;
}
private AbstractStringBuilder appendNull() { //2
int c = count;
ensureCapacityInternal(c + 4);
final char[] value = this.value;
value[c++] = 'n';
value[c++] = 'u';
value[c++] = 'l';
value[c++] = 'l';
count = c;
return this;
}
其他append重载方法实现大都类似,不再讲解。
1、如果指定字符串为null,则调用appendNull将‘null’四个字符串添加到char数组中,而不是什么都没有添加。
2、appendNull方法先调用ensureCapacityInternal确保当前数组容量是否能够存储count+4大小的数据,然后当前char数组添加null四个字符,更新count后返回。
3、调用ensureCapacityInternal确保容量足够添加指定字符串
4、调用str.getChars(0, len, value, count)将str的数据拷贝到value从count的开头处,getChars方法内部也是调用System.arraycopy,更新count后返回。
public AbstractStringBuilder insert(int offset, String str) 指定位置处插入字符串
public AbstractStringBuilder insert(int offset, String str) {
if ((offset < 0) || (offset > length())) //1
throw new StringIndexOutOfBoundsException(offset);
if (str == null) //2
str = "null";
int len = str.length();
ensureCapacityInternal(count + len); //3
System.arraycopy(value, offset, value, offset + len, count - offset); //4
str.getChars(value, offset); //5
count += len;
return this;
}
1、确保offset参数是否合法,非法则抛出索引越界异常。
2、如果指定字符串为null,则将字符串指定“null”字符串,字符序列会添加"null"字符串。
3、确保容量大小是否足够。
4、 调用System.arraycopy(value, offset, value, offset + len, count - offset)将value中索引为offset以后的字符序列拷贝到offset + len处,len为指定字符串的长度,offset到offset + len处的索引存储指定的字符串str
5、调用str.getChars(value, offset)将指定字符串str拷贝到value的索引offset处,value中的索引offset到offset + len存储了指定字符串str.,更新count后返回。
AbstractStringBuilder delete(int start, int end) 删除索引start到end的字符序列
public AbstractStringBuilder delete(int start, int end) {
if (start < 0) //1
throw new StringIndexOutOfBoundsException(start);
if (end > count)
end = count;
if (start > end)
throw new StringIndexOutOfBoundsException();
int len = end - start;
if (len > 0) { //2
System.arraycopy(value, start+len, value, start, count-end);
count -= len;
}
return this;
1、判断start和end是否合法
2、将val中start+len索引后的字符序列拷贝到start索引处,start索引到start+len索引的字符序列被start+len索引后的字符序列覆盖,相当与删除,更新count后返回。注意,System.arraycopy只是拷贝数组中的数据,原来位置的数据仍然没有变,只不过toString的时候只获取char数组中count范围内的字符序列。
public AbstractStringBuilder reverse() 字符序列翻转
public AbstractStringBuilder reverse() {
boolean hasSurrogates = false;
int n = count - 1;
for (int j = (n-1) >> 1; j >= 0; j--) { //1
int k = n - j;
char cj = value[j];
char ck = value[k];
value[j] = ck;
value[k] = cj;
if (Character.isSurrogate(cj) ||
Character.isSurrogate(ck)) {
hasSurrogates = true;
}
}
if (hasSurrogates) { //2
reverseAllValidSurrogatePairs();
}
return this;
}
/** Outlined helper method for reverse() */
private void reverseAllValidSurrogatePairs() {
for (int i = 0; i < count - 1; i++) { //3
char c2 = value[i];
if (Character.isLowSurrogate(c2)) {
char c1 = value[i + 1];
if (Character.isHighSurrogate(c1)) {
value[i++] = c1;
value[i] = c2;
}
}
}
}
1、遍历char数组,计算j和k,j相当于中位数往左第一个字符,k相当于中位数往右一个字符,依次递减j,递增k,交换j处和k处的字符,完成char数组中的字符的翻转,hasSurrogates 记录字符数组中是否存在增补字符的标识,增补字符是由两个char组成,第一个char是高代理项,第二个char是低代理项,大家自行查询资料。
2、hasSurrogates为true,char数组中存在增补字符,调用reverseAllValidSurrogatePairs处理增补字符
3、遍历char数组,如果当前索引下存在增补字符,说明这个char是翻转后的增补字符,我们举个例子,例如1234字符串翻转后为4321,如果12是增补字符,翻转不应该改变增补字符的顺序,则翻转后应该是4312,因此我们要改变翻转后增补字符两个char的顺序,翻转前增补字符是高代理项在前,低代理项在后,翻转后低代理项在前,高代理项在后,因此代码中isLowSurrogate判断索引下的char是否是低代理项,是的话与下一个高代理项char交换顺序。
其他方法大都类似或者调用的String的方法实现,不再讲解,下篇文章给大家讲解Integer类。