由于BigDecimal的原因,差点被提桶

举报
三婶儿 发表于 2023/04/29 17:37:57 2023/04/29
【摘要】 咱就是说,金额相关计算第一考虑肯定是确保精准,优选BigDecimal类型呀,Double类型很容易丢失精度的。尤其是金额,一定要严谨!

不是三婶儿偏执,非要吐槽。家人们,咱就是说,按照基操逻辑谁会把严格金额计算相关的数据使用double类型呢…
“我以为吕布已经够勇猛了,这是谁的部下?”

image.png

前几天,一xxx让帮忙写段代码。内容比较常规,就是按照自定义规则自动计算出一些金额数据。楼主想着暂时也不忙,就帮着写写呗。好家伙!不写不知道,当看到他用Double类型定义存储金额时,内心瞬间沸腾了。

咱就是说,金额相关计算第一考虑肯定是确保精准,优选BigDecimal类型呀,Double类型很容易丢失精度的。尤其是金额,一定要严谨!

果不其然,这…真是暴风雨的前奏。我发现有好几个业务模块都使用了这些金额数据做运算,多次加减乘除之类的…

好家伙,“海燕啊,你可长点心吧!”。

image.png

那下面楼主就来详细分析一下,为什么更加建议使用BigDecimal。开整!

一、BigDecimal类型数据和Double类型

首先,先来了解一下什么是BigDecimal、什么是Double、什么是double。以及Double和double之间有什么关系。

1.1、BigDecimal

对于什么是BigDecimal,百度百科中这样描述:

Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数。在实际应用中,需要对更大或者更小的数进行运算和处理。float和double只能用来做科学计算或者是工程计算,在商业计算中要用java.math.BigDecimal。BigDecimal所创建的是对象,我们不能使用传统的+、-、*、/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。方法中的参数也必须是BigDecimal的对象。构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象。

主要是: 可对超过16位有效位的数进行精确的运算。

1.2、Double和double的区别

1.2.1、double

八大基本数据类型之一,双精度浮点数。

double(双精度浮点型)是计算机使用的一种资料型别。比起单精度浮点数(float),double(双精度浮点数)使用 64 位(8字节) 来储存一个浮点数。 它可以表示十进制的15或16位有效数字,负值取值范围为 -1.7976E+308 到 -4.94065645841246544E-324,正值取值范围为 4.94065645841246544E-324 到 1.797693E+308

1.2.2、Double

Double是基于基本数据类型double的一个封装类,就是我们常用的java.lang.Double。

image.png

1.2.3、double和Double之间的关系

嗯,是有关系,但也不是情侣关系…

image.png

从jdk1.5开始,引入了“自动装箱”、“自动拆箱”的概念,简化了基本数据类型和包装类之间的转化,提高了使用效率。

关于什么是装箱、拆箱。举个栗子:比如我们常用的List就是一个自动装箱、拆箱的体现。

image.png

如图所示,楼主定义了一个Integer类型的list。当往list中放入数据1和2时,会把int类型自动转换为Integer类型,这个过程就是自动装箱。

image.png

反过来说,当从list中取出数据时,会把Integer类型自动转换为int类型,这个过程就是自动拆箱。

自动装箱最大的优点就是:可以直接使用包装类中所有的方法。

我们直接调用即可,方法内部都已经帮我们处理好了。感兴趣的可以看下源码深入了解哈,这里不再过多介绍。

image.png

二、两种类型数据的适用场景、优缺点

2.1、Double类型

适用场景:双精度、非重要性、或“相对模糊”的数据存储。比如:百分比计算。

缺点:数值容易丢失精度。

优点:执行效率高。

2.2、BigDecimal类型

适用场景:较为严谨的数值计算。比如:交易金额相关计算。

优点:数值计算较为精准。

缺点:较Double类型执行效率弱一些。

三、为什么不建议用Double类型计算金额

3.1、Double类型计算精度易丢失

当我们进行数值加减乘除运算时,Double类型很大程度上会产生误差。

如下图示例,楼主定义了2个Double类型数据的加减乘除,运算结果有3个产生了误差。有误差,但是误差不大。

image.png

那么为什么会存在这个问题呢?

可想而知,double类型在运算时,会先将数值转换成二进制然后再做运算。但是在转换过程中,可能会发生存储小数部分的位数不够的现象(无限循环小数),所以很大程度上可能会有非常小的误差产生

因此,不要直接使用入参double类型数值直接进行运算。可考虑使用string类型参数进行处理。

image.png

3.2、数值过大会变为科学记数法形式

当数值过大时,会变为这种科学记数法形式。

image.png

解决方案:可考虑使用BigDecimal类型进行转换。

四、BigDecimal常用方法

4.1、BigDecimal的初始化

楼主分别定义了2个不同类型的入参。

image.png

眼尖的小伙伴估计注意到了,实例化a对象时new BigDecimal(“0.12”)传入的是字符串,实例化b对象时new BigDecimal(0.12)传入的是double数值。大家觉得运行结果会一样吗?

可能不一样吧…嗯,不是可能,是一定!!!

image.png

字符串类型的输出了“0.12”,而double数值类型的却输出了“0.11999999999999999555910790149937383830547332763671875”。

image.png

这又回归到了上面说所的二进制转换存储小数部分的位数不够,造成的误差。使用double类型初始化BigDecimal对象,进行运算时可能会出现精度不准确的问题。

image.png

所以一定注意:不要使用double类型作为入参,直接去new一个BigDecimal对象!可能会有精度误差!

那么,为什么string就可以呢?

string是不可变的,定义为string之后再转换为数值肯定是固定的啊。

底层很多实现也是这个原理。比如BigDecimal.valueOf()方法,如果输入的是double类型,实质上源码中还是先转化为了字符串。

image.png

对于整型或保留小数位的,也有相应的处理方法。

image.png

方法有很多,大家想了解的可自行研究下,楼主就不再一一示例啦。

image.png

4.2、BigDecimal加法

BigDecimal add(BigDecimal augend)

image.png

两个BigDecimal类型数据相加,方法调用、及运行效果。

image.png

4.3、BigDecimal减法

BigDecimal subtract(BigDecimal subtrahend)

image.png

两个BigDecimal类型数据相减,方法调用、及运行效果。

image.png

4.4、BigDecimal乘法

BigDecimal multiply(BigDecimal multiplicand)

image.png

两个BigDecimal类型数据相乘,方法调用、及运行效果。

image.png

4.5、BigDecimal除法

BigDecimal divide(BigDecimal divisor)

image.png

两个BigDecimal类型数据相除,方法调用、及运行效果。

image.png

哎呦,惊喜不惊喜,意外不意外?相除的时候出现了无限循环小数

因此 相除的场景下尽可能设置保留小数位,可避免运算当中抛出异常

image.png

4.6、BigDecimal比较大小

楼主分别定义了2个值为“0.12”的数据a、b,以及值为“0.120”的数据c进行比较。

image.png

可以发现:相同值“0.12”通过双等号对比返回的结果是false,equals对比返回的是true。而“0.12”和“0.120”实质上数值大小是一样的,但通过双等号或equals进行对比,返回均为false

那么,对于BigDecimal类型如何比较大小呢?(叫我靓妹我就告诉你…)

image.png

方法就是:使用compareTo方法进行比较

image.png

a、b两值进行比较:a.compareTo(b) 。 结果为0表示a、b值相等。结果为-1表示a小于b。结果为1表示a大于b

4.7、BigDecimal工具类

为大家附上常用的操作工具类,直接调用即可。


package com.wss.demo.cas;

import java.math.BigDecimal;

public class ArithUtil {


    private static final int DEF_DIV_SCALE = 2; // 小数点后的保留位数

    /**
     * Double精确的加法运算
     *
     * @param d1 被加数
     * @param d2 加数
     * @return 两个参数的和
     */
    public static BigDecimal add(double d1, double d2) {
        BigDecimal value1 = new BigDecimal(Double.toString(d1));
        BigDecimal value2 = new BigDecimal(Double.toString(d2));
        return value1.add(value2);
    }

    /**
     * Double精确的减法运算
     *
     * @param d1 被减数
     * @param d2 减数
     * @return 两个参数的差
     */
    public static BigDecimal sub(double d1, double d2) {
        BigDecimal value1 = new BigDecimal(Double.toString(d1));
        BigDecimal value2 = new BigDecimal(Double.toString(d2));
        return value1.subtract(value2);
    }

    /**
     * Double精确的乘法运算
     *
     * @param d1 被乘数
     * @param d2 乘数
     * @return 两个参数的积
     */
    public static BigDecimal mul(double d1, double d2) {
        BigDecimal value1 = new BigDecimal(Double.toString(d1));
        BigDecimal value2 = new BigDecimal(Double.toString(d2));
        return value1.multiply(value2);
    }

    /**
     * Double精确的除法运算, 当出现除不尽的情况时, 精确到小数点以后n位, 以后的数字四舍五入
     *
     * @param d1 被除数
     * @param d2 除数
     * @return 两个参数的商
     */
    public static BigDecimal div(double d1, double d2) {
        return div(d1, d2, DEF_DIV_SCALE);
    }

    /**
     * Double精确的除法运算, 当出现除不尽的情况时, 精确到小数点以后n位, 以后的数字四舍五入
     *
     * @param d1    被除数
     * @param d2    除数
     * @param scale 表示需要精确到小数点的后几位
     * @return 两个参数的商
     */
    public static BigDecimal div(double d1, double d2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException("参数[scale]必须是正整数或者零");
        }
        BigDecimal value1 = new BigDecimal(Double.toString(d1));
        BigDecimal value2 = new BigDecimal(Double.toString(d2));
        return value1.divide(value2, scale, BigDecimal.ROUND_HALF_UP);
    }

    /**
     * 比较大小
     */
    public static boolean equalTo(BigDecimal b1, BigDecimal b2) {
        if(b1 == null || b2 == null) {
            return false;
        }
        return 0 == b1.compareTo(b2);
    }

}

嗯,今天的总结就先到这吧。散会了,别忘记给三婶儿点个赞哈~

image.png

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。