不要以为你用了BigDecimal后,计算结果就一定精确了

欢迎关注本人公众号

在这里插入图片描述

异常再现

大家应该都已经知道,涉及到金钱的计算应该使用BigDecimal,没有使用BigDecimal的已经被开除。

但是使用了BigDecimal后计算结果就一定是精确的吗?未必。
看下面测试

BigDecimal a = new BigDecimal(1.01);
        BigDecimal b = new BigDecimal(2.02);

        BigDecimal c = new BigDecimal("1.01");
        BigDecimal d = new BigDecimal("2.02");

        BigDecimal e = new BigDecimal(Double.toString(1.01));
        BigDecimal f = new BigDecimal(Double.toString(2.02));

        BigDecimal g = BigDecimal.valueOf(1.01);
        BigDecimal h = BigDecimal.valueOf(2.02);

        System.out.println(a.add(b));
        System.out.println(c.add(d));
        System.out.println(e.add(f));
        System.out.println(g.add(h));

输出结果为:

3.0300000000000000266453525910037569701671600341796875
3.03
3.03
3.03

可以看到第一行输出是有问题的。

float和double精度问题

在java中,double是双精度,64位,浮点数,默认是0.0d。float是单精度,32位,浮点数,默认是0.0f;
其中float的存储方式如下图所示:

image

而双精度的存储方式为:

image
float和double的精度是由尾数的位数来决定的。浮点数在内存中是按科学计数法来存储的,其整数部分始终是一个隐含着的“1”,由于它是不变的,故不能对精度造成影响。

float:2^23 = 8388608,一共七位,这意味着最多能有7位有效数字,但绝对能保证的为6位,也即float的精度为6~7位有效数字;

double:2^52 = 4503599627370496,一共16位,同理,double的精度为15~16位。

为什么会精度丢失?

计算机在处理数据都涉及到数据的转换和各种复杂运算,比如,不同单位换算,不同进制(如二进制十进制)换算等,很多除法运算不能除尽,比如10÷3=3.3333…无穷无尽,而精度是有限的,3.3333333x3并不等于10,经过复杂的处理后得到的十进制数据并不精确,精度越高越精确。float和double的精度是由尾数的位数来决定的,其整数部分始终是一个隐含着的“1”,由于它是不变的,故不能对精度造成影响。float:2^23 = 8388608,一共七位,由于最左为1的一位省略了,这意味着最多能表示8位数: 28388608 = 16777216 。有8位有效数字,但绝对能保证的为7位,也即float的精度为7~8位有效数字;double:2^52 = 4503599627370496,一共16位,同理,double的精度为16~17位。
在这里插入图片描述

当到达一定值自动开始使用科学计数法,并保留相关精度的有效数字,所以结果是个近似数,并且指数为整数。在十进制中小数有些是无法完整用二进制表示的。所以只能用有限位来表示,从而在存储时可能就会有误差。对于十进制的小数转换成二进制采用乘2取整法进行计算,取掉整数部分后,剩下的小数继续乘以2,直到小数部分全为0。

BigDecimal分析

BigDecimal(double val)这个方法是无法保证精度的,源码注释中也已经写明:

 /**
     * Translates a {@code double} into a {@code BigDecimal} which
     * is the exact decimal representation of the {@code double}'s
     * binary floating-point value.  The scale of the returned
     * {@code BigDecimal} is the smallest value such that
     * <code>(10<sup>scale</sup> &times; val)</code> is an integer.
     * <p>
     * <b>Notes:</b>
     * <ol>
     * <li>
     * The results of this constructor can be somewhat unpredictable.
     * One might assume that writing {@code new BigDecimal(0.1)} in
     * Java creates a {@code BigDecimal} which is exactly equal to
     * 0.1 (an unscaled value of 1, with a scale of 1), but it is
     * actually equal to
     * 0.1000000000000000055511151231257827021181583404541015625.
     * This is because 0.1 cannot be represented exactly as a
     * {@code double} (or, for that matter, as a binary fraction of
     * any finite length).  Thus, the value that is being passed
     * <em>in</em> to the constructor is not exactly equal to 0.1,
     * appearances notwithstanding.
     *
     * <li>
     * The {@code String} constructor, on the other hand, is
     * perfectly predictable: writing {@code new BigDecimal("0.1")}
     * creates a {@code BigDecimal} which is <em>exactly</em> equal to
     * 0.1, as one would expect.  Therefore, it is generally
     * recommended that the {@linkplain #BigDecimal(String)
     * String constructor} be used in preference to this one.
     *
     * <li>
     * When a {@code double} must be used as a source for a
     * {@code BigDecimal}, note that this constructor provides an
     * exact conversion; it does not give the same result as
     * converting the {@code double} to a {@code String} using the
     * {@link Double#toString(double)} method and then using the
     * {@link #BigDecimal(String)} constructor.  To get that result,
     * use the {@code static} {@link #valueOf(double)} method.
     * </ol>
     *
     * @param val {@code double} value to be converted to
     *        {@code BigDecimal}.
     * @throws NumberFormatException if {@code val} is infinite or NaN.
     */
    public BigDecimal(double val) {
        this(val,MathContext.UNLIMITED);
    }

并且告诉我们应该使用BigDecimal(String val):

public BigDecimal(String val) {
        this(val.toCharArray(), 0, val.length());
    }

当然使用BigDecimal valueOf(double val)也是可以的,valueOf实际是先将double或flout的数据转为了string:

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

另外,特别注意,以下代码是错误的哦:

@Test
public void a(){
    float a= 0.1f;
    double b = a;
    System.out.println(a);
    System.out.println(b);

    BigDecimal bigDecimal = BigDecimal.valueOf(0.1f);
    System.out.println(bigDecimal);
}

输出:

0.1
0.10000000149011612
0.10000000149011612

其实这个不是BigDecimal.valueOf的问题,而是float转double时精度丢失了。

BigDecimal比较大小注意事项(equals)

在项目中使用BigDecimal的equals方法比较大小时,结果不为true,直接上示例

    public static void main(String[] args) {
        BigDecimal a = new BigDecimal(0.00);
        BigDecimal b = new BigDecimal(0);
 
        boolean result = a.equals(b);
        System.out.println("a equals b -->" + result);
 
        BigDecimal c = new BigDecimal("0.00");
        BigDecimal d = new BigDecimal("0");
 
        boolean result1 = c.equals(d);
        System.out.println("c equals d -->" + result1);
    }

结果:

a equals b -->true
c equals d -->false

可以看到a和b比较结果是true,c和d比较的结果为fasle

c、d使用传入字符串的构造器(等同于数据库查询出来的值)

项目中从数据库查询出来的值进行比较时(上例中c、d)显然不是我们期望的结果,因此修改为如下方法

boolean result2 = c.compareTo(d) == 0;
System.out.println("c compareTo d -->" + result2);

结果:

c compareTo d -->true
我们来看下构造器:

    public BigDecimal(String val) {
        this(val.toCharArray(), 0, val.length());
    }

该构造器的注释中有这么一段:

* <p><b>Examples:</b><br>
     * The value of the returned {@code BigDecimal} is equal to
     * <i>significand</i> &times; 10<sup>&nbsp;<i>exponent</i></sup>.
     * For each string on the left, the resulting representation
     * [{@code BigInteger}, {@code scale}] is shown on the right.
     * <pre>
     * "0"            [0,0]
     * "0.00"         [0,2]
     * "123"          [123,0]
     * "-123"         [-123,0]
     * "1.23E3"       [123,-1]
     * "1.23E+3"      [123,-1]
     * "12.3E+7"      [123,-6]
     * "12.0"         [120,1]
     * "12.3"         [123,1]
     * "0.00123"      [123,5]
     * "-1.23E-12"    [-123,14]
     * "1234.5E-4"    [12345,5]
     * "0E+7"         [0,-7]
     * "-0"           [0,0]
     * </pre>
     *

可以看出,“0”传入构造器得到的是0且没有小数位,“0.00”传入构造器得到的是0.00,含有2位小数

再看看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方法比较了小数位数 -----> if (scale != xDec.scale) return false;

到这里可以理解上面C、Dequals比较结果为什么是false了

再来看看compareTo方法

/**
     * Compares this {@code BigDecimal} with the specified
     * {@code BigDecimal}.  Two {@code BigDecimal} objects that are
     * equal in value but have a different scale (like 2.0 and 2.00)
     * are considered equal by this method.  This method is provided
     * in preference to individual methods for each of the six boolean
     * comparison operators ({@literal <}, ==,
     * {@literal >}, {@literal >=}, !=, {@literal <=}).  The
     * suggested idiom for performing these comparisons is:
     * {@code (x.compareTo(y)} &lt;<i>op</i>&gt; {@code 0)}, where
     * &lt;<i>op</i>&gt; is one of the six comparison operators.
     *
     * @param  val {@code BigDecimal} to which this {@code BigDecimal} is
     *         to be compared.
     * @return -1, 0, or 1 as this {@code BigDecimal} is numerically
     *          less than, equal to, or greater than {@code val}.
     */
    public int compareTo(BigDecimal val) {
        // Quick path for equal scale and non-inflated case.
        if (scale == val.scale) {
            long xs = intCompact;
            long ys = val.intCompact;
            if (xs != INFLATED && ys != INFLATED)
                return xs != ys ? ((xs > ys) ? 1 : -1) : 0;
        }
        int xsign = this.signum();
        int ysign = val.signum();
        if (xsign != ysign)
            return (xsign > ysign) ? 1 : -1;
        if (xsign == 0)
            return 0;
        int cmp = compareMagnitude(val);
        return (xsign > 0) ? cmp : -cmp;
    }

可以看到,分了2种情况,一种是含有小数位相同,另一种时不相同的情况。所以不管2个数的小数位是否相同,都会进行值的比较。

总结

(1)商业计算使用BigDecimal。构造函数一定要用new BigDecimal(String str)方法。
(2)使用参数类型为String的构造函数或valueOf()方法。但不要传float类型的值,如果不确定,则用new BigDecimal(String str)方法。
(3) BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值。
(4) 比较使用compareTo而不是equals。除非你如果需要比较小数位都位数

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

快乐崇拜234

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值