打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
你真的会用StringBuffer吗?

最近在看《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个苹果,想买个篮子来装。 StringBuffer根据我的实际情况,帮我做了一个能装19个苹果的篮子。 如果一开始我一个苹果都没有,StringBuffer就会给我做一个能装16个苹果的篮子。 

篮子里已经装了3个苹果了。现在我又有17个苹果了,这个篮子已经装不下了。 

StringBuffer试着将篮子的容量扩大为现有容量的2倍 2,如果能装完,就好得很。 如果还是装不完,就把容量改为 刚好能 容的下 已有 和 现有的苹果数量之和。


注意此处为什么老是判断小于0.   因为newCapacity = value.length * 2 2;    minimumCapacity = count len; 两个都是通过计算得到的。 int型的数如果太大就会溢出,溢出后的结果就是负数了。溢出时最高位是1,也就是符号位是1,可不就是负数嘛。


可以看到扩充载荷的时候有一个数组的拷贝动作。expandCapacity里有好几行代码呢。


写到这里您可能不信,我偶然在网上看到这篇文章Java性能陷阱:StringBuffer的性能真的比String好吗?

文章是2007年的,比较老,我不知道以前的jdk的StringBuffer有没有有参构造函数,我假设是有的。且不说他的测试方法是否合理。我现在就用他的代码和测试方法,大家看看对比结果。

他的环境是jdk1.2/jdk1.3,linux。 我的环境是 jdk1.8 win7 x64 eclipse。


package pyrmont;public class TestStringBuffer { public static void main(String[] args) { String s1 = 'This is a sssssssssssssssssss'; String s2 = 'long test string for '; String s3 = 'different JDK performance sssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss'; String s4 = 'testing.'; long start = System.currentTimeMillis(); for (int i = 0; i < 10000; i ) { String s = s1 s2 s3 s4; } long end = System.currentTimeMillis(); System.out.println('Directly string contact:' (end - start)); start = System.currentTimeMillis(); for (int i = 0; i < 10000; i ) { StringBuffer buffer = new StringBuffer(); buffer.append(s1); buffer.append(s2); buffer.append(s3); buffer.append(s4); String ss = buffer.toString(); } end = System.currentTimeMillis(); System.out.println('StringBuffer contact:' (end - start)); start = System.currentTimeMillis(); for (int i = 0; i < 10000; i ) { StringBuffer buffer = new StringBuffer(400); buffer.append(s1); buffer.append(s2); buffer.append(s3); buffer.append(s4); String sss = buffer.toString(); } end = System.currentTimeMillis(); System.out.println('StringBuffer contact:' (end - start)); }}


Ctrl F11多运行几次:

Directly string contact:9
StringBuffer contact:9
StringBuffer contact:4

-----------------------------------

Directly string contact:9
StringBuffer contact:18
StringBuffer contact:3

------------------------------------

Directly string contact:9
StringBuffer contact:10
StringBuffer contact:3

------------------------------------

Directly string contact:10
StringBuffer contact:10
StringBuffer contact:5

------------------------------------

Directly string contact:9
StringBuffer contact:8
StringBuffer contact:4

-------------------------------------

Directly string contact:10
StringBuffer contact:10
StringBuffer contact:3

可以看到StringBuffer传一个合适的capacity是多么的重要。 字符串里的'sssssssssssssssssssssss'是我故意加的。加这个的目的是为了说明如何传合适的capacity.

去掉“ssssssssssssssssssssssssssss'后,最长的单个字符串大概是25。 预判一下字符串的总长度大概是100。所以capacity传100就差不多了。

就算把循环的次数减小到1000,,500。 多运行几次,可以看到StringBuffer也绝对不会比直接拼接字符串慢!

当然这里也并不十分准确,因为StringBuffer是线程安全的,实际运行中可能会有些变数。 就拿上面的测试代码来说,如果改成StringBuilder的话,最好的情况下传capacity比不传capacity快4倍。StringBuffer有时也能达到4倍,但是次数比StringBuilder少一些。 两者基本上都稳定在2倍以上。

也许你会说这也没有多大优势啊。如果拼接字符串是几百毫秒,用StringBuffer是十几秒或者几秒那才叫优势呢。这么说也有道理,但是一个大的工程里有很多代码,每段代码都能快出几毫秒,累积起来也许能达到1s甚至更多。 1ms 对计算机来说是什么概念,更别说 1s 了。


谷歌上搜到的一篇文章,用的是jdk1.2。  String Buffer Example

文章说拼接字符串和用StringBuffer生成的字节码几乎一模一样。  拼接字符串不是为每个字符串生成一个String对象,而是为每个字符串生成一个StringBuffer对象。

在for循环里拼接字符串,时间复杂度就是O(n^2),因为每生成一个StringBuffer对象就会创建一个默认的buffer。然后将字符串拷贝到buffer里,n次循环*n次拷贝。而用StringBuffer的话,因为buffer是倍增的,所以时间复杂度是O(nlgn), n次循环*lgn次拷贝。

总结:

如果在new StringBuffer的时候不传递一个字符串或者int型的值, 那么capacity的值将会是默认的16。以后append字符串的时候调用expandCapacity的几率比较大,这个方法里有好几行代码呢,而且还有数组的拷贝动作。 

1)如果每次append的字符串长度都差不多,这样capacity的大小会一直慢慢变大,这意味着频繁调用了expandCapacity。 

2)如果append的字符串一次比一次长,突然某一次字符串的长度非常大时。capacity的大小就是目前字符串的总长度, 再append一次的话,capacity将会增加2倍。这个时候就很会出现浪费了。

一句话:capacity调小了会频繁调用expandCapacity,调大了可能会出现浪费。 虽然StringBuffer的容量是指数级增长的,已经尽了最大努力,但是我们程序员没有尽最大的努力。


建议:

在使用StringBuffer的时候应该对最终字符串的长度有一个预判,然后传入capacity的值。这样就能避免浪费和频繁扩展capacity。 实在不好判断,capacity就以 某一次append的字符串长度最大的那个来算。 

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Java:String和StringBuffer的区别
StringBuffer类的使用方法
Java 字符串拼接效率分析及最佳实践
String、StringBuffer和StringBuilder类的区别
十六进制转String字符串
StringBuffer的内存溢出实例(转自Ahuaxuan的文章)
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服