java中Double类型的运算精度问题

一、简述
在很多编程语言中,浮点数类型float和double运算会丢失精度。

在大多数情况下,计算的结果是准确的,float和double只能用来做科学计算或者是工程计算,在银行、帐户、计费等领域,BigDecimal提供了精确的数值计算。

Java在商业计算中要用 java.math.BigDecimal。

  public static void main(String[] args) {
    System.out.println(0.05 + 0.01);  // 0.060000000000000005
    System.out.println(1.0 - 0.42);   // 0.5800000000000001
    System.out.println(4.015 * 100);  // 401.49999999999994
    System.out.println(123.3 / 100);  // 1.2329999999999999
    System.out.println(Math.round(4.015 * 100) / 100.0);  // 4.01 四舍五入保留两位
  }

java.math.BigDecimal:不可变的、任意精度的有符号十进制数。BigDecimal 由任意精度的整数非标度值(unscaledValue)和32位的整数标度(scale)组成。其值为该数的非标度值乘以10的负scale次幂,即为(unscaledValue * 10-scale)。

public class Test{
    public static void main(String args[]){
        System.out.println(0.05+0.01);
        System.out.println(1.0-0.42);
        System.out.println(4.015*100);
        System.out.println(123.3/100);
    }
};
你没有看错!结果确实是
0.060000000000000005
0.5800000000000001
401.49999999999994
1.2329999999999999
Java中的简单浮点数类型float和double不能够进行运算。不光是Java,在其它很多编程语言中也有这样的问题。在大多数情况下,计算的结果是准确的,但是多试几次(可以做一个循环)就可以试出类似上面的错误。现在终于理解为什么要有BCD码了。
这个问题相当严重,如果你有9.999999999999元,你的计算机是不会认为你可以购买10元的商品的。
在有的编程语言中提供了专门的货币类型来处理这种情况,但是Java没有。现在让我们看看如何解决这个问题。
 
四舍五入
我们的第一个反应是做四舍五入。Math类中的round方法不能设置保留几位小数,我们只能象这样(保留两位):
public double round(double value){
    return Math.round(value*100)/100.0;
}
非常不幸,上面的代码并不能正常工作,给这个方法传入4.015它将返回4.01而不是4.02,如我们在上面看到的
4.015*100=401.49999999999994
因此如果我们要做到精确的四舍五入,不能利用简单类型做任何运算
java.text.DecimalFormat也不能解决这个问题:
System.out.println(new java.text.DecimalFormat("0.00").format(4.025));
输出是4.02
 

BigDecimal
在《Effective Java》这本书中也提到这个原则,float和double只能用来做科学计算或者是工程计算,在商业计算中我们要用 java.math.BigDecimal。BigDecimal一共有4个够造方法,我们不关心用BigInteger来够造的那两个,那么还有两个,它们是:
BigDecimal(double val) 
          Translates a double into a BigDecimal

BigDecimal(String val) 
          Translates the String repre sentation of a BigDecimal into a BigDecimal

BigDecimal有多种构造函数,常用的有2种。建议使用String构造方式,不建议使用double构造方式。

/**
 *  强制使用String的构造函数,double也有可能计算不太准确
 *  原则是使用BigDecimal并且一定要用String来构造。
 */
 public BigDecimal(int);       创建一个具有参数所指定整数值的对象
 public BigDecimal(double);    创建一个具有参数所指定双精度值的对象
 public BigDecimal(long);      创建一个具有参数所指定长整数值的对象
 public BigDecimal(String);    创建一个具有参数所指定以字符串表示的数值的对象

1)参数类型为double的构造方法的结果有一定的不可预知性。在Java中写入newBigDecimal(0.1)实际上等于0.1000000000000000055511151231257827021181583404541015625。这是因为0.1无法准确地表示为 double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样,传入到构造方法的值不会正好等于 0.1(虽然表面上等于该值)。

2)String 构造方法是完全可预知的:写入 newBigDecimal(“0.1”) 将创建一个 BigDecimal,它正好等于预期的 0.1。因此,比较而言,通常建议优先使用String构造方法。

3)当double必须用作BigDecimal对象时,最好先使用Double.toString(double)方法,或者String.valueOf(double)将double转换为String,然后使用BigDecimal(String)构造方法。

原则是使用BigDecimal并且一定要用String来够造。
但是想像一下吧,如果我们要做一个加法运算,需要先将两个浮点数转为String,然后够造成BigDecimal,在其中一个上调用add方法,传入另一个作为参数,然后把运算的结果(BigDecimal)再转换为浮点数。

import java.math.BigDecimal;
/**
 * 由于Java的简单类型不能够精确的对浮点数进行运算,这个工具类提供精
 * 确的浮点数运算,包括加减乘除和四舍五入。
 */
public class Arith{
    //默认除法运算精度
    private static final int DEF_DIV_SCALE = 10;
    //这个类不能实例化
    private Arith(){
    }
 
    /**
     * 提供精确的加法运算。
     * @param v1 被加数
     * @param v2 加数
     * @return 两个参数的和
     */
    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 两个参数的差
     */
    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 两个参数的积
     */
    public static double mul(double v1,double v2){
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.multiply(b2).doubleValue();
    }
 
    /**
     * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
     * 小数点以后10位,以后的数字四舍五入。
     * @param v1 被除数
     * @param v2 除数
     * @return 两个参数的商
     */
    public static double div(double v1,double v2){
        return div(v1,v2,DEF_DIV_SCALE);
    }
 
    /**
     * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
     * 定精度,以后的数字四舍五入。
     * @param v1 被除数
     * @param v2 除数
     * @param scale 表示表示需要精确到小数点以后几位。
     * @return 两个参数的商
     */
    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 one = new BigDecimal("1");
        return b.divide(one,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
    }
}; 

2、 BigDecimal类中函数的使用

public BigDecimal add(BigDecimal)       BigDecimal对象中的值相加,然后返回这个对象
public BigDecimal subtract(BigDecimal)  BigDecimal对象中的值相减,然后返回这个对象
public BigDecimal multiply(BigDecimal)  BigDecimal对象中的值相乘,然后返回这个对象
public BigDecimal divide(BigDecimal)    BigDecimal对象中的值相除,然后返回这个对象
public BigDecimal toString()            将BigDecimal对象的数值转换成字符串    
public BigDecimal doubleValue()         将BigDecimal对象中的值以双精度数返回   
public BigDecimal floatValue()          将BigDecimal对象中的值以单精度数返回   
public BigDecimal longValue()           将BigDecimal对象中的值以长整数返回    
public BigDecimal intValue()            将BigDecimal对象中的值以整数返回     

函数使用如下:

//尽量用字符串的形式初始化(构造对象)
BigDecimal StringFir = new BigDecimal("0.005");
BigDecimal stringSec = new BigDecimal("1000000");
BigDecimal stringThi = new BigDecimal("-1000000");

BigDecimal doubleFir = new BigDecimal(0.005);
BigDecimal doubleSec = new BigDecimal(1000000);
BigDecimal doubleThi = new BigDecimal(-1000000);

//加法
BigDecimal addVal = doubleFir.add(doubleSec);
System.out.println("加法用double结果:" + addVal);
BigDecimal addStr = StringFir.add(stringSec);
System.out.println("加法用string结果:" + addStr);

//减法
BigDecimal subtractVal = doubleFir.subtract(doubleSec);
System.out.println("减法用double结果:" + subtractVal);
BigDecimal subtractStr = StringFir.subtract(stringSec);
System.out.println("减法用string结果:" + subtractStr);

//乘法
BigDecimal multiplyVal = doubleFir.multiply(doubleSec);
System.out.println("乘法用double结果:" + multiplyVal);
BigDecimal multiplyStr = StringFir.multiply(stringSec);
System.out.println("乘法用string结果:" + multiplyStr);

//除法
BigDecimal divideVal = doubleSec.divide(doubleFir, 20, BigDecimal.ROUND_HALF_UP);
System.out.println("除法用double结果:" + divideVal);
BigDecimal divideStr = stringSec.divide(StringFir, 20, BigDecimal.ROUND_HALF_UP);
System.out.println("除法用string结果:" + divideStr);

//绝对值
BigDecimal absVal = doubleThi.abs();
System.out.println("绝对值用double结果:" + absVal);
BigDecimal absStr = stringThi.abs();
System.out.println("绝对值用string结果:" + absStr);

结果打印如下:

加法用double结果:1000000.005000000000000000104083408558608425664715468883514404296875
加法用string结果:1000000.005
减法用double结果:-999999.994999999999999999895916591441391574335284531116485595703125
减法用string结果:-999999.995
乘法用double结果:5000.000000000000104083408558608425664715468883514404296875000000
乘法用string结果:5000.000
除法用double结果:199999999.99999999583666365766
除法用string结果:200000000.00000000000000000000
绝对值用double结果:1000000
绝对值用string结果:1000000

总结:

System.out.println()中的数字默认是double类型的,double类型小数计算不精准。

使用BigDecimal类构造方法传入double类型时,计算的结果也是不精确的。

因为不是所有的浮点数都能够被精确的表示成一个double 类型值,因此它会被表示成与它最接近的 double 类型的值。必须改用传入String的构造方法。这一点在BigDecimal类的构造方法注释中有说明。

MySQL数据类型 DECIMAL

float、double这些浮点数类型同样可以存储小数,但是无法确保精度,很容易产生误差,特别是在求和计算的时候,所有当存储小数,特别是涉及金额时推荐使用DECIMAL类型。

注意事项:

DECIMAL(M,D)中,M范围是1到65,D范围是0到30。

M默认为10,D默认为0,D不大于M。

DECIMAL(5,2)可存储范围是从-999.99到999.99,超出存储范围会报错。

存储数值时,小数位不足会自动补0,首位数字为0自动忽略。

小数位超出会截断,产生告警,并按四舍五入处理。

使用DECIMAL字段时,建议M,D参数手动指定,并按需分配。

原文地址:https://www.cnblogs.com/h-c-g/p/14975799.html