如何讓你的程序運行的更快(1)---StringVSStringBuffer

馬嘉楠 2006-09-14 

最開始給這篇文章起名的時候準備叫“如何讓你的程序運行的更快(1)之String StringBuffer誰與爭鋒!”,後來想想武俠的味道太濃了,有吸引眼球騙取點擊率的嫌疑,還是簡單一點的好,畢竟這是技術不是文學,需要嚴謹。你說呢?^+^

在總體的框架設計確定以後,多注意一些編程細節,積少成多,可以獲得更佳的性能,讓程序跑的更快!

前些天同事優化代碼時提到了String和StringBuffer。仔細想想,發現自己也就是知道個大概,所以查了 一下資料。文章中有錯誤的地方你可一定要告訴我啊,呵呵^+^

一.介紹

String:非可變類(immutable),一旦創建就不能再被改變。
StringBuffer:可變類,創建之後可以被改變。

何謂非可變類?
簡單說,非可變類的實例是不能被修改的,每個實例中包含的信息都必須在該實例創建的時候就提供出來
,並且在對象的整個生存週期內固定不變。

非可變類好處:狀態單一,對象簡單,便於維護;通常是線程安全的;用戶可以共享非可變對象,甚至可 以共享它們的內部信息


二.創建字符串

兩種方法:
1. String s1 = "hello";
2. String s2 = new String("hello");

哪種方式性能更好?
例1:

 1 //create String without "new" keyword
 2 long startTime1 =  System.currentTimeMillis();
 3 for(int i=0;i<100000;i++
){
 4        String str1  =   " hello "
;
 5 
}
 6 long endTime1 =
 System.currentTimeMillis();
 7 System.out.println("create String without 'new' keyword : " + (endTime1 - startTime1) + " milli seconds"
 );
 8 
     
 9 //create String with 'new' keyword       

10 long startTime2 =  System.currentTimeMillis();
11 for(int i=0;i<100000;i++
){
12       String str2 = new String("hello"
);
13 
}
14 long endTime2 =
 System.currentTimeMillis();
15 System.out.println("create String with 'new' keyword : " + (endTime2 - startTime2) + " milli seconds");


輸出結果爲(注:程序的輸出結果也許和你的結果不同,但是總體趨勢應該是一致的):

create String without  ' new '  keyword :  0  milli seconds
create String with 
' new '  keyword :  16  milli seconds

結論: 創建字符串變量時儘可能不使用new關鍵字
 
說明:
雖然兩個語句都是返回一個String對象的引用,但是jvm對兩者的處理方式是不一樣的。

對於第一種不用new關鍵字創建String對象:
JVM首先會在內部維護的滯留字符串中通過String的equels方法查找是對象池中是否存放有該
String對象。如果有,返回已有的String對象給用戶,而不會在heap中重新創建一個新的String對象;如 果對象池中沒有該String對象,JVM則在heap中創建新的String對象,將其引用返回給用戶,同時將該引 用添加至滯留字符串中。

對於第二種使用new關鍵字創建String對象:
JVM會馬上在heap中創建一個String對象,然後將該對象的引用返回給用戶。JVM是不會主動把該
對象放到滯留字符串裏面的,除非程序調用 String的intern()方法.


JVM爲字符串自變量維護一些唯一的String對象,程序員不需要爲字符串自變量而發愁。但是使用new關鍵 字可能會在內存中創建重複的String對象,你不必爲此而煩惱,intern()方法可以幫你解決問題。

String.intern():檢查字符串對象的存在性,如果需要的字符串對象已經存在,那麼它會將引用指向已 經存在的字符串對象而不是重新創建一個。

例2:

 

 

 1 String str3 = "world";      /*JVM在滯留字符串中找不到值爲“world”的字符串,就在堆上創建一個string對象,並將該對象的引用加入到滯留字符串中*/
 2 
 3 String str4 = new String("world");      /*JVM在堆上重新創建一個值爲“world”的字符串,此時堆上有兩個值爲“world”的字符串*/
 4 if(str3 ==  str4){
 5       System.out.println("str3 == str4"
);
 6 
}
 7 else
{
 8       System.out.println("str3 != str4"
);
 9 
}
10 //輸出:str3 != str4

11 
12 String str5 = "world";      /*JVM在發現滯留字符串中存在“world”對象,因此返回str3指向的對象給str5,即str3和str5是指向同一個對象的引用*/
13 
14 if(str3 ==  str5){
15       System.out.println("str3 == str5"
);
16 
}
17 else
{
18       System.out.println("str3 != str5"
);
19 
}
20 //輸出:str3 == str5     

21 
22 str4 = str4.intern();      /*此時,JVM發現滯留字符串中已經存在“world”對象,因此返回str3指向的對象給str4,即str3和str4是指向同一個對象的引用*/
23       
24 if(str3 ==
 str4){
25       System.out.println("after intern() str3 == str4"
);
26 
}
27 else
{
28       System.out.println("after intern() str3 != str4"
);
29 
}
30 //輸出:after intern() str3 == str4

31 
32 

結論: 如果使用new關鍵字創建了字符串變量,則儘可能使用intern()方法。

上面的例子執行正是用到了string對象的不可變性質。既然string對象一旦創建就不可以改變,那麼多個 引用指向同一個對象就不會對彼此產生影響。


三.字符串連接

  你可以使用+操作符或者String.concat()或者StringBuffer.append()等辦法來連接多個字符串,哪 一種方法性能最佳?

如何選擇取決於兩種情景:
第一種情景:需要連接的字符串是在編譯期間決定的,還是在運行期間決定。
第二種情景:你使用的是
StringBuffer還是String。

通常程序員會認爲StringBuffer.append()方法會優於+操作符或 String.concat()方法,但是在一些特定 的情況下這個假想是不成立的。
 

1) 第一種情景:編譯期間決定VS運行期間決定

 

 1 //test the string Concatenation
 2 long startTime6 =  System.currentTimeMillis();
 3 for(int k=0; k<100000; k++
){
 4       String str6 = "this is " + "a test " + "for string concatenation"
;
 5 
}
 6 long endTime6 =
 System.currentTimeMillis();
 7 System.out.println("string concatenation using '+' : " + (endTime6 - startTime6) + " milli seconds"
);
 8 
  
 9 long startTime7 =
 System.currentTimeMillis();
10 for(int k=0; k<100000; k++
){
11       String str7 = "this is "
;
12       str7.concat("a test "
);
13       str7.concat("for string concatenation"
);
14 
}
15 long endTime7 =
 System.currentTimeMillis();
16 System.out.println("string concatenation using concat() : " + (endTime7 - startTime7) + " milli seconds"
);
17 

18 long startTime8 =  System.currentTimeMillis();
19 for(int l=0; l<100000; l++
){
20       StringBuffer sb8 = new
 StringBuffer();
21       sb8.append("this is "
);
22       sb8.append("a test "
);
23       sb8.append("for string concatenation"
);
24 
}
25 long endTime8 =
 System.currentTimeMillis();
26 System.out.println("string concatenation using append() : " + (endTime8 - startTime8) + " milli seconds"
);

上面代碼的輸出結果:

string concatenation using  ' + '  :  0  milli seconds
string concatenation using concat() : 
31
 milli seconds
string concatenation using append() : 
62  milli seconds

很有趣,+操作符比StringBuffer.append()方法要快. Why?
 
這是因爲編譯器對簡單的字符串進行了優化。即使使用new關鍵字來創建String對象的時候也是如此。例
如,

編譯前:
String str6 = "this is " + "a test " + "for string concatenation";
編譯後:
String str6 = "this is a test for string concatenation";

這裏String對象在編譯期間就決定了而StringBuffer對象是在運行期間決定的。運行期間決定需要額外的 開銷。

結論: 如果字符串在編譯期間就可以決定它的值,則字符串拼接的時候, “+”操作符效率更高,簡單的 認爲append()效率高於“+”是錯誤的。
 

2) 第二種情景:StringBufferVS
String

 1 //string concatenation using '+='
 2 long startTime9 =  System.currentTimeMillis();
 3 String str9 = "hello"
;
 4 for(int i=0; i<10000; i++
){
 5       str9 += "hello"
;
 6 
}
 7 long endTime9 =
 System.currentTimeMillis();
 8 System.out.println("string concatenation using '+=' : " + (endTime9 - startTime9) + " milli seconds"
);
 9 
  
10 //string concatenation using append()

11 long startTime10 =  System.currentTimeMillis();
12 StringBuffer sb10 = new StringBuffer("hello"
);
13 for(int i=0; i<10000; i++
){
14       sb10.append("hello"
);
15 
}
16 long endTime10 =
 System.currentTimeMillis();
17 System.out.println("string concatenation using append() : " + (endTime10 - startTime10) + " milli seconds"
);


上面代碼的輸出結果:

string concatenation using  ' += '  :  3094  milli seconds
string concatenation using append() : 
16  milli seconds

 

結論: 避免使用“+=”來構造字符串

雖然兩者都是在運行期間決定字符串對象,但是使用+=操作符會產生更多的臨時對象。

在上例中,由於String類是不可變的,所以進行字符串拼接的時候,每循環一次就會產生臨時對象來保存 str9和“hello”的值,之後創建一個臨時的StringBuffer對象,並調用其append()方法來完成字符串的 拼接,最後調用toString()方法,將臨時StringBuffer對象轉爲String再賦值給str9。此時str9已經改變 ,指向了新的對象。


3) 第三種情景:設置StringBuffer的容量來提升性能

 

 1 long startTime10 =  System.currentTimeMillis();
 2 StringBuffer sb10 = new StringBuffer("hello"
);
 3 for(int i=0; i<10000; i++
){
 4       sb10.append("hello"
);
 5 
}
 6 long endTime10 =
 System.currentTimeMillis();
 7 System.out.println("string concatenation using append() : " + (endTime10 - startTime10) + " milli seconds"
);
 8 
  
 9 //set the StringBuffer capacity

10 long startTime11 =  System.currentTimeMillis();
11 StringBuffer sb11 = new StringBuffer("hello"
);
12 sb11.ensureCapacity(10000
);
13 for(int i=0; i<10000; i++
){
14       sb11.append("hello"
);
15 
}
16 long endTime11 =
 System.currentTimeMillis();
17 System.out.println("string concatenation using append() after set the StringBuffer capacity : " + (endTime11 - startTime11) + " milli seconds");


  上面代碼的輸出結果:

 

string concatenation using append() :  16  milli seconds
string concatenation using append() after set the StringBuffer capacity : 
0  milli seconds

結論:
聲明StringBuffer對象的時候,指定合適的capacity,會提升程序性能。

1)使用StringBuffer的構造函數來設定它的初始化容量:StringBuffer(int length)
2)使用ensureCapacity(int minimumcapacity)方法在StringBuffer對象創建之後設置它的容量。

首先我們看看StringBuffer的缺省行爲,然後再找出一條更好的提升性能的途徑。
 
StringBuffer的缺省行爲:
  StringBuffer在內部維護一個字符數組,當你使用缺省的構造函數來創建StringBuffer對象的時候,
StringBuffer的容量被初始化爲16個字符,也就是說缺省容量就是16個字符。當StringBuffer達到最大容 量的時候,它會將自身容量增加到當前的2倍再加2,也就是(2*舊值+2)。
  如果你使用缺省值,初始化之後接着往裏面追加字符,在你追加到第17(原文是16,其實是錯誤的,因爲在追加到第16個字符的時候,容量不會發生變化,很抱歉,以後會更嚴謹一些^+^)個字符的時候它會將容量增加
到34(2*16+2),當追加到34個字符的時候就會將容量增加到70(2*34+2)。無論何事只要StringBuffer 到達它的最大容量它就不得不創建一個新的字符數組然後重新將舊字符和新字符都拷貝一遍。所以給 StringBuffer設置一個合理的初始化容量值,會提升程序的性能。

但是爲什麼容量變化的時候是2*舊值+2呢?有誰能告訴我麼?查資料的時候沒有找到

 

附:查資料的過程中發現了jdk 5.0還提供了StringBuilder類,我是沒有用過。不過也介紹一下好了。( 來源 JavaWorld ):

       Java.lang.StringBuffer 線程安全的可變字符序列。類似於 String 的字符串緩衝區,但不能修 改。可將字符串緩衝區安全地用於多個線程。可以在必要時對這些方法進行同步,因此任意特定實例上的 所有操作就好像是以串行順序發生的,該順序與所涉及的每個線程進行的方法調用順序一致。

       每個字符串緩衝區都有一定的容量。只要字符串緩衝區所包含的字符序列的長度沒有超出此容量 ,就無需分配新的內部緩衝區數組。如果內部緩衝區溢出,則此容量自動增大。從 JDK 5.0 開始,爲該 類增添了一個單個線程使用的等價類,即 StringBuilder 。與該類相比,通常應該優先使用 StringBuilder 類,因爲它支持所有相同的操作,但由於它不執行同步,所以速度更快。 但是如果將 StringBuilder 的實例用於多個線程是不安全的。需要這樣的同步,則建議使用 StringBuffer 。  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章