专栏首页码农小胖哥的码农生涯Java开发中商业计算请务必使用BigDecimal来进行计算!

Java开发中商业计算请务必使用BigDecimal来进行计算!

1.

前言

今天群里一个初级开发者问为什么测试人员测出来他写的价格计算模块有计算偏差的问题,他检查了半天也没找出问题。这里小胖哥要提醒你,商业计算请务必使用`BigDecimal`,浮点做商业运算是不精确的。因为计算机无法使用二进制小数来精确描述我们程序中的十进制小数。《Effective Java》在第48条也推荐“使用BigDecimal来做精确运算”。今天我们就来总结归纳其相关的知识点。

2.

BigDecimal

BigDecimal表示不可变的任意精度带符号十进制数。它由两部分组成:

  • intVal - 未校正精度的整数,类型为`BigInteger`
  • Scale - 一个32位整数,表示小数点右边的位数

例如,BigDecimal 3.14的未校正值为314,缩放为2。我们使用BigDecimal进行高精度算术运算。我们还将它用于需要控制比例和舍入行为的计算。如果你的计算是商业计算请务必使用计算精确的`BigDecimal` 。

3.

构造BigDecimal实例

我们可以从`String`,`character` 数组,`int`,`long`和`BigInteger`创建一个`BigDecimal`对象:

@Test
public void theValueMatches() {
    BigDecimal bdFromString = new BigDecimal("0.12");
    BigDecimal bdFromCharArray = new BigDecimal(new char[]{'3', '.', '1', '4', '1', '5'});
    BigDecimal bdlFromInt = new BigDecimal(42);
    BigDecimal bdFromLong = new BigDecimal(123412345678901L);
    BigInteger bigInteger = BigInteger.probablePrime(100, new Random());
    BigDecimal bdFromBigInteger = new BigDecimal(bigInteger);
    assertEquals("0.12", bdFromString.toString());
    assertEquals("3.1415", bdFromCharArray.toString());
    assertEquals("42", bdlFromInt.toString());
    assertEquals("123412345678901", bdFromLong.toString());
    assertEquals(bigInteger.toString(), bdFromBigInteger.toString());
}

我们还可以从`double`创建`BigDecimal`:

@Test
public void whenBigDecimalCreatedFromDouble_thenValueMayNotMatch() {
    BigDecimal bdFromDouble = new BigDecimal(0.1d);
    assertNotEquals("0.1", bdFromDouble.toString());
}

我们发现在这种情况下,结果与预期的结果不同(即0.1)。这是因为:这个转换结果是`double`的二进制浮点值的精确十进制表示,其值得结果不是我们可以预测的.我们应该使用`String`构造函数而不是`double`构造函数。另外,我们可以使用`valueOf`静态方法将`double`转换为`BigDecimal` 或者直接使用其未校正数加小数位数 :

@Test
public void whenBigDecimalCreatedUsingValueOf_thenValueMatches() {
       BigDecimal bdFromDouble = BigDecimal.valueOf(0.1d);
       BigDecimal bigFromLong=BigDecimal.valueOf(1,1);
       
       assertEquals("0.1", bdFromDouble.toString());
       assertEquals("0.1", bigFromLong.toString());
}

在转换为BigDecimal之前,此方法将double转换为其String表示形式。此外,它可以重用对象实例。因此,我们应该优先使用valueOf方法来构造函数。

4.

常用API

对应方法相关用法解释

5.

BigDecimal操作

BigDecimal上的操作就像其他Number类(Integer,Long,Double等)一样,BigDecimal提供算术和比较操作的操作。它还提供了缩放操作,舍入和格式转换的操作。它不会使算术运算符(+ - /*)或逻辑运算符(> < | &) 过载。相反,我们使用`BigDecimal`相应的方法 - 加,减,乘,除和比较。并且`BigDecimal`具有提取各种属性的方法。

5.1

提取属性

精度,小数位数和符号:

@Test
public void whenGettingAttributes_thenExpectedResult() {
    BigDecimal bd = new BigDecimal("-12345.6789");
         
    assertEquals(9, bd.precision());
    assertEquals(4, bd.scale());
    assertEquals(-1, bd.signum());
}

5.2

比较大小

我们使用`compareTo`方法比较两个`BigDecimal`的值:

@Test
public void whenComparingBigDecimals_thenExpectedResult() {
    BigDecimal bd1 = new BigDecimal("1.0");
    BigDecimal bd2 = new BigDecimal("1.00");
    BigDecimal bd3 = new BigDecimal("2.0");
 
    assertTrue(bd1.compareTo(bd3) < 0);
    assertTrue(bd3.compareTo(bd1) > 0);
    assertTrue(bd1.compareTo(bd2) == 0);
    assertTrue(bd1.compareTo(bd3) <= 0);
    assertTrue(bd1.compareTo(bd2) >= 0);
    assertTrue(bd1.compareTo(bd3) != 0);
}

上面的方法在比较时忽略了小数位。如果你既要比较精度又要比较小数位数那么请使用`equals`方法:

@Test
public void whenEqualsCalled_thenSizeAndScaleMatched() {
    BigDecimal bd1 = new BigDecimal("1.0");
    BigDecimal bd2 = new BigDecimal("1.00");
         
    assertFalse(bd1.equals(bd2));
}

5.3

四则运算

BigDecimal 提供了以下四则运算的方法:

  • add ——加法
  • subtract ——减法
  • divide ——除法,有可能除不尽,必须显式声明保留小数位数避免抛出`ArithmeticException`异常
  • multiply ——乘法
@Test
public void whenPerformingArithmetic_thenExpectedResult() {
    BigDecimal bd1 = new BigDecimal("4.0");
    BigDecimal bd2 = new BigDecimal("2.0");
 
    BigDecimal sum = bd1.add(bd2);
    BigDecimal difference = bd1.subtract(bd2);
    BigDecimal quotient = bd1.divide(bd2);
    BigDecimal product = bd1.multiply(bd2);
 
    assertTrue(sum.compareTo(new BigDecimal("6.0")) == 0);
    assertTrue(difference.compareTo(new BigDecimal("2.0")) == 0);
    assertTrue(quotient.compareTo(new BigDecimal("2.0")) == 0);
    assertTrue(product.compareTo(new BigDecimal("8.0")) == 0);
}

5.4

四舍五入

既然是数学运算就不得不讲四舍五入。比如我们在金额计算中很容易遇到最终结算金额为人民币`22.355`的情况。因为货币没有比分更低的单位所以我们要使用精度和舍入模式规则对数字进行剪裁。java提供有两个类控制舍入行为`RoundingMode`和`MathContext` 。`MathContext`执行的是IEEE 754R标准目前不太明白其使用场景,我们使用的比较多的是枚举`RoundingMode`。它提供了八种模式:

  • RoundingMode.UP:以小数位为原点 是正数取右边,负数取左边
  • RoundingMode.DOWN:以小数位为原点 也就是正数取左边,负数取右边
  • RoundingMode.FLOOR:取左边最近的正数
  • RoundingMode.CEILING:取右边最近的整数
  • RoundingMode.HALF_DOWN:五舍六入,负数先取绝对值再五舍六入再负数
  • RoundingMode.HALF_UP:四舍五入,负数原理同上
  • RoundingMode.HALF_EVEN:这个比较绕,整数位若是奇数则四舍五入,若是偶数则五舍六入
  • RoundingMode.ROUND_UNNECESSARY:不需要取整,如果存在小数位,就抛ArithmeticException 异常

6.

格式化

数字格式化可通过操作类`java.text.NumberFormat`和`java.text.DecimalFormat`提供的api进行操作。其实我们只需要使用`java.text.DecimalFormat`,因为它代理了`NumberFormat`。我们来看一下它们的api:

6.1

NumberFormat

  • NumberFormat.getInstance(Locale)、getNumberInstance(Locale)。返回指定语言环境的通用数值格式。
  • NumberFormat.getCurrencyInstance(Locale)。返回指定语言环境的货币格式。
  • NumberFormat.getPercentInstance(Locale)。返回指定语言环境的百分比格式。
  • NumberFormat.getIntegerInstance(Locale)。返回指定语言环境的整数数值格式。
  • NumberFormat.setMinimumIntegerDigits(int)。设置数的整数部分所允许的最小位数。
  • NumberFormat.setMaximumIntegerDigits(int)。设置数的整数部分所允许的最大位数。
  • NumberFormat.setMinimumFractionDigits(int)。设置最少小数点位数,不足的位数以0补位,超出的话按实际位数输出。
  • NumberFormat.setMaximumFractionDigits(int)。设置最多保留小数位数,不足不补0。

6.2

DecimalFormat

`DecimalFormat`除了能代理上面的`NumberFormat`以外,还提供了基于`pattern`字符串的格式化风格,有点类似格式化时间一样。我们来看看`pattern`的规则:

  • “0”——表示一位数值,如没有,显示0。如“0000.0000”,整数位或小数位>4,按实际输出,<4整数位前面补0小数位后面补0,凑足4位。
  • “#”——表示任意位数的整数。如没有,则不显示。在小数点位使用,只表示一位小数,超出部分四舍五入。如:“#”:无小数,小数部分四舍五入。“.#”:整数部分不变,一位小数,四舍五入。“.##”:整数部分不变,二位小数,四舍五入。
  • “.”——表示小数点。注意一个pattern中只能出现一次,超过一次将格式化异常。
  • “,”——与模式“0”一起使用,表示逗号。注意一定不能在小数点后用,否则格式化异常。

7.

总结

今天对`BigDecimal`进行了总结归纳,这篇文章建议你收藏备用,也可以转给其他需要的同学。

本文分享自微信公众号 - 码农小胖哥(Felordcn),作者:码农小胖哥

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-09-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • java随机数中的陷阱

    随机数我们应该不陌生,业务中我们用它来生成验证码,或者对重复性要求不高的id,甚至我们还用它在年会上搞抽奖。今天我们来探讨一下这个东西。如果使用不当会引发一系列...

    码农小胖哥
  • 微服务下 Spring Boot Maven 工程依赖关系管理

    最基本的 pom.xml 包含工程信息、Spring Boot 父工程、属性配置、依赖包、构建插件

    码农小胖哥
  • 分布式搜索引擎面试题(一)

    Lucene是一套用于全文检索和搜索的开放源代码程序库。实际上lucene的功能很单一,说到底,就是你给它若干个字符串,然后它为你提供一个全文搜索服务,告诉你你...

    码农小胖哥
  • 【答疑解惑】Java中的高精度数字

    前几天网友在群里有问BigDecimal能直接赋值吗?就像使用基本数据类型那样,答案是不能。 Java中的基本数据类型有的时候是不能满足实际编程需要的,特别是在...

    程序员互动联盟
  • Java中的BigDecimal类你了解多少?

    可以看到在Java中进行浮点数运算的时候,会出现丢失精度的问题。那么我们如果在进行商品价格计算的时候,就会出现问题。很有可能造成我们手中有0.06元,却无法购买...

    用户1516716
  • BigDecimal大小判断

    equals源码: public boolean equals(Object anObject) {//name2传入equals方法,anObject指向n...

    居士
  • Java-BigDecimal数据类型

    我们知道在Java中有float和double类型,它们的主要设计目标是为了科学计算和工程计算。然而,它们没有提供完全精确的结果【因为其有限的有效位数】,所以不...

    老九学堂-小师弟
  • 【刨根问底】BigDecimal 案例和部分源码分析

    在咱们开发过程中很容易遇到计算的问题,普通计算其实也还好使用int、long、double、float基本上能应付。但是如果涉及到数据类型转后在处理等就不是很好...

    用户4143945
  • Java之BigDecimal详解

    ​ Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效...

    用户1195962
  • Java工具集-数学(一次函数)

    cwl_java

扫码关注云+社区

领取腾讯云代金券