JavaRush /Java Blog /Random-TW /FindBugs 幫助您更好地學習 Java
articles
等級 15

FindBugs 幫助您更好地學習 Java

在 Random-TW 群組發布
靜態程式碼分析器很受歡迎,因為它們可以幫助發現由於粗心而造成的錯誤。但更有趣的是,它們有助於糾正由於無知而犯下的錯誤。即使所有內容都寫在該語言的官方文件中,但事實並非所有程式設計師都仔細閱讀過它。程式設計師可以理解:你會厭倦閱讀所有文件。在這方面,靜態分析器就像一位經驗豐富的朋友,坐在您旁邊看著您編寫程式碼。他不僅告訴你:“這是你複製貼上時出錯的地方”,而且還說:“不,你不能這樣寫,你自己看一下文件。” 這樣的朋友比文件本身更有用,因為他只建議你在工作中實際遇到的那些東西,而對那些永遠對你沒有用處的東西保持沉默。在這篇文章中,我將討論我從使用 FindBugs 靜態分析器中學到的一些 Java 複雜性。也許有些事情也會讓你意想不到。重要的是,所有範例都不是推測性的,而是基於真實的程式碼。

三元運算子 ?:

似乎沒有什麼比三元運算子更簡單的了,但它也有其缺陷。我相信這些設計之間沒有根本的區別 Type var = condition ? valTrue : valFalse; ,但 Type var; if(condition) var = valTrue; else var = valFalse; 事實證明這裡有一個微妙之處。由於三元運算子可以是複雜表達式的一部分,因此其結果必須是在編譯時確定的具體類型。因此,假設 if 形式的 true 條件,編譯器將 valTrue 直接引向類型 Type,而以三元運算子的形式,它首先引向公共類型 valTrue 和 valFalse(儘管事實上 valFalse 不是評估),然後結果導致類型Type。如果表達式涉及原始類型及其包裝器(整數、雙精度型等),則轉換規則並非完全微不足道。JLS 15.25 中詳細描述了所有規則。讓我們來看一些例子。 Number n = flag ? new Integer(1) : new Double(2.0); 如果設置了標誌,n 會發生什麼?值為 1.0 的 Double 物件。編譯器發現我們創建物件的笨拙嘗試很有趣。由於第二個和第三個參數是不同基元類型的包裝器,因此編譯器會將它們解包並產生更精確的類型(在本例中為 double)。並且執行完三元運算子進行賦值後,再次進行裝箱。本質上,程式碼與此等效: Number n; if( flag ) n = Double.valueOf((double) ( new Integer(1).intValue() )); else n = Double.valueOf(new Double(2.0).doubleValue()); 從編譯器的角度來看,程式碼沒有任何問題,可以完美編譯。但 FindBugs 給了警告:
BX_UNBOXED_AND_COERCED_FOR_TERNARY_OPERATOR:在TestTernary.main(String[]) 中,原始值被拆箱並強制用於三元運算子。包裝的原始值被拆箱並轉換為另一個原始類型,作為條件三元運算符(b ? e1: e2 運算子)評估的一部分)。Java 的語意規定,如果e1 和e2 是包裝的數值,則這些值將被拆箱並轉換/強制為其公共類型(例如,如果e1 是In​​teger 類型,e2 是Foat 類型,則e1 是拆箱的,轉換為浮點值,並裝箱。參見 JLS 第 15.25 節。當然,FindBugs 也警告 Integer.valueOf(1) 比 new Integer(1) 更有效率,但每個人都已經知道這一點。
或者這個例子: Integer n = flag ? 1 : null; 如果沒有設定標誌,作者希望將 null 放入 n 中。你認為這會起作用嗎?是的。但讓我們把事情複雜化: Integer n = flag1 ? 1 : flag2 ? 2 : null; 看起來沒有太大差別。但是,現在如果兩個標誌都已清除,則此行將引發 NullPointerException。右側三元運算子的選項為 int 和 null,因此結果型別為 Integer。左邊的選項是int和Integer,所以根據Java規則,結果就是int。為此,您需要透過呼叫 intValue 來執行拆箱,這會引發異常。程式碼等效於: Integer n; if( flag1 ) n = Integer.valueOf(1); else { if( flag2 ) n = Integer.valueOf(Integer.valueOf(2).intValue()); else n = Integer.valueOf(((Integer)null).intValue()); } 這裡 FindBugs 產生兩個訊息,這足以懷疑有錯誤:
BX_UNBOXING_IMMEDIATELY_REBOXED:裝箱值被取消裝箱,然後立即在TestTernary.main(String[]) 中重新裝箱NP_NULL_ON_SOME_PATH:TestTernary.main(String[]) 中可能對null 進行空句指標執行,則保證null 值將被取消引用,這將在執行程式碼時產生 NullPointerException。
好吧,關於這個主題的最後一個例子: double[] vals = new double[] {1.0, 2.0, 3.0}; double getVal(int idx) { return (idx < 0 || idx >= vals.length) ? null : vals[idx]; } 這段程式碼不起作用並不奇怪:傳回基本類型的函數如何傳回 null?令人驚訝的是,它編譯沒有問題。好吧,你已經明白為什麼它可以編譯了。

日期格式

若要在 Java 中格式化日期和時間,建議使用實作 DateFormat 介面的類別。例如,它看起來像這樣: public String getDate() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } 通常一個類別會一遍又一遍地使用相同的格式。很多人會產生最佳化的想法:既然可以使用通用實例,為什麼每次都要建立一個格式物件呢? private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public String getDate() { return format.format(new Date()); } 它是如此美麗和酷,但不幸的是它不起作用。更準確地說,它可以工作,但偶爾會崩潰。事實上,DateFormat 的文檔說:
日期格式不同步。建議為每個執行緒建立單獨的格式實例。如果多個執行緒同時存取某種格式,則必須進行外部同步。
如果您查看 SimpleDateFormat 的內部實現,情況確實如此。在執行 format() 方法期間,物件會寫入類別字段,因此兩個執行緒同時使用 SimpleDateFormat 將有一定機率導致不正確的結果。FindBugs 對此的描述如下:
STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE:呼叫 TestDate.getDate() 中靜態 java.text.DateFormat 的方法 如 JavaDoc 所述,DateFormat 對於多執行緒使用本質上是不安全的。檢測器發現對透過靜態欄位取得的 DateFormat 實例的呼叫。這看起來很可疑。有關這方面的更多信息,請參閱 Sun Bug #6231579 和 Sun Bug #6178997。

BigDecimal 的陷阱

在了解 BigDecimal 類別可讓您儲存任意精確度的小數,並且看到它有一個 double 的建構函式後,有些人會認為一切都清楚了,您可以這樣做: System.out.println(new BigDecimal( 1.1));沒有人真正禁止這樣做,但結果可能看起來出乎意料:1.100000000000000088817841970012523233890533447265625。發生這種情況是因為原始 double 以 IEEE754 格式存儲,在這種格式中不可能完美準確地表示 1.1(在二進制數係統中,獲得無限週期分數)。因此,最接近 1.1 的值儲存在那裡。相反,BigDecimal(double) 構造函數的工作方式完全一樣:它完美地將 IEEE754 中的給定數字轉換為十進制形式(最終的二進制小數始終可表示為最終的十進制)。如果您想將 1.1 精確地表示為 BigDecimal,則可以編寫 new BigDecimal("1.1") 或 BigDecimal.valueOf(1.1)。如果你不立即顯示該數字,而是對其進行一些操作,你可能不明白錯誤來自哪裡。FindBugs 發出警告 DMI_BIGDECIMAL_CONSTRUCTED_FROM_DOUBLE,它給出了相同的建議。還有一件事: BigDecimal d1 = new BigDecimal("1.1"); BigDecimal d2 = new BigDecimal("1.10"); System.out.println(d1.equals(d2)); 實際上,d1 和 d2 代表相同的數字,但 equals 傳回 false,因為它不僅比較數字的值,還比較目前的順序(小數位數)。這是在文件中寫的,但是對於 equals 這麼熟悉的方法,很少人會去讀文件。這樣的問題可能不會立即出現。不幸的是,FindBugs 本身並沒有對此發出警告,但它有一個流行的擴展 - fb-contrib,它考慮了這個錯誤:
呼叫 MDM_BIGDECIMAL_EQUALS equals() 來比較兩個 java.math.BigDecimal 數字。這通常是一個錯誤,因為兩個 BigDecimal 物件只有在值和小數位數都相等時才相等,因此 2.0 不等於 2.00。要比較 BigDecimal 物件的數學相等性,請改用compareTo()。

換行符和 printf

通常,在 C 之後轉向 Java 的程式設計師很高興發現PrintStream.printf(以及PrintWriter.printf等)。就像,太好了,我知道,就像 C 語言一樣,你不需要學習任何新事物。實際上是有差異的。其中之一在於行翻譯。C語言分為文字流和二進位流。透過任何方式將 '\n' 字元輸出到文字流都會自動轉換為系統相關的換行符(在 Windows 上為“\r\n”)。Java 中沒有這樣的分離:必須將正確的字元序列傳遞到輸出流。這是自動完成的,例如,透過 PrintStream.println 系列的方法。但是當使用 printf 時,在格式字串中傳遞 '\n' 只是 '\n',而不是系統相關的換行符。例如,讓我們編寫以下程式碼: System.out.printf("%s\n", "str#1"); System.out.println("str#2"); 將結果重新導向到檔案後,我們將看到: FindBugs 幫助您更好地學習 Java - 1 因此,您可以在一個執行緒中得到一種奇怪的換行符組合,這看起來很草率,並且可能會讓某些解析器大吃一驚。該錯誤可能會在很長一段時間內被忽視,特別是如果您主要在 Unix 系統上工作。要使用 printf 插入有效的換行符,需要使用特殊的格式字元「%n」。FindBugs 對此的描述如下:
VA_FORMAT_STRING_USES_NEWLINE:格式字串應在 TestNewline.main(String[]) 中使用 %n 而不是 \n 此格式字串包含換行符 (\n)。在格式字串中,通常最好使用 %n,這將產生特定於平台的行分隔符號。
或許,對某些讀者來說,以上這些都是早已知曉的。但我幾乎可以肯定,對他們來說,靜態分析器將會發出一個有趣的警告,這將向他們揭示所使用的程式語言的新功能。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION