java BigDecimal精度丢失及常见问分析

 更新时间:2023年02月03日 11:39:21   作者:podongfeng  
这篇文章主要为大家介绍了java BigDecimal精度丢失及常见问分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

概述

作为JAVA程序员,应该或多或少跟BigDecimal打过交道。JAVA在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。

精度丢失

先从1个问题说起,看如下代码

System.out.println(0.1 + 0.2);

最后打印出的结果是0.30000000000000004,而不是预期的0.3。

有经验的开发同学应该一下子看出来这就是因为double丢失精度导致。更深层次的原因,是因为我们的计算机底层是二进制的,只有0和1,对于整数来说,从低到高的每1位代表了1、2、4、8、16...这样的2的正次数幂,只要位数足够,每个整数都可以分解成这样的2的正次数幂组合,例如7D=111B13D=1101B。但是到了小数这里,就会发现2的负次数幂值是0.5、0.25、0.125、0.0625这样的值,但是并不是每个小数都可以分解成这样的2的负次数幂组合,例如你无法精确凑出0.1。所以,double的0.1其实并不是精确的0.1,只是通过几个2的负次数幂值凑的近似的0.1,所以会出现前面0.1 + 0.2 = 0.30000000000000004这样的结果。

适用场景

双精度浮点型变量double可以处理16位有效数,但是某些场景下,即使已经做到了16位有效位的数还是不够,比如涉及金额计算,差一点就会导致账目不平。

常用方法

加减乘除

既然BigDecimal主要用于数值计算,那么最基础的方法就是加减乘除。BigDecimal没有对应的数值类的基本数据类型,所以不能直接使用+-*/这样的符号来进行计算,而要使用BigDecimal内部的方法。

public BigDecimal add(BigDecimal augend)
public BigDecimal subtract(BigDecimal subtrahend)
public BigDecimal multiply(BigDecimal multiplicand)
public BigDecimal divide(BigDecimal divisor)

需要注意的是,BigDecimal是不可变的,所以,addsubtractmultiplydivide方法都是有返回值的,返回值是一个新的BigDecimal对象,原来的BigDecimal值并没有变。

设置精度和舍入策略

可以通过setScale方法来设置精度和舍入策略。

public BigDecimal setScale(int newScale, RoundingMode roundingMode)

第1个参数newScale代表精度,即小数点后位数;第2个参数roundingMode代表舍入策略,RoundingMode是一个枚举,用来替代原来在BigDecimal定义的常量,原来在BigDecimal定义的常量已经标记为Deprecated。在RoundingMode类中也通过1个valueOf方法来给出映射关系

/**
 * Returns the {@code RoundingMode} object corresponding to a
 * legacy integer rounding mode constant in {@link BigDecimal}.
 *
 * @param  rm legacy integer rounding mode to convert
 * @return {@code RoundingMode} corresponding to the given integer.
 * @throws IllegalArgumentException integer is out of range
 */
public static RoundingMode valueOf(int rm) {
    return switch (rm) {
        case BigDecimal.ROUND_UP          -> UP;
        case BigDecimal.ROUND_DOWN        -> DOWN;
        case BigDecimal.ROUND_CEILING     -> CEILING;
        case BigDecimal.ROUND_FLOOR       -> FLOOR;
        case BigDecimal.ROUND_HALF_UP     -> HALF_UP;
        case BigDecimal.ROUND_HALF_DOWN   -> HALF_DOWN;
        case BigDecimal.ROUND_HALF_EVEN   -> HALF_EVEN;
        case BigDecimal.ROUND_UNNECESSARY -> UNNECESSARY;
        default -> throw new IllegalArgumentException("argument out of range");
    };
}

我们逐一看一下每个值的含义

  • UP
    直接进位,例如下面代码结果是3.15
BigDecimal pi = BigDecimal.valueOf(3.141);
System.out.println(pi.setScale(2, RoundingMode.UP));
  • DOWN
    直接舍去,例如下面代码结果是3.1415
BigDecimal pi = BigDecimal.valueOf(3.14159);
System.out.println(pi.setScale(4, RoundingMode.DOWN));
  • CEILING
    如果是正数,相当于UP;如果是负数,相当于DOWN。
  • FLOOR
    如果是正数,相当于DOWN;如果是负数,相当于UP。
  • HALF_UP
    就是我们正常理解的四舍五入,实际上应该也是最常用的。 下面的代码结果是3.14
BigDecimal pi = BigDecimal.valueOf(3.14159);
System.out.println(pi.setScale(2, RoundingMode.HALF_UP));

下面的代码结果是3.142

BigDecimal pi = BigDecimal.valueOf(3.14159);
System.out.println(pi.setScale(3, RoundingMode.HALF_UP));
  • HALF_DOWN
    与四舍五入类似,这种是五舍六入。我们对于HALF_UP和HALF_DOWN可以理解成对于5的处理不同,UP遇到5是进位处理,DOWN遇到5是舍去处理,
  • HALF_EVEN
    如果舍弃部分左边的数字为偶数,相当于HALF_DOWN;如果舍弃部分左边的数字为奇数,相当于HALF_UP
  • UNNECESSARY
    非必要舍入。如果除去小数的后导0后,位数小于等于scale,那么就是去除scale位数后面的后导0;位数大于scale,抛出ArithmeticException。
    下面代码结果是3.14
BigDecimal pi = BigDecimal.valueOf(3.1400);
System.out.println(pi.setScale(2, RoundingMode.UNNECESSARY));

下面代码抛出ArithmeticException

BigDecimal pi = BigDecimal.valueOf(3.1400);
System.out.println(pi.setScale(1, RoundingMode.UNNECESSARY));

常见问题

创建BigDecimal对象

先看下面代码

BigDecimal a = new BigDecimal(0.1);
System.out.println(a);

实际输出的结果是0.1000000000000000055511151231257827021181583404541015625。其实这跟我们开篇引出的精度丢失是同一个问题,这里构造方法中的参数0.1是double类型,本身无法精确表示0.1,虽然BigDecimal并不会导致精度丢失,但是在更加上游的源头,double类型的0.1已经丢失了精度,这里用一个已经丢失精度的0.1来创建不会丢失精度的BigDecimal,精度还是会丢失。类似于使用2K的清晰度重新录制了一遍原始只有360P的视频,清晰度也不会优于原始的360P。
所以,我们应该尽量避免使用double来创建BigDecimal,确实源头是double的,我们可以使用valueOf方法,这个方法会先调用Double.toString(val)来转成String,这样就不会产生精度丢失,下面的代码结果就是0.1

BigDecimal a = BigDecimal.valueOf(0.1);
System.out.println(a);

顺便说一下,BigDecimal还内置了ZEROONETEN这样的常量可以直接使用。

toString

这个问题比较隐蔽,在数据比较小的时候不会遇到,但是看如下代码

BigDecimal a = BigDecimal.valueOf(987654321987654321.123456789123456789);
System.out.println(a);

最后实际输出的结果是9.8765432198765427E+17。原因是System.out.println会自动调用BigDecimal的toString方法,而这个方法会在必要时使用科学计数法,如果不想使用科学计数法,可以使用BigDecimal的toPlainString方法。另外提一下,BigDecimal还提供了一个toEngineeringString方法,这个方法也会使用科学技术法,不一样的是,这里面的10都是3、6、9这样的幂,对应我们在查看大数的时候,很多都是每3位会增加1个逗号。

comparTo 和 equals

这个问题出现的不多,有经验的开发同学在比较数值的时候,会自然而然使用comparTo方法。这里说一下BigDecimal的equals方法除了比较数值之外,还会比较scale精度,不同精度不会equles。
例如下面代码分别会返回0false

BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.10");
System.out.println(a.compareTo(b));
System.out.println(a.equals(b));

不能除尽时ArithmeticException异常

上面提到的加减乘除的4个方法中,除法会比较特殊,因为可能出现除不尽的情况,这时如果没有设置精度,就会抛出ArithmeticException,因为这个是否能除尽是跟具体数值相关的,这会导致偶现的bug,更加难以排查。
例如下面代码就会抛出ArithmeticException异常

BigDecimal a = new BigDecimal(1);
BigDecimal b = new BigDecimal(3);
System.out.println(a.divide(b));

应对的方法是,在除法运算时,注意设置结果的精度和舍入模式,下面的代码就能正常输出结果0.33

BigDecimal a = new BigDecimal(1);
BigDecimal b = new BigDecimal(3);
System.out.println(a.divide(b, 2, RoundingMode.HALF_UP));

总结

BigDecimal主要用于double因为精度丢失而不满足的某些特殊业务场景,例如会计金额计算。在可以忍受略微不精确的场景还是使用内部提供的addsubtractmultiplydivide方法来进行基础的加减乘除运算,运算后会返回新的对象,原始的对象并不会改变。在使用BigDecimal的过程中,要注意创建对象、toString、比较数值、不能除尽时需要设置精度等问题。

以上就是java BigDecimal精度丢失及常见问分析的详细内容,更多关于java BigDecimal精度丢失的资料请关注脚本之家其它相关文章!

相关文章

  • CorsFilter 过滤器解决跨域的处理

    CorsFilter 过滤器解决跨域的处理

    这篇文章主要介绍了CorsFilter 过滤器解决跨域的处理操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • Java中Map集合的常用方法(非常详细!)

    Java中Map集合的常用方法(非常详细!)

    Java中的Map是一种键值对存储的数据结构,它提供了快速查找和访问数据的能力,下面这篇文章主要给大家介绍了关于Java中Map集合的常用方法,需要的朋友可以参考下
    2024-01-01
  • Spring cloud oauth2如何搭建认证资源中心

    Spring cloud oauth2如何搭建认证资源中心

    这篇文章主要介绍了Spring cloud oauth2如何搭建认证资源中心,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-11-11
  • Mybatis中拦截器的使用场景和技巧分享

    Mybatis中拦截器的使用场景和技巧分享

    Mybatis提供了一些机制,可以允许我们在做数据库操作的时候进行我们额外的一些程序,当然,这看起来并没有JPA的EntityListener好用,本文小编将给大家详细的介绍了Mybatis中拦截器的使用场景和技巧,需要的朋友可以参考下
    2023-10-10
  • Java实现深度优先搜索(DFS)和广度优先搜索(BFS)算法

    Java实现深度优先搜索(DFS)和广度优先搜索(BFS)算法

    深度优先搜索(DFS)和广度优先搜索(BFS)是两种基本的图搜索算法,可用于图的遍历、路径搜索等问题。DFS采用栈结构实现,从起点开始往深处遍历,直到找到目标节点或遍历完整个图;BFS采用队列结构实现,从起点开始往广处遍历,直到找到目标节点或遍历完整个图
    2023-04-04
  • Java中几个Reference常见的作用详解

    Java中几个Reference常见的作用详解

    这篇文章主要给大家介绍了Java中关于Reference多个作用的相关资料,文中通过示例代码介绍的非常详细,对大家具有一定的参考学习价值,需要的朋友们下面跟着小编一起来学习学习吧。
    2017-06-06
  • springboot+dubbo启动项目时报错 zookeeper not connected的问题及解决方案

    springboot+dubbo启动项目时报错 zookeeper not connect

    这篇文章主要介绍了springboot+dubbo项目启动项目时报错 zookeeper not connected的问题,本文给大家定位问题及解决方案,结合实例代码给大家讲解的非常详细,需要的朋友可以参考下
    2023-06-06
  • 用java的spring实现一个简单的IOC容器示例代码

    用java的spring实现一个简单的IOC容器示例代码

    本篇文章主要介绍了用java实现一个简单的IOC容器示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-03-03
  • Java关于远程调试程序教程(以Eclipse为例)

    Java关于远程调试程序教程(以Eclipse为例)

    这篇文章主要介绍了Java关于远程调试程序教程(以Eclipse为例),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-06-06
  • 详解Spring Security的formLogin登录认证模式

    详解Spring Security的formLogin登录认证模式

    对于一个完整的应用系统,与登录验证相关的页面都是高度定制化的,非常美观而且提供多种登录方式。这就需要Spring Security支持我们自己定制登录页面,也就是本文给大家介绍的formLogin模式登录认证模式,感兴趣的朋友跟随小编一起看看吧
    2019-11-11

最新评论