程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java理論與實踐: 消除bug

Java理論與實踐: 消除bug

編輯:關於JAVA

很多有關編程風格的建議都是為了創建高質量、可維護的代碼,這很合理, 因為最容易修復 bug 的時間就是在產生 bug 之前(少量的預防措施……)。遺 憾的是,只預防往往是不夠的,雖然有一些精巧的工具可以幫助您創建好的代碼 ,但是很少有工具可以幫助您分析、維護或提高現有代碼的質量。

寫線程安全的類很難,而分析現有類的線程安全性更難,增強類使其仍然保 持線程安全也很難。以隱含假定、不變式以及預期用例(雖然在開發人員的頭腦 中很清晰,但是沒有以設計筆記、注釋或者文檔的方式記錄下來)的方式編寫完 類之後,人們很快就不再了解類的工作方式(或者應該如何工作),現有代碼總 是比新代碼難以使用。

需求:更好的代碼審核工具

當然,確保高質量代碼的最佳時機就是在編寫代碼時,因為在這個時期您最 了解它的組織方式。關於如何編寫高質量代碼可以找到很多建議(閱讀本欄目即 可!),但是未必能從頭編寫所有代碼或花很多時間來編寫它。那麼在這種情況 下該怎麼辦?開發人員通常喜歡重新編寫代碼(畢竟,與修復他人的代碼或修復 自己編寫但 bug 很多的代碼相比,編寫新代碼有趣得多),但是這也是一種奢 侈,並且通常只是用今天已知的錯誤與明天未知的錯誤交換。您需要的是下面這 種工具:分析和審核現有的代碼庫以幫助開發人員進行代碼審核並找出 bug。

我很高興地說,隨著 FindBugs 的引入,在自動代碼檢測和審核工具方面已 經取得重大進步。到目前為止,大多數檢測工具要麼極力試圖證明程序是正確的 ,要麼注重一些表面問題,如代碼的格式編排和命名規則,最多還關注一些簡單 的 bug 模式,如自賦值、未使用的域或潛在的錯誤(如未使用的方法參數,或 可以聲明為私有或保護的方法被聲明為公共的)。但是 FindBugs 不同,它利用 字節碼分析和很多內置的 bug 模式檢測器來查找代碼中的常見 bug。它可以幫 助您找出代碼的哪些位置有意或者無意地偏離了良好的設計原理。(有關 FindBugs 的介紹,請參閱 Chris Grindstaff 的文章,“ FindBugs,第 1 部 分: 提高代碼質量”和“ FindBugs,第 2 部分: 編寫自定義檢測器”。)

設計建議和 bug 模式

對於每種 bug 模式,設計建議中都存在相應的預防要素,用於告誡我們避免 這種 bug 模式。因此如果 FindBugs 是 bug 模式檢測器,那麼它理所當然可以 用作審核工具,衡量代碼與一組設計原理的符合程度。Java 理論與實踐的很多 期文章都專門講述設計建議的具體要素(或相應的 bug 模式)。在這一期,我 將解釋 FindBugs 如何確保現有代碼庫遵循設計建議。讓我們以新方式重復前面 的一些建議,並了解在沒有遵守這些建議時,FindBugs 如何幫助檢測。

關於異常的爭論

在“ Java 理論與實踐: 關於異常的爭論”中,反對檢查型異常的一個論據 是:“摸索”(也就是捕獲)這種異常太容易了,並且它既不采取修正行為,也 不拋出其他異常,如清單 1 所示。在原型設計中,有時僅僅為了使程序編譯, 編寫空的 catch 塊,目的是以後返回並填充某種錯誤處理策略,這時經常出現 這種“摸索”。雖然一些人提供發生這種情景的頻率,是為了作為例子說明 Java 語言設計采用的異常處理方法的不易操作性,但是我認為這僅僅是錯誤地 使用了正確的工具。FindBugs 可以方便地檢測和標記這些空的 catch 塊。如果 想要忽略這種異常,可以方便地給該異常添加描述性注釋,這樣讀者就知道您是 有意的忽略它,而不是僅僅忘了處理。

清單 1. “摸索”異常

try {
  mumbleFoo();
}
catch (MumbleFooException e) {
}

哈希

在“ Java 理論與實踐: 哈希”中,我略述了正確地重載 Object.equals() 和 Object.hashCode() 的基本規則,特別是相等對象(根據 equals() ) 的 hashCode() 值必須相等。雖然只要了解了這項規則,遵守起來就相當簡單(並 且有些 IDE 包含一些向導,用於以一致的風格為您定義這兩個方法),但是如 果重載了其中一個方法,而忘記重載另一個方法,那麼通過檢測很難找出 bug, 因為錯誤並非位於存在的代碼中,而是位於不存在碼中。

FindBugs 有一個檢測器用於檢測這個問題的很多實例,如重載了 equals() 但沒有重載 hashCode() ,或重載了 hashCode() 但沒有重載 equals() 。這些 檢測器是 FindBugs 中最簡單的,因為它們只需要檢查該類中一組方法簽名,並 確定是否同時重載了 equals() 和 hashCode() 。還可能錯誤地使用 Object 之 外的參數類型定義 equals() ;雖然這個構造是合法的,但是它的行為和您想像 的不同。Covariant Equals 檢測器將檢測如下有問題的重載:

public void boolean equals(Foo other) { ... }

與這個檢測器相關的是 Confusing Method Names 檢測器,它是對名稱類似 hashcode() 和 tostring() 的方法觸發的,對於下面這些類也會觸發這個檢測 器:具有一些只在名稱大小寫方面存在差異的方法,或者其方法與超類構造函數 的名稱相同。雖然根據該語言的規范,這些方法名稱是合法的,但是它們可能不 是您想要的。類似地,如果域 serialVersionUID 不是 final ,不是 long , 也不是 static ,就會觸發 Serialization 檢測器。

Finalizer 不是朋友

在“ Garbage collection and performance”中,我盡力阻止使用 finalizer。Finalizer 需要犧牲很多性能,並且它們不能(甚至完全不能)保 證在預計的時間段運行。仍然有些時候需要使用 finalizer,而這樣做的過程中 可能產生很多錯誤。如果必須使用 finalizer,通常應該如清單 2 所示來組織 它:

清單 2. 正確的 finalizer 定義

protected void finalize() {
   try {
    doStuff();
   }
   finally {
    super.finalize();
   }
  }

FindBugs 檢測很多有問題的 finalizer 構造,如:

空的 finalizer(它抵消超類 finalizer 的作用)。

不實現任何功能的 finalizer(它只調用 super.finalize() ,但是這對運 行時優化可能造成一些損害)。

顯式的 finalizer 調用(從用戶代碼中調用 finalize() )。

公共 finalizer(finalizer 應該聲明為 protected )。

沒有調用 super.finalize() 的 finalizer。

這些 bug 模式的例子如清單 3 所示:

清單 3. 常見的 finalizer 錯誤

// negates effect of superclass finalizer
  protected void finalize() { }
  // fails to call superclass finalize method
  protected void finalize() { doSomething(); }
  // useless (or worse) finalizer
  protected void finalize() { super.finalize(); }
  // public finalizer
  public void finalize() { try { doSomething(); } finally { super.finalize() } }

在“ Garbage collection and performance”中,還講到另一種垃圾收集危 險:顯式地調用 System.gc() 。這種顯式的調用幾乎完全是“幫助”或“欺騙 ”來機收集器的誤導嘗試,並且它們最終經常損害性能,而不是對其有利。 FindBugs 可以檢測顯式的 System.gc() 調用,並標記它們(在 Sun JVM 上, 還可以使用 -XX:+DisableExplicitGC 啟動選項,禁用顯式的垃圾收集)。

安全構造技術

在“ Java 理論和實踐:安全構造技術”中,我展示了允許對象的引用逃避 其構造函數如何導致一些嚴重的問題。從那時起,允許 this 引用逃避構造的風 險變得越來越嚴重。如果允許對象的引用逃避其構造函數,新的 Java Memory Model(如 JSR 133 所指定,並由 JDK 1.5 實現的)抵消了所有初始化安全保 證。

對象的引用可以以幾種方式逃避它的構造函數,直接和簡接都可以。絕對不 可以將 this 引用保存在靜態變量或數據結構中,但是有更微妙的方式允許引用 逃避構造,如公布對非靜態內部類的引用,或者從構造函數中啟動一個線程(這 幾乎總是公布對新線程的引用)。FindBugs 有一個檢測器,用於尋找從構造函 數啟動線程的實例,雖然目前它不能檢測所有這些危險,但是未來的版本很可能 包括用於其他初始化安全模式的檢測器。

為內存模型帶來好處

在“ 修復 Java 內存模型,第 1 部分”中,我回顧了同步的基本規則:只 要讀取可能由其他線程寫入的變量,或者寫入隨後由其他線程讀取的變量,就必 須進行同步。很容易“忘記”這個規則,特別是在讀取時 —— 但是這麼做可以 造成很多有關程序線程安全的風險。這種 bug 通常是在維護類時引入的:這個 類原來是正確同步的,但是維護人員並沒有完全理解線程安全需求。

幸運的是,FindBugs 擁有大量的檢測器,它們可以幫助識別錯誤同步的類。 Inconsistent Synchronization 檢測器很可能是 FindBugs 所使用的最復雜的 檢測器;它必須分析整個程序,而不僅僅是單個方法,使用數據流分析來確定什 麼時候加鎖,並使用直觀推斷來推出一個類想要提供線程安全保證。基本上,對 於每個域,它都會查看該域的訪問模式,並且如果大多數訪問都是同步實現的, 那麼沒有同步的訪問將被標記為可能的錯誤。類似地,如果一個屬性的設置函數 是同步的,而獲取函數不是,那麼 Inconsistent Synchronization 檢測器將生 成一條警告。

除了 inconsistent synchronization 之外,FindBugs 還包含其他很多用於 檢測常見線程錯誤的檢測器,如在加鎖兩次的情況下等待監視器(這雖然不一定 是 bug,但是可能導致死鎖),使用雙檢測加鎖模式,不正確地初始化非易失性 的域,對線程調用 run() 而不是啟動線程,從構造函數中調用 Thread.start() ,或者沒有將 wait() 包裝到循環中就調用它。

變化,或不變化

在“ 變還是不變?”(和其他文章中),我贊揚了不可變的優點,不可變對 象不能進入不穩定的狀態。它們在本質上就是線程安全的(假設它們的不可變性 是通過使用 final 關鍵字保證的),並且您可以隨意共享和緩存對不可變對象 的引用,而不必復制或者克隆它們。

Java 語言中包括 final 關鍵字是為了幫助開發人員創建不可變類,並允許 編譯器和運行時環境以聲明的不可變性為基礎進行優化。然而,雖然域可以是 final,但是數組元素不可以。通過正確地使用 final 和 private 域,可以使 對象成為不可變的,但是如果對象的狀態包括數組,那麼防止對這些內部數組的 引用逃避該類的方法是很重要的。清單 4 展示的類嘗試成為不可變的,但是不 是,因為在調用 getStates() 之後,調用者可以修改狀態數組。(相關的可能 bug 是在可變類可能返回可變數組的引用時,並且在調用者使用這個數組時,它 的內容可能已經更改了。)雖然通常將其看作一種“惡意代碼”脆弱性(並且很 多開發人員並不關心“惡意代碼”,因為他們的系統並不加載“不受信任”的類 ),但是這種習慣仍然可能導致各種與惡意代碼無關的問題。返回一個不可修改 的 List 或者在返回之前克隆該數組可能更好。FindBugs 可以檢測類似 getStates() 中的錯誤(如清單 4 所示)—— 雖然它不必知道 States 類是假 定為不可變的,但是知道這個設置函數返回了可變私有數組的句柄,並且相應地 做了標記。

清單 4. 錯誤地返回可變數組的引用

public class States {
   private final String[] states = { "AL", "AR", "AZ", ... };
   public boolean isState(String stateCandidate) { ... }
   public String[] getStates() { return states; }
  }

bug 都很重要

FindBugs 確實是一種不尋常的工具,它幾乎可以在任何時間找出實際的 bug 。您可能認為它搜索的一些變量自賦值之類的 bug 模式,它們太微不足道了, 以至於不必麻煩地查找,但是您錯了 —— FindBugs 的每個檢測器都已經在測 試、產品、專業的開發代碼中發現了 bug。您的代碼中是否潛藏著未知的 bug? 下載一個 FindBugs,並嘗試對您的代碼使用它。結果可能會啟發(和干擾)您 。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved