茫茫網海中的冷日
         
茫茫網海中的冷日
發生過的事,不可能遺忘,只是想不起來而已!
 恭喜您是本站第 1728844 位訪客!  登入  | 註冊
主選單

Google 自訂搜尋

Goole 廣告

隨機相片
FF18_Cosplayer_00019.jpg

授權條款

使用者登入
使用者名稱:

密碼:


忘了密碼?

現在就註冊!

爪哇咖啡屋 : [分享]Java 5.0新增的StringBuilder類別

發表者 討論內容
冷日
(冷日)
Webmaster
  • 註冊日: 2008/2/19
  • 來自:
  • 發表數: 15771
[分享]Java 5.0新增的StringBuilder類別
爪哇教室 >> Java 5.0新增的StringBuilder類別 (吳宇澤)
--------------------------------------------------------

致理技術學院資管系學生 吳宇澤 j2se@pchome.com.tw


常在Java週報上看到一些業界高手所發表的文章,今天我這個還在學的小毛頭斗膽向大家分享自己的一些學習心得,若有謬誤還望見諒並指正,並歡迎來函交流。

開發人員在撰寫網頁應用程式時,常會用到字串以及字串連接(concatenation)作為SQL指令以便去資料庫裡撈資料,因此本文將簡單探討增進字串連接效能的技巧。另外因為Java和.NET都有一相同名稱與功能的StringBuffer類別,因此本文一開始也會提到.NET中的字串連接相關議題,但並不做兩方效能上的比較。

不論是Java或.NET的書籍或文件,在提到字串連接時都會闡述一個概念:配置多個字串、重複修改字串是很消耗資源的動作。因為兩方陣營的型別系統都分為兩大類,亦即reference type(在.NET和Java中名稱相同),和primitive type(.NET叫做valuetype)。包括整數、浮點數、Decimal、字元、布林值都屬於primitive type;而字串、陣列、類別則屬於reference type。兩者在記憶體中的處理方式有很大的不同。至於為什麼會分成這兩大類,主要原因在於執行效能的考量,其原理相信會訂閱Java週報的高手都已經瞭解了,本文這裡只再稍微提一下。primitive type變數會直接存放在Stack(堆疊) 中,而reference type變數在Stack或Heap(堆積)中存放的則是指向實際物件的指標(reference),而實際的物件會建立在Heap的記憶體中,由指標指向實際位址。因此總的來說,primitive type在記憶體的使用以及存取的效能會比reference type來得好,因為後者必須多進行一次的記憶體參考動作才可取得資料,且其物件必須配置多餘的記憶體來存放函數指標,以及執行緒同步區塊(threadsynchronization block),對於記憶體的需求較大。

由上述的說明,我們瞭解大量的字串連接動作會產生兩點影響,第一是不斷地建立新的String物件,而且不斷地在Heap中取得新的記憶體空間的動作,會耗用CPU和大量記憶體;第二則是有許多記憶體空間是取得後立即棄置的,雖然GC會有效地回收這些空間,但回收動作本身也會耗用少許CPU而影響到系統整體效能。因此,當要在程式中進行多個字串連接的動作時,應該避免使用早期開發人員慣用的java.lang.String類別(或.NET的System.String類別),而改用java.lang.StringBuffer類別(或.NET的System.Text命名空間中的StringBuilder類別),因為StringBuffer可以修改一個字串而不需要建立一個新的物件,因此可大幅提升應用程式效能。不過上述的觀念,小弟我相信大家都已經瞭到不能再瞭了。但至於其間的效能差異有多少呢?本文接下來要分別在.NET和Java平台上做些小實驗。

接下來的實驗我們先在.NET平台上測試。我用VB.NET的追蹤(trace)功能、和微軟的壓力測試工具ACT(內建在Visual Studio.NET企業架構版之中且為預設安裝)來測試兩個ASP.NET的範例,分別以「String 的 += 」和「StringBuilder 的 append() 方法」重複串連「ABC」這一個字串,將其放在迴圈中重複跑一萬次,以比較兩者之間的效能差異。ACT模擬的「瀏覽器同時連線數」設定為5個。壓力測試工具當中的5代表執行緒,而非真實使用者。因為其為由client端的瀏覽器對server端發出大量的request施壓,以模擬系統真實上線時的情形。其為每一秒都不斷地發出要求,而若是真人使用者的話一定大部份時間都為閒置。因此在一般效能測試工具上模擬的5個執行緒,至少可代表50以上的真人使用者在線上操作的情形。

執行結果,兩者在效能上產生相當驚人的差距。在數據結果方面,以trace追蹤功能所測得的程式執行時間,String花了0.874秒,而StringBuilder僅花了0.001秒。而且StringBuilder在實驗過程的CPU使用率相當低,我們發現在內容長度相差無幾的情況下,兩者的request數目(數值越大代表在相同時間內所能處理的要求數量越多,效能也越好)和等待回應平均時間都有相當驚人的差距,頁面效能居然相差達到將近86倍之多,這也意味著以Java的StringBuffer或.NET的StringBuilder取代過去的字串連接演算法絕對是划算的投資。

由實驗的結果可推知,配置多個字串、重複修改字串是很消耗資源的動作。如果只做一個字串連接或運作,則使用String即可。然而若要進行多個字串的新增、連接、修改、插入,甚至只是刪除,則務必改用Java的StringBuffer或.NET的StringBuilder類別。其可避免因為字串解體與建立程序所造成的系統資源消耗,除了CPU使用率較低以外,用於記憶體回收的時間也相當少。至於其運作原理,為StringBuffer物件包含了一個用來做字串初始化的「緩衝區」(buffer),這個字串緩衝區可以當下做一些管理,而不需要每次修改字串時都再重新建立一個新的字串,亦即當字串的長度或內容改變時,StringBuffer每次都使用相同的字串緩衝區,而傳統的String連接方式卻會為每一項串連作業建立一個新字串,也因此造成兩者在效能和回應時間上的大幅差距。上述原理在不論在Java或.NET中亦然,有興趣的讀者可試著改用http://jakarta.apache.org/網站上的一套JMeter來測試相同的Java或JSP程式,在此筆者就不做兩方效能上的比較。而接下來我們才要進入本文的主題,回頭來測試Java的字串緩衝功能。

在本文一開始有提到Java 1.4.x或更早的JDK版本,也有提供一個相同字串緩衝功能的類別,名為StringBuffer,其使用方式和.NET的StringBuilder幾乎相同,甚至連一些如append()等method的名稱也相同。不過新版的J2SE 5.0不但保留StringBuffer類別,還新增了一個也叫做StringBuilder的類別,好像擺明要蓋過另一陣營。

Java 1.4或之前版本的StringBuffer,和5.0版的StringBuffer、StringBuilder都是直接繼承自java.lang.Object類別。它們的作用如剛才提到的,都是為了避免字串的連接或修改時產生許多的暫存字串(temporary strings)。但在J2SE Documentaion 1.4.2、5.0中都提到,StringBuffer在多執行緒(multiple threads)下是安全的(thread-safe),亦即該類別下的method會視需要鎖定,或稱為同步化(synchronized)資料處理,亦即保護程式碼不會被分割執行,多個thread物件會照應有順序交互執行,避免執行結果的不一致。亦即一次只有一個thread可以修改StringBuffer實體的狀態,無論你在何時修改StringBuffer,都不用擔心會有其它的執行緒出現,中斷你正在執行的工作,而毀損了字串資料[1],[4]:

String buffers are safe for use by multiple threads. The methods are synchronized where necessary so that all the operations on any particular instance behave as if they occur in some serial order that is consistent with the order of the method calls made by each of the individual threads involved.

而Java 5.0新推出的StringBuilder可與StringBuffer相容,但不應用在同步(synchronization)或多重執行緒的情況。但據Documentation和某書[2]的說法,若是在單一執行緒的環境下,或一段你不擔心會有多個執行緒存取的程式碼中,或是不需要同步處理的情況下,就應該儘可能地以新的StringBuilder替換StringBuffer,因為在大多數的情況下,前者的運作速度都比後者要快。且若非synchronized的話,應該也可以省去一些overhead。且聽說過去在StringBuffer上看過的method在StringBuilder上都有,所以直接對舊有的程式碼作搜尋與替換,原則上不會有編譯上的問題[2]。

以下我實際撰碼[3],測試不同版本的JDK和各種類別的效能差距。測試主機硬體配備為AMD Athlon 1.09GHz、1024 MB DDR記憶體。
 01 public class Test1 {
 02   public static void main(String xxx[]) {
 03     String x = "";
 04 
 05     long startTime = System.currentTimeMillis();
 06     long startMem = Runtime.getRuntime().freeMemory();
 07 
 08     for(int i=0; i<1000 ; i++)
 09       x += "Java" + i + "dotNet";
 10 
 11     long endMem = Runtime.getRuntime().freeMemory();
 12     System.err.println("使用記憶體(Bytes): " + (startMem - endMem));
 13     long endTime = System.currentTimeMillis();
 14     System.err.println("使用時間(毫秒): " + (endTime - startTime));
 15   }
 16 }

第一項測試,先以JDK 1.4.1測試,用迴圈讓一個字串x重複連接兩個字串及一個數字1,000次,結果耗費記憶體約828,336 Bytes,花費時間約125-141毫秒,但這種寫法事實上很沒有效率。若我們寫碼時真的有需要將primitive type轉換成String,我們應該使用java.lang.Object的toString()方法或去override它,或幹脆使用String類別內建的String.valueOf(i)這個參數多載的方法。有興趣的讀者可再自行測試這幾種轉換方式在效能上的差異。

第二項測試,我們將第9行的整數i加上雙引號,改成讓字串x和三個字串連接1,000次:
 09       x += "Java" + "i" + "dotNet";

結果耗費記憶體大幅縮減為35,128 Bytes,花費時間也減為93-110毫秒。兩項測試在編譯時都加上了-verbose參數,在編譯過程中所loading的class也都相同。我們發現在第一項測試中,讓int和String型別的變數做轉換的動作相當消耗系統資源,因為Java的整數為primitive type,而Java的字串是物件,亦即為reference type,只是透過complier反而使得String物件看起來很像primitive type。且測試範例中還牽扯到「執行時期」的自動轉型(promotion),但這不同於Java 5.0新增的auto-boxing轉換機制,因為boxing是特指將primitive轉換到「對應的」wrapper物件型別:Byte、Short、Integer、Long、……。

第三項測試,將測試二的程式碼改用JDK 5.0 Update1做同樣的動作,在編譯過程中除了像1.4.1版會loading String、StringBuffer等class,也會另外loading許多額外的像是StringBuilder、AbstractStringBuilder等class。執行結果,耗費記憶體大幅增加至222,376 Bytes,花費時間也增加至156-172毫秒。我們發現單就這個測試範例來看,JDK 5.0表現出來的效能反而不如JDK 1.4.1。

第四項測試,將分別測試新版和舊版JDK的StringBuffer效能,程式碼如下。主要是將測試二、測試三範例中,String的串接改成以StringBuffer來實作。
 01 public class Test4 {
 02   public static void main(String xxx[]) {
 03   StringBuffer x = new StringBuffer();
 04 
 05   long startTime = System.currentTimeMillis();
 06   long startMem = Runtime.getRuntime().freeMemory();
 07 
 08   for(int i=0; i<1000 ; i++)
 09     x = x.append("Java").append("i").append("dotNet");
 10 
 11   long endMem = Runtime.getRuntime().freeMemory();
 12   System.err.println("使用記憶體(Bytes): " + (startMem - endMem));
 13   long endTime = System.currentTimeMillis();
 14   System.err.println("使用時間(毫秒): " + (endTime - startTime));
 15   }
 16 }

測試結果,JDK 1.4.1耗費記憶體73,736 Bytes,花費時間15-16毫秒。JDK 5.0 Update1耗費記憶體72,624 Bytes,花費時間15-16毫秒。證實不論新舊版本的JDK,使用StringBuffer連接字串的效能,都遠遠高於使用String來連接。而新版的StringBuffer比舊版的在節省記憶體資源方面也僅略勝一籌。常在網頁或應用程式中,用SQL指令去資料庫撈資料的人必須注意這種字串變數修改、連接的觀念,不論是用Java或某陣營的技術都一樣。

第五項測試,我們來測試JDK 5.0新增的StringBuilder。測試程式碼與實驗四雷同,僅將第3行的StringBuffer改成StringBuilder。結果實驗數據和第四項實驗,測試StringBuffer的數據一模一樣,仍為72,624 Bytes、15-16毫秒。即使我們把迴圈的執行次數加大到10,000次,兩種類別的測試數據依然都相同。不知道JDK 5.0 Documentation中及某本書[2]上宣稱的StringBuilder效能較StringBuffer快的論點:Where possible, it is recommended that this class(StringBuilder) be used in preference to StringBuffer as it will be faster under most implementations.,是否是以不同的觀點、方式或其它專業的效能測試工具做測試。

總之,若不需顧慮multiple threads和synchronized的問題,則可儘量使用StringBuilder類別,但其相較於舊有StringBuffer所能帶來的效能增益,小弟我暫時持保留態度。而若是我在實驗上或寫碼測試觀念有所謬誤,也歡迎各位來函指正。此外,不論是新版的StringBuilder,或新、舊版的StringBuffer都有容量上的限制,不過程式員無需手動去控制buffer array,如果內部將產生buffer overflow時,它們都會自動加大容量[4]。而本文所做的五項測試,若讀者有興趣的話也可改用另一陣營的語法和編譯器測試看看,如果各位剛過完年太無聊的話。
前一個主題 | 下一個主題 | 頁首 | | |



Powered by XOOPS 2.0 © 2001-2008 The XOOPS Project|