1. 程式人生 > >你真的會用StringBuffer嗎?

你真的會用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裡有好幾行程式碼呢。

文章是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 了。

文章說拼接字串和用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的字串長度最大的那個來算。