Java開發效能優化乾貨
程式碼優化最重要的作用應該是避免未知的錯誤,因此在寫程式碼的時候,從源頭開始注意各種細節,權衡並使用最優的選擇,將會很大程度上避免出現未知的錯誤,從長遠看也極大的降低了工作量。所以說程式碼優化的目標是減小程式碼體積、提高程式碼執行效率。優化是無止境的,本文也只給出整理的一些常見優化建議。
(1)儘量指定類、方法的 final 修飾符。
帶有 final 修飾符的類是不可派生的。在 Java 核心 API 中,有許多應用 final 的例子,例如 java.lang.String,整個類都是 final 的。為類指定 final 修飾符可以讓類不可以被繼承,為方法指定 final 修飾符可以讓方法不可以被重寫。如果指定了一個類為 final,則該類所有的方法都是 final 的。Java 編譯器會尋找機會內聯所有的 final 方法,內聯對於提升 Java 執行效率作用重大,具體可以查閱 Java 執行期優化相關資料,此舉能夠使效能平均提高 50%。
(2)儘量重用物件。
特別是 String 物件的使用,出現字串連線時應該使用 StringBuilder/StringBuffer 代替。由於 Java 虛擬機器不僅要花時間生成物件,以後可能還需要花時間對這些物件進行垃圾回收和處理,因此生成過多的物件將會給程式的效能帶來很大的影響。
(3)儘可能使用區域性變數。
呼叫方法時傳遞的引數以及在呼叫中建立的臨時變數都儲存在棧中,速度較快,其他變數,如靜態變數、例項變數等,都在堆中建立,速度較慢。另外,棧中建立的變數,隨著方法的執行結束,這些內容就沒了,不需要額外的垃圾回收。
(4)及時關閉流。
Java 程式設計過程中,進行資料庫連線、I/O 流操作時務必小心,在使用完畢後,及時關閉以釋放資源。因為對這些大物件的操作會造成系統大的開銷,稍有不慎,將會導致嚴重的後果。
//效能不好,list.size() 會重複呼叫
for (int i = 0; i < list.size(); i++) {
...
}
//建議替換為如下
for (int i = 0, 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)不要在迴圈中使用 try-catch,應該把其放在最外層。
根據網友們提出的意見,這一點我認為值得商榷,其實分業務場景吧,有些場景需要迴圈終止,有些只是為了忽略當此迴圈處理。
(9)如果能估計到待新增的內容長度,為底層以陣列方式實現的集合、工具類指定初始長度。
比如 ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet 等,以 StringBuilder 為例,StringBuilder() 構造方法預設分配 16 個字元的空間,StringBuilder(int size) 構造方法預設分配 size 個字元的空間,StringBuilder(String str) 構造方法預設分配 16 個字元加 str.length() 個字元空間,所以可以通過類的構造方法來設定它的初始化容量,這樣可以明顯地提升效能。
(10)當複製大量資料時,使用 System.arraycopy() 命令。
這個肯定大家都沒有疑問的,效能優化的實現而已。
(11)乘法和除法使用移位操作。
用移位操作可以極大地提高效能,因為在計算機底層,對位的操作是最方便、最快的,但是移位操作雖然快,可能會使程式碼不太好理解,因此最好加上相應的註釋。
//不好的示範
for (val = 0; val < 100000; val += 5) {
a = val * 8;
b = val / 2;
}
//建議修改實現
for (val = 0; val < 100000; val += 5) {
a = val << 3;
b = val >> 1;
}
(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。
因為這毫無意義,這樣只是定義了引用為 static final,陣列的內容還是可以隨意改變的,將陣列宣告為 public 更是一個安全漏洞,這意味著這個陣列可以被外部類所改變。
(16)儘量在合適的場合使用單例。
使用單例可以減輕載入的負擔、縮短載入的時間、提高載入的效率,但並不是所有地方都適用於單例,簡單來說,單例主要適用於以下三個方面:
控制資源的使用,通過執行緒同步來控制資源的併發訪問;
控制例項的產生,以達到節約資源的目的;
控制資料的共享,在不建立直接關聯的條件下,讓多個不相關的程序或執行緒之間實現通訊;
(17)儘量避免隨意使用靜態變數。
因為當某個物件被定義為 static 的變數所引用,那麼 gc 通常是不會回收這個物件所佔有的堆記憶體的。
public class A {
private static B b = new B();
}
//此時靜態變數 b 的生命週期與 A 類相同,
//如果 A 類不被解除安裝,那麼引用 B 指向的 B 物件會常駐記憶體,直到程式終止。
(18)及時清除不再需要的會話。
為了清除不再活動的會話,許多應用伺服器都有預設的會話超時時間,一般為 30 分鐘。當應用伺服器需要儲存更多的會話時,如果記憶體不足,那麼作業系統會把部分資料轉移到磁碟,應用伺服器也可能根據MRU(最近最頻繁使用)演算法把部分不活躍的會話轉儲到磁碟,甚至可能丟擲記憶體不足的異常。如果會話要被轉儲到磁碟,那麼必須要先被序列化,在大規模叢集中,對物件進行序列化的代價是很昂貴的。因此,當會話不再需要時,應當及時呼叫 HttpSession 的 invalidate() 方法清除會話。
(19)實現 RandomAccess 介面的集合(比如 ArrayList)應當使用最普通的 for 迴圈而不是 foreach 迴圈來遍歷。
這是 JDK 推薦給使用者的,JDK API 對於 RandomAccess 介面的解釋是實現 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()}
}
(20)使用同步程式碼塊替代同步方法。
儘量使用同步程式碼塊,避免對那些不需要進行同步的程式碼也進行了同步,影響了程式碼執行效率。
(21)將常量宣告為 static final,並以大寫命名。
這樣在編譯期間就可以把這些內容放入常量池中,避免執行期間計算生成常量的值。另外,將常量的名字以大寫命名也可以方便區分出常量與變數。
(22)不要建立一些不使用的物件,不要匯入一些不使用的類。
這毫無意義,如果程式碼中出現 "The value of the local variable i is not used"、"The import java.util is never used",那麼請刪除這些無用的內容,雖說沒啥影響,但是有些時候編譯期會報錯,譬如沒 import 用到的類被刪掉了。
(23)程式執行過程中避免使用反射。
不建議在程式執行過程中使用,除非萬不得已,尤其是頻繁使用反射機制,特別是 Method 的 invoke 方法,如果確實有必要,一種建議性的做法是將那些需要通過反射載入的類在專案啟動的時候通過反射例項化出一個物件並放入記憶體,使用者只關心和對端互動的時候獲取最快的響應速度,並不關心對端的專案啟動花多久時間。
(24)使用資料庫連線池和執行緒池。
這兩個池都是用於重用物件的,前者可以避免頻繁地開啟和關閉連線,後者可以避免頻繁地建立和銷燬執行緒。
(25)使用帶緩衝的輸入輸出流進行 IO 操作。
帶緩衝的輸入輸出流,即 BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,這可以極大地提升 IO 效率。
(26)順序插入和隨機訪問比較多的場景使用 ArrayList,元素刪除和中間插入比較多的場景使用 LinkedList。
(27)不要讓 public 方法中有太多的形參。
public 方法即對外提供的方法,如果給這些方法太多形參的話主要壞處是違反了面向物件的程式設計思想,Java 講求一切都是物件,太多的形參和麵向物件的程式設計思想並不契合,引數太多勢必導致方法呼叫的出錯概率增加。
(28)字串變數和字串常量 equals 的時候將字串常量寫在前面,這樣可以避免空指標。
(29)建議使用 if (i == 1) 而不是 if (1 == i) 的方式。
因為有可能 == 會誤寫成 =,而在 C/C++ 中 if (i = 1) 是會出問題的,而 Java 會在編譯時報錯 "Type mismatch: cannot convert from int to boolean",但是,儘管Java的 if (i == 1) 和 if (1 == i) 在語義上沒有任何區別,從閱讀習慣上講,建議使用前者會更好些。
(30)不要對陣列使用 toString() 方法。
本意是想打印出陣列內容,卻打出來的是物件資訊,甚至有可能因為陣列引用為空而導致空指標異常。對於集合 toString() 是可以打印出集合裡面的內容的,因為集合的父類 AbstractCollections<E> 重寫了 Object 的 toString() 方法。
(31)不要對超出範圍的基本資料型別做向下強制轉型。
這很明確,譬如 long 轉 int 是會存在潛在風險的。
(32)公用的集合類中不使用的資料一定要及時 remove 掉。
如果一個集合類是公用的(也就是說不是方法裡面的屬性),那麼這個集合裡面的元素是不會自動釋放的,因為始終有引用指向它們。所以,如果公用集合裡面的某些資料不使用而不去remove掉它們,那麼將會造成這個公用集合不斷增大,使得系統有記憶體洩露的隱患。
(33)把一個基本資料型別轉為字串,基本資料型別.toString() 是最快的方式、String.valueOf(資料) 次之、資料+"" 最慢。
因為 String.valueOf() 方法底層呼叫了 Integer.toString() 方法,但是會在呼叫前做空判斷;Integer.toString() 是直接呼叫;i + "" 底層使用了 StringBuilder 實現,先用 append 方法拼接,再用 toString() 方法獲取字串。
(34)使用最有效率的方式去遍歷 Map。
遍歷 Map 的方式有很多,通常場景下我們需要的是遍歷 Map 中的 Key 和 Value,那麼推薦使用的、效率最高的方式是 entrySet(),如果只是想遍歷一下這個 Map 的 key 值則 keySet() 會比較合適一些。
(35)對資源的 close() 建議分開操作。
雖然有些麻煩,卻能避免資源洩露,這其實和 try-catch 機制相關,各自分開 close 各自的 try-catch 就會互不影響,防止寫在一個 try-catch 中因為一個異常了後面的釋放不了。
(36)對於 ThreadLocal 線上程池場景使用前或者使用後一定要先 remove。
因為執行緒池技術做的是一個執行緒重用,這意味著程式碼執行過程中一條執行緒使用完畢並不會被銷燬而是等待下一次的使用,而 Thread 類中持有 ThreadLocal.ThreadLocalMap 的引用,執行緒不銷燬意味著上條執行緒 set 的 ThreadLocal.ThreadLocalMap 中的資料依然存在,那麼在下一條執行緒重用這個 Thread 的時候很可能 get 到的是上條執行緒 set 的資料而不是自己想要的內容。這個問題非常隱晦,一旦出現這個原因導致的錯誤,沒有相關經驗或者沒有紮實的基礎非常難發現這個問題,因此在寫程式碼的時候就要注意這一點,這將給你後續減少很多的工作量。
(37)切記以常量定義的方式替代魔鬼數字,魔鬼數字的存在將極大地降低程式碼可讀性,字串常量是否使用常量定義可以視情況而定。
(38)long 或者 Long 初始賦值時使用大寫的 L 而不是小寫的 l,因為字母 l 極易與數字 1 混淆,這個點非常細節,值得注意。
(39)所有重寫的方法必須保留 @Override 註解。
這麼做可以清楚地知道這個方法由父類繼承而來,同時可以保證重寫成功,此外在抽象類中對方法簽名進行修改,實現類會馬上報出編譯錯誤。
(40)推薦使用 JDK7 中新引入的 Objects 工具類來進行物件的 equals 比較,直接 a.equals(b) 有空指標異常的風險。
(41)迴圈體內不要使用 "+" 進行字串拼接,而直接使用 StringBuilder 不斷 append。
因為每次虛擬機器碰到 "+" 這個操作符對字串進行拼接的時候會 new 出一個 StringBuilder,然後呼叫 append 方法,最後呼叫 toString() 方法轉換字串賦值給物件,所以迴圈多少次,就會 new 出多少個 StringBuilder() 來,這對於記憶體是一種浪費。
(42)不捕獲 Java 類庫中定義的繼承自 RuntimeException 的執行時異常類。
異常處理效率低,RuntimeException 的執行時異常中絕大多數完全可以由程式設計師來規避,比如 ArithmeticException 可以通過判斷除數是否為空來規避,NullPointerException 可以通過判斷物件是否為空來規避,IndexOutOfBoundsException 可以通過判斷陣列/字串長度來規避,ClassCastException 可以通過 instanceof 關鍵字來規避,ConcurrentModificationException 可以使用迭代器來規避。
(43)靜態類、單例類、工廠類將它們的建構函式置為 private。
這是因為靜態類、單例類、工廠類這種類本來我們就不需要外部將它們 new 出來,將建構函式置為 private 之後,保證了這些類不會產生例項物件。
如有披露或問題歡迎留言或者入群探討