“危险“的 Double,使用的时候要小心了!

浮点数的精度问题

我们首先看一个例子:

 System.out.println(0.1 +0.2);
 System.out.println(1.0-0.8);
  System.out.println(4.015*100);
 System.out.println(123.3/100);

 double amount1 = 2.15;
 double amount2 = 1.10;
 if (amount1 - amount2 == 1.05)
     System.out.println("OK");

我们预测的结果:

System.out.println(0.1 +0.2);// 0.3
System.out.println(1.0-0.8);// 0.2
System.out.println(4.015*100);//401.5
System.out.println(123.3/100)//1.233
OK

而真正的结果呢?

0.30000000000000004
0.19999999999999996
401.49999999999994
1.2329999999999999

那么为什么会出现上述的结果呢?
因为Java浮点数使用二进制数据的科学计数法来表示浮点数,因此可能不能精确表示一个浮点数。

比如,0.1 的二进制表示为 0.0 0011 0011 0011… (0011 无限循环),再转换为十进制就是 0.1000000000000000055511151231257827021181583404541015625。对于计算机而言,0.1 无法精确表达,这是浮点数计算造成精度损失的根源。

如果在一些精度要求很高的系统中,这种问题是非常严重的,因为如果你有9.99999999元,计算机是不会问题你可以购买10元商品的。特别是做金钱交易的时候,最终损失的精度就是大量的资金出入的。

在<> 一书中到了一个原则,那就是float 和 double 只能用来科学计算或者工程计算,但是在商业计算中我们要用 java.math.BigDecimal。通过使用BigDecimal 类可以解决上面问题。

我们以相加为例:

       double d1 = 0.1;
        double d2 = 0.2;
        BigDecimal b1 = new BigDecimal(Double.toString(d1));
        BigDecimal b2 = new BigDecimal(Double.toString(d2));
        System.out.println(b1.add(b2).doubleValue());

错误用法如下:

    double d1 = 0.1;
        double d2 = 0.2;
        BigDecimal b1 = new BigDecimal(d1);
        BigDecimal b2 = new BigDecimal(d2);
        System.out.println(b1.add(b2).doubleValue());

得到的结果:0.30000000000000004

可以看到,运算结果还是不精确,只不过是精度高了而已。这时因为 BigDecimal(doubel var) 构造方法的结果有一定的不可预知性。有人认为在java中写入new BigDecimal(0.1) 所创建的BigDecimal 正好等于0.1,但是它实际等于 0.1000000000005555533377873773. 这时因为0.1 无法准确地表示为double ,这样传入到构造方法的值不会正好等于0.1。

Stirng 了你行难呢过参数的构造方法是完全可预知的:写入 new BigDecimal(“0.1”) 将创建一个BigDecimal, 它正好等于预期的0.1.因此,比较而言,通常建议优先使用String 类型参数的构造方法。

第一个坑 : 使用BigDecimal 表示和计算浮点数,且务必使用字符串的构造方法来初始化BigDecimal

浮点数的舍入和格式化的方式

String.format 的格式化舍入方式,结果可能也是你预想不到的,我们看下面的例子:


double num1 = 3.35;
float num2 = 3.35f;
System.out.println(String.format("%.1f", num1));//四舍五入
System.out.println(String.format("%.1f", num2));

结果竟然是:

3.4
3.3

那么为什么呢?
因为这时因为精度问题和舍入方式共同导致的,double 和 float 的3.35 想当于 3.350xxx 和 3.349xxx。

3.350000000000000088817841970012523233890533447265625
3.349999904632568359375

String.format 采用四舍五入的方式进行舍入,取 1 位小数,double 的 3.350 四舍五入为 3.4,而 float 的 3.349 四舍五入为 3.3。

我们看一下Formatter 类的相关源码,可以发现使用的舍入模式是 HALF_UP


else if (c == Conversion.DECIMAL_FLOAT) {
    // Create a new BigDecimal with the desired precision.
    int prec = (precision == -1 ? 6 : precision);
    int scale = value.scale();

    if (scale > prec) {
        // more "scale" digits than the requested "precision"
        int compPrec = value.precision();
        if (compPrec <= scale) {
            // case of 0.xxxxxx
            value = value.setScale(prec, RoundingMode.HALF_UP);
        } else {
            compPrec -= (scale - prec);
            value = new BigDecimal(value.unscaledValue(),
                                   scale,
                                   new MathContext(compPrec));
        }
    }

如果我们呢希望使用其他舍入方式来格式化字符串的话,可以设置 DecimalFormatter,如下代码所示:

       double num1 = 3.3555;
        float num2 = 3.35f;
        DecimalFormat format = new DecimalFormat("#.##");
        format.setRoundingMode(RoundingMode.FLOOR);
        //format.setRoundingMode(RoundingMode.DOWN);
        System.out.println(format.format(num1));
        format.setRoundingMode(RoundingMode.DOWN);
        System.out.println(format.format(num2)); 

DecimalFormatter 其他用法如下:

 double pi = 3.1415927;//圆周率
        //取一位整数
        System.out.println(new DecimalFormat("0").format(pi));//3
        //取一位整数和两位小数
        System.out.println(new DecimalFormat("0.00").format(pi));//3.14
        //取两位整数和三位小数,整数不足部分以0填补。
        System.out.println(new DecimalFormat("00.000").format(pi));// 03.142
        //取所有整数部分
        System.out.println(new DecimalFormat("#").format(pi));//3
        //以百分比方式计数,并取两位小数
        System.out.println(new DecimalFormat("#.##%").format(pi));//314.16%
          long c =299792458;//光速
        //显示为科学计数法,并取五位小数
        System.out.println(new DecimalFormat("#.#####E0").format(c));//2.99792E8
        //显示为两位整数的科学计数法,并取四位小数
        System.out.println(new DecimalFormat("00.####E0").format(c));//29.9792E7
        //每三位以逗号进行分隔。
        System.out.println(new DecimalFormat(",###").format(c));//299,792,458
        //将格式嵌入文本
        System.out.println(new DecimalFormat("光速大小为每秒,###米。").format(c));

第二个坑: 浮点数的字符串格式化也要通过BigDecimal进行

BigDecimal 怎样比较相等

我们知道包装类的比较要通过equals进行,而不能使用 ==。我们使用一个例子如下:

System.out.println(new BigDecimal("1.0").equals(new BigDecimal("1")))

得到的结果时false. 我们看下BigDecimal 的equals 方法

    /**
     * Compares this {@code BigDecimal} with the specified
     * {@code Object} for equality.  Unlike {@link
     * #compareTo(BigDecimal) compareTo}, this method considers two
     * {@code BigDecimal} objects equal only if they are equal in
     * value and scale (thus 2.0 is not equal to 2.00 when compared by
     * this method).
     *
     * @param  x {@code Object} to which this {@code BigDecimal} is
     *         to be compared.
     * @return {@code true} if and only if the specified {@code Object} is a
     *         {@code BigDecimal} whose value and scale are equal to this
     *         {@code BigDecimal}'s.
     * @see    #compareTo(java.math.BigDecimal)
     * @see    #hashCode
     */

上面的注释说明,equals 比较的是两个参数 一个是value 一个 是scale

即 2.0 是不等于 2.00 的。

如果我们只是比较值的话,可以使用compareTo方法

System.out.println(new BigDecimal("2.0").compareTo(new BigDecimal("2.00"))==0);

结果为 true

小心值的溢出

数值计算还有一个要小心的点是溢出,不管是 int 还是 long,所有的基本数值类型都有超出表达范围的可能性。
比如,对 Long 的最大值进行 +1 操作:

        long l = Long.MAX_VALUE;
        System.out.println(l + 1);
        System.out.println(l + 1 == Long.MIN_VALUE);

结果:

-9223372036854775808
true

结果 为负数,因为long溢出了,
虽然发生了溢出,但是没有提示任何异常。这类问题容易被忽略,改进的方式有以下两种:

方法一是,考虑使用 Math 类的 addExact、subtractExact 等 xxExact 方法进行数值运算,这些方法可以在数值溢出时主动抛出异常。我们来测试一下,使用 Math.addExact 对 Long 最大值做 +1 操作:


try {
    long l = Long.MAX_VALUE;
    System.out.println(Math.addExact(l, 1));
} catch (Exception ex) {
    ex.printStackTrace();
}

执行后,可以得到 ArithmeticException,这是一个 RuntimeException:

java.lang.ArithmeticException: long overflow
	at java.lang.Math.addExact(Math.java:809)
	at com.md.DateTimeTest.main(DateTimeTest.java:14)

方法二是,使用大数类 BigInteger。BigDecimal 是处理浮点数的专家,而 BigInteger 则是对大数进行科学计算的专家。

如下代码,使用 BigInteger 对 Long 最大值进行 +1 操作;如果希望把计算结果转换一个 Long 变量的话,可以使用 BigInteger 的 longValueExact 方法,在转换出现溢出时,同样会抛出 ArithmeticException:


BigInteger i = new BigInteger(String.valueOf(Long.MAX_VALUE));
System.out.println(i.add(BigInteger.ONE).toString());

try {
    long l = i.add(BigInteger.ONE).longValueExact();
} catch (Exception ex) {
    ex.printStackTrace();
}

输出结果如下:

9223372036854775808
java.lang.ArithmeticException: BigInteger out of long range
	at java.math.BigInteger.longValueExact(BigInteger.java:4531)
	at com.md.DateTimeTest.main(DateTimeTest.java:17)

可以看到,通过 BigInteger 对 Long 的最大值加 1 一点问题都没有,当尝试把结果转换为 Long 类型时,则会提示 BigInteger out of long range。

总结

第一,切记,要精确表示浮点数应该使用 BigDecimal。并且,使用 BigDecimal 的 Double 入参的构造方法同样存在精度丢失问题,应该使用 String 入参的构造方法或者 BigDecimal.valueOf 方法来初始化。

第二,对浮点数做精确计算,参与计算的各种数值应该始终使用 BigDecimal,所有的计算都要通过 BigDecimal 的方法进行,切勿只是让 BigDecimal 来走过场。任何一个环节出现精度损失,最后的计算结果可能都会出现误差。

第三,对于浮点数的格式化,如果使用 String.format 的话,需要认识到它使用的是四舍五入,可以考虑使用 DecimalFormat 来明确指定舍入方式。但考虑到精度问题,我更建议使用 BigDecimal 来表示浮点数,并使用其 setScale 方法指定舍入的位数和方式。

第四,进行数值运算时要小心溢出问题,虽然溢出后不会出现异常,但得到的计算结果是完全错误的。我们考虑使用 Math.xxxExact 方法来进行运算,在溢出时能抛出异常,更建议对于可能会出现溢出的大数运算使用 BigInteger 类。

总之,对于金融、科学计算等场景,请尽可能使用 BigDecimal 和 BigInteger,避免由精度和溢出问题引发难以发现,但影响重大的 Bug。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

半夏_2021

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

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

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

打赏作者

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

抵扣说明:

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

余额充值