1. 踩坑經歷
上周,一個用戶反饋他創建的某個銷售單無法打開,但其余銷售單都可以正常打開,當時查看了生產環境的ERROR日志,發現拋了這樣的異常:java.lang.NumberFormatException: For input string: "E"
。
相信大家對這個異常都不陌生,很顯然,是因為將字符串轉換為數字時拋出的,比如下面這樣:
但仔細查看了用戶報錯的單據,也沒有發現哪里有輸入“E”這樣的字符串(請原諒我第一時間沒有想到是科學計數法造成的,哈哈),最后把生產環境的這條數據插入到了開發環境中,定位到原來是因為將金額轉換為大寫時導致的,報錯的關鍵代碼如下所示:
String totalAmountStr = String.valueOf(totalAmount / 100.0);
String amountCN = MoneyUtils.toChinese(totalAmountStr);
其中totalAmount是一個long類型的變量,之所以除以100.0,是因為我們數據庫中存儲金額都是按分為單位存儲的(相信很多小伙伴也是這么存儲的),第2行代碼主要是為了將金額轉換為大寫,比如將105000.50轉換為壹拾萬零伍仟元伍角。
用戶報錯的那個單據,totalAmount為2700萬,轉換為分就是:2700000000,執行完totalAmount / 100.0
,輸出結果竟然是2.7E7,而不是預期的27000000,因此導致了異常:java.lang.NumberFormatException: For input string: "E"
。
最后的解決方案是將金額轉換為BigDecimal來處理,代碼修改為如下所示:
String totalAmountStr = new BigDecimal(String.valueOf(totalAmount)).divide(new BigDecimal("100"),2, RoundingMode.HALF_UP).toString();
String amountCN = MoneyUtils.toChinese(totalAmountStr);
2. 原因分析
在Java中,當浮點數(float、double)的整數部分達到8位及以上,會以科學計數法表示,如下所示:
double firstAmount = 2700000D;
double secondAmount = 27000000D;
double thirdAmount = 2700000.25D;
double fourthAmount = 27000000.25D;
System.out.println(firstAmount);
System.out.println(secondAmount);
System.out.println(thirdAmount);
System.out.println(fourthAmount);
默默數了下,整數部分8位的話,都是千萬級別了,估計遇到這個問題的用戶很豪,哈哈。
所以使用double來表示金額,當金額遇到科學計數法時,就會顯示不正常、甚至造成一些意想不到的異常。
3. 解決方案
如果不想用科學計數法顯示,而是顯示金額本身,有以下2種解決方案:
- 使用NumberFormat
- 使用BigDecimal
3.1 方案一:使用NumberFormat
使用NumberFormat的方法如下所示:
NumberFormat numberFormat = NumberFormat.getInstance();
numberFormat.setGroupingUsed(false);
double secondAmount = 27000000D;
double fourthAmount = 27000000.25D;
System.out.println(numberFormat.format(secondAmount));
System.out.println(numberFormat.format(fourthAmount));
當將numberFormat.setGroupingUsed(false);
注釋掉或者修改為numberFormat.setGroupingUsed(true);
時,輸出結果就變為了:
3.2 方案二:使用BigDecimal(推薦)
使用BigDecimal的方法如下所示:
double secondAmount = 27000000D;
double fourthAmount = 27000000.25D;
System.out.println(new BigDecimal(String.valueOf(secondAmount)).setScale(2,RoundingMode.HALF_UP).toString());
System.out.println(new BigDecimal(String.valueOf(fourthAmount)).setScale(2,RoundingMode.HALF_UP).toString());
相比而言,我更推薦使用BigDecimal的這種方案。
關於BigDecimal的更多用法,可以查看我寫的另一篇博客:Java BigDecimal使用指南。