StringBuffer,由名字可以看出,是一个String的缓冲区,也就是说一个类似于String的字符串缓冲区,和String不同的是,它可以被修改,而且是线程安全的。StringBuffer在任意时刻都有一个特定的字符串序列,不过这个序列和它的长度可以通过一些函数调用进行修改。它的结构层次如下图:
StringBuffer是线程安全的,因此如果有几个线程同时操作StringBuffer,对它来说也只是一个操作序列,所有操作串行发生。每一个StringBuffer都有一个容量,如果内容的大小不超过容量,StringBuffer就不会分配更大容量的缓冲区;如果需要更大的容量,StringBuffer会自动增加容量。和StringBuffer类似的有StringBuilder,两者之间的操作相同,不过StringBuilder不是线程安全的。虽然如此,由于StringBuilder没有同步,所以它的速度更快一些。
当发生与源序列有关的操作(如源序列中的追加或插入操作)时,该类只在执行此操作的字符串缓冲区上而不是在源上实现同步。
每个字符串缓冲区都有一定的容量。只要字符串缓冲区所包含的字符序列的长度没有超出此容量,就无需分配新的内部缓冲区数组。如果内部缓冲区溢出,则此容量自动增大。从 JDK 5开始,为该类补充了一个单个线程使用的等价类,即 StringBuilder。与该类相比,通常应该优先使用 StringBuilder 类,因为它支持所有相同的操作,但由于它不执行同步,所以速度更快。
7.7.2 StringBuffer的初始容量和扩容机制
7.7.2.1 StringBuffer的初始容量
既然是容器,那么是一定会有个初始容量的,目的在于避免在内存中过度占用内存.容器的初始容量有默认和使用构造函数申明两种.
查看API我们知道StringBuffer有以下几种构造方法:
StringBuffer() |
StringBuffer(CharSequence seq) |
StringBuffer(int capacity) |
StringBuffer(String str) |
知道了构造方法,我们来了解一下StringBuffer的容量的底层原理;
7.7.2.1.1 不声明长度,默认分配16
查看StringBuffer的空参构造,发现其容量受父类AbstractStringBuilder控制
public StringBuffer() {
super(16);
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
查看上面代码发现默认容量实际上就是创建了一个长度16的字符数组;
7.7.2.1.2 声明长度
查看StringBuffer的有参构造
public StringBuffer(int capacity) {
super(capacity);
}
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
public StringBuffer(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
我们发现有参构造有三个,直接给予长度的很好理解,底层是根据这个长度来创建了一个字符数组,而使用字符串的创建呢,底层的数组长度是字符串长度+16,如果是用字符序列那么和字符串是一样的,然后执行append操作.
7.7.2.2 StringBuffer的原理
StringBuffer继承了抽象类AbstractStringBuilder,在AbstractStringBuilder类中,有两个字段分别是char[]类型的value和int类型的count,也就是说,StringBuffer本质上是一个字符数组:
char[] value;
int count;
value用来存储字符,而count表示数组中已有内容的大小,也就是长度。StringBuffer的主要操作有append、insert等,这些操作都是在value上进行的,而不是像String一样每次操作都要new一个String,因此,StringBuffer在效率上要高于String。有了append、insert等操作,value的大小就会改变,那么StringBuffer是如何操作容量的改变的呢?我们发现最后所有的方法,其实都是在AbstractStringBuilder类中的,以前总结过一次是以JDK1.7位蓝本总结的,这次以JDK1.8为蓝本总结,发现有不同,以此次为准;
//执行插入操作offset - 偏移量。 str - 一个 string。
public AbstractStringBuilder insert(int offset, String str) {
//offset指的是我们 要将字符串插入到的原字符串的位置,所以要注意索引越界异常
if ((offset < 0) || (offset > length()))
throw new StringIndexOutOfBoundsException(offset);
if (str == null)//注意这一点,如果插入的字符串是空字符串null.那么这里不会有nullPointException,而是掺入一个字符串”null”
str = "null";
int len = str.length();//获取要插入的字符串的长度
ensureCapacityInternal(count + len);//注意了:这是扩容扩容机制
//arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。
public static void arraycopy(Object src,
int srcPos,
Object dest,
int destPos,
int length)
从 src 引用的源数组到 dest 引用的目标数组,数组组件的一个子序列被复制下来。被复制的组件的编号等于 length 参数。源数组中位置在 srcPos 到 srcPos+length-1 之间的组件被分别复制到目标数组中的 destPos 到 destPos+length-1 位置。
System.arraycopy(value, offset, value, offset + len, count - offset);
str.getChars(value, offset);
count += len;
return this;
}
//测试null字符串的插入
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
sb.append("西风多少恨,吹不散眉弯!");
String str = null;
sb.insert(2, str);
System.out.println(sb);//西风null多少恨,吹不散眉弯!
}
//添加
public AbstractStringBuilder append(String str) {
if (str == null)//假如添加的是null字符串
return appendNull();//添加字符串”null”
int len = str.length();
ensureCapacityInternal(count + len);//扩容
str.getChars(0, len, value, count);//复制数组到指定数组,这就是为什么这些字符串是可变的,是通过扩容机制重新创建了数组,然后将原来的数组指向新数组,旧数组会被回收
count += len;
return this;
}
private AbstractStringBuilder appendNull() {
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;
}
/**StringBuffer和StringBuilder除了通过插入和添加字符串动态扩容外,还可以通过动态设置容量的方法,来扩容,源码如下,我们观察和上面其实是一致的,这里不再分析: */ public void setLength(int newLength) { if (newLength < 0) throw new StringIndexOutOfBoundsException(newLength); ensureCapacityInternal(newLength); if (count < newLength) { Arrays.fill(value, count, newLength, '\0'); } count = newLength; }
//重头戏,扩容机制//minimumCapacity = count +length 是原字符串长度+插入或添加的字符串长度,注意是字符串长度不是字符数组长度private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) {//假如我们的参数长度大于未扩容前的字符数组长度,那么复制原字符数组,到一个指定长度newCapacity(minimumCapacity)的新字符数组 value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } }//对于字符串的长度,是返回的字符数组中有效的字符个数,而非字符长度 @Override public synchronized int length() { return count; }
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//这是扩容中的新字符数组创建,扩容的本质是创建一个新数组,然后将原来的数据添加到新数组,这里是确定新数组的长度 private int newCapacity(int minCapacity) { // overflow-conscious code int newCapacity = (value.length << 1) + 2;//长度=原长度*2+2 if (newCapacity - minCapacity < 0) {//假如新长度比原有字符串长度和新加字符串长度小,那么以后者为准 newCapacity = minCapacity; } //这里是要返回的新字符数组的长度,如果长度小于等于0,或者newCapacity比MAX_ARRAY_SIZE大,那么返回hugeCapacity(minCapacity),否则返回长度newCapacity
(一般返回后者,返回前者的都”不寻常”)
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) ? hugeCapacity(minCapacity) : newCapacity; }private int hugeCapacity(int minCapacity) { //如果原字符串和新字符串长度超过String的最大值,那么就内存溢出异常 if (Integer.MAX_VALUE - minCapacity < 0) { // overflow throw new OutOfMemoryError(); } //否则根据原字符串与新字符串的长度与 MAX_ARRAY_SIZE的判定决定返回的值 return (minCapacity > MAX_ARRAY_SIZE) ? minCapacity : MAX_ARRAY_SIZE; }
总结:StringBuffer的扩容实际上就是新建了一个数组,将原来旧数组的内容复制到新数组,扩容机制根据当前数组长度的2倍+2和新增加字符串长度+原有数组长度进行比较,如果前者小于后者,那么扩容后的长度就是后者,如果前者大于后者那么扩容后的数组长度就是前者,每次append或者insert会再次进行比较.
前面我们是对StringBuffer的扩容进行了讲解,StringBuilder和StringBuffer都是继承AbstractStringBuilder,所以扩容机制是一样的,这里不讲解了!