最近在看《How Tomcat Works》这本书,其中有这样一句代码:
public void parse() { // Read a set of characters from the socket StringBuffer request = new StringBuffer(2048); int i; byte[] buffer = new byte[2048];
从我会用StringBuffer开始,一直都是
StringBuffer sb = new StringBuffer(); sb.append("sb"); sb.append("another sb"); sb.append("只要用StringBuffer了就能提高性能了,管它呢");
关键点就在StringBuffer的构造函数里。
如果不传任何参数,capacity默认的值是16.
如果传一个int型的值,就把这个值赋给capacity.
如果传的是一个字符串,capacity的值就是 字符串的长度+16.
StringBuffer把capacity传给了它的父类AbstractStringBuilder,它的父类用capacity做了什么事?
/** * The value is used for character storage. */ char[] value; /** * The count is the number of characters used. */ int count;
AbstractStringBuilder(int capacity) { value = new char[capacity]; }
就是new了一个char型的数组。
******************************************************************************************************************************************************************
重点跟一下StringBuffer的append方法。 假设代码是 new StringBuffer("abc"); 看上图140行,在构造函数内部调用了append方法。
StringBuffer调用父类的append方法:
@Override public synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this; }
public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; }
注意count还没有被赋值过,此时count=0。 如果StringBuffer sb = new StringBuffer(); sb.append("sb"); 这种情况下count的值也是0. 只要是第一次调用append,count的值都是0。 假设我们传入的初始字符串是"abc"。 那么此处的count+len就是3。
private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) expandCapacity(minimumCapacity); }
3-19<0 所以不会调用expandCapacity(minimumCapacity);
但是当我们再次append一个长度为17的字符串时。 count+len=3+17=20。 这时就会调用expandCapacity(minimumCapacity);
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); }
可以看到,这个方法的目的是扩大载荷。
通俗点说,我有3个苹果,想买个篮子来装。 JDK根据我的实际情况,帮我做了一个能装19个苹果的篮子。 如果一开始我一个苹果都没有,JDK就会给我做一个能装16个苹果的篮子。
篮子里已经装了3个苹果了。现在我又有17个苹果了,这个篮子已经装不下了。
JDK试着将篮子的容量扩大为现有容量的2倍+2,如果能装完,就好得很。 如果还是装不完,就把容量改为 刚好能 容的下 已有 和 现有的苹果数量之和。
注意此处为什么老是判断小于0. 因为newCapacity = value.length * 2 + 2; minimumCapacity = count + len; 两个都是通过计算得到的。 int型的数如果太大就会溢出,溢出后的结果就是负数了。溢出时最高位是1,也就是符号位是1,可不就是负数嘛。
可以看到扩充载荷的时候有一个数组的拷贝动作。expandCapacity里有好几行代码呢。
总结:
如果在new StringBuffer的时候不传递一个字符串或者int型的值, 那么capacity的值将会是默认的16。以后append字符串的时候调用expandCapacity的几率比较大,这个方法里有好几行代码呢,而且还有数组的拷贝动作。 如果每次append的字符串长度都差不多,那么可以不传任何值。 但是如果append的字符串一次比一次长,突然某一次字符串的长度非常大时。这时capacity是这一次字符串的长度, 再append一次的话,capacity将会增加2倍。这个时候就很有可能出现浪费。
capacity调小了会频繁调用expandCapacity,调大了可能会出现浪费。
建议:
在使用StringBuffer的时候应该对最终字符串的长度有一个预判,然后传入capacity的值。这样就能避免浪费和频繁扩展capacity。 实在不行,capacity就以 某一次append的字符串长度最大的那个来算。