【代碼修煉系列分享】改掉這些壞習慣,還怕寫不出健壯的代碼?(二)


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」可以獲取精心為您准備的職場打怪進階資料。

好了,代碼修煉的系列分享,本次就談到這里,不知道有多少是觸動了你的心弦,希望有則改之。

一起聊技術、談業務、噴架構,少走彎路,不踩大坑。會持續輸出原創精彩分享,敬請期待!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM