歡迎光臨
每天分享高質量文章

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

點擊上方“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     ...
}

//建議替換為如下
for (int i = 0, length = list.size(); 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 } 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 重寫了 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 之後,保證了這些類不會產生實體物件。

一個致命的 Redis 命令,導致公司損失 400 萬!!

美團三面:一個執行緒OOM,行程里其他執行緒還能運行麽?

設計樣式六大原則,你真的懂了嗎?

6 個實體詳解如何把 if-else 代碼重構成高質量代碼

Get史上最優雅的加密方式!沒有之一!

如何 “幹掉” if…else

淺談String的intern

END


我是 Java 技術驛站,感謝有你


>>>>>> 加群交流技術 <<<<<<

赞(0)

分享創造快樂