BigDecimal的應用場景及使用方法

一、簡述

在很多編程語言中,浮點數類型float和double運算會丟失精度。

在大多數情況下,計算的結果是準確的,float和double只能用來做科學計算或者是工程計算,在銀行、帳戶、計費等領域,BigDecimal提供了精確的數值計算。

Java在商業計算中要用 java.math.BigDecimal

   public static void main(String[] args) {
     System.out.println(0.05 + 0.01);  // 0.060000000000000005
     System.out.println(1.0 - 0.42);   // 0.5800000000000001
     System.out.println(4.015 * 100);  // 401.49999999999994
     System.out.println(123.3 / 100);  // 1.2329999999999999
     System.out.println(Math.round(4.015 * 100) / 100.0);  // 4.01 四捨五入保留兩位
   }





java.math.BigDecimal:不可變的、任意精度的有符號十進制數。BigDecimal 由任意精度的整數非標度值(unscaledValue)和32位的整數標度(scale)組成。其值爲該數的非標度值乘以10的負scale次冪,即爲(unscaledValue * 10-scale)。與之相關的還有兩個類:  1、java.math.MathContext  該對象是封裝上下文設置的不可變對象,它描述數字運算符的某些規則,如數據的精度,舍入方式等。  2、java.math.RoundingMode  這是一種枚舉類型,定義了很多常用的數據舍入方式。這個類用起來還是很比較複雜的,原因在於舍入模式,數據運算規則太多,不是數學專業出身的人看着中文API都難以理解,這些規則在實際中使用的時候再翻閱都來得及。RoundingMode這個對象可以通過MathContext這個對象來獲取。

二、方法使用

1、構造函數的使用

BigDecimal有多種構造函數,常用的有2種。建議使用String構造方式,不建議使用double構造方式。

 /**
  *  強制使用String的構造函數,double也有可能計算不太準確
  *  原則是使用BigDecimal並且一定要用String來構造。
  */
  public BigDecimal(int);       創建一個具有參數所指定整數值的對象
  public BigDecimal(double);    創建一個具有參數所指定雙精度值的對象
  public BigDecimal(long);      創建一個具有參數所指定長整數值的對象
  public BigDecimal(String);    創建一個具有參數所指定以字符串表示的數值的對象






1)參數類型爲double的構造方法的結果有一定的不可預知性。在Java中寫入newBigDecimal(0.1)實際上等於0.1000000000000000055511151231257827021181583404541015625。這是因爲0.1無法準確地表示爲 double(或者說對於該情況,不能表示爲任何有限長度的二進制小數)。這樣,傳入到構造方法的值不會正好等於 0.1(雖然表面上等於該值)。

2)String 構造方法是完全可預知的:寫入 newBigDecimal("0.1") 將創建一個 BigDecimal,它正好等於預期的 0.1。因此,比較而言,通常建議優先使用String構造方法。

3)當double必須用作BigDecimal對象時,最好先使用Double.toString(double)方法,或者String.valueOf(double)將double轉換爲String,然後使用BigDecimal(String)構造方法。

2、 BigDecimal類中函數的使用

 public BigDecimal add(BigDecimal)       BigDecimal對象中的值相加,然後返回這個對象
 public BigDecimal subtract(BigDecimal)  BigDecimal對象中的值相減,然後返回這個對象
 public BigDecimal multiply(BigDecimal)  BigDecimal對象中的值相乘,然後返回這個對象
 public BigDecimal divide(BigDecimal)    BigDecimal對象中的值相除,然後返回這個對象
 public BigDecimal toString()            將BigDecimal對象的數值轉換成字符串    
 public BigDecimal doubleValue()         將BigDecimal對象中的值以雙精度數返回  
 public BigDecimal floatValue()          將BigDecimal對象中的值以單精度數返回  
 public BigDecimal longValue()           將BigDecimal對象中的值以長整數返回    
 public BigDecimal intValue()            將BigDecimal對象中的值以整數返回    







函數使用如下:

 //儘量用字符串的形式初始化(構造對象)
 BigDecimal StringFir = new BigDecimal("0.005");
 BigDecimal stringSec = new BigDecimal("1000000");
 BigDecimal stringThi = new BigDecimal("-1000000");
 
 BigDecimal doubleFir = new BigDecimal(0.005);
 BigDecimal doubleSec = new BigDecimal(1000000);
 BigDecimal doubleThi = new BigDecimal(-1000000);
 
 //加法
 BigDecimal addVal = doubleFir.add(doubleSec);
 System.out.println("加法用double結果:" + addVal);
 BigDecimal addStr = StringFir.add(stringSec);
 System.out.println("加法用string結果:" + addStr);
 
 //減法
 BigDecimal subtractVal = doubleFir.subtract(doubleSec);
 System.out.println("減法用double結果:" + subtractVal);
 BigDecimal subtractStr = StringFir.subtract(stringSec);
 System.out.println("減法用string結果:" + subtractStr);
 
 //乘法
 BigDecimal multiplyVal = doubleFir.multiply(doubleSec);
 System.out.println("乘法用double結果:" + multiplyVal);
 BigDecimal multiplyStr = StringFir.multiply(stringSec);
 System.out.println("乘法用string結果:" + multiplyStr);
 
 //除法
 BigDecimal divideVal = doubleSec.divide(doubleFir, 20, BigDecimal.ROUND_HALF_UP);
 System.out.println("除法用double結果:" + divideVal);
 BigDecimal divideStr = stringSec.divide(StringFir, 20, BigDecimal.ROUND_HALF_UP);
 System.out.println("除法用string結果:" + divideStr);
 
 //絕對值
 BigDecimal absVal = doubleThi.abs();
 System.out.println("絕對值用double結果:" + absVal);
 BigDecimal absStr = stringThi.abs();
 System.out.println("絕對值用string結果:" + absStr);




































結果打印如下:

 加法用double結果:1000000.005000000000000000104083408558608425664715468883514404296875
 加法用string結果:1000000.005
 減法用double結果:-999999.994999999999999999895916591441391574335284531116485595703125
 減法用string結果:-999999.995
 乘法用double結果:5000.000000000000104083408558608425664715468883514404296875000000
 乘法用string結果:5000.000
 除法用double結果:199999999.99999999583666365766
 除法用string結果:200000000.00000000000000000000
 絕對值用double結果:1000000
 絕對值用string結果:1000000








總結:

  • System.out.println()中的數字默認是double類型的,double類型小數計算不精準。

  • 使用BigDecimal類構造方法傳入double類型時,計算的結果也是不精確的。

因爲不是所有的浮點數都能夠被精確的表示成一個double 類型值,因此它會被表示成與它最接近的 double 類型的值。必須改用傳入String的構造方法。這一點在BigDecimal類的構造方法註釋中有說明。

三、舍入模式

  1.ROUND_UP

舍入遠離零的舍入模式。在丟棄非零部分之前始終增加數字(始終對非零捨棄部分前面的數字加1)。注意,此舍入模式始終不會減少計算值的大小。

  2.ROUND_DOWN

接近零的舍入模式。在丟棄某部分之前始終不增加數字(從不對捨棄部分前面的數字加1,即截短)。注意,此舍入模式始終不會增加計算值的大小。

 3.ROUND_CEILING

接近正無窮大的舍入模式。如果 BigDecimal 爲正,則舍入行爲與 ROUND_UP 相同;如果爲負,則舍入行爲與 ROUND_DOWN 相同。注意,此舍入模式始終不會減少計算值。

 4.ROUND_FLOOR

接近負無窮大的舍入模式。如果 BigDecimal 爲正,則舍入行爲與 ROUND_DOWN 相同;如果爲負,則舍入行爲與 ROUND_UP 相同。注意,此舍入模式始終不會增加計算值。

 5.ROUND_HALF_UP

向“最接近的”數字舍入,如果與兩個相鄰數字的距離相等,則爲向上舍入的舍入模式。如果捨棄部分 >= 0.5,則舍入行爲與 ROUND_UP 相同;否則舍入行爲與 ROUND_DOWN 相同。注意,這是我們大多數人在小學時就學過的舍入模式(四捨五入)。

 6.ROUND_HALF_DOWN

向“最接近的”數字舍入,如果與兩個相鄰數字的距離相等,則爲上舍入的舍入模式。如果捨棄部分 > 0.5,則舍入行爲與 ROUND_UP 相同;否則舍入行爲與 ROUND_DOWN 相同(五舍六入)。

 7.ROUND_HALF_EVEN

向“最接近的”數字舍入,如果與兩個相鄰數字的距離相等,則向相鄰的偶數舍入。如果捨棄部分左邊的數字爲奇數,則舍入行爲與 ROUND_HALF_UP 相同;如果爲偶數,則舍入行爲與 ROUND_HALF_DOWN 相同。注意,在重複進行一系列計算時,此舍入模式可以將累加錯誤減到最小。此舍入模式也稱爲“銀行家舍入法”,主要在美國使用。四捨六入,五分兩種情況。如果前一位爲奇數,則入位,否則捨去。以下例子爲保留小數點1位,那麼這種舍入方式下的結果。1.15>1.2 1.25>1.2

8.ROUND_UNNECESSARY

斷言請求的操作具有精確的結果,因此不需要舍入。如果對獲得精確結果的操作指定此舍入模式,則拋出ArithmeticException。

四、bigdecimal.divide除法運算及常見的異常

使用除法函數在divide的時候要設置各種參數,要有除數、精確的小數位數和舍入模式,不然會出現報錯。

常用的兩個divide重載方法:

 /**
  * 第一個參數時被除數
  * 第二個參數是選擇的舍入模式
  */
 public BigDecimal divide(BigDecimal divisor, int roundingMode);
 /**
  * 第一個參數時被除數
  * 第二個參數是一個整數類型,實際意思是最終結果小數點後面保留幾位小數
  * 第三個參數就是小數點後面保留小數時省略或者進位的選擇模式,該模式可以有多種選擇
  */
 public BigDecimal divide(BigDecimal divisor,int scale, int roundingMode);









示例:

 BigDecimal bcs = new BigDecimal("1");
 BigDecimal cs = new BigDecimal("3");
 
 BigDecimal res1 = bcs.divide(cs,3,BigDecimal.ROUND_UP);
 System.out.println("除法ROUND_UP:"+res1);
 BigDecimal res2 = bcs.divide(cs,3,BigDecimal.ROUND_DOWN);
 System.out.println("除法ROUND_DOWN:"+res2);
 BigDecimal res3 = bcs.divide(cs,3,BigDecimal.ROUND_CEILING);
 System.out.println("除法ROUND_CEILING:"+res3);
 BigDecimal res4 = bcs.divide(cs,3,BigDecimal.ROUND_FLOOR);
 System.out.println("除法ROUND_FLOOR:"+res4);
 BigDecimal res5 = bcs.divide(cs,3,BigDecimal.ROUND_HALF_UP);
 System.out.println("除法ROUND_HALF_UP:"+res5);
 BigDecimal res6 = bcs.divide(cs,3,BigDecimal.ROUND_HALF_DOWN);
 System.out.println("除法ROUND_HALF_DOWN:"+res6);
 BigDecimal res7 = bcs.divide(cs,3,BigDecimal.ROUND_HALF_EVEN);
 System.out.println("除法ROUND_HALF_EVEN:"+res7);
 BigDecimal res8 = bcs.divide(cs,3,BigDecimal.ROUND_UNNECESSARY);
 System.out.println("除法ROUND_UNNECESSARY:"+res8);

















打印結果如下:

 除法ROUND_UP:0.334
 除法ROUND_DOWN:0.333
 除法ROUND_CEILING:0.334
 除法ROUND_FLOOR:0.333
 除法ROUND_HALF_UP:0.333
 除法ROUND_HALF_DOWN:0.333
 除法ROUND_HALF_EVEN:0.333
 Exception in thread "main" java.lang.ArithmeticException: Rounding necessary
  at java.math.BigDecimal.divideAndRound(BigDecimal.java:1452)
  at java.math.BigDecimal.divide(BigDecimal.java:1398)
  at springbootpro.zz.BigDecimalDemoTest.main(BigDecimalDemoTest.java:18)









不設置小數點及舍入模式的示例:

 //不設置小數點及舍入模式
 BigDecimal res9 = bcs.divide(cs);
 System.out.println("除法ROUND_UNNECESSARY:"+res9);

打印結果如下:

 Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
  at java.math.BigDecimal.divide(BigDecimal.java:1616)
  at springbootpro.zz.BigDecimalDemoTest.main(BigDecimalDemoTest.java:02)

注意:

bigdecimal.divide方法使用時拋出異常的情況,有時候我們在項目中使用該方法進行計算時時而拋出異常時而不拋異常,異常信息爲Non-terminating decimal expansion,具體異常截圖如上。

從上面的分析中時而異常時而不異常的bug情況,我們分析應該不是語法的問題,應該是傳入的數據有問題,後來發現這個方法使用時,如果不傳入第二個參數不設置保留幾位小數的情況下,如果計算結果是無限循環的小數,就會拋出上文中的異常信息。因此,爲了避免以後出現這種情況,有必要限制一下保留幾位小數。

五、MySQL數據類型 DECIMAL

float、double這些浮點數類型同樣可以存儲小數,但是無法確保精度,很容易產生誤差,特別是在求和計算的時候,所有當存儲小數,特別是涉及金額時推薦使用DECIMAL類型。

注意事項:

  • DECIMAL(M,D)中,M範圍是1到65,D範圍是0到30。

  • M默認爲10,D默認爲0,D不大於M。

  • DECIMAL(5,2)可存儲範圍是從-999.99到999.99,超出存儲範圍會報錯。

  • 存儲數值時,小數位不足會自動補0,首位數字爲0自動忽略。

  • 小數位超出會截斷,產生告警,並按四捨五入處理。

  • 使用DECIMAL字段時,建議M,D參數手動指定,並按需分配。

    總結:

    本文比較簡單實用,通讀下來,你大概會明白DECIMAL字段的使用場景及注意事項,其實對於常見的字段類型,我們只需要瞭解其使用場景及注意事項即可。當我們建表時,能夠快速選出合適的字段類型纔是我們的目的,比如當我們需要存儲小數時,能夠使用DECIMAL類型並且根據業務需要選擇合適的精度。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章