java开发使用BigDecimal避坑四则

目录
  • 引言
    • 第一:浮点类型的坑
    • 第二:浮点精度的坑
    • 第三:设置精度的坑
    • 第四:三种字符串输出的坑
  • 小结

引言

在使用BigDecimal时,有4种使用场景下的坑,你一定要了解一下,如果使用不当,必定很惨。掌握这些案例,当别人写出有坑的代码,你也能够一眼识别出来,大牛就是这么练成的。

第一:浮点类型的坑

在学习了解BigDecimal的坑之前,先来说一个老生常谈的问题:如果使用Float、Double等浮点类型进行计算时,有可能得到的是一个近似值,而不是精确的值。

比如下面的代码:

  @Test
  public void test0(){
    float a = 1;
    float b = 0.9f;
    System.out.println(a - b);
  }

结果是多少?0.1吗?不是,执行上面代码执行的结果是0.100000024。之所以产生这样的结果,是因为0.1的二进制表示是无限循环的。由于计算机的资源是有限的,所以是没办法用二进制精确的表示 0.1,只能用「近似值」来表示,就是在有限的精度情况下,最大化接近 0.1 的二进制数,于是就会造成精度缺失的情况。

关于上述的现象大家都知道,不再详细展开。同时,还会得出结论在科学计数法时可考虑使用浮点类型,但如果是涉及到金额计算要使用BigDecimal来计算。

那么,BigDecimal就一定能避免上述的浮点问题吗?来看下面的示例:

  @Test
  public void test1(){
    BigDecimal a = new BigDecimal(0.01);
    BigDecimal b = BigDecimal.valueOf(0.01);
    System.out.println("a = " + a);
    System.out.println("b = " + b);
  }

上述单元测试中的代码,a和b结果分别是什么?

a = 0.01000000000000000020816681711721685132943093776702880859375
b = 0.01

上面的实例说明,即便是使用BigDecimal,结果依旧会出现精度问题。这就涉及到创建BigDecimal对象时,如果有初始值,是采用new BigDecimal的形式,还是通过BigDecimal#valueOf方法了。

之所以会出现上述现象,是因为new BigDecimal时,传入的0.1已经是浮点类型了,鉴于上面说的这个值只是近似值,在使用new BigDecimal时就把这个近似值完整的保留下来了。

而BigDecimal#valueOf则不同,它的源码实现如下:

    public static BigDecimal valueOf(double val) {
        // Reminder: a zero double returns '0.0', so we cannot fastpath
        // to use the constant ZERO.  This might be important enough to
        // justify a factory approach, a cache, or a few private
        // constants, later.
        return new BigDecimal(Double.toString(val));
    }

在valueOf内部,使用Double#toString方法,将浮点类型的值转换成了字符串,因此就不存在精度丢失问题了。

此时就得出一个基本的结论:第一,在使用BigDecimal构造函数时,尽量传递字符串而非浮点类型;第二,如果无法满足第一条,则可采用BigDecimal#valueOf方法来构造初始化值。

这里延伸一下,BigDecimal常见的构造方法有如下几种:

BigDecimal(int)       创建一个具有参数所指定整数值的对象。
BigDecimal(double)    创建一个具有参数所指定双精度值的对象。
BigDecimal(long)      创建一个具有参数所指定长整数值的对象。
BigDecimal(String)    创建一个具有参数所指定以字符串表示的数值的对象。

其中涉及到参数类型为double的构造方法,会出现上述的问题,使用时需特别留意。

第二:浮点精度的坑

如果比较两个BigDecimal的值是否相等,你会如何比较?使用equals方法还是compareTo方法呢?

先来看一个示例:

  @Test
  public void test2(){
    BigDecimal a = new BigDecimal("0.01");
    BigDecimal b = new BigDecimal("0.010");
    System.out.println(a.equals(b));
    System.out.println(a.compareTo(b));
  }

乍一看感觉可能相等,但实际上它们的本质并不相同。

equals方法是基于BigDecimal实现的equals方法来进行比较的,直观印象就是比较两个对象是否相同,那么代码是如何实现的呢?

    @Override
    public boolean equals(Object x) {
        if (!(x instanceof BigDecimal))
            return false;
        BigDecimal xDec = (BigDecimal) x;
        if (x == this)
            return true;
        if (scale != xDec.scale)
            return false;
        long s = this.intCompact;
        long xs = xDec.intCompact;
        if (s != INFLATED) {
            if (xs == INFLATED)
                xs = compactValFor(xDec.intVal);
            return xs == s;
        } else if (xs != INFLATED)
            return xs == compactValFor(this.intVal);
        return this.inflated().equals(xDec.inflated());
    }

仔细阅读代码可以看出,equals方法不仅比较了值是否相等,还比较了精度是否相同。上述示例中,由于两者的精度不同,所以equals方法的结果当然是false了。而compareTo方法实现了Comparable接口,真正比较的是值的大小,返回的值为-1(小于),0(等于),1(大于)。

基本结论:通常情况,如果比较两个BigDecimal值的大小,采用其实现的compareTo方法;如果严格限制精度的比较,那么则可考虑使用equals方法。

另外,这种场景在比较0值的时候比较常见,比如比较BigDecimal("0")、BigDecimal("0.0")、BigDecimal("0.00"),此时一定要使用compareTo方法进行比较。

第三:设置精度的坑

在项目中看到好多同学通过BigDecimal进行计算时不设置计算结果的精度和舍入模式,真是着急人,虽然大多数情况下不会出现什么问题。但下面的场景就不一定了:

  @Test
  public void test3(){
    BigDecimal a = new BigDecimal("1.0");
    BigDecimal b = new BigDecimal("3.0");
    a.divide(b);
  }

执行上述代码的结果是什么?ArithmeticException异常!

java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
  at java.math.BigDecimal.divide(BigDecimal.java:1690)
  ...

这个异常的发生在官方文档中也有说明:

If the quotient has a nonterminating decimal expansion and the operation is specified to return an exact result, an ArithmeticException is thrown. Otherwise, the exact result of the division is returned, as done for other operations.

总结一下就是,如果在除法(divide)运算过程中,如果商是一个无限小数(0.333…),而操作的结果预期是一个精确的数字,那么将会抛出ArithmeticException异常。

此时,只需在使用divide方法时指定结果的精度即可:

  @Test
  public void test3(){
    BigDecimal a = new BigDecimal("1.0");
    BigDecimal b = new BigDecimal("3.0");
    BigDecimal c = a.divide(b, 2,RoundingMode.HALF_UP);
    System.out.println(c);
  }

执行上述代码,输入结果为0.33。

基本结论:在使用BigDecimal进行(所有)运算时,一定要明确指定精度和舍入模式。

拓展一下,舍入模式定义在RoundingMode枚举类中,共有8种:

  • RoundingMode.UP:舍入远离零的舍入模式。在丢弃非零部分之前始终增加数字(始终对非零舍弃部分前面的数字加1)。注意,此舍入模式始终不会减少计算值的大小。
  • RoundingMode.DOWN:接近零的舍入模式。在丢弃某部分之前始终不增加数字(从不对舍弃部分前面的数字加1,即截短)。注意,此舍入模式始终不会增加计算值的大小。
  • RoundingMode.CEILING:接近正无穷大的舍入模式。如果 BigDecimal 为正,则舍入行为与 ROUNDUP 相同;如果为负,则舍入行为与 ROUNDDOWN 相同。注意,此舍入模式始终不会减少计算值。
  • RoundingMode.FLOOR:接近负无穷大的舍入模式。如果 BigDecimal 为正,则舍入行为与 ROUNDDOWN 相同;如果为负,则舍入行为与 ROUNDUP 相同。注意,此舍入模式始终不会增加计算值。
  • RoundingMode.HALF_UP:向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为向上舍入的舍入模式。如果舍弃部分 >= 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同。注意,这是我们在小学时学过的舍入模式(四舍五入)。
  • RoundingMode.HALF_DOWN:向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为上舍入的舍入模式。如果舍弃部分 > 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同(五舍六入)。
  • RoundingMode.HALF_EVEN:向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则向相邻的偶数舍入。如果舍弃部分左边的数字为奇数,则舍入行为与 ROUNDHALFUP 相同;如果为偶数,则舍入行为与 ROUNDHALF_DOWN 相同。注意,在重复进行一系列计算时,此舍入模式可以将累加错误减到最小。此舍入模式也称为“银行家舍入法”,主要在美国使用。四舍六入,五分两种情况。如果前一位为奇数,则入位,否则舍去。以下例子为保留小数点1位,那么这种舍入方式下的结果。1.15 ==> 1.2 ,1.25 ==> 1.2
  • RoundingMode.UNNECESSARY:断言请求的操作具有精确的结果,因此不需要舍入。如果对获得精确结果的操作指定此舍入模式,则抛出ArithmeticException。

通常我们使用的四舍五入即RoundingMode.HALF_UP。

第四:三种字符串输出的坑

当使用BigDecimal之后,需要转换成String类型,你是如何操作的?直接toString?

先来看看下面的代码:

@Test
public void test4(){
  BigDecimal a = BigDecimal.valueOf(35634535255456719.22345634534124578902);
  System.out.println(a.toString());
}

执行的结果是上述对应的值吗?并不是:

3.563453525545672E+16

也就是说,本来想打印字符串的,结果打印出来的是科学计数法的值。

这里我们需要了解BigDecimal转换字符串的三个方法

  • toPlainString():不使用任何科学计数法;
  • toString():在必要的时候使用科学计数法;
  • toEngineeringString() :在必要的时候使用工程计数法。类似于科学计数法,只不过指数的幂都是3的倍数,这样方便工程上的应用,因为在很多单位转换的时候都是10^3;

三种方法展示结果示例如下:

基本结论:根据数据结果展示格式不同,采用不同的字符串输出方法,通常使用比较多的方法为toPlainString() 。

另外,NumberFormat类的format()方法可以使用BigDecimal对象作为其参数,可以利用BigDecimal对超出16位有效数字的货币值,百分值,以及一般数值进行格式化控制。

使用示例如下:

NumberFormat currency = NumberFormat.getCurrencyInstance(); //建立货币格式化引用
NumberFormat percent = NumberFormat.getPercentInstance();  //建立百分比格式化引用
percent.setMaximumFractionDigits(3); //百分比小数点最多3位
BigDecimal loanAmount = new BigDecimal("15000.48"); //金额
BigDecimal interestRate = new BigDecimal("0.008"); //利率
BigDecimal interest = loanAmount.multiply(interestRate); //相乘
System.out.println("金额:\t" + currency.format(loanAmount));
System.out.println("利率:\t" + percent.format(interestRate));
System.out.println("利息:\t" + currency.format(interest));

输出结果如下:

金额: ¥15,000.48 
利率: 0.8% 
利息: ¥120.00

小结

本篇文章介绍了BigDecimal使用中场景的坑,以及基于这些坑我们得出的“最佳实践”。虽然某些场景下推荐使用BigDecimal,它能够达到更好的精度,但性能相较于double和float,还是有一定的损失的,特别在处理庞大,复杂的运算时尤为明显。故一般精度的计算没必要使用BigDecimal。而必须使用时,一定要规避上述的坑,更多关于java开发BigDecimal避坑的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java踩坑记录之BigDecimal类

    前言 在java.math包中提供了对大数字的操作类,用于进行高精确计算,如BigInteger,BigDecimal类.而平常我们开发中使用最多的float和double只能适用于一般的科学和工程计算,如果要在比较精确的计算方面如货币,那么使用float和double会相应的丢失精度,因此用于精密计算大数字的类BigDecimal就必不可少了.所以BigDecimal适合商业计算场景,用来对超过16位有效位的数进行精确的运算.但是BigDecimal的使用并不像float和double那样,使

  • Java之BigDecimal的坑及解决

    目录 Java BigDecimal的坑 采坑处 原因是 解决方式 BigDecimal容易出现的坑 Java BigDecimal的坑 采坑处 BigDecimal bd =new BigDecimal(0.1); System.out.println("结果是:" + bd); 结果是:158.740000000000009094947017729282379150390625 原因是 1)参数类型为double的构造方法的结果有一定的不可预知性.有人可能认为在Java中写入new

  • JAVA biginteger类bigdecimal类的使用示例学习

    目录 biginteger类的使用 BigInteger案例代码 bigdecimal类的使用 BigDecimal的案例代码 biginteger类的使用 A:BigInteger的概述 可以让超过Integer范围内的数据进行运算 B:构造方法 public BigInteger(String val) C:成员方法 * public BigInteger add(BigInteger val) * public BigInteger subtract(BigInteger val) * p

  • 解决Java中new BigDecimal()的坑

    目录 new BigDecimal()的坑 关于BigDecimal用法 1.实例 BigDecimal 对象 2. BigDecimal 加减乘除 3. Scale 属性操作 4. compareTo 比较大小 new BigDecimal()的坑 先看一段代码示例: System.out.println(new BigDecimal(0.99)); System.out.println(new BigDecimal("0.99")); System.out.println(BigD

  • Java Big Number操作BigInteger及BigDecimal类详解

    目录 BigInteger类 构造函数 类方法 BigDecimal类 BigInteger类 java.math.BigInteger 类的使用场景是大整数操作.它提供类似所有Java的基本整数运算符和java.lang.Math中的所有相关的方法的操作,如+.-.*./.%.&.|.mod.>>.<<,以及min().max()等等. 只不过它操作的整数都是极其大的,为科学计算提供了很大的便利.比如下面的代码就是计算20000000000000000000 * 3000

  • java开发使用BigDecimal避坑四则

    目录 引言 第一:浮点类型的坑 第二:浮点精度的坑 第三:设置精度的坑 第四:三种字符串输出的坑 小结 引言 在使用BigDecimal时,有4种使用场景下的坑,你一定要了解一下,如果使用不当,必定很惨.掌握这些案例,当别人写出有坑的代码,你也能够一眼识别出来,大牛就是这么练成的. 第一:浮点类型的坑 在学习了解BigDecimal的坑之前,先来说一个老生常谈的问题:如果使用Float.Double等浮点类型进行计算时,有可能得到的是一个近似值,而不是精确的值. 比如下面的代码: @Test p

  • java开发使用StringUtils.split避坑详解

    目录 正文 StringUtils.split 的坑 StringUtils.split 源码分析 如何解决? 正文 在日常的 Java 开发中,由于 JDK 未能提供足够的常用的操作类库,通常我们会引入 Apache Commons Lang 工具库或者 Google Guava 工具库简化开发过程.两个类库都为 java.lang API 提供了很多实用工具,比如经常使用的字符串操作,基本数值操作.时间操作.对象反射以及并发操作等. <dependency> <groupId>

  • Java多线程基本概念以及避坑指南

    目录 前言 1. 多线程基本概念 1.1 轻量级进程 1.2 JMM 1.3 Java中常见的线程同步方式 2. 避坑指南 2.1. 线程池打爆机器 2.2. 锁要关闭 2.3. wait要包两层 2.4. 不要覆盖锁对象 2.5. 处理循环中的异常 2.6. HashMap正确用法 2.7. 线程安全的保护范围 2.8. volatile作用有限 2.9. 日期处理要小心 2.10. 不要在构造函数中启动线程 End 前言 多核的机器,现在已经非常常见了.即使是一块手机,也都配备了强劲的多核处

  • java ThreadPoolExecutor线程池拒绝策略避坑

    目录 1.场景 2. 原因分析 3.总结 4.思考 1.场景 线程池使用DiscardOldestPolicy拒绝策略,阻塞队列使用ArrayBlockingQueue,发现在某些情形下对于得到的Future,调用get()方法当前线程会一直阻塞. 为了便于理解,将实际情景抽象为下面的代码: ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( 1, 1, 1, TimeUnit.SECONDS, new ArrayBlo

  • Vxe-Table开发中的各种坑以及避坑指南

    目录 背景: 开发阶段遇到的各种问题 全局size的问题 按钮的问题 合并单元格的问题 reload和load的问题 总结 背景: 由于公司要开发erp,采用了element-plus做为UI基础框架,但是回想往事点点滴滴,element-ui表格的种种表现令人痛心,于是跟leader商量之后决定使用Vxe-Table做表格插件,虽然element-plus在表格上也在大力优化,但就目前来看可用度确实不高,刚出了一个虚拟滚动,但看上去确实让人有点心急... 开发阶段遇到的各种问题 全局size的

  • Java开发SSM框架微信支付的实现

    微信小程序的Java支付开发一直是一块坑,网上的教程也是琳琅满目.笔者六月的时候接触到了微信的小程序开发摸到了微信支付方面的东西,腾讯的官方文档也是一言难尽很多地方看不懂,而且官方也没有提供Java的示范导致Java做微信支付不得不自己踩坑.现在我把自己微信支付开发的步骤和代码都在下面展示出来,希望有没有做出来的朋友不要心急跟着我的步骤走就没问题. 第一步:首先微信支付的话只能是企业的开发账户才能使用的如果你是个人开发者是无法开通微信支付的.我们首先拿到账号,然后拿到微信支付相关的商户号和商户支

  • 《阿里巴巴 Java开发手册》读后感小结

    前言 只有光头才能变强 前一阵子一直在学Redis,结果在黄金段位被虐了,暂时升不了段位了,每天都拿不到首胜(好烦). 趁着学校校运会,合理地给自己放了一个小长假,然后就回家了.回到家才发现当时618买了一堆书,这堆书还有没撕包装的呢....于是我翻出了最薄的一本<阿里巴巴 Java开发手册> 这本书一共就90多页,一天就可以通读完了,看完之后我又来水博文了. 注意: 书上很多的规范是可以用IDE来避免的,也有很多之前已经知道的了. 所以,这篇文章只记录我认为比较重要,或者说是我之前开发时没有

  • ant-design-vue 快速避坑指南(推荐)

    ant-design-vue是蚂蚁金服 Ant Design 官方唯一推荐的Vue版UI组件库,它其实是Ant Design的Vue实现,组件的风格与Ant Design保持同步,组件的html结构和css样式也保持一致. 用下来发现它的确称得上为数不多的完整的VUE组件库与开发方案集成项目. 本文主要目的是总结一些开发过程中比较耗时间去查找,文档中没有具体说明的常见问题,同时希望能给新上手此框架的同学提供一些参考作用. 1.Table对接后台返回数据 针对Table数据格式与后他接口返回数据格

随机推荐