很多程式員在一開始並不注重效能的設計,只有當系統交付運行時,才 發現問題並且開始解決這一問題,但往往這隻能挽救一點點。效能的管理應該一開始 就被整合到設計和開發當中去。
最普遍的問題就是臨時對象大量經常的建立,這為效能埋下隱患。
效能的問題來自很多原因,最容易解決的可能是:你選擇了不好的演算法來進行計算,如 用冒泡法來排序巨量資料,或者你每次使用資料時都要反覆計算一次,這應該使用Cache。
你能很容易的使用工具(如Borland的Optimizeit)或壓力測試發現這些問題, 一旦發現,就能夠立即被糾正,但是很多Java的效能問題隱藏得更深,難於修改源碼就能糾正, 如程式組件的介面設計。
現在我們倡導物件導向的組件可複用設計,無疑這樣設計的優點是巨大的, 但是也要注意到對效能的影響。
一個java效能設計原則是,避免不必要的對象建立,對象的建立是非常耗時的, 所以你要避免不必要的臨時或過多的對象建立,
String是程式中最主要建立的對象,因為String是不變的,如果String長度被修改 將導致String對象再次建立,所以對效能有所注意的一般人就是盡量迴避使用String, 但是這幾乎是不可能的。
介面參數設計
舉例 MailBot:
MailBot郵件系統的有一個Header資料,它是character buffer,需要對這個character buffer 進行分析比較,那麼你要做一個類Matcher,在這個類中你將Header資料讀入然後配比, 一個不好的做法是:
public class BadRegExpMatcher {
public BadRegExpMatcher(String regExp);
/** Attempts to match the specified regular expression against the input text, returning the matched text if possible or null if not
*/
public String match(String inputText);
}
這個BadRegExpMatche要求入口參數是String ,那麼如果MailBot要調用他,必須自己做一個 character buffer到String的轉換:
BadRegExpMatcher dateMatcher = new BadRegExpMatcher(...);
while (...) { ...
//產生新的String
String headerLine = new String(myBuffer, thisHeaderStart, thisHeaderEnd-thisHeaderStart);
String result = dateMatcher.match(headerLine);
if (result == null) { ... }
}
很明顯,這裡這個由於介面不一致導致了多餘的對象String headerline的建立,這是不能允許的, 應該將Matcher的介面設計成能夠接納character buffer,當然為通用性,也應該提供String的 介面參數:
class BetterRegExpMatcher {
public BetterRegExpMatcher(...);
/** 提供多個介面參數的match方法
Provide matchers for multiple formats of input -- String,
character array, and subset of character array. Return
-1 if no match was made; return offset of match start if
a match was made. */
public int match(String inputText);
public int match(char[] inputText);
public int match(char[] inputText, int offset, int length);
/** Get the next match against the input text, if any */
public int getNextMatch();
public int getMatchLength();
public String getMatchText();
}
很明顯BetterRegExpMatcher的運行速度將比前面BadRegExpMatcher運行速度快。
因為在你已經寫好代碼的情況下,你比較難於更改一個類的介面參數,那就應該在寫程式之前多 多考慮你這些介面參數的類型設定,最好有一個通盤的介面類型規定。
減少對象的建立
臨時對象是那些有很短的生命週期,通常服務一些非十分有用的目標,程式員通常使用臨時對象作為 資料混合包傳送或者返回,為避免上述樣本哪些轉換介面對象的構造,你應該巧妙的避免創造這些臨時 對象,以防止給你的程式留下效能的陰影。
上述樣本說明效能問題在於String對象,但是String在對象建立中又是如此的普遍,String是不變的,一旦賦值,就不會變化,不少程式員 認為不變的東西總是會導致壞的效能,其實它並不是這麼簡單,實際上,效能好壞在於你如何使用這個東西。
對於經常需要變化的String,很明顯使用Stringbuffer來代替。
舉例:
看下面兩種實現:
public class Component {
...
protected Rectangle myBounds;
public Rectangle getBounds() { return myBounds; }
}
和
public class Component {
public Rectangle getBounds() {
return new Rectangle(myBounds.x, myBounds.y, myBounds.height,
myBounds.width);
}
}
當使用Component分別對應有如下兩種:
Rectangle r = component.getBounds();
...
r.height *= 2;
和
int x = component.getBounds().x;
int y = component.getBounds().y;
int h = component.getBounds().height;
int w = component.getBounds().width;
第一種使用方式缺點,r.height的使用已經脫離component,容易引起溝通上的誤解,因為 Rectangle變化必須涉及component內容重新重新整理,萬一其它程式員不知道這個規則,修改 r.height(乘2),將不會去重新整理component,
第二中方式是個提高,迫使componenet和其組件跟隨在一起。但是帶來問題是:建立了 四個臨時對象。
改進辦法是,在第一種的基礎上,在Commponent中增加
public int getX() { return myBounds.x; }
public int getY() { return myBounds.y; }
public int getHeight() { return myBounds.height; }
public int getWidth() { return myBounds.width; }
這樣調用變成:
int x = component.getX();
int y = component.getY();
int h = component.getHeight();
int w = component.getWidth();
兩全其美了不是?
這就是減少建立對象技巧之一: 增加finer-grained協助工具功能
第二種技巧是:Exploit mutability
上例還有一種實現方式:
public Rectangle getBounds(Rectangle returnVal) {
returnVal.x = myBounds.x;
returnVal.y = myBounds.y;
returnVal.height = myBounds.height;
returnVal.width = myBounds.width;
return returnVal;
}
多巧妙,把Rectangle作為參數傳進來修改一下再送出去。
技巧3是 融合變和不變於一身。
總結上面一些例子,發現一個規律:臨時對象產生是在這種情況下產生的: 不變的要轉換成可變的。那麼針對這個根本原因我們設計出各取所需的方案。
以下例說明:
Point是不變的,我們繼承它,定義一個可變的子類。
public class Point {
protected int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
public final int getX() { return x; }
public final int getY() { return y; }
}
public class MutablePoint extends Point {
public final void setX(int x) { this.x = x; }
public final void setY(int y) { this.y = y; }
}
這樣,可變的需求和不可變的需求各自滿足,分別調用。
public class Shape {
private MutablePoint myLocation;
//返回可變的
public Shape(int x, int y) {
myLocation = new MutablePoint(x, y);
}
//返回不變的
public Point getLocation() { return (Point) myLocation; } }
遠程介面
在分布式應用中,效能也是相當重要的,這裡介紹如何通過檢查class的介面 能簡單預知分布式應用中的效能問題。
在分布式應用中,一個在這個系統中啟動並執行對象能夠調用另外一個系統的對象的方法,這是通過很多 內部機制來實現將遠程對象貌似本機物件的,為了發現遠程對象,你首先必鬚髮現它,這是通過一種 名稱目錄服務機制,比如RMO的註冊,JNDO和CORBA的名稱服務。
當你通過目錄服務得到一個遠端對象,你不是得到一個實際的指向,而是一個和遠程行為一樣的stub對象的 指向, 當你調用stub對象的一個方法時,這個得marshal這個方法參數:也就是轉換成byte-stream,這類似 於序列化,這個stub對象通過網路將marshal後的參數發送給skeleton對象,後者負責unmarshal這些參數然後 調用真正實際的你要調用的遠程方法,然後,這個方法返回一個值給skeleton,再逐個沿著剛才路線返回, 一個簡單方法要做這麼多工作啊。
很顯然,遠程方法調用要比本地方法調用來得耗時昂貴。
上面返回情況是是指返回原始型primitive,如果返回的是對象,怎麼辦?如果這個對象支援 遠程調用,它又會通過查詢創造一個stub和skeleton對象,這又是耗時的;如果這個對象 不支援遠程調用,那麼所有的對象的欄位和任何涉及引用的對象都要被marshal,這也是 相當耗時的。
由此可見,一個不好的遠程介面設計將完全扼殺程式的效能,為了避免網路開銷,設計一次 遠程調用返回多值總比多次調用,每次只返回一個值要好得多。
還有提防在不需要返回遠程對象時,返回一個遠程對象。不要傳遞很複雜不必要的對象給遠程。
假設遠程伺服器有一個目錄列表對象,每個目錄項目中包含姓名 電話號碼 和郵件地址等值, 下列程式:
public interface Directory extends Remote {
DirectoryEntry[] getEntries();
void addEntry(DirectoryEntry entry);
void removeEntry(DirectoryEntry entry);
}
public interface DirectoryEntry extends Remote {
String getName();
String getPhoneNumber();
String getEmailAddress();
}
這樣設計導致結果是,當我需要一個姓名值時,首先要獲得Directory 對象,再獲得DirectoryEntry, 獲得DirectoryEntry才能獲得getName,這麼來來回回,需要多少次網路開銷啊。
public interface Directory extends Remote {
String[] getNames();
DirectoryEntry[] getEntries();
//加入這個方法
DirectoryEntry getEntryByName(String name);
void addEntry(DirectoryEntry entry);
void removeEntry(DirectoryEntry entry);
}
這樣直接在Directory加上DirectoryEntry和getNames(),一次網路開銷就全部解決。
當然這樣的解決方案是完全建立在對分布式應用原理瞭解的基礎上。