你真的會用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的字串長度最大的那個來算。