成大事不在於力量多少,而在能堅持多久。
阿新 • • 發佈:2019-02-01
可供程式利用的資源(記憶體、CPU時間、網路頻寬等)是有限的,優化的目的就是讓程式用盡可能
少的資源完成預定的任務。優化通常包含兩方面的內容:減小程式碼的體積,提高程式碼的執行率。
實現優化的方式我想不外乎兩種:一是對資料庫操縱語言的優化,二是提高程式碼的效率。當然啦還包括一些高階的技術,如運用分散式技術等等,這裡就不提及了。
對資料庫操縱語言的優化請看本部落格的資料庫優化篇。
現在來討論如何提高程式碼的效率。
在 Java程式中,效能問題的大部分原因並不在於Java語言,而是在於程式本身。養成好的程式碼編
寫習慣非常重要,比如正確地、巧妙地運用 java.lang.String類和java.util.Vector類,它能夠
顯著地提高程式的效能。下面我們就來具體地分析一下這方面的問題。
1、儘量指定類的final修飾符帶有final修飾符的類是不可派生的。在Java核心API中,有許多應用
final的例子,例如 java.lang.String。為String類指定final防止了人們覆蓋length()方法。另
外,如果指定一個類為final,則該類所有的方法都是final。Java編譯器會尋找機會內聯(inline
)所有的final方法(這和具體的編譯器實現有關)。此舉能夠使效能平均提高 50% 。
2、量重用物件。特別是String 物件的使用中,出現字串連線情況時應用StringBuffer 代替。
由於系統不僅要花時間生成物件,以後可能還需花時間對這些物件進行垃圾回收和處理。因此,生
成過多的物件將會給程式的效能帶來很大的影響。
3、儘量使用區域性變數,呼叫方法時傳遞的引數以及在呼叫中建立的臨時變數都儲存在棧(Stack)
中,速度較快。其他變數,如靜態變數、例項變數等,都在堆(Heap)中建立,速度較慢。另外,
依賴於具體的編譯器/JVM,區域性變數還可能得到進一步優化。請參見《儘可能使用堆疊變數》。
4、不要重複初始化變數 預設情況下,呼叫類的建構函式時, Java會把變數初始化成確定的值:
所有的物件被設定成null,整數變數(byte、 short、int、long)設定成0,float和double變數
設定成0.0,邏輯值設定成false。當一個類從另一個類派生時,這一點尤其應該注意,因為用new
關鍵詞建立一個物件時,建構函式鏈中的所有建構函式都會被自動呼叫。
5、在JAVA + ORACLE 的應用系統開發中,java中內嵌的SQL語句儘量使用大寫的形式,以減輕
ORACLE解析器的解析負擔。
6、Java 程式設計過程中,進行資料庫連線、I/O流操作時務必小心,在使用完畢後,及時關閉以釋放
資源。因為對這些大物件的操作會造成系統大的開銷,稍有不慎,會導致嚴重的後果。
7、由於JVM的有其自身的GC機制,不需要程式開發者的過多考慮,從一定程度上減輕了開發者負擔
,但同時也遺漏了隱患,過分的建立物件會消耗系統的大量記憶體,嚴重時會導致記憶體洩露,因此,
保證過期物件的及時回收具有重要意義。JVM回收垃圾的條件是:物件不在被引用;然而,JVM的GC
並非十分的機智,即使物件滿足了垃圾回收的條件也不一定會被立即回收。所以,建議我們在物件
使用完畢,應手動置成null。
8、在使用同步機制時,應儘量使用方法同步代替程式碼塊同步。
9、儘量減少對變數的重複計算
例如:for(int i = 0;i < list.size; i ++) {
…
}
應替換為:
for(int i = 0,int len = list.size();i < len; i ++) {
…
}
10、儘量採用lazy loading 的策略,即在需要的時候才開始建立。
例如: String str = “aaa”;
if(i == 1) {
list.add(str);
}
應替換為:
if(i == 1) {
String str = “aaa”;
list.add(str);
}
11、慎用異常
異常對效能不利。丟擲異常首先要建立一個新的物件。Throwable介面的建構函式呼叫名為
fillInStackTrace()的本地(Native)方法,fillInStackTrace()方法檢查堆疊,收集呼叫跟蹤信
息。只要有異常被丟擲,VM就必須調整呼叫堆疊,因為在處理過程中建立了一個新的物件。 異常
只能用於錯誤處理,不應該用來控制程式流程。
12、不要在迴圈中使用:
Try {
} catch() {
}
應把其放置在最外層。
13、設定StringBuffer的初始化容量
StringBuffer表示了可變的、可寫的字串。
有三個構造方法 :
StringBuffer (); //預設分配16個字元的空間
StringBuffer (int size); //分配size個字元的空間
StringBuffer (String str); //分配16個字元+str.length()個字元空間
你可以通過StringBuffer的建構函式來設定它的初始化容量,這樣可以明顯地提升效能。這裡
提到的建構函式是 StringBuffer(int length),length引數表示當前的StringBuffer能保持的字
符數量。你也可以使用 ensureCapacity(int minimumcapacity)方法在StringBuffer物件建立之後
設定它的容量。首先我們看看 StringBuffer的預設行為,然後再找出一條更好的提升效能的途徑
。
StringBuffer在內部維護一個字元陣列,當你使用預設的建構函式來建立StringBuffer物件的
時候,因為沒有設定初始化字元長度,StringBuffer的容量被初始化為16個字元,也就是說預設容
量就是16個字元。當StringBuffer達到最大容量的時候,它會將自身容量增加到當前的2倍再加2,
也就是(2*舊值+2)。如果你使用預設值,初始化之後接著往裡面追加字元,在你追加到第16個字
符的時候它會將容量增加到34(2*16+2),當追加到34個字元的時候就會將容量增加到 70
(2*34+2)。無論何事只要StringBuffer到達它的最大容量它就不得不建立一個新的字元陣列然後
重新將舊字元和新字元都拷貝一遍――這也太昂貴了點。所以總是給StringBuffer設定一個合理的
初始化容量值是錯不了的,這樣會帶來立竿見影的效能增益。
StringBuffer初始化過程的調整的作用由此可見一斑。所以,使用一個合適的容量值來初始化
StringBuffer永遠都是一個最佳的建議。
14、合理的使用Java類 java.util.Vector。
簡單地說,一個Vector就是一個java.lang.Object例項的陣列。Vector與陣列相似,它的元素
可以通過整數形式的索引訪問。但是,Vector型別的物件在建立之後,物件的大小能夠根據元素的
增加或者刪除而擴充套件、縮小。請考慮下面這個向Vector加入元素的例子:
Object obj = new Object();
Vector v = new Vector(100000);
for(int I=0;
I<100000; I++) { v.add(0,obj); }
除非有絕對充足的理由要求每次都把新元素插入到Vector的前面,否則上面的程式碼對效能不利
。在預設建構函式中,Vector的初始儲存能力是10個元素,如果新元素加入時儲存能力不足,則以
後儲存能力每次加倍。Vector類就象StringBuffer類一樣,每次擴充套件儲存能力時,所有現有的元素
都要複製到新的儲存空間之中。下面的程式碼片段要比前面的例子快幾個數量級:
Object obj = new Object();
Vector v = new Vector(100000);
for(int I=0; I<100000; I++) { v.add(obj); }
同樣的規則也適用於Vector類的remove()方法。由於Vector中各個元素之間不能含有“空隙”
,刪除除最後一個元素之外的任意其他元素都導致被刪除元素之後的元素向前移動。也就是說,從
Vector刪除最後一個元素要比刪除第一個元素“開銷”低好幾倍。
假設要從前面的Vector刪除所有元素,我們可以使用這種程式碼:
for(int I=0; I<100000; I++)
{
v.remove(0);
}
但是,與下面的程式碼相比,前面的程式碼要慢幾個數量級:
for(int I=0; I<100000; I++)
{
v.remove(v.size()-1);
}
從Vector型別的物件v刪除所有元素的最好方法是:
v.removeAllElements();
假設Vector型別的物件v包含字串“Hello”。考慮下面的程式碼,它要從這個Vector中刪除
“Hello”字串:
String s = "Hello";
int i = v.indexOf(s);
if(I != -1) v.remove(s);
這些程式碼看起來沒什麼錯誤,但它同樣對效能不利。在這段程式碼中,indexOf()方法對v進行順
序搜尋尋找字串“Hello”,remove(s)方法也要進行同樣的順序搜尋。改進之後的版本是:
String s = "Hello";
int i = v.indexOf(s);
if(I != -1) v.remove(i);
這個版本中我們直接在remove()方法中給出待刪除元素的精確索引位置,從而避免了第二次搜
索。一個更好的版本是:
String s = "Hello"; v.remove(s);
最後,我們再來看一個有關Vector類的程式碼片段:
for(int I=0; I++;I < v.length)
如果v包含100,000個元素,這個程式碼片段將呼叫v.size()方法100,000次。雖然size方法是一
個簡單的方法,但它仍舊需要一次方法呼叫的開銷,至少JVM需要為它配置以及清除堆疊環境。在
這裡,for迴圈內部的程式碼不會以任何方式修改Vector型別物件v的大小,因此上面的程式碼最好改寫
成下面這種形式:
int size = v.size(); for(int I=0; I++;I<size)
雖然這是一個簡單的改動,但它仍舊贏得了效能。畢竟,每一個CPU週期都是寶貴的。
15、當複製大量資料時,使用System.arraycopy()命令。
16、程式碼重構:增強程式碼的可讀性。
例如:
public class ShopCart {
private List carts ;
…
public void add (Object item) {
if(carts == null) {
carts = new ArrayList();
}
crts.add(item);
}
public void remove(Object item) {
if(carts. contains(item)) {
carts.remove(item);
}
}
public List getCarts() {
//返回只讀列表
return Collections.unmodifiableList(carts);
}
//不推薦這種方式
//this.getCarts().add(item);
}
17、不用new關鍵詞建立類的例項
用new關鍵詞建立類的例項時,建構函式鏈中的所有建構函式都會被自動呼叫。但如果一個對
象實現了Cloneable介面,我們可以呼叫它的clone()方法。clone()方法不會呼叫任何類建構函式
。
在使用設計模式(Design Pattern)的場合,如果用Factory模式建立物件,則改用clone()方
法建立新的物件例項非常簡單。例如,下面是Factory模式的一個典型實現:
public static Credit getNewCredit() {
return new Credit();
}
改進後的程式碼使用clone()方法,如下所示:
private static Credit BaseCredit = new Credit();
public static Credit getNewCredit() {
return (Credit) BaseCredit.clone();
}
上面的思路對於陣列處理同樣很有用。
18、乘法和除法
考慮下面的程式碼:
for (val = 0; val < 100000; val +=5) {
alterX = val * 8; myResult = val * 2;
}
用移位操作替代乘法操作可以極大地提高效能。下面是修改後的程式碼:
for (val = 0; val < 100000; val += 5) {
alterX = val << 3; myResult = val << 1;
}
修改後的程式碼不再做乘以8的操作,而是改用等價的左移3位操作,每左移1位相當於乘以2。相
應地,右移1位操作相當於除以2。值得一提的是,雖然移位操作速度快,但可能使程式碼比較難於理
解,所以最好加上一些註釋。
19、在JSP頁面中關閉無用的會話。
一個常見的誤解是以為session在有客戶端訪問時就被建立,然而事實是直到某server端程式
呼叫 HttpServletRequest.getSession(true)這樣的語句時才被建立,注意如果JSP沒有顯示的使
用 <%@page session="false"%> 關閉session,則JSP檔案在編譯成Servlet時將會自動加上這樣一
條語句HttpSession session = HttpServletRequest.getSession(true);這也是JSP中隱含的
session物件的來歷。由於session會消耗記憶體資源,因此,如果不打算使用session,應該在所有
的JSP中關閉它。
對於那些無需跟蹤會話狀態的頁面,關閉自動建立的會話可以節省一些資源。使用如下page指令:
<%@ page session="false"%>
20、JDBC與I/O
如果應用程式需要訪問一個規模很大的資料集,則應當考慮使用塊提取方式。預設情況下,
JDBC每次提取32行資料。舉例來說,假設我們要遍歷一個5000行的記錄集,JDBC必須呼叫資料庫
157次才能提取到全部資料。如果把塊大小改成512,則呼叫資料庫的次數將減少到10次。
21、Servlet與記憶體使用
許多開發者隨意地把大量資訊儲存到使用者會話之中。一些時候,儲存在會話中的物件沒有及時
地被垃圾回收機制回收。從效能上看,典型的症狀是使用者感到系統週期性地變慢,卻又不能把原因
歸於任何一個具體的元件。如果監視JVM的堆空間,它的表現是記憶體佔用不正常地大起大落。
解決這類記憶體問題主要有二種辦法。第一種辦法是,在所有作用範圍為會話的Bean中實現
HttpSessionBindingListener介面。這樣,只要實現valueUnbound()方法,就可以顯式地釋放Bean
使用的資源。 另外一種辦法就是儘快地把會話作廢。大多數應用伺服器都有設定會話作廢間隔時
間的選項。另外,也可以用程式設計的方式呼叫會話的setMaxInactiveInterval()方法,該方法用來設
定在作廢會話之前,Servlet容器允許的客戶請求的最大間隔時間,以秒計。
22、使用緩衝標記
一些應用伺服器加入了面向JSP的緩衝標記功能。例如,BEA的WebLogic Server從6.0版本開始
支援這個功能,Open Symphony工程也同樣支援這個功能。JSP緩衝標記既能夠緩衝頁面片斷,也能
夠緩衝整個頁面。當JSP頁面執行時,如果目標片斷已經在緩衝之中,則生成該片斷的程式碼就不用
再執行。頁面級緩衝捕獲對指定 URL的請求,並緩衝整個結果頁面。對於購物籃、目錄以及門戶網
站的主頁來說,這個功能極其有用。對於這類應用,頁面級緩衝能夠儲存頁面執行的結果,供後繼
請求使用。
23、選擇合適的引用機制
在典型的JSP應用系統中,頁頭、頁尾部分往往被抽取出來,然後根據需要引入頁頭、頁尾。
當前,在JSP頁面中引入外部資源的方法主要有兩種:include指令,以及include動作。
include 指令:例如<%@ include file="copyright.html" %>。該指令在編譯時引入指定的資
源。在編譯之前,帶有 include指令的頁面和指定的資源被合併成一個檔案。被引用的外部資源在
編譯時就確定,比執行時才確定資源更高效。
include動作:例如<jsp:include page="copyright.jsp" />。該動作引入指定頁面執行後生
成的結果。由於它在執行時完成,因此對輸出結果的控制更加靈活。但時,只有當被引用的內容頻
繁地改變時,或者在對主頁面的請求沒有出現之前,被引用的頁面無法確定時,使用include 動作
才合算。
24、及時清除不再需要的會話
為了清除不再活動的會話,許多應用伺服器都有預設的會話超時時間,一般為30分鐘。當應用
伺服器需要儲存更多會話時,如果記憶體容量不足,作業系統會把部分記憶體資料轉移到磁碟,應用服
務器也可能根據“最近最頻繁使用 ”(Most Recently Used)演算法把部分不活躍的會話轉儲到磁
盤,甚至可能丟擲“記憶體不足”異常。在大規模系統中,序列化會話的代價是很昂貴的。當會話不
再需要時,應當及時呼叫HttpSession.invalidate()方法清除會話。 HttpSession.invalidate()
方法通常可以在應用的退出頁面呼叫。
25、不要將陣列宣告為:public static final 。
26、HashMap的遍歷效率討論
經常遇到對HashMap中的key和value值對的遍歷操作,有如下兩種方法:Map<String, String
[]> paraMap = new HashMap<String, String[]>();
................//第一個迴圈
Set<String> appFieldDefIds = paraMap.keySet();
for (String appFieldDefId : appFieldDefIds) {
String[] values = paraMap.get(appFieldDefId);
......
}
//第二個迴圈
for(Entry<String, String[]> entry : paraMap.entrySet()){
String appFieldDefId = entry.getKey();
String[] values = entry.getValue();
.......
}
第一種實現明顯的效率不如第二種實現。
分析如下 Set<String> appFieldDefIds = paraMap.keySet(); 是先從HashMap中取得keySet
程式碼如下:
public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
}
private class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
return newKeyIterator();
}
public int size() {
return size;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
return HashMap.this.removeEntryForKey(o) != null;
}
public void clear() {
HashMap.this.clear();
}
}
其實就是返回一個私有類KeySet, 它是從AbstractSet繼承而來,實現了Set介面。
再來看看for/in迴圈的語法
for(declaration : expression)
statement
在執行階段被翻譯成如下各式
for(Iterator<E> #i = (expression).iterator(); #i.hashNext();){
declaration = #i.next();
statement
}因此在第一個for語句for (String appFieldDefId : appFieldDefIds) 中呼叫了
HashMap.keySet().iterator() 而這個方法呼叫了newKeyIterator()
Iterator<K> newKeyIterator() {
return new KeyIterator();
}
private class KeyIterator extends HashIterator<K> {
public K next() {
return nextEntry().getKey();
}
}
所以在for中還是呼叫了
在第二個迴圈for(Entry<String, String[]> entry : paraMap.entrySet())中使用的
Iterator是如下的一個內部類
private class EntryIterator extends HashIterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() {
return nextEntry();
}
}
此時第一個迴圈得到key,第二個迴圈得到HashMap的Entry
效率就是從迴圈裡面體現出來的第二個迴圈此致可以直接取key和value值
而第一個迴圈還是得再利用HashMap的get(Object key)來取value值
現在看看HashMap的get(Object key)方法
public V get(Object key) {
Object k = maskNull(key);
int hash = hash(k);
int i = indexFor(hash, table.length); //Entry[] table
Entry<K,V> e = table;
while (true) {
if (e == null)
return null;
if (e.hash == hash && eq(k, e.key))
return e.value;
e = e.next;
}
}
少的資源完成預定的任務。優化通常包含兩方面的內容:減小程式碼的體積,提高程式碼的執行率。
實現優化的方式我想不外乎兩種:一是對資料庫操縱語言的優化,二是提高程式碼的效率。當然啦還包括一些高階的技術,如運用分散式技術等等,這裡就不提及了。
對資料庫操縱語言的優化請看本部落格的資料庫優化篇。
現在來討論如何提高程式碼的效率。
在 Java程式中,效能問題的大部分原因並不在於Java語言,而是在於程式本身。養成好的程式碼編
寫習慣非常重要,比如正確地、巧妙地運用 java.lang.String類和java.util.Vector類,它能夠
顯著地提高程式的效能。下面我們就來具體地分析一下這方面的問題。
1、儘量指定類的final修飾符帶有final修飾符的類是不可派生的。在Java核心API中,有許多應用
final的例子,例如 java.lang.String。為String類指定final防止了人們覆蓋length()方法。另
外,如果指定一個類為final,則該類所有的方法都是final。Java編譯器會尋找機會內聯(inline
)所有的final方法(這和具體的編譯器實現有關)。此舉能夠使效能平均提高 50% 。
2、量重用物件。特別是String 物件的使用中,出現字串連線情況時應用StringBuffer 代替。
由於系統不僅要花時間生成物件,以後可能還需花時間對這些物件進行垃圾回收和處理。因此,生
成過多的物件將會給程式的效能帶來很大的影響。
3、儘量使用區域性變數,呼叫方法時傳遞的引數以及在呼叫中建立的臨時變數都儲存在棧(Stack)
中,速度較快。其他變數,如靜態變數、例項變數等,都在堆(Heap)中建立,速度較慢。另外,
依賴於具體的編譯器/JVM,區域性變數還可能得到進一步優化。請參見《儘可能使用堆疊變數》。
4、不要重複初始化變數 預設情況下,呼叫類的建構函式時, Java會把變數初始化成確定的值:
所有的物件被設定成null,整數變數(byte、 short、int、long)設定成0,float和double變數
設定成0.0,邏輯值設定成false。當一個類從另一個類派生時,這一點尤其應該注意,因為用new
關鍵詞建立一個物件時,建構函式鏈中的所有建構函式都會被自動呼叫。
5、在JAVA + ORACLE 的應用系統開發中,java中內嵌的SQL語句儘量使用大寫的形式,以減輕
ORACLE解析器的解析負擔。
6、Java 程式設計過程中,進行資料庫連線、I/O流操作時務必小心,在使用完畢後,及時關閉以釋放
資源。因為對這些大物件的操作會造成系統大的開銷,稍有不慎,會導致嚴重的後果。
7、由於JVM的有其自身的GC機制,不需要程式開發者的過多考慮,從一定程度上減輕了開發者負擔
,但同時也遺漏了隱患,過分的建立物件會消耗系統的大量記憶體,嚴重時會導致記憶體洩露,因此,
保證過期物件的及時回收具有重要意義。JVM回收垃圾的條件是:物件不在被引用;然而,JVM的GC
並非十分的機智,即使物件滿足了垃圾回收的條件也不一定會被立即回收。所以,建議我們在物件
使用完畢,應手動置成null。
8、在使用同步機制時,應儘量使用方法同步代替程式碼塊同步。
9、儘量減少對變數的重複計算
例如:for(int i = 0;i < list.size; i ++) {
…
}
應替換為:
for(int i = 0,int len = list.size();i < len; i ++) {
…
}
10、儘量採用lazy loading 的策略,即在需要的時候才開始建立。
例如: String str = “aaa”;
if(i == 1) {
list.add(str);
}
應替換為:
if(i == 1) {
String str = “aaa”;
list.add(str);
}
11、慎用異常
異常對效能不利。丟擲異常首先要建立一個新的物件。Throwable介面的建構函式呼叫名為
fillInStackTrace()的本地(Native)方法,fillInStackTrace()方法檢查堆疊,收集呼叫跟蹤信
息。只要有異常被丟擲,VM就必須調整呼叫堆疊,因為在處理過程中建立了一個新的物件。 異常
只能用於錯誤處理,不應該用來控制程式流程。
12、不要在迴圈中使用:
Try {
} catch() {
}
應把其放置在最外層。
13、設定StringBuffer的初始化容量
StringBuffer表示了可變的、可寫的字串。
有三個構造方法 :
StringBuffer (); //預設分配16個字元的空間
StringBuffer (int size); //分配size個字元的空間
StringBuffer (String str); //分配16個字元+str.length()個字元空間
你可以通過StringBuffer的建構函式來設定它的初始化容量,這樣可以明顯地提升效能。這裡
提到的建構函式是 StringBuffer(int length),length引數表示當前的StringBuffer能保持的字
符數量。你也可以使用 ensureCapacity(int minimumcapacity)方法在StringBuffer物件建立之後
設定它的容量。首先我們看看 StringBuffer的預設行為,然後再找出一條更好的提升效能的途徑
。
StringBuffer在內部維護一個字元陣列,當你使用預設的建構函式來建立StringBuffer物件的
時候,因為沒有設定初始化字元長度,StringBuffer的容量被初始化為16個字元,也就是說預設容
量就是16個字元。當StringBuffer達到最大容量的時候,它會將自身容量增加到當前的2倍再加2,
也就是(2*舊值+2)。如果你使用預設值,初始化之後接著往裡面追加字元,在你追加到第16個字
符的時候它會將容量增加到34(2*16+2),當追加到34個字元的時候就會將容量增加到 70
(2*34+2)。無論何事只要StringBuffer到達它的最大容量它就不得不建立一個新的字元陣列然後
重新將舊字元和新字元都拷貝一遍――這也太昂貴了點。所以總是給StringBuffer設定一個合理的
初始化容量值是錯不了的,這樣會帶來立竿見影的效能增益。
StringBuffer初始化過程的調整的作用由此可見一斑。所以,使用一個合適的容量值來初始化
StringBuffer永遠都是一個最佳的建議。
14、合理的使用Java類 java.util.Vector。
簡單地說,一個Vector就是一個java.lang.Object例項的陣列。Vector與陣列相似,它的元素
可以通過整數形式的索引訪問。但是,Vector型別的物件在建立之後,物件的大小能夠根據元素的
增加或者刪除而擴充套件、縮小。請考慮下面這個向Vector加入元素的例子:
Object obj = new Object();
Vector v = new Vector(100000);
for(int I=0;
I<100000; I++) { v.add(0,obj); }
除非有絕對充足的理由要求每次都把新元素插入到Vector的前面,否則上面的程式碼對效能不利
。在預設建構函式中,Vector的初始儲存能力是10個元素,如果新元素加入時儲存能力不足,則以
後儲存能力每次加倍。Vector類就象StringBuffer類一樣,每次擴充套件儲存能力時,所有現有的元素
都要複製到新的儲存空間之中。下面的程式碼片段要比前面的例子快幾個數量級:
Object obj = new Object();
Vector v = new Vector(100000);
for(int I=0; I<100000; I++) { v.add(obj); }
同樣的規則也適用於Vector類的remove()方法。由於Vector中各個元素之間不能含有“空隙”
,刪除除最後一個元素之外的任意其他元素都導致被刪除元素之後的元素向前移動。也就是說,從
Vector刪除最後一個元素要比刪除第一個元素“開銷”低好幾倍。
假設要從前面的Vector刪除所有元素,我們可以使用這種程式碼:
for(int I=0; I<100000; I++)
{
v.remove(0);
}
但是,與下面的程式碼相比,前面的程式碼要慢幾個數量級:
for(int I=0; I<100000; I++)
{
v.remove(v.size()-1);
}
從Vector型別的物件v刪除所有元素的最好方法是:
v.removeAllElements();
假設Vector型別的物件v包含字串“Hello”。考慮下面的程式碼,它要從這個Vector中刪除
“Hello”字串:
String s = "Hello";
int i = v.indexOf(s);
if(I != -1) v.remove(s);
這些程式碼看起來沒什麼錯誤,但它同樣對效能不利。在這段程式碼中,indexOf()方法對v進行順
序搜尋尋找字串“Hello”,remove(s)方法也要進行同樣的順序搜尋。改進之後的版本是:
String s = "Hello";
int i = v.indexOf(s);
if(I != -1) v.remove(i);
這個版本中我們直接在remove()方法中給出待刪除元素的精確索引位置,從而避免了第二次搜
索。一個更好的版本是:
String s = "Hello"; v.remove(s);
最後,我們再來看一個有關Vector類的程式碼片段:
for(int I=0; I++;I < v.length)
如果v包含100,000個元素,這個程式碼片段將呼叫v.size()方法100,000次。雖然size方法是一
個簡單的方法,但它仍舊需要一次方法呼叫的開銷,至少JVM需要為它配置以及清除堆疊環境。在
這裡,for迴圈內部的程式碼不會以任何方式修改Vector型別物件v的大小,因此上面的程式碼最好改寫
成下面這種形式:
int size = v.size(); for(int I=0; I++;I<size)
雖然這是一個簡單的改動,但它仍舊贏得了效能。畢竟,每一個CPU週期都是寶貴的。
15、當複製大量資料時,使用System.arraycopy()命令。
16、程式碼重構:增強程式碼的可讀性。
例如:
public class ShopCart {
private List carts ;
…
public void add (Object item) {
if(carts == null) {
carts = new ArrayList();
}
crts.add(item);
}
public void remove(Object item) {
if(carts. contains(item)) {
carts.remove(item);
}
}
public List getCarts() {
//返回只讀列表
return Collections.unmodifiableList(carts);
}
//不推薦這種方式
//this.getCarts().add(item);
}
17、不用new關鍵詞建立類的例項
用new關鍵詞建立類的例項時,建構函式鏈中的所有建構函式都會被自動呼叫。但如果一個對
象實現了Cloneable介面,我們可以呼叫它的clone()方法。clone()方法不會呼叫任何類建構函式
。
在使用設計模式(Design Pattern)的場合,如果用Factory模式建立物件,則改用clone()方
法建立新的物件例項非常簡單。例如,下面是Factory模式的一個典型實現:
public static Credit getNewCredit() {
return new Credit();
}
改進後的程式碼使用clone()方法,如下所示:
private static Credit BaseCredit = new Credit();
public static Credit getNewCredit() {
return (Credit) BaseCredit.clone();
}
上面的思路對於陣列處理同樣很有用。
18、乘法和除法
考慮下面的程式碼:
for (val = 0; val < 100000; val +=5) {
alterX = val * 8; myResult = val * 2;
}
用移位操作替代乘法操作可以極大地提高效能。下面是修改後的程式碼:
for (val = 0; val < 100000; val += 5) {
alterX = val << 3; myResult = val << 1;
}
修改後的程式碼不再做乘以8的操作,而是改用等價的左移3位操作,每左移1位相當於乘以2。相
應地,右移1位操作相當於除以2。值得一提的是,雖然移位操作速度快,但可能使程式碼比較難於理
解,所以最好加上一些註釋。
19、在JSP頁面中關閉無用的會話。
一個常見的誤解是以為session在有客戶端訪問時就被建立,然而事實是直到某server端程式
呼叫 HttpServletRequest.getSession(true)這樣的語句時才被建立,注意如果JSP沒有顯示的使
用 <%@page session="false"%> 關閉session,則JSP檔案在編譯成Servlet時將會自動加上這樣一
條語句HttpSession session = HttpServletRequest.getSession(true);這也是JSP中隱含的
session物件的來歷。由於session會消耗記憶體資源,因此,如果不打算使用session,應該在所有
的JSP中關閉它。
對於那些無需跟蹤會話狀態的頁面,關閉自動建立的會話可以節省一些資源。使用如下page指令:
<%@ page session="false"%>
20、JDBC與I/O
如果應用程式需要訪問一個規模很大的資料集,則應當考慮使用塊提取方式。預設情況下,
JDBC每次提取32行資料。舉例來說,假設我們要遍歷一個5000行的記錄集,JDBC必須呼叫資料庫
157次才能提取到全部資料。如果把塊大小改成512,則呼叫資料庫的次數將減少到10次。
21、Servlet與記憶體使用
許多開發者隨意地把大量資訊儲存到使用者會話之中。一些時候,儲存在會話中的物件沒有及時
地被垃圾回收機制回收。從效能上看,典型的症狀是使用者感到系統週期性地變慢,卻又不能把原因
歸於任何一個具體的元件。如果監視JVM的堆空間,它的表現是記憶體佔用不正常地大起大落。
解決這類記憶體問題主要有二種辦法。第一種辦法是,在所有作用範圍為會話的Bean中實現
HttpSessionBindingListener介面。這樣,只要實現valueUnbound()方法,就可以顯式地釋放Bean
使用的資源。 另外一種辦法就是儘快地把會話作廢。大多數應用伺服器都有設定會話作廢間隔時
間的選項。另外,也可以用程式設計的方式呼叫會話的setMaxInactiveInterval()方法,該方法用來設
定在作廢會話之前,Servlet容器允許的客戶請求的最大間隔時間,以秒計。
22、使用緩衝標記
一些應用伺服器加入了面向JSP的緩衝標記功能。例如,BEA的WebLogic Server從6.0版本開始
支援這個功能,Open Symphony工程也同樣支援這個功能。JSP緩衝標記既能夠緩衝頁面片斷,也能
夠緩衝整個頁面。當JSP頁面執行時,如果目標片斷已經在緩衝之中,則生成該片斷的程式碼就不用
再執行。頁面級緩衝捕獲對指定 URL的請求,並緩衝整個結果頁面。對於購物籃、目錄以及門戶網
站的主頁來說,這個功能極其有用。對於這類應用,頁面級緩衝能夠儲存頁面執行的結果,供後繼
請求使用。
23、選擇合適的引用機制
在典型的JSP應用系統中,頁頭、頁尾部分往往被抽取出來,然後根據需要引入頁頭、頁尾。
當前,在JSP頁面中引入外部資源的方法主要有兩種:include指令,以及include動作。
include 指令:例如<%@ include file="copyright.html" %>。該指令在編譯時引入指定的資
源。在編譯之前,帶有 include指令的頁面和指定的資源被合併成一個檔案。被引用的外部資源在
編譯時就確定,比執行時才確定資源更高效。
include動作:例如<jsp:include page="copyright.jsp" />。該動作引入指定頁面執行後生
成的結果。由於它在執行時完成,因此對輸出結果的控制更加靈活。但時,只有當被引用的內容頻
繁地改變時,或者在對主頁面的請求沒有出現之前,被引用的頁面無法確定時,使用include 動作
才合算。
24、及時清除不再需要的會話
為了清除不再活動的會話,許多應用伺服器都有預設的會話超時時間,一般為30分鐘。當應用
伺服器需要儲存更多會話時,如果記憶體容量不足,作業系統會把部分記憶體資料轉移到磁碟,應用服
務器也可能根據“最近最頻繁使用 ”(Most Recently Used)演算法把部分不活躍的會話轉儲到磁
盤,甚至可能丟擲“記憶體不足”異常。在大規模系統中,序列化會話的代價是很昂貴的。當會話不
再需要時,應當及時呼叫HttpSession.invalidate()方法清除會話。 HttpSession.invalidate()
方法通常可以在應用的退出頁面呼叫。
25、不要將陣列宣告為:public static final 。
26、HashMap的遍歷效率討論
經常遇到對HashMap中的key和value值對的遍歷操作,有如下兩種方法:Map<String, String
[]> paraMap = new HashMap<String, String[]>();
................//第一個迴圈
Set<String> appFieldDefIds = paraMap.keySet();
for (String appFieldDefId : appFieldDefIds) {
String[] values = paraMap.get(appFieldDefId);
......
}
//第二個迴圈
for(Entry<String, String[]> entry : paraMap.entrySet()){
String appFieldDefId = entry.getKey();
String[] values = entry.getValue();
.......
}
第一種實現明顯的效率不如第二種實現。
分析如下 Set<String> appFieldDefIds = paraMap.keySet(); 是先從HashMap中取得keySet
程式碼如下:
public Set<K> keySet() {
Set<K> ks = keySet;
return (ks != null ? ks : (keySet = new KeySet()));
}
private class KeySet extends AbstractSet<K> {
public Iterator<K> iterator() {
return newKeyIterator();
}
public int size() {
return size;
}
public boolean contains(Object o) {
return containsKey(o);
}
public boolean remove(Object o) {
return HashMap.this.removeEntryForKey(o) != null;
}
public void clear() {
HashMap.this.clear();
}
}
其實就是返回一個私有類KeySet, 它是從AbstractSet繼承而來,實現了Set介面。
再來看看for/in迴圈的語法
for(declaration : expression)
statement
在執行階段被翻譯成如下各式
for(Iterator<E> #i = (expression).iterator(); #i.hashNext();){
declaration = #i.next();
statement
}因此在第一個for語句for (String appFieldDefId : appFieldDefIds) 中呼叫了
HashMap.keySet().iterator() 而這個方法呼叫了newKeyIterator()
Iterator<K> newKeyIterator() {
return new KeyIterator();
}
private class KeyIterator extends HashIterator<K> {
public K next() {
return nextEntry().getKey();
}
}
所以在for中還是呼叫了
在第二個迴圈for(Entry<String, String[]> entry : paraMap.entrySet())中使用的
Iterator是如下的一個內部類
private class EntryIterator extends HashIterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() {
return nextEntry();
}
}
此時第一個迴圈得到key,第二個迴圈得到HashMap的Entry
效率就是從迴圈裡面體現出來的第二個迴圈此致可以直接取key和value值
而第一個迴圈還是得再利用HashMap的get(Object key)來取value值
現在看看HashMap的get(Object key)方法
public V get(Object key) {
Object k = maskNull(key);
int hash = hash(k);
int i = indexFor(hash, table.length); //Entry[] table
Entry<K,V> e = table;
while (true) {
if (e == null)
return null;
if (e.hash == hash && eq(k, e.key))
return e.value;
e = e.next;
}
}