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

前言

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

1. BigDecimal

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

  • intVal - 未校正精度的整数,类型为BigInteger
  • Scale - 一个32位整数,表示小数点右边的位数
    例如,BigDecimal 3.14的未校正值为314,缩放为2。我们使用BigDecimal进行高精度算术运算。我们还将它用于需要控制比例和舍入行为的计算。如果你的计算是商业计算请务必使用计算精确的BigDecimal

2. 构造BigDecimal

我们可以从Stringcharacter 数组,intlongBigInteger创建一个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方法来构造函数。

3. 常用API

方法名 对应方法相关用法解释
abs() 绝对值,scale不变
add(BigDecimal augend) 加,scale为augend和原值scale的较大值
subtract(BigDecimal augend) 减,scale为augend和原值scale的较大值
multiply(BigDecimal multiplicand) 乘,scale为augend和原值scale的和
divide(BigDecimal divisor) 除,原值/divisor,如果不能除尽会抛出异常,scale与原值一致
divide(BigDecimal divisor, int roundingMode) 除,指定舍入方式,scale与原值一致
divide(BigDecimal divisor, int scale, int roundingMode) 除,指定舍入方式和scale
remainder(BigDecimal divisor) 取余,scale与原值一致
divideAndRemainder(BigDecimal divisor) 除法运算后返回一个数组存放除尽和余数 如 23/3 返回 {7,2}
divideToIntegralValue(BigDecimal divisor) 除,只保留整数部分,但scale仍与原值一致
max(BigDecimal val) 较大值,返回原值与val中的较大值,与结果的scale一致
min(BigDecimal val) 较小值,与结果的scale一致
movePointLeft(int n) 小数点左移,scale为原值scale+n
movePointRight(int n) 小数点右移,scale为原值scale+n
negate() 取反,scale不变
pow(int n) 幂,原值^n,原值的n次幂
scaleByPowerOfTen(int n) 相当于小数点右移n位,原值*10^n

4. BigDecimal操作

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

4.1 提取属性

精度,小数位数和符号:

@Test
public void whenGettingAttributes_thenExpectedResult() {
    BigDecimal bd = new BigDecimal("-12345.6789");

    assertEquals(9, bd.precision());
    assertEquals(4, bd.scale());
    assertEquals(-1, bd.signum());
}

4.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));
}

4.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);
}

4.4 四舍五入

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

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

5. 格式化

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

5.1 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。

5.2 DecimalFormat

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

  • “0”——表示一位数值,如没有,显示0。如“0000.0000”,整数位或小数位>4,按实际输出,<4整数位前面补0小数位后面补0,凑足4位。

  • “#”——表示任意位数的整数。如没有,则不显示。在小数点位使用,只表示一位小数,超出部分四舍五入。如:“#”:无小数,小数部分四舍五入。“.#”:整数部分不变,一位小数,四舍五入。“.##”:整数部分不变,二位小数,四舍五入。

  • “.”——表示小数点。注意一个pattern中只能出现一次,超过一次将格式化异常。

  • “,”——与模式“0”一起使用,表示逗号。注意一定不能在小数点后用,否则格式化异常。

总结

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

原创作者:码农小胖哥
转载地址:https://gper.club/articles/7e7e7f7ff7g59gcag66

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容