標籤:關鍵字 建構函式 for html return java 左右 test 重要
Java效能最佳化之編程技巧總結https://www.cnblogs.com/yueshutong/p/9555174.html
程式的效能受代碼品質的直接影響。在本文中,主要介紹一些代碼編寫的小技巧和慣例,這些技巧有助於在代碼層級上提升系統效能。
1、慎用異常
在Java軟體開發中,經常使用 try-catch 進行錯誤捕獲,但是,try-catch 語句對系統效能而言是非常糟糕的。雖然在一次 try-catch中,無法察覺到它對效能帶來的損失,但是,一旦try-catch被應用於迴圈之中,就會給系統效能帶來極大的傷害。
以下是一段將try-catch應用於for迴圈內的樣本
public void test() {
int a = 0;
for (int i = 0; i < 1000000; i++) {
try {
a = a + 1;
System.out.println(i);
} catch (Exception e) {
e.printStackTrace();
}
}
}
這段代碼我已耗用時間是 27211 ms。如果將try-catch移到迴圈體外,那麼就能提升系統效能,如下代碼
public void test() { int a = 0; try { for (int i = 0; i < 1000000; i++) { a = a + 1; System.out.println(i); } } catch (Exception e) { e.printStackTrace(); }}
運行耗時 15647 ms。可見tyr-catch對系統效能的影響。
2、使用局部環境
調用方法時傳遞的參數以及在調用中建立的臨時變數都儲存在棧(Stack)中,速度較快。其他變數,如靜態變數、執行個體變數等,都在堆(Heap)中建立,速度較慢。
下面是一段測試案例
// private static int a = 0;
public static void main(String[] args) { int a = 0; long start = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { a = a + 1; System.out.println(i); } System.out.println(System.currentTimeMillis() - start);}
運行結果很明顯,使用靜態變數耗時15677ms,使用局部變數耗時13509ms。由此可見,局部變數的訪問速度高於類的成員變數。
3、位元運算代替乘除法
在所有的運算中,位元運算是最為高效的。因此,可以嘗試使用位元運算代替部分算術運算,來提高系統的運行速度。
比如在HashMap的源碼中使用了位元運算
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16static final int MAXIMUM_CAPACITY = 1 << 30;
對於整數的乘除運算最佳化
a*=2
a/=2
用位元運算可以寫為
a<<=1
a>>=1
4、替換switch
關鍵字 switch 語句用於多條件判斷, switch 語句的功能類似於 if-else 語句,兩者效能也差不多。因此,不能說 switch 語句會降低系統的效能。但是,在絕大部分情況下,switch 語句還是有效能提升空間的。
來看下面的例子:
public static void main(String[] args) { long start = System.currentTimeMillis(); int re = 0; for (int i = 0;i<1000000;i++){ re = switchInt(i); System.out.println(re); } System.out.println(System.currentTimeMillis() - start+"毫秒");//17860}public static int switchInt(int z){ int i = z%10+1; switch (i){ case 1:return 3; case 2:return 6; case 3:return 7; case 4:return 8; case 5:return 10; case 6:return 16; case 7:return 18; case 8:return 44; default:return -1; }}
就分支邏輯而言,這種 switch 模式的效能並不差。但是如果換一種新的思路替代switch,實現相同的程式功能,效能就能有很大的提升空間。
public static void main(String[] args) { long start = System.currentTimeMillis(); int re = 0; int[] sw = new int[]{0,3,6,7,8,10,16,18,44}; for (int i = 0;i<1000000;i++){ re = arrayInt(sw,i); System.out.println(re); } System.out.println(System.currentTimeMillis() - start+"毫秒");//12590}public static int arrayInt(int[] sw,int z){ int i = z%10+1; if (i>7 || i<1){ return -1; }else { return sw[i]; }}
以上代碼使用全新的思路,使用一個連續的數組代替了 switch 語句。因為對資料的隨機訪問是非常快的,至少好於 switch 的分支判斷。通過實驗,使用switch的語句耗時17860ms,使用數組的實現只耗時12590ms,提升了5s多。在軟體開發中,換一種思路可能會取得更好的效果,比如使用數組替代switch語句就是就是一個很好的例子。
5、一維數組代替二維數組
由於數組的隨機訪問的效能非常好,許多JDK類庫,如ArrayList、Vector等都是使用了數組作為其數組實現。但是,作為軟體開發人員也必須知道,一位元組和二維數組的訪問速度是不一樣的。一位元組的訪問速度要優於二維數組。因此,在效能敏感的系統中要使用二維數組的,可以嘗試通過可靠地演算法,將二維數組轉為一維數組再進行處理,以提高系統的響應速度。
6、提取運算式
在軟體開發過程中,程式員很容易有意無意讓代碼做一些“重複勞動”,在大部分情況下,由於電腦的告訴運行,這些“重複勞動”並不會對效能構成太大的威脅,但若將系統效能發揮到極致,提取這些“重複勞動”相當有意義。
來看下面的測試案例:
@Testpublic void test(){ long start = System.currentTimeMillis(); ArrayList list = new ArrayList(); for (int i = 0;i<100000;i++){ System.out.println(list.add(i)); } //以上是為了做準備 for (int i = 0;i<list.size();i++){ System.out.println(list.get(i)); } System.out.println(System.currentTimeMillis() - start);//5444}
如果我們把list.size()方法提取出來,最佳化後的代碼如下:
@Testpublic void test(){ long start = System.currentTimeMillis(); ArrayList list = new ArrayList(); for (int i = 0;i<100000;i++){ System.out.println(list.add(i)); } //以上是為了做準備 int n = list.size(); for (int i = 0;i<n;i++){ System.out.println(list.get(i)); } System.out.println(System.currentTimeMillis() - start);//3514}
在我的機器上,前者耗時5444ms,後者耗時3514ms,相差2s左右,可見,提取重複的操作是相當有意義的。
7、展開迴圈
與前面所介紹的最佳化技巧略有不同,筆者認為展開迴圈是一種在極端情況下使用的最佳化手段,因為展開迴圈很可能會影響代碼的可讀性和可維護性,而這兩者對軟體系統來說也是極為重要的。但是,當效能問題成為系統主要矛盾時,展開迴圈絕對是一種值得嘗試的技術。
8、布爾運算代替位元運算
雖然位元運算的速度遠遠高於算術運算,但是在條件判斷時,使用位元運算替代布爾運算卻是非常錯誤的選擇。
在條件判斷時,Java會對布爾運算做相當充分的最佳化。假設有運算式 a,b,c 進行布爾運算“a&&b&&c” ,根據邏輯與的特點,只要在整個布林運算式中有一項返回false,整個運算式就返回false,因此,當運算式a為false時,該運算式將立即返回 false ,而不會再去計算運算式b 和c。同理,當計算運算式為“a||b||c”時,也是一樣。
若使用位元運算(按位與”&“、按位或”|“)代替邏輯與和邏輯或,雖然位元運算本身沒有效能問題,但是位元運算總是要將所有的子運算式全部計算完成後,再給出最終結果。因此,從這個角度來說,使用位元運算替代布爾運算會使系統進行很多無效計算。
9、使用arrayCopy()
數組複製是一項使用頻率很高的功能,JDK中提供了一個高效的API來實現它:
如果在應用程式需要進行數組複製,應該使用這個函數,而不是自己實現。
方法代碼:
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
它的用法是將源數組 src 從索引 srcPos 處複製到目標數組 dest 的 索引destPos處,複製的長度為 length。
System.arraycopy() 方法是 native 方法,通常 native 方法的效能要優於普通的方法。僅出於效能考慮,在軟體開發中,儘可能調用 native 方法。
10、使用Buffer進行I/O流操作
除NIO外,使用 Java 進行 I/O操作有兩種基本方法:
使用基於InputStream 和 OutputStream 的方式;(位元組流)
使用 Writer 和 Reader。(字元流)
無論使用哪種方式進行檔案 I/O,如果能合理地使用緩衝,就能有效提高I/O的效能。
11、使用clone()代替new
在Java中建立對象執行個體最常用的方法是使用 new 關鍵字。JDK對 new 的支援非常好,使用 new 關鍵字建立輕量級對象時,速度非常快。但是,對於重量級對象,由於對象在建構函式中可能會進行一些複雜且耗時的操作,因此,建構函式的執行時間可能會比較長。導致系統短期內無法獲得大量的執行個體。為瞭解決這個問題,可以使用Object.clone() 方法。
Object.clone() 方法可以繞過建構函式,快速複製一個對象執行個體。但是,在預設情況下,clone()方法產生的執行個體只是原對象的淺拷貝。
這裡不得不提Java只有值傳遞了,關於這點,我的理解是基礎資料型別 (Elementary Data Type)引用的是值,普通對象引用的也是值,不過這個普通對象引用的值其實是一個對象的地址。程式碼範例:
int i = 0;
int j = i; //i的值是0
User user1 = new User();
User user2 = user1; //user1值是new User()的記憶體位址
如果需要深拷貝,則需要重新實現 clone() 方法。下面看一下ArrayList實現的clone()方法:
public Object clone() { try { ArrayList<?> v = (ArrayList<?>) super.clone(); v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // this shouldn't happen, since we are Cloneable throw new InternalError(e); }}
在ArrayList的clone()方法中,首先使用 super.clone() 方法產生一份淺拷貝對象。然後拷貝一份新的elementData數組讓新的ArrayList去引用。使複製後的ArrayList對象與原對象持有不同的引用,實現了深拷貝。
12、靜態方法替代執行個體方法
使用 static 關鍵字描述的方法為靜態方法。在Java中,由於執行個體方法需要維護一張類似虛函數表的結構,以實現對多態的支援。與靜態方法相比,執行個體方法的調用需要更多的資源。因此,對於一些常用的工具類方法,沒有對其進行重載的必要,那麼將它們聲明為 static,便可以加速方法的調用。同時,調用 static 方法不需要產生類的執行個體。比調用執行個體方法更為方便、易用。
參考
《Java程式效能最佳化》葛一鳴著
Java效能最佳化