一、問題背景
近期公司項目開發過程中,犯了一個小錯誤,在使用java計算金額時,使用double直接使用加減乘除。代碼評審時,同事指出浮點數會出現丟失進度的問題,自己深思了一下,還是自己不夠細心,之前也知道浮點數會丟失精度,但是再開發過程中給忘記了,非常感謝同事的指出。讓我有機會記錄下,在此編寫這篇文章用於記錄下問題出現及其解決方案,加深自己的印象,后續不要再犯這樣低級的錯誤了。也希望各位開發朋友看到后避免此問題,自己也是小白一個,如有寫的錯誤的地方,還望各位大佬積極指出,謝謝!
二、JAVA浮點型知識回顧
float
float屬於Java中的浮點型,也叫單精度浮點型,長度為4字節32bit,變量初始化默認值0.0f,包裝類Float
1. float結構
包含三部分:符號位、指數位、尾數位
符號位(S) | 指數位(E) | 尾數位(M) | |
---|---|---|---|
長度 | 1bit | 8bit | 23bit |
說明 | 0表示正數,1表示負數 | 格式為![]() ![]() ![]() |
形式為1.M或0.M。其中當E=0時,取1.M,稱為正規形式,當E!= 0時,取0.M,稱為非正規形式 |
2. float取值
正規形式:
非正規形式:
根據上面公式很容易計算出float的取值范圍為:
(最小值,當符號位S取1,指數位E取255)
(最大值,當符號位S取0,指數位E取255)
能取到其間的近似數據。
注意:根據指數位和尾數位的取值不同,還有很多特殊情況,如NAN,正無窮,負無窮,但平時基本不會用到,這里不再深入;同時由於是近似值,因此無法表示金額,表示金額建議使用BigDecimal
double
double屬於Java中的浮點型,也叫雙精度浮點型,長度為8字節64bit,變量初始化默認值0.0d,包裝類Double
1. double結構
包含三部分:符號位、指數位、尾數位
符號位(S) | 指數位(E) | 尾數位(M) | |
---|---|---|---|
長度 | 1bit | 11bit | 52bit |
說明 | 0表示正數,1表示負數 | 格式為![]() ![]() ![]() |
形式為1.M或0.M。其中當E=0時,取1.M,稱為正規形式,當E!= 0時,取0.M,稱為非正規形式 |
2. double取值
正規形式:
非正規形式:
根據上面公式很容易計算出double的取值范圍為:
(最小值,當符號位S取1,指數位E取2047)
(最大值,當符號位S取0,指數位E取2047)
能取到其間的近似數據。
注意:根據指數位和尾數位的取值不同,還有很多特殊情況,如NAN,正無窮,負無窮,但平時基本不會用到,這里不再深入;同時由於是近似值,因此無法表示金額,表示金額建議使用BigDecimal
三、問題解析
float和double類型主要是為了科學計算和工程計算而設計的。他們執行二進制浮點運算,這是為了在廣泛的數值范圍上提供較為精確的快速近似計算而精心設計的。然而,它們並沒有提供完全精確的結果,所以不應該被用於需要精確結果的場合。float和double類型尤其不是和用於貨幣計算,因為要讓一個float或者double精確地標識0.1(或者10的任何其他負數次方值)是不能的。
舉例說明代碼如下:
public static void main(String[] args) { //所有金額 double funds = 1; //購買次數 int itemsBought = 0; //初次購買是0.1,后續每次購買都會漲價0.1 for (double price = 0.1; funds >= price; price += 0.1) { funds -= price; itemsBought++; } System.out.println(String.format("一共能買%d件物品", itemsBought)); System.out.println("余額:" + funds); }
運行結果為:
如上運行的是丟失精度問題。
修改代碼如下:
//所有金額 BigDecimal funds = new BigDecimal(1); //購買次數 int itemsBought = 0; //初次購買是0.1,后續每次購買都會漲價0.1 for (BigDecimal price = new BigDecimal("0.1"); funds.compareTo(price) >= 0;price = price.add(new BigDecimal("0.1"))) { funds =funds.subtract(price); itemsBought++; } System.out.println(String.format("一共能買%d件物品", itemsBought)); System.out.println("余額:" + funds);
運行結果為:
Java中的簡單浮點數類型float和double不能夠精確運算。這個問題其實不是JAVA的bug,因為計算機本身是二進制的,而浮點數實際上只是個近似值,所以從二進制轉化為十進制浮點數時,精度容易丟失,導致精度下降。
四、BigDecimal用法
如下:加減乘除用法
/** * 相加 * @param doubleValA * @param doubleValB * @return */ public static double add(String doubleValA, String doubleValB) { BigDecimal a2 = new BigDecimal(doubleValA); BigDecimal b2 = new BigDecimal(doubleValB); return a2.add(b2).doubleValue(); } /** * 相減 * @param doubleValA * @param doubleValB * @return */ public static double sub(String doubleValA, String doubleValB) { BigDecimal a2 = new BigDecimal(doubleValA); BigDecimal b2 = new BigDecimal(doubleValB); return a2.subtract(b2).doubleValue(); } /** * 相乘 * @param doubleValA * @param doubleValB * @return */ public static double mul(String doubleValA, String doubleValB) { BigDecimal a2 = new BigDecimal(doubleValA); BigDecimal b2 = new BigDecimal(doubleValB); return a2.multiply(b2).doubleValue(); } /** * 相除 * @param doubleValA * @param doubleValB * @param scale 除不盡時指定精度 * @return */ public static double div(String doubleValA, String doubleValB, int scale) { BigDecimal a2 = new BigDecimal(doubleValA); BigDecimal b2 = new BigDecimal(doubleValB); return a2.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue(); }
主函數調用結果:
public static void main(String[] args) { String doubleValA = "3.14159267"; String doubleValB = "2.358"; System.out.println("add:" + add(doubleValA, doubleValB)); System.out.println("sub:" + sub(doubleValA, doubleValB)); System.out.println("mul:" + mul(doubleValA, doubleValB)); System.out.println("div:" + div(doubleValA, doubleValB, 8)); }
運行如下:
總結:
1、對於任何需要精確答案的計算任務,請不要使用float或者double。
2、如果你想讓系統來記錄十進制小數點,並且不介意因為不使用基本類型而帶來的不便,就請使用BigDecimal。使用BigDecimal還有一些額外的好處,他允許你完全控制舍入,每當一個操作涉及舍入的時候,他允許你從8種舍入模式中選擇其一。如果你正通過法定要求的舍入行為進行業務計算,使用BigDecimal是非常方便的。
3、如果性能非常關鍵,並且你不介意自己記錄十進制小數點,而且所涉及的數值又不太大,就可以使用int或者long。如果數值范圍內沒有超過9位十進制數字,就可以使用int;如果不超過18位數字,就可以使用long。如果數值可能超過18位數字,就必須使用BigDecimal。