一、简述

在很多编程语言中,浮点数类型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类型并且根据业务需要选择合适的精度。