JAVA效能優化:35個小細節讓你提升java程式碼的執行效率
程式碼優化,一個很重要的課題。可能有些人覺得沒用,一些細小的地方有什麼好修改的,改與不改對於程式碼的執行效率有什麼影響呢?這個問題我是這麼考慮的,就像大海里面的鯨魚一樣,它吃一條小蝦米有用嗎?沒用,但是,吃的小蝦米一多之後,鯨魚就被餵飽了。
程式碼優化也是一樣,如果專案著眼於儘快無BUG上線,那麼此時可以抓大放小,程式碼的細節可以不精打細磨;但是如果有足夠的時間開發、維護程式碼,這時候就必須考慮每個可以優化的細節了,一個一個細小的優化點累積起來,對於程式碼的執行效率絕對是有提升的。
程式碼優化的目標是:
減小程式碼的體積
提高程式碼執行的效率
程式碼優化細節
1、儘量使用final修飾符
帶有final修飾符的類是不可派生的,在Java核心API中,有很多應用final的例子,例如Java.lang.String,整個類都是final的。為類指定final修飾符可以讓類不可繼承,為方法指定final修飾符可以讓方法不可以被重寫。如果制定了一個類為final,則該類所有的方法都是final的。Java編譯器會尋找機會內聯所有的final方法,內聯對於提升Java執行效率作用巨大,能使效能平均提升50%。
2、儘量重用物件
特別是String物件的使用,出現字串連線時應該使用StringBuilder或StringBuffer代替。由於Java虛擬機器不僅要花時間生成物件,以後可能還需要花時間對這些物件進行垃圾回收和處理,因此,生成過多的物件將會給程式的效能帶來很大的影響。
3、儘可能使用區域性變數
呼叫方法時傳遞的引數以及在呼叫中建立的臨時變數都會儲存在棧中,速度較快,其它變數,如靜態變數、例項變數等,都在堆中建立,速度較慢。另外,棧中建立的變數,隨著方法的執行結束,這些內容就沒了,不需要額外的垃圾回收。
4、及時關閉流
Java程式設計過程中,進行資料庫連線、IO流操作時務必小心,在使用完畢後,及時關閉以釋放資源。因為對這些大物件的操作會造成很大的開銷,稍有不慎,將會導致嚴重的後果。
5、儘量減少對變數的重複計算
明確一個概念,對方法的呼叫,即使方法中只有一句語句,也是消耗的,包括建立棧幀、呼叫方法時保護現場、呼叫方法完畢時恢復現場。所以例如下面的操作:
for(int i=0;i<list.size;i++){}
建議替換為:
for(int i=0,int length=list.size;i<length;i++){}
這樣,在list.size很大的時候,就減少了很多的消耗。
6、儘量採用懶載入的策略,即在需要的時候才建立
例如:
String str = "aaa";if (i == 1){list.add(str);}
建議替換為:
if (i == 1){String str = "aaa";list.add(str);}
7、慎用異常
異常對效能不利。丟擲異常首先要建立一個新的物件,Throwable介面的建構函式呼叫名為fillInStackTrace的本地同步方法,fillInStackTrace方法檢查堆疊,收集呼叫跟蹤資訊。只要有異常被丟擲,Java虛擬機器就必須調整呼叫堆疊,因為在處理過程中建立了一個新的物件。異常只能用於錯誤處理,不應該用來控制程式流程。
8、不要在迴圈中使用trycatch,應該把其放在最外層
除非不得已。如果毫無理由地這麼寫了,只要你的領導資深一點、有強迫症一點,八成就要罵你為什麼寫出這種垃圾程式碼來了。
9、儘量能估計到待新增的內容的長度,為底層以陣列方式實現的集合、工具類指定初始長度
比如ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet等等,以StringBuilder為例:
① StringBuilder //預設分配16個字元的空間
② StringBuilder(int size)//預設分配size個字元的空間
③ StringBuider(String str)//預設分配16個字元+st.length個字元空間
設定初始化容量,可以明顯提升效能。比如StringBuilder,length表示當前StringBuilder能保持的字元數量。因為當StringBuilder達到最大容量的時候,它會將自身容量增加到當前的2倍再加2,無論何時只StringBuilder達到它的最大容量,它就不得不建立一個新的字元陣列然後將舊的字元陣列內容拷貝到新字元陣列中--這是一個非常耗費效能的操作。試想,如果能預估到字元陣列中大概要存放5000個字元而不指定長度,最接近5000的2次冪是4096,每次擴容2倍加2,那麼:
① 在4096的基礎上,再申請8194個大小的字元陣列,加起來相當於一次申請了12290個大小的字元陣列,如果一開始能指定5000個大小的字元陣列,就節省了一倍以上的空間。
② 把原來的4096個字元拷貝到新的字元陣列中去。
這樣,既浪費記憶體空間又降低程式碼執行效率。所以,給底層以陣列實現的集合、工具類設定一個合理的初始化容量是錯不了的,這會帶來立竿見影的效果。但是,注意,像hashmap這種以陣列+連結串列實現的集合,別把初始大小和你估計的大小設定的一樣,因為一個table上只連線一個物件的可能性幾乎為0。初始大小建議設定為2的N次冪,如果能估計到有2000個元素,設定成new hashmap(128)、new hashmap(256)都可以。
10、當複製大量資料時,使用system.arraycopy命令
11、乘法和除法使用移位操作
例如:
int a = 0;
int b = 0;
for (int i = 0; i < 1000000000; i++){
a = i * 8;
b = i / 2;
}
用移位操作可以極大地提高效能,因為在計算機底層,對位的操作是最方便、最快的,因此建議修改為:
int a1 = 0;
int b1 = 0;
for (int i = 0; i < 1000000000; i++){
a1 = i << 3;
b1 = i >> 1;
}
移位操作雖然快,但是可能會使程式碼不太好理解,因此最好加上相應的註釋。
感覺有點吹毛求疵了,這都多少迴圈了,才差了2毫秒!!!
12、迴圈內不要不斷建立物件引用
例如:
for (int i = 1; i <= count; i++){
Object obj = new Object;
}
這種做法會導致記憶體中有count份Object物件引用存在,count很大的話,就耗費記憶體了,建議為改為:
Object obj = null;
for (int i = 0; i <= count; i++) {
obj = new Object;
}
這樣的話,記憶體中只有一份Object物件引用,每次new Object的時候,Object物件引用指向不同的Object罷了,但是記憶體中只有一份,這樣就大大節省了記憶體空間了。
13、基於效率和型別檢查的考慮,應該儘可能使用array,無法確定陣列大小時才使用ArrayList
14、儘量使用hashmap、ArrayList、stringbuilder,除非執行緒安全需要,否則不建議使用hashtable、vector、stringbuffer,後三者由於使用同步機制而導致了效能開銷。
15、不要講陣列宣告為public static final
16、儘量在核實的場合使用單例模式
使用單例模式可以減輕載入的負擔、縮短載入的時間、提高載入的效率,單並不是所有地方都適合單例模式,簡單來說,單例模式主要適用於以下三個方面:
① 控制資源的使用,通過執行緒同步來控制資源的併發訪問
② 控制例項的產生,以達到節約資源的目的
③ 控制資料的共享,在不建立直接關聯的條件下,讓多個不相關的程序或執行緒之間實現通訊
17、儘量避免使用靜態變數
要知道,當某個物件被定義為static的,那麼gc通常不會回收這個物件所佔有的堆記憶體的,
public class A{
private static B b = new B;
}
此時靜態變數b的宣告週期與A類相同,如果A類不被解除安裝,那麼引用B指向的物件會常駐記憶體,知道程式終止。
18、及時清除不再需要的會話
為了清除不再活動的會話,許多伺服器都會有預設的超時時間,一般為30分鐘。當應用伺服器需要儲存更多的會話時,如果記憶體不足,那麼作業系統會把部分資料轉移到磁碟,應用伺服器也可能把不活躍的會話轉儲到磁碟,甚至可能丟擲記憶體不足的異常。如果會話要被轉儲到磁碟,那麼必須要先被序列化,在大規模叢集中,對物件序列化的代價是很高昂的。因此,當會話不再需要時,應當及時呼叫httpseesion的invalidate方法清除會話。
19、實現RandomAccess介面的集合比如ArrayList,應當使用最普通的for迴圈而不是foreach迴圈
實現RandomAccess介面用來表明其支援快速隨機訪問,此介面的主要目的是允許一般的演算法更改其行為,從而將其應用到隨機或連續訪問列表時能提供良好的效能。實際經驗表明,實現RandomAccess介面的類例項,假如是隨機訪問的,使用普通for迴圈效率將高於使用foreach迴圈,反過來,如果是順序訪問的,使用Iterator效率會更高。可以使用類似如下程式碼作判斷:
if (list instanceof RandomAccess){
for (int i = 0; i < list.size; i++){}
}else{
Iterator<?> iterator = list.iterable; while (iterator.hasNext){iterator.next}
}
foreach的底層實現就是Iterator。
20、使用同步程式碼塊代替同步方法
21、將常量宣告為static final,並以大寫命名
這樣在編譯期間就可以將這些內容放入常量池中,避免執行期間計算生成常量的值。另外,將常量的名字以大寫命名也可以方便區分出常量與變數。
22、不要建立一些不使用的物件,不要匯入一些不使用的類
23、程式執行過程中避免使用反射
反射是Java提供的一個很強大的功能,功能強大意味著效率不高。不建議在程式執行過程中使用尤其是頻繁的使用反射機制,特別是Method的invoke方法,如果確實有必要,一種建議性的做法是將那些需要通過反射載入的類在專案啟動的時候通過反射例項化出一個物件並放入記憶體--使用者只關心互動的速度,而不關心專案啟動的時間。
24、使用資料庫連線池和執行緒池
前者可以避免頻繁的開啟關閉連線;後者可以避免頻繁的建立和銷燬執行緒
25、使用帶緩衝的輸入輸出流進行IO操作
帶緩衝的輸入輸出流,即BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,這可以極大地提升IO效率。
26、順序插入和隨機訪問比較多的場景用ArrayList,元素刪除和中間插入比較多的場景使用LinkedList
27、不要讓public方法中有太多的引數
28、字串變數和字串常量equals的時候講字串常量寫在前面
這麼做主要是可以避免空指標異常。
29、請知道,在java中if (i == 1)和if (1 == i)是沒有區別的,但從閱讀習慣上講,建議使用前者
30、不要對陣列使用toString方法
31、不要對超出範圍的基礎資料型別做向下強制型別轉型
32、公用的集合類中不使用的資料一定要及時的remove掉
如果一個集合類是公用的(也就是說不是方法裡面的屬性),那麼這個集合裡面的元素是不會自動釋放的,因為始終有引用指向它們。所以,如果公用集合裡面的某些資料不使用而不去remove掉它們,那麼將會造成這個公用集合不斷增大,使得系統有記憶體洩露的隱患。
33、把一個基本型別轉為字串,toString最快,String.valueOf次之,資料+“”最慢
所以以後遇到把一個基本資料型別轉為String的時候,優先考慮使用toString方法。至於為什麼,很簡單:
① String.valueOf方法底層呼叫了Integer.toString方法,但是會在呼叫前做空判斷
② Integer.toString方法就不說了,直接呼叫了
③ i + “”底層使用了StringBuilder實現,先用append方法拼接,再用toString方法獲取字串
三者對比下來,明顯是②最快、①次之、③最慢
34、使用最有效率的方法去遍歷map
35、對資源的close()建議分開操作
意思是,比如我有這麼一段程式碼:
try{
XXX.close;
YYY.close;
}catch (Exception e){
...
}
建議修改為:
try{
XXX.close;
}catch (Exception e) {
...
}
try{
YYY.close;
}catch (Exception e) {
...
}
雖然有些麻煩,卻能避免資源洩露。我想,如果沒有修改過的程式碼,萬一XXX.close拋異常了,那麼就進入了cath塊中了,YYY.close不會執行,YYY這塊資源就不會回收了,一直佔用著,這樣的程式碼一多,是可能引起資源控制代碼洩露的。而改為上面的寫法之後,就保證了無論如何XXX和YYY都會被close掉。