对浮点数进行精确的计算,留下来学习

时间:2023-01-08 09:54:47

我们先来看一个例子,你可以猜猜运行结果是啥:
public class Test {
    public static void main(String args[]) {
        System.out.println(0.05 + 0.01);
    }
}
如果以前你没有接触过浮点数运算的话,可能会比较意外,不过你要相信你确实没有看错,结果是
0.060000000000000005

    事实上不仅仅是java,很多编程语言都有这个问题。 (vc6.0肯定有这问题,其他的我暂时还没碰到过) 在某些情况下计算结果是正确的,但是多试几个用例就会出现类似上述程序产生的结果。
    不要小看这一点点误差,这点点误差会造成非常严重的后果,最常见的就是牵涉到现金的问题时。 比如在网上购物,如果你有9.999999999999元,计算机是不可能认为你能买10元的商品的,但是如果有这点误差就很难说了。 很多系统内提供了专门的货币类型就是这个道理,不过java没有。 那么究竟为什么会造成这个情况呢?我们又如何来解决这个问题呢?
    很多事情解决起来容易,真的要解释为什么反而比较困难。 所以这里我先给出一个解决方法,然后再来解释为什么。
    通常最简单的方法是用java.math.BigDecimal来进行浮点数的精确运算。 要注意的是,BigDecimal中有多个构造函数,但是只有BigDecimal(String val)能够解决这个问题。 (有兴趣的可以自己看下BigDecimal.java的源代码,或者看下《Effective Java》里,关于这个问题的详细解释)
    不过由于用了BigDecimal(String val),使得计算过程会频繁的用到类型转换,这既不方便又容易出错,所以我们自己写一个类来解决这个问题。 在最后我会给出类的源代码。
    接下来简单解释一下原因。
    java里的float和double是遵循IEEE754标准的。 如图:

对浮点数进行精确的计算,留下来学习
    关于ieee754的详细解释可以参考以下两个链接。
   http://www.cnblogs.com/bossin/archive/2007/04/08/704567.html
   http://vegeta.blog.enorth.com.cn/article/874.shtml
    由于公式比较麻烦,这里我就不具体展开讲了。 简单地说,因为并非所有小数都可以用这种形式进行精确表示,所以在某些无法用这种形式精确表示的数据上会导致误差的产生。
   
附:Arithmetic4Double.java

import java.math.BigDecimal;
 /**
  * 由于Java的基本数据类型不能够对浮点数进行精确运算,这个工具类提供精确的浮点数运算,包括加减乘除和四舍五入。
  */
 public class Arithmetic4Double {
     //默认除法运算精度
     private static final int DEF_DIV_SCALE = 10;
    
     //所有方法均用静态方法实现,不允许实例化
     private Arithmetic4Double() {}
 
     /**
      * 实现浮点数的加法运算功能
      * @param v1 加数1
      * @param v2 加数2
      * @return v1+v2的和
      */
     public static double add(double v1,double v2) {
         BigDecimal b1 = new BigDecimal(Double.toString(v1));
         BigDecimal b2 = new BigDecimal(Double.toString(v2));
         return b1.add(b2).doubleValue();
     }
     /**
      * 实现浮点数的减法运算功能
      * @param v1 被减数
      * @param v2 减数
      * @return v1-v2的差
      */
     public static double sub(double v1,double v2) {
         BigDecimal b1 = new BigDecimal(Double.toString(v1));
         BigDecimal b2 = new BigDecimal(Double.toString(v2));
         return b1.subtract(b2).doubleValue();
     }
     /**
      * 实现浮点数的乘法运算功能
      * @param v1 被乘数
      * @param v2 乘数
      * @return v1×v2的积
      */
     public static double multi(double v1,double v2) {
         BigDecimal b1 = new BigDecimal(Double.toString(v1));
         BigDecimal b2 = new BigDecimal(Double.toString(v2));
         return b1.multiply(b2).doubleValue();
     }
 
     /**
      * 实现浮点数的除法运算功能
      * 当发生除不尽的情况时,精确到小数点以后DEF_DIV_SCALE位(默认为10位),后面的位数进行四舍五入。
      * @param v1 被除数
      * @param v2 除数
      * @return v1/v2的商
      */
     public static double div(double v1,double v2) {
      BigDecimal b1 = new BigDecimal(Double.toString(v1));
         BigDecimal b2 = new BigDecimal(Double.toString(v2));
         return b1.divide(b2,DEF_DIV_SCALE,BigDecimal.ROUND_HALF_UP).doubleValue();
     }
 
     /**
      * 实现浮点数的除法运算功能
      * 当发生除不尽的情况时,精确到小数点以后scale位,后面的位数进行四舍五入。
      * @param v1 被除数
      * @param v2 除数
      * @param scale 表示需要精确到小数点以后几位
      * @return v1/v2的商
      */
     public static double div(double v1,double v2,int scale) {
         if (scale < 0) {
             throw new IllegalArgumentException(
                 "The scale must be a positive integer or zero");
         }
         BigDecimal b1 = new BigDecimal(Double.toString(v1));
         BigDecimal b2 = new BigDecimal(Double.toString(v2));
         return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
     }
 
     /**
      * 提供精确的小数位四舍五入功能
      * @param v 需要四舍五入的数字
      * @param scale 小数点后保留几位
      * @return 四舍五入后的结果
      */
     public static double round(double v,int scale) {
         if (scale < 0) {
             throw new IllegalArgumentException(
                 "The scale must be a positive integer or zero");
         }
         BigDecimal b = new BigDecimal(Double.toString(v));
         BigDecimal ōne = new BigDecimal("1");
         return b.divide(one,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
     }
 }