推薦四十多條純乾貨 Java 代碼優化建議

推薦四十多條純乾貨 Java 代碼優化建議

儘量指定類、方法的 final 修飾符

帶有 final 修飾符的類是不可派生的。在 Java 核心 API 中,有許多應用 final 的例子,例如 java.lang.String,整個類都是 final 的。為類指定 final 修飾符可以讓類不可以被繼承,為方法指定 final 修飾符可以讓方法不可以被重寫。如果指定了一個類為 final,則該類所有的方法都是 final 的。Java 編譯器會尋找機會內聯所有的 final 方法,內聯對於提升 Java 運行效率作用重大,具體可以查閱 Java 運行期優化相關資料,此舉能夠使性能平均提高 50%。

儘量重用對象

特別是 String 對象的使用,出現字符串連接時應該使用 StringBuilder/StringBuffer 代替。由於 Java 虛擬機不僅要花時間生成對象,以後可能還需要花時間對這些對象進行垃圾回收和處理,因此生成過多的對象將會給程序的性能帶來很大的影響。

儘可能使用局部變量

調用方法時傳遞的參數以及在調用中創建的臨時變量都保存在棧中,速度較快,其他變量,如靜態變量、實例變量等,都在堆中創建,速度較慢。另外,棧中創建的變量,隨著方法的運行結束,這些內容就沒了,不需要額外的垃圾回收。

及時關閉流。

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() 很大的時候,就減少了很多的消耗。

儘量採用懶加載的策略,即在需要的時候才創建。

這個原則其實就是節約,具體樣例如下。

//不好的示範
String str = "aaa";
if (i == 1) {
  list.add(str);
}
//建議替換為如下
if (i == 1) {
  String str = "aaa";
  list.add(str);

}

慎用異常。

異常對性能不利,拋出異常首先要創建一個新的對象,Throwable 接口的構造函數調用名為 fillInStackTrace() 的本地同步方法,fillInStackTrace() 方法檢查堆棧,收集調用跟蹤信息。只要有異常被拋出,Java 虛擬機就必須調整調用堆棧,因為在處理過程中創建了一個新的對象。異常只能用於錯誤處理,不應該用來控制程序流程。

不要在循環中使用 try-catch,應該把其放在最外層

根據網友們提出的意見,這一點我認為值得商榷,其實分業務場景吧,有些場景需要循環終止,有些只是為了忽略當此循環處理。

如果能估計到待添加的內容長度,為底層以數組方式實現的集合、工具類指定初始長度

比如 ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet 等,以 StringBuilder 為例,StringBuilder() 構造方法默認分配 16 個字符的空間,StringBuilder(int size) 構造方法默認分配 size 個字符的空間,StringBuilder(String str) 構造方法默認分配 16 個字符加 str.length() 個字符空間,所以可以通過類的構造方法來設定它的初始化容量,這樣可以明顯地提升性能。

當複製大量數據時,使用 System.arraycopy() 命令。

這個肯定大家都沒有疑問的,性能優化的實現而已。

乘法和除法使用移位操作。

用移位操作可以極大地提高性能,因為在計算機底層,對位的操作是最方便、最快的,但是移位操作雖然快,可能會使代碼不太好理解,因此最好加上相應的註釋。

//不好的示範
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;
}

循環內不要不斷創建對象引用。

見如下案例解釋分析原因。

//不好的示範
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 罷了,
//但是內存中只有一份,這樣就大大節省了內存空間了。

基於效率和類型檢查的考慮,應該儘可能使用 array,無法確定數組大小時才使用 ArrayList。

儘量使用 HashMap、ArrayList、StringBuilder,除非線程安全需要,否則不推薦使用 Hashtable、Vector、StringBuffer,後三者由於使用同步機制而導致了性能開銷。

不要將數組聲明為 public static final。

因為這毫無意義,這樣只是定義了引用為 static final,數組的內容還是可以隨意改變的,將數組聲明為 public 更是一個安全漏洞,這意味著這個數組可以被外部類所改變。

儘量在合適的場合使用單例。

使用單例可以減輕加載的負擔、縮短加載的時間、提高加載的效率,但並不是所有地方都適用於單例,簡單來說,單例主要適用於以下三個方面:

控制資源的使用,通過線程同步來控制資源的併發訪問;

控制實例的產生,以達到節約資源的目的;

控制數據的共享,在不建立直接關聯的條件下,讓多個不相關的進程或線程之間實現通信;

儘量避免隨意使用靜態變量。

因為當某個對象被定義為 static 的變量所引用,那麼 gc 通常是不會回收這個對象所佔有的堆內存的。

public class A {
private static B b = new B();
}
//此時靜態變量 b 的生命週期與 A 類相同,
//如果 A 類不被卸載,那麼引用 B 指向的 B 對象會常駐內存,直到程序終止。

及時清除不再需要的會話。

為了清除不再活動的會話,許多應用服務器都有默認的會話超時時間,一般為 30 分鐘。當應用服務器需要保存更多的會話時,如果內存不足,那麼操作系統會把部分數據轉移到磁盤,應用服務器也可能根據MRU(最近最頻繁使用)算法把部分不活躍的會話轉儲到磁盤,甚至可能拋出內存不足的異常。如果會話要被轉儲到磁盤,那麼必須要先被序列化,在大規模集群中,對對象進行序列化的代價是很昂貴的。因此,當會話不再需要時,應當及時調用 HttpSession 的 invalidate() 方法清除會話。

實現 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()}
}

使用同步代碼塊替代同步方法。

儘量使用同步代碼塊,避免對那些不需要進行同步的代碼也進行了同步,影響了代碼執行效率。

將常量聲明為 static final,並以大寫命名。

這樣在編譯期間就可以把這些內容放入常量池中,避免運行期間計算生成常量的值。另外,將常量的名字以大寫命名也可以方便區分出常量與變量。

不要創建一些不使用的對象,不要導入一些不使用的類。

這毫無意義,如果代碼中出現 "The value of the local variable i is not used"、"The import java.util is never used",那麼請刪除這些無用的內容,雖說沒啥影響,但是有些時候編譯期會報錯,譬如沒 import 用到的類被刪掉了。

程序運行過程中避免使用反射。

不建議在程序運行過程中使用,除非萬不得已,尤其是頻繁使用反射機制,特別是 Method 的 invoke 方法,如果確實有必要,一種建議性的做法是將那些需要通過反射加載的類在項目啟動的時候通過反射實例化出一個對象並放入內存,用戶只關心和對端交互的時候獲取最快的響應速度,並不關心對端的項目啟動花多久時間。

使用數據庫連接池和線程池。

這兩個池都是用於重用對象的,前者可以避免頻繁地打開和關閉連接,後者可以避免頻繁地創建和銷燬線程。

使用帶緩衝的輸入輸出流進行 IO 操作。

帶緩衝的輸入輸出流,即 BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,這可以極大地提升 IO 效率。

順序插入和隨機訪問比較多的場景使用 ArrayList,元素刪除和中間插入比較多的場景使用 LinkedList。

不要讓 public 方法中有太多的形參。

public 方法即對外提供的方法,如果給這些方法太多形參的話主要壞處是違反了面向對象的編程思想,Java 講求一切都是對象,太多的形參和麵向對象的編程思想並不契合,參數太多勢必導致方法調用的出錯概率增加。

字符串變量和字符串常量 equals 的時候將字符串常量寫在前面,這樣可以避免空指針。

建議使用 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) 在語義上沒有任何區別,從閱讀習慣上講,建議使用前者會更好些。

不要對數組使用 toString() 方法。

本意是想打印出數組內容,卻打出來的是對象信息,甚至有可能因為數組引用為空而導致空指針異常。對於集合 toString() 是可以打印出集合裡面的內容的,因為集合的父類 AbstractCollections 重寫了 Object 的 toString() 方法。

不要對超出範圍的基本數據類型做向下強制轉型。

這很明確,譬如 long 轉 int 是會存在潛在風險的。

公用的集合類中不使用的數據一定要及時 remove 掉。

如果一個集合類是公用的(也就是說不是方法裡面的屬性),那麼這個集合裡面的元素是不會自動釋放的,因為始終有引用指向它們。所以,如果公用集合裡面的某些數據不使用而不去remove掉它們,那麼將會造成這個公用集合不斷增大,使得系統有內存洩露的隱患。

把一個基本數據類型轉為字符串,基本數據類型.toString() 是最快的方式、String.valueOf(數據) 次之、數據+"" 最慢。

因為 String.valueOf() 方法底層調用了 Integer.toString() 方法,但是會在調用前做空判斷;Integer.toString() 是直接調用;i + "" 底層使用了 StringBuilder 實現,先用 append 方法拼接,再用 toString() 方法獲取字符串。

使用最有效率的方式去遍歷 Map。

遍歷 Map 的方式有很多,通常場景下我們需要的是遍歷 Map 中的 Key 和 Value,那麼推薦使用的、效率最高的方式是 entrySet(),如果只是想遍歷一下這個 Map 的 key 值則 keySet() 會比較合適一些。

對資源的 close() 建議分開操作。

雖然有些麻煩,卻能避免資源洩露,這其實和 try-catch 機制相關,各自分開 close 各自的 try-catch 就會互不影響,防止寫在一個 try-catch 中因為一個異常了後面的釋放不了。

對於 ThreadLocal 在線程池場景使用前或者使用後一定要先 remove。

因為線程池技術做的是一個線程重用,這意味著代碼運行過程中一條線程使用完畢並不會被銷燬而是等待下一次的使用,而 Thread 類中持有 ThreadLocal.ThreadLocalMap 的引用,線程不銷燬意味著上條線程 set 的 ThreadLocal.ThreadLocalMap 中的數據依然存在,那麼在下一條線程重用這個 Thread 的時候很可能 get 到的是上條線程 set 的數據而不是自己想要的內容。這個問題非常隱晦,一旦出現這個原因導致的錯誤,沒有相關經驗或者沒有紮實的基礎非常難發現這個問題,因此在寫代碼的時候就要注意這一點,這將給你後續減少很多的工作量。

切記以常量定義的方式替代魔鬼數字,魔鬼數字的存在將極大地降低代碼可讀性,字符串常量是否使用常量定義可以視情況而定。

long 或者 Long 初始賦值時使用大寫的 L 而不是小寫的 l,因為字母 l 極易與數字 1 混淆,這個點非常細節,值得注意。

所有重寫的方法必須保留 @Override 註解。

這麼做可以清楚地知道這個方法由父類繼承而來,同時可以保證重寫成功,此外在抽象類中對方法簽名進行修改,實現類會馬上報出編譯錯誤。

推薦使用 JDK7 中新引入的 Objects 工具類來進行對象的 equals 比較,直接 a.equals(b) 有空指針異常的風險。

循環體內不要使用 "+" 進行字符串拼接,而直接使用 StringBuilder 不斷 append。

因為每次虛擬機碰到 "+" 這個操作符對字符串進行拼接的時候會 new 出一個 StringBuilder,然後調用 append 方法,最後調用 toString() 方法轉換字符串賦值給對象,所以循環多少次,就會 new 出多少個 StringBuilder() 來,這對於內存是一種浪費。

不捕獲 Java 類庫中定義的繼承自 RuntimeException 的運行時異常類。

異常處理效率低,RuntimeException 的運行時異常中絕大多數完全可以由程序員來規避,比如 ArithmeticException 可以通過判斷除數是否為空來規避,NullPointerException 可以通過判斷對象是否為空來規避,IndexOutOfBoundsException 可以通過判斷數組/字符串長度來規避,ClassCastException 可以通過 instanceof 關鍵字來規避,ConcurrentModificationException 可以使用迭代器來規避。

靜態類、單例類、工廠類將它們的構造函數置為 private。

這是因為靜態類、單例類、工廠類這種類本來我們就不需要外部將它們 new 出來,將構造函數置為 private 之後,保證了這些類不會產生實例對象


分享到:


相關文章: