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

一猿小講發表於2020-07-02

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

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

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

相關文章