你真的会用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的字符串长度最大的那个来算。 

时间: 2024-10-16 05:55:27

你真的会用StringBuffer吗?的相关文章

Java 6中的线程优化真的有效么?

介绍 - Java 6中的线程优化 Sun.IBM.BEA和其他公司在各自实现的Java 6虚拟机上都花费了大量的精力 优化锁的管理和同步.诸如偏向锁(biased locking).锁粗化(lock coarsening).由逸出(escape)分析产生的锁省略.自适应自旋锁(adaptive spinning)这些特性,都是通过在应用程序线程之间更高效地共享数据,从而提 高并发效率.尽管这些特性都是成熟且有趣的,但是问题在于:它们的承诺真的 能实现么?在这篇由两部分组成的文章里,我将逐一探究

StringBuffer使用append提示String concatenation as argument to &amp;#39;StringBuffer.append()&amp;#39; call

昨天发现一个IDE提示: String concatenation as argument to 'StringBuffer.append()' call less... (Ctrl+F1) Reports String concatenation used as the argument to StringBuffer.append(),StringBuilder.append() orAppendable.append(). Such calls may profitably be turn

详细分析Java中String、StringBuffer、StringBuilder类的性能_java

我们先要记住三者的特征: String 字符串常量 StringBuffer 字符串变量(线程安全) StringBuilder 字符串变量(非线程安全) 一.定义查看API会发现,String.StringBuffer.StringBuilder都实现了 CharSequence接口,虽然它们都与字符串相关,但是其处理机制不同. String:是不可改变的量,也就是创建后就不能在修改了. StringBuffer:是一个可变字符串序列,它与String一样,在内存中保存的都是一个有序的字符串序

你真的了解:IIS连接数、IIS并发连接数、IIS最大并发工作线程数、应用程序池的队列长度、应用程序池的最大工作进程数 吗?

原文:你真的了解:IIS连接数.IIS并发连接数.IIS最大并发工作线程数.应用程序池的队列长度.应用程序池的最大工作进程数 吗? IIS连接数   一般购买过虚拟主机的朋友都熟悉购买时,会限制IIS连接数,这边先从普通不懂代码用户角度理解IIS连接数 顾名思义即为IIS服务器可以同时容纳客户请求的最高连接数,准确的说应该叫"IIS限制连接数" 这边客户请求的连接内容包括: 1.网站html请求,html中的图片资源,html中的脚本资源,其他需要连接下载的资源等等,任何一个资源的请求

41岁阿里工程师:35岁转管理,真的是必经之路吗?

今年的程序员节,也恰恰是我在阿里工作满3年的时候,借此机会盘点一下自己近3年来的工作,也为自己后续发展把把关.个人的眼界和思考总是有限的,特别是对于研究和技术领域来说,知道得越多,其实就会知道自己有多无知,从而对未知心生敬畏,并因未知的广阔而兴奋. 我是1976年生人,属龙,今年41岁,所以可以算是老程序员了,15年前我读研的时候,就被一起创业的小伙伴称为老何了.我对写代码确实喜欢,大概在96年,大三的时候拿到了高级程序员证书,算是一桩可以拿来吹的事. 博士毕业工作以来,最大的乐趣就是学习和深入

Java中利用final关键字inline编译优化真的有效吗?

(inkfish原创,转载请注明出处:http://blog.csdn.net/inkfish/) 为寻求java代码的性能优化,从网上搜到利用final关键字进行编译时inline优化的方法,但是真的有效吗?实际测试中发现未必,甚至性能影响巨大,最终放弃了使用final优化的想法. 测试环境:Windows XP SP2,JDK 1.6.0_15-b03,Eclipse 3.5 SR1.   package test; public class Test { public static voi

工厂方法真的支持OCP 开闭原则吗?

问题描述 工厂方法真的支持OCP 开闭原则吗? 开闭原则:我们在设计一个模块的时候应当使这个模块可以在不被修改的前提下被扩展换句话说就是应当可以在不必修改源代码的情况下改变这个模块的行为.工厂方法增加新的方法类的时候,不是要修改接口.然后再修改所有的相关类么.这岂不是违背了开闭原则 解决方案 工厂什么的都只是假象,利用反射可以

你真的了解iOS代理设计模式吗?

本文是投稿文章,作者:刘小壮 在项目中我们经常会用到代理的设计模式,这是iOS中一种消息传递的方式,也可以通过这种方式来传递一些参数.这篇文章会涵盖代理的使用技巧和原理,以及代理的内存管理等方面的知识.我会通过这些方面的知识,带大家真正领略代理的奥妙.写的有点多,但都是干货,我能写下去,不知道你有没有耐心看下去.本人能力有限,如果文章中有什么问题或没有讲到的点,请帮忙指出,十分感谢! iOS中消息传递方式 在iOS中有很多种消息传递方式,这里先简单介绍一下各种消息传递方式. 通知:在iOS中由通

c3p0连接池中获取的Connection对象的close()方法是真的把连接给关闭了?

问题描述 c3p0连接池中获取的Connection对象的close()方法是真的把连接给关闭了? 自己做写了一个管理数据源的DBManager,构想中从数据源里面获取的Connection使用完之后执行close()方法,然后把Connection对象闲置回连接池中. 但是测试出来的结果好像每次执行close()之后connection就销毁了. 代码如下: DBManager.java package ben.DBUtils; import java.sql.Connection; impo