数值计算:注意精度、舍入和溢出问题

数值计算:注意精度、舍入和溢出问题

数值计算:注意精度、舍入和溢出问题

在《Effective Java》这本书中也提到这个原则,float和double只能用来做科学计算或者是工程计算,在商业计算中我们要用java.math.BigDecimal


1. Double的坑

四则运算:

public static void main(String[] args) throws Exception {
        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");
    }

数值计算:注意精度、舍入和溢出问题
可以看到,输出结果和我们预期的很不一样。比如,0.1+0.2 输出的不是 0.3 而是0.30000000000000004;再比如,对 2.15-1.10 和 1.05 判等,结果判等不成立。


  • 出现这种问题的主要原因是,计算机是以二进制存储数值的,浮点数也不例外。Java 采用
    了IEEE 754 标准实现浮点数的表达和运算,你可以通过这里查看数值转化为二进制的

结果。

  • 比如,0.1 的二进制表示为 0.0 0011 0011 0011… (0011 无限循环),再转换为十进制就
    是 0.1000000000000000055511151231257827021181583404541015625。对于计算

机而言,0.1 无法精确表达,这是浮点数计算造成精度损失的根源。

在《Effective Java》这本书中也提到这个原则,float和double只能用来做科学计算或者是工程计算,在商业计算中我们要用java.math.BigDecimal


#### BigDecimal 四则运算:


public static void main(String[] args) throws Exception {

        System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)));
        System.out.println(new BigDecimal(1.0).subtract(new BigDecimal(0.8)));
        System.out.println(new BigDecimal(4.015).multiply(new BigDecimal(100)));
        System.out.println(new BigDecimal(123.3).divide(new BigDecimal(100)));

    }

数值计算:注意精度、舍入和溢出问题

可以看到 ,运算还是不精确,只不过是精度高了,使用 BigDecimal 表示和计算浮点数,且务必使用字符串的构造方法来初始化


public static void main(String[] args) throws Exception {

        System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
        System.out.println(new BigDecimal("1.0").subtract(new BigDecimal("0.8")));
        System.out.println(new BigDecimal("4.015").multiply(new BigDecimal("100")));
        System.out.println(new BigDecimal("123.3").divide(new BigDecimal("100")));

    }

数值计算:注意精度、舍入和溢出问题


2. 考虑浮点数舍入和格式化的方式

关于浮点类型的四舍五入 也是千奇百怪,让人莫不着头脑


 public static void main(String[] args) throws Exception {

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

    }

数值计算:注意精度、舍入和溢出问题

如果我们希望使用其他舍入方式来格式化字符串的话,可以设置 DecimalFormat


 public static void main(String[] args) throws Exception {

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

    }
  

数值计算:注意精度、舍入和溢出问题
因此,即使通过 DecimalFormat 来精确控制舍入方式,double 和 float 的问题也可能产
生意想不到的结果,所以浮点数避坑第二原则:浮点数的字符串格式化也要通过BigDecimal 进行。


public static void main(String[] args) throws Exception {

        BigDecimal num1 = new BigDecimal("3.35");
        BigDecimal num2 = num1.setScale(1, BigDecimal.ROUND_DOWN);
        System.out.println(num2);
        BigDecimal num3 = num1.setScale(1, BigDecimal.ROUND_HALF_UP);
        System.out.println(num3);

    }

数值计算:注意精度、舍入和溢出问题

3. BigDecimal不能使用equals

案例 :

   public static void main(String[] args) throws Exception {

        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

我们来看下 BigDecimal 的 equals 方法 源码,equals 比较的是 BigDecimal 的 value 和 scale,1.0 的 scale 是 1,1 的scale 是 0,所以结果一定是 false:

/**
     * 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
     */
    @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());
    }

如果我们希望只比较 BigDecimal 的 value,可以使用 compareTo 方法

 public static void main(String[] args) throws Exception {
        
        BigDecimal c = new BigDecimal("0.00");
        BigDecimal d = new BigDecimal("0");

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

    }

结果:

c compareTo d -->true

4. 数值溢出问题

数值计算还有一个要小心的点是溢出,不管是 int 还是 long,所有的基本数值类型都有超
出表达范围的可能性。

对Long 的最大值 进行 + 1

   public static void main(String[] args) throws Exception {
        long l = Long.MAX_VALUE;
        System.out.println(l + 1);
        System.out.println(l + 1 == Long.MIN_VALUE);

    }

结果: 输出结果是一个负数,因为 Long 的最大值 +1 变为了 Long 的最小值:

-9223372036854775808
true

改进:

 public static void main(String[] args) throws Exception {
        BigInteger i = new BigInteger(String.valueOf(Long.MAX_VALUE));
        System.out.println(i.add(BigInteger.ONE).toString());
 }

结果 :

9223372036854775808
  • 通过 BigInteger 对 Long 的最大值加 1 一点问题都没有

个人博客地址:http://blog.yanxiaolong.cn/

上一篇:DataWorks中业务日期和定时时间的理解


下一篇:知识图谱实战开发案例剖析(1)