Code Review 是一場苦澀但有意思的修行。書接上篇,本次繼續探討一下,該如何寫出健壯的程式碼?
一、編碼時:看似順眼,實則不然。
舉個例子:
String amount = request.getParameter("amount"); // 校驗金額小數點後最多兩位小數 BigDecimal a = new BigDecimal(amount); if (a.doubleValue() * 100 - Math.floor(a.doubleValue() * 100) != 0) { System.out.println("交易金額錯誤"); // do something ... ... }
摘一段跑在生產環境上的程式碼,程式碼咋一看沒啥問題,主要功能是獲取請求引數;然後完成資料校驗。
看似很順眼,但是你細品,就會發現其中之奧祕,下面一起在本地跑跑程式碼,來分析一下到底會存在什麼問題?
問題一:坑死人的 NPE
輸入:
null(當 amount 輸入為空時)
輸出:
Exception in thread "main" java.lang.NullPointerException at java.math.BigDecimal.<init>(BigDecimal.java:806) at PayController.main(PayController.java:300)
分析:
根據上面異常資訊,見 BigDecimal 的原始碼第 806 行,如下圖所示,很顯然 BigDecimal 構造不會判斷傳入的 val 是否為空,所以會出現空指標異常。
目前沒有出現問題,那隻能算慶幸,不過終究是個定時炸彈。切記呼叫 BigDecimal 的構造時,請勿傳入 null 值。
心聲:
身邊老碼農真真的排查了好長時間,問題場景與此類似,直接阻斷了程式後續的流程。
問題二:同樣是傳入數字,結果咋就匪夷所思。
輸入:
6666.66(當 amount 輸入為 6666.66)
當 amount 輸入為 6666.66 時,amount 的值校驗通過。
真的是看到的這個樣子嗎?換個數試試唄。
輸入:
8888.88(當 amount 輸入為 8888.88)
輸出:交易金額錯誤
分析:容我拆解一下程式碼,當 amount 傳入為 8888.88 時:
double d1 = a.doubleValue() * 100; double d2 = Math.floor(a.doubleValue() * 100); System.out.println(d1); // 輸出:888887.9999999999 System.out.println(d2); // 輸出:888887.0 System.out.println(d1 - d2); // 輸出:0.9999999998835847
很顯然, d1 - d2 的值 != 0,那麼如下表示式的值則滿足,會輸出交易金額錯誤。
為什麼呢?歸根揭底是 double 運算時精度丟失而導致程式處理出錯,雖然在 Java 中提倡用 BigDecimal 進行四則運算,但是上面的校驗實現,貌似跟 BigDecimal 沒有啥關係,到底該怎麼解決呢?
不費腦簡單實現方式:
if (amount.contains(".") && amount.substring(amount.indexOf(".") + 1).length() > 2) { System.out.println("校驗失敗 2"); // do something ... ... }
如上面程式碼段所示,直接判斷傳入的 amount 字串小數點後面的位數就可以啦。
當然,仁者見仁智者見智,實現方式有很多,不去多深究。
二、編碼時:時間轉換也作祟。
舉個例子:
public static long convertDaysToMilliseconds(int days) { return 1000 * 3600 * 24 * days; }
分析:1000 * 3600 * 24 * days 結果預設為 int 型別,最大值為 2147483647,如果超過 int 範圍,則會出現截斷,程式不會出錯,但是結果卻匪夷所思。
例如:當 days 輸入為 30 時,程式輸出:-1702967296。
改進方式一:
改進方式二:
再舉個例子:
public static Date getDate(int seconds) { return new Date(seconds * 1000); }
分析:當 seconds * 1000 值為 int 型別,當超過 int 最大值為2147483647 時,程式不會出錯,但是結果卻匪夷所思。
改進方式:
分享一下心聲:
1. 禁止使用 double 直接參與金額運算,會出現意想不到的結果。
浮點數採用“尾數+階碼”的編碼方式,類似於科學計數法的“有效數字+指數”的表示方式。 二進位制無法精確表示大部分的十進位制小數。 —— 請自行科普,留作業。
2. 禁止使用構造方法 BigDecimal(double)的方式把 double 值轉化為 BigDecimal 物件。
BigDecimal(double)存在精度損失風險,在精確計算或值比較的場景中可能會導致業務邏輯異常。 如:BigDecimal g = new BigDecimal(0.1f); 實際的儲存值為:0.10000000149 優先推薦入參為 String 的構造方法,或使用 BigDecimal 的 valueOf 方法,此方法內部其實執行了 Double 的 toString,而 Double 的 toString 按 double 的實際能表達的精度對尾數進行了截斷。 —— 阿里開發手冊
3. 那些看似順眼的程式碼,或者線上跑著的程式碼,未必就沒問題,只是沒有走到異常分支上去,隨著時間的推移,定時炸彈遲早會爆,定期審查程式碼,以及充分的測試是非常的必要。
三、編碼時:少一點不行。
壞習慣一:記錄日誌時,缺失引數。
反例:
正解:
1. 日誌列印時,佔位符 {} 要嚴格與引數相對應,如果對應不上,按照截圖示意,日誌輸出則不會列印 queryString 的引數,會直接輸出 {},但是某些版本下會出現空指標異常。
2.說一句廢話:圖中的 isVarfiy 是什麼鬼?莫非是 isVerify,單詞好好拼,千萬別拼錯,不然易被後人拍磚。
壞習慣二:記錄日誌時,缺失佔位符 {}。
反例:
正解:
類似的這種問題,多數程式設計師都犯過。記錄日誌時佔位符少,而引數值多,日誌輸出時想列印的引數,日誌中卻沒有列印。
如上面截圖中程式碼所示,想輸出請求的 queryString,但是由於缺失對應的佔位符 {},則不會列印到日誌中。
四、寄語寫最後
老子曰:有道無術,術尚可求也。有術無道,止於術。
莊子曰:以道馭術,術必成。離道之術,術必衰。
古人曰:上人用道,中人用術,下人用力。
小猿曰:管它什麼道與術,能助力搬磚採石就足矣,因為我等採石之人心懷大教堂之願景,哈哈。
常在河邊站哪有不溼鞋,金無足赤人無完人,再牛逼的團隊,編碼都會有出 Bug 的時候。近期微信公眾號推出了一個專輯功能,而我迫不及待的想體驗。
誰成想,當我點選建立專輯時,輸入專輯名稱「碼農心聲」等資訊,然後點選儲存,卻發現列表頁面出現了多個「碼農心聲」,而且趕緊截了個圖,不知道是不是個 Bug?
But who cares?多出來的直接刪除就行啦,又不影響使用。關注同名公眾號:一猿小講,回覆「1024」可以獲取精心為您準備的職場打怪進階資料。
好了,程式碼修煉的系列分享,本次就談到這裡,不知道有多少是觸動了你的心絃,希望有則改之。
一起聊技術、談業務、噴架構,少走彎路,不踩大坑。會持續輸出原創精彩分享,敬請期待!