如果要求精确的答案,请避免使用float和double

来源:互联网 发布:淘宝无线店铺装修教程 编辑:程序博客网 时间:2024/04/25 20:08

1. 概述

float和double类型的主要设计目的是为了科学计算和工程计算。它们执行二进制浮点运算,这是为了在广域数值范围上提供较为精确的快速近似计算而精心设计的。然而,它们没有提供完全精确的结果,所以不应该被用于要求精确结果的场合。float和double类型对于货币计算尤为不合适,因为要让一个float或者double精确地表示0.1(或者10的任何负数次方值)是不可能的,比如System.out.println(2.0-1.1)将会打印0.899999999999999,而不是你所希望的0.9,这种舍入错误产生的原因是浮点数实际上是用二进制系统实现的,而分数1/10在二进制系统中没有精确的表示,其道理就如同在十进制系统中无法精确表示1/3一样;再比如0.5在二进制系统中有精确表示,而0.55在二进制系统中没有精确表示。

许多数都是无法在有限的n内完全精确的表示出来的,我们只能利用更大的n值来更精确的表示这个数,但更大的n也只是更加逼近精确值而无法得到精确值。例如

  1. public static void main(String[] args) {
  2.     System.out.println("100.5 - 100                 = " + (100.5 - 100));
  3.     System.out.println("100.5F - 100F               = " + (100.5F - 100F));
  4.     System.out.println("100.55 - 100.00             = " + (100.55 - 100.00));
  5.     System.out.println("100.55F - 100.00F           = " + (100.55F - 100.00F));
  6.     System.out.println("new Double(100.55 - 100.00) = " + new Double(100.55 - 100.00));
  7.     System.out.println("new Float(100.55 - 100.00)  = " + new Float(100.55 - 100.00));
  8.     System.out.println("(double)(100.55 - 100.00)   = " + (double)(100.55 - 100.00));
  9.     System.out.println("(float)(100.55 - 100.00)    = " + (float)(100.55 - 100.00));
  10.     
  11.     System.out.println("2.0 - 1.1               = " + (2.0 - 1.1));
  12.     System.out.println("2.0F - 1.1F             = " + (2.0F - 1.1F));
  13.     System.out.println("new Double(2.0 - 1.1)   = " + new Double(2.0 - 1.1));
  14.     System.out.println("new Float(2.0 - 1.1)    = " + new Float(2.0 - 1.1));
  15.     System.out.println("(double)(2.0 - 1.1)     = " + (double)(2.0 - 1.1));
  16.     System.out.println("(float)(2.0 - 1.1)      = " + (float)(2.0 - 1.1));
  17.     System.out.println("1.0 - 0.9               = " + (1.0 - 0.9));
  18.     System.out.println("1.0F - 0.9F             = " + (1.0F - 0.9F));
  19.     System.out.println("new Double(1.0 - 0.9)   = " + new Double(1.0 - 0.9));
  20.     System.out.println("new Float(1.0 - 0.9)    = " + new Float(1.0 - 0.9));
  21.     System.out.println("(double)(1.0 - 0.9)     = " + (double)(1.0 - 0.9));
  22.     System.out.println("(float)(1.0 - 0.9)      = " + (float)(1.0 - 0.9));
  23.     /*
  24.      * 输出结果:
  25.      * 100.5 - 100                 = 0.5
  26.      * 100.5F - 100F               = 0.5
  27.      * 100.55 - 100.00             = 0.5499999999999972
  28.      * 100.55F - 100.00F           = 0.55000305
  29.      * new Double(100.55 - 100.00) = 0.5499999999999972
  30.      * new Float(100.55 - 100.00)  = 0.55 (将一个Double转换为Float自然会有精度丢失, 所以舍入为0.55)
  31.      * (double)(100.55 - 100.00)   = 0.5499999999999972
  32.      * (float)(100.55 - 100.00)    = 0.55 (将一个double转换为float自然会有精度丢失, 所以舍入为0.55)
  33.      * 
  34.      * 2.0 - 1.1                = 0.8999999999999999
  35.      * 2.0F - 1.1F              = 0.9 (这里用Float可以得到正确值, 我不知道原因, 你去问cpu吧. 只要记住这是一个巧合就可以了)
  36.      * new Double(2.0 - 1.1)    = 0.8999999999999999
  37.      * new Float(2.0 - 1.1)     = 0.9 (将一个Double转换为Float自然会有精度丢失, 所以舍入为0.9)
  38.      * (double)(2.0 - 1.1)      = 0.8999999999999999
  39.      * (float)(2.0 - 1.1)       = 0.9 (将一个double转换为float自然会有精度丢失, 所以舍入为0.9)
  40.      * 
  41.      * 1.0 - 0.9                = 0.09999999999999998
  42.      * 1.0F - 0.9F              = 0.100000024
  43.      * new Double(1.0 - 0.9)    = 0.09999999999999998
  44.      * new Float(1.0 - 0.9)     = 0.1 (将一个Double转换为Float自然会有精度丢失, 所以舍入为0.1)
  45.      * (double)(1.0 - 0.9)      = 0.09999999999999998
  46.      * (float)(1.0 - 0.9)       = 0.1 (将一个double转换为float自然会有精度丢失, 所以舍入为0.1)
  47.      * */
  48. }

2. 为什么浮点数会丢失精度(浮点数没办法用十进制来精确表示)

(1)十进制与二进制转换

要归咎于CPU表示浮点数的方法:2.4的二进制表示并非就是精确的2.4,而是最为接近2.3999999999999999;原因在于浮点数由两部分组成:指数和尾数,浮点数的值实际上是由cpu的某个数学公式计算得到的,所以您所遇到的精度损失会在任何操作系统和编程环境中遇到。

(2)类型失配

您可能混合了浮点数和双精度浮点数类型。请确定您在进行数学运算的时候所有的数据类型全部相同。注意:float类型的变量只有7位的精度,而double类型的变量有15位的精度。

3. 解决方法

(1)java.lang.Math中的public static int round(float a)和public static long round(double a)不能解决问题; java.lang.Object --> java.text.Format --> java.text.NumberFormat --> java.text.DecimalFormat 也不能解决问题
(2)使用BigDecimal、int或long(将小数转换为整数再进行计算,例如:将以美元为单位的货币计算改为以美分为单位的货币计算)。java.lang.Object --> java.lang.Number -- Direct Known Subclasses: BigDecimal, Byte, Double, Float, Integer, Long, Short

BigDecimal常用构造方法:

public BigDecimal(double val) --> Translates a double into a BigDecimal which is the exact decimal representation of the double's binary floating-point value.

public BigDecimal(String val) --> Translates the string representation of a BigDecimal into a BigDecimal

通常情况下,上面的那一个使用起来要方便一些。我们可能想都不想就用上了,会有什么问题呢?等到出了问题的时候,才发现上面哪个够造方法的详细说明中有这么一段。

The results of this constructor can be somewhat unpredictable. One might assume that writing new BigDecimal(0.1) in Java creates a 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 double (or, for that matter, as a binary fraction of any finite length). Thus, the value that is being passed in to the constructor is not exactly equal to 0.1, appearances notwithstanding.

The String constructor, on the other hand, is perfectly predictable: writing new BigDecimal("0.1") creates a BigDecimal which is exactly equal to 0.1, as one would expect. Therefore, it is generally recommended that the String constructor be used in preference to this one.

When a double must be used as a source for a BigDecimal, note that this constructor provides an exact conversion; it does not give the same result as converting the double to a String using the Double.toString(double) method and then using the BigDecimal(String) constructor. To get that result, use the static valueOf(double) method.

原来我们如果需要精确计算,非要用String来够造BigDecimal不可!在《Effective Java》一书中的例子也是用String来够造BigDecimal的,但是书上却没有强调这一点,这也许是一个小小的失误吧。

使用BigDecimal加法运算步骤:(1)将两个浮点数转为String,然后构造成BigDecimal;(2)在其中一个上调用add()方法,传入另一个作为参数,然后把运算的结果(BigDecimal)再转换为浮点数。这样比较繁琐,我们可以自己写一个工具类Arith来简化操作:

  1. package edu.hust.test;
  2. import java.math.BigDecimal;
  3. /**
  4.  * 由于Java的简单类型不能够精确的对浮点数进行运算,这个工具类提供精确的浮点数运算,包括加减乘除和四舍五入。
  5.  * 所以提供以下静态方法:
  6.  * public static double add(double v1,double v2)
  7.  * public static double sub(double v1,double v2)
  8.  * public static double mul(double v1,double v2)
  9.  * public static double div(double v1,double v2)
  10.  * public static double div(double v1,double v2,int scale)
  11.  * public static double round(double v,int scale)
  12.  * 
  13.  */
  14. public class Arith {
  15.     //默认除法运算精度
  16.     private static final int DEF_DIV_SCALE = 10;
  17.     //这个类不能实例化
  18.     private Arith(){}
  19.     /**
  20.      * 提供精确的加法运算。
  21.      * @param v1 被加数
  22.      * @param v2 加数
  23.      * @return 两个参数的和
  24.      */
  25.     public static double add(double v1,double v2){
  26.         BigDecimal b1 = new BigDecimal(Double.toString(v1));
  27.         BigDecimal b2 = new BigDecimal(Double.toString(v2));
  28.         return b1.add(b2).doubleValue();
  29.     }
  30.     /**
  31.      * 提供精确的减法运算。
  32.      * @param v1 被减数
  33.      * @param v2 减数
  34.      * @return 两个参数的差
  35.      */
  36.     public static double sub(double v1,double v2){
  37.         BigDecimal b1 = new BigDecimal(Double.toString(v1));
  38.         BigDecimal b2 = new BigDecimal(Double.toString(v2));
  39.         return b1.subtract(b2).doubleValue();
  40.     } 
  41.     /**
  42.      * 提供精确的乘法运算。
  43.      * @param v1 被乘数
  44.      * @param v2 乘数
  45.      * @return 两个参数的积
  46.      */
  47.     public static double mul(double v1,double v2){
  48.         BigDecimal b1 = new BigDecimal(Double.toString(v1));
  49.         BigDecimal b2 = new BigDecimal(Double.toString(v2));
  50.         return b1.multiply(b2).doubleValue();
  51.     }
  52.     /**
  53.      * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到小数点以后10位,以后的数字四舍五入。
  54.      * @param v1 被除数
  55.      * @param v2 除数
  56.      * @return 两个参数的商
  57.      */
  58.     public static double div(double v1,double v2){
  59.         return div(v1,v2,DEF_DIV_SCALE);
  60.     }
  61.     /**
  62.      * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指定精度,以后的数字四舍五入。
  63.      * @param v1 被除数
  64.      * @param v2 除数
  65.      * @param scale 表示表示需要精确到小数点以后几位。
  66.      * @return 两个参数的商
  67.      */
  68.     public static double div(double v1,double v2,int scale){
  69.         if(scale<0){
  70.             throw new IllegalArgumentException("The scale must be a positive integer or zero");
  71.         }
  72.         BigDecimal b1 = new BigDecimal(Double.toString(v1));
  73.         BigDecimal b2 = new BigDecimal(Double.toString(v2));
  74.         return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
  75.     }
  76.     /**
  77.      * 提供精确的小数位四舍五入处理。
  78.      * @param v 需要四舍五入的数字
  79.      * @param scale 小数点后保留几位
  80.      * @return 四舍五入后的结果
  81.      */
  82.     public static double round(double v,int scale){
  83.         if(scale<0){
  84.             throw new IllegalArgumentException("The scale must be a positive integer or zero");
  85.         }
  86.         BigDecimal b = new BigDecimal(Double.toString(v));
  87.         BigDecimal one = new BigDecimal("1");
  88.         return b.divide(one,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
  89.     }
  90. }

使用BigDecimal缺点:要创建对象并占用内存;其floatValue()方法有时会精度丢失而造成计算错误(所以一般小数计算尽量使用double)。

最后补充一点:也正因为此,浮点数不能用于循环判断,否则容易造成死循环。

原创粉丝点击