BigDecimal 精度 和准确度问题

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了BigDecimal 精度 和准确度问题相关的知识,希望对你有一定的参考价值。

package aa;import java.math.BigDecimal;public class zz /** * @param args */ public static void main(String[] args) BigDecimal i=new BigDecimal(11000); i = i.multiply(new BigDecimal(0.0005)); System.out.println(i); 输出结果 5.5000000000000001144917494144692682311870157718658447265625000不准确

参考技术A Api写的很清楚:
public class BigDecimalextends Numberimplements Comparable<BigDecimal>
不可变的、任意精度的有符号十进制数。BigDecimal 由任意精度的整数非标度值 和 32 位的整数标度
(scale) 组成。如果为零或正数,则标度是小数点后的位数。如果为负数,则将该数的非标度值乘以 10 的负 scale
次幂。因此,BigDecimal 表示的数值是 (unscaledValue × 10-scale)。

BigDecimal 类提供以下操作:算术、标度操作、舍入、比较、哈希算法和格式转换。toString()
方法提供 BigDecimal 的规范表示形式。
BigDecimal 类使用户能完全控制舍入行为。如果未指定舍入模式,并且无法表示准确结果,则抛出一个异常;否则,通过向该操作提供适当的
MathContext
对象,可以对已选择的精度和舍入模式执行计算。在任何情况下,可以为舍入控制提供八种舍入模式。使用此类(例如,ROUND_HALF_UP)中的整数字段来表示舍入模式已过时;应改为使用
RoundingModeenum(例如,RoundingMode.HALF_UP)的枚举值。

当为 MathContext 对象提供 0 的精度设置(例如,MathContext.UNLIMITED)时,算术运算是准确的,它们是不采用任何
MathContext 对象的算术方法。(这是第 5 版之前的版本支持的惟一行为。)为了计算准确结果,不使用附带 0 精度设置的
MathContext 对象的舍入模式设置,因此与该对象无关。在除法中,准确的商可能是一个无限长的十进制扩展;例如,1 除以 3
所得的商。如果商具有无穷的十进制扩展,但是指定了该操作返回准确结果,则抛出
ArithmeticException。否则,像其他操作那样,返回除法运算的准确结果。
当精度设置不为 0 时,BigDecimal 算法的规则完全符合 ANSI X3.274-1996 和 ANSI
X3.274-1996/AM 1-2000( 7.4 节)中定义的算法的可选操作模式。与上述标准不同,BigDecimal
包括多种舍入模式,它们对于版本 5 以前的 BigDecimal 版本中的除法是强制性的。这些 ANSI 标准和
BigDecimal 规范之间的任何冲突都按照有利于 BigDecimal 的方式进行解决。
由于同一数值可以有不同的表示形式(具有不同的标度),因此运算和舍入的规则必须同时指定数值结果和结果表示形式中所用的标度。
一般情况下,当准确结果(在除法中,可能有无限多位)比返回的数值具有更多位数时,舍入模式和精度设置确定操作如何返回具有有限位数的结果。
首先,MathContext 的 precision
设置指定要返回的总位数;这确定了结果的精度。位数计数从准确结果的最左边的非零数字开始。舍入模式确定丢弃的尾部位数如何影响返回的结果。
对于所有算术运算符,运算的执行方式是,首先计算准确的中间结果,然后,使用选择的舍入模式将其舍入为精度设置(如有必要)指定的位数。如果不返回准确结果,则将丢弃准确结果的某些数位。当舍入增加了返回结果的大小时,前导数字“9”的进位传播可能会创建新的数位。例如,将值
999.9 舍入为三位数字,则在数值上等于一千,表示为 100×101。在这种情况下,新的 "1" 是返回结果的前导数位。
除了逻辑的准确结果外,每种算术运算都有一个表示结果的首选标度。下表列出了每个运算的首选标度。
算术运算结果的首选标度
运算结果的首选标度
加 max(addend.scale(), augend.scale())
减 max(minuend.scale(), subtrahend.scale())
乘 multiplier.scale() + multiplicand.scale()
除 dividend.scale() -
divisor.scale()
这些标度是返回准确算术结果的方法使用的标度;准确相除可能必须使用较大的标度除外,因为准确的结果可能有较多的位数。例如,1/32
得到 0.03125。
舍入之前,逻辑的准确中间结果的标度是该运算的首选标度。如果用 precision
位数无法表示准确的数值结果,则舍入会选择要返回的一组数字,并将该结果的标度从中间结果的标度减小到可以表示实际返回的 precision
位数的最小标度。如果准确结果可以使用最多 precision
个数字表示,则返回具有最接近首选标度的标度的结果表示形式。尤其是,通过移除结尾零并减少标度,可以用少于 precision
个数字来表示准确的可表示的商。例如,使用 floor
舍入模式将结果舍入为三个数字,
19/100 = 0.19 // integer=19, scale=2
但是
21/110 = 0.190 // integer=190, scale=3
注意,对于加、减和乘,标度的缩减量将等于丢弃的准确结果的数字位置数。如果舍入导致进位传播创建一个新的高位,则当未创建新的数位时,会丢弃该结果的附加数字。
其他方法可能与舍入语义稍微不同。例如,使用指定的算法的
pow 方法得到的结果可能偶尔不同于舍入得到的算术结果,如最后一位有多个单位(ulp)。
可以通过两种类型的操作来处理 BigDecimal 的标度:标度/舍入操作和小数点移动操作。标度/舍入操作(setScale
和 round)返回
BigDecimal,其值近似地(或精确地)等于操作数的值,但是其标度或精度是指定的值;即:它们会增加或减少对其值具有最小影响的存储数的精度。小数点移动操作(movePointLeft
和 movePointRight)返回从操作数创建的
BigDecimal,创建的方法是按指定方向将小数点移动一个指定距离。
为了简洁明了起见,整个 BigDecimal 方法的描述中都使用了伪代码。伪代码表达式 (i + j) 是“其值为
BigDecimali 加 BigDecimalj 的
BigDecimal”的简写。伪代码表达式 (i == j) 是“当且仅当 BigDecimali 表示与 BigDecimalj 相同的值时,则为
true”的简写。可以类似地解释其他伪代码表达式。方括号用于表示特定的 BigInteger 和定义
BigDecimal 值的标度对;例如,[19, 2] 表示 BigDecimal 在数值上等于 0.19,标度是 2。
注:如果 BigDecimal 对象用作 SortedMap 中的键或 SortedSet 中的元素,则应特别小心,因为
BigDecimal 的自然排序与 equals 方法不一致。有关更多信息,请参见 Comparable、SortedMap 或 SortedSet。
当为任何输入参数传递 null 对象引用时,此类的所有方法和构造方法都将抛出
NullPointerException。
参考技术B 要用string构造器,不要用double构造器,把new BigDecimal(0.0005)改为new BigDecimal("0.0005")就行了

java用double和float进行小数计算精度不准确

java用double和float进行小数计算精度不准确

大多数情况下,使用double和float计算的结果是准确的,但是在一些精度要求很高的系统中或者已知的小数计算得到的结果会不准确,这种问题是非常严重的。

《Effective Java》中提到一个原则,那就是float和double只能用来作科学计算或者是工程计算,但在商业计算中我们要用java.math.BigDecimal,通过使用BigDecimal类可以解决上述问题,java的设计者给编程人员提供了一个很有用的类BigDecimal,他可以完善float和double类无法进行精确计算的缺憾。

使用BigDecimal,但一定要用BigDecimal(String)构造器,而千万不要用BigDecimal(double)来构造(也不能将float或double型转换成String再来使用BigDecimal(String)来构造,因为在将float或double转换成String时精度已丢失)。例如new BigDecimal(0.1),它将返回一个BigDecimal,也即0.1000000000000000055511151231257827021181583404541015625,正确使用BigDecimal,程序就可以打印出我们所期望的结果0.9: 
Java代码  
System.out.println(new BigDecimal("2.0").subtract(new BigDecimal("1.10")));// 0.9  

另外,如果要比较两个浮点数的大小,要使用BigDecimal的compareTo方法。 

实例代码如下:

package ex;

import java.math.*;

public class BigDecimalDemo {
    public static void main(String[] args){
        System.out.println(ArithUtil.add(0.01, 0.05));
        System.out.println(ArithUtil.sub(1.0, 0.42));
        System.out.println(ArithUtil.mul(4.015, 100));
        System.out.println(ArithUtil.div(123.3, 100));
    }
}

class ArithUtil{
    private static final int DEF_DIV_SCALE=10;

    private ArithUtil(){}
    //相加
    public static double add(double d1,double d2){
        BigDecimal b1=new BigDecimal(Double.toString(d1));
        BigDecimal b2=new BigDecimal(Double.toString(d2));
        return b1.add(b2).doubleValue();

    }
    //相减
    public static double sub(double d1,double d2){
        BigDecimal b1=new BigDecimal(Double.toString(d1));
        BigDecimal b2=new BigDecimal(Double.toString(d2));
        return b1.subtract(b2).doubleValue();

    }
    //相乘
    public static double mul(double d1,double d2){
        BigDecimal b1=new BigDecimal(Double.toString(d1));
        BigDecimal b2=new BigDecimal(Double.toString(d2));
        return b1.multiply(b2).doubleValue();

    }
    //相除
    public static double div(double d1,double d2){

        return div(d1,d2,DEF_DIV_SCALE);

    }

    public static double div(double d1,double d2,int scale){
        if(scale<0){
            throw new IllegalArgumentException("The scale must be a positive integer or zero");
        }
        BigDecimal b1=new BigDecimal(Double.toString(d1));
        BigDecimal b2=new BigDecimal(Double.toString(d2));
        return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();

    }

}

现在我们就详细剖析一下浮点型运算为什么会造成精度丢失?

1、小数的二进制表示问题

首先我们要搞清楚下面两个问题:

     (1) 十进制整数如何转化为二进制数

           算法很简单。举个例子,11表示成二进制数:

                     11/2=51

                       5/2=21

                       2/2=10

                       1/2=01

                          0结束         11二进制表示为(从下往上):1011

          这里提一点:只要遇到除以后的结果为0了就结束了,大家想一想,所有的整数除以2是不是一定能够最终得到0。换句话说,所有的整数转变为二进制数的算法会不会无限循环下去呢?绝对不会,整数永远可以用二进制精确表示 ,但小数就不一定了。

      (2) 十进制小数如何转化为二进制数

           算法是乘以2直到没有了小数为止。举个例子,0.9表示成二进制数

                     0.9*2=1.8   取整数部分 1

                     0.8(1.8的小数部分)*2=1.6    取整数部分 1

                     0.6*2=1.2   取整数部分 1

                     0.2*2=0.4   取整数部分 0

                     0.4*2=0.8   取整数部分 0

                     0.8*2=1.6 取整数部分 1

                     0.6*2=1.2   取整数部分 0

                              .........      0.9二进制表示为(从上往下): 1100100100100......

           注意:上面的计算过程循环了,也就是说*2永远不可能消灭小数部分,这样算法将无限下去。很显然,小数的二进制表示有时是不可能精确的 。其实道理很简单,十进制系统中能不能准确表示出1/3呢?同样二进制系统也无法准确表示1/10。这也就解释了为什么浮点型减法出现了"减不尽"的精度丢失问题。

2、 float型在内存中的存储

众所周知、 Java 的float型在内存中占4个字节。float的32个二进制位结构如下



float内存存储结构

             4bytes      31    30    29----23    22----0         

                        表示       实数符号位    指数符号位        指数位          有效数位

        其中符号位1表示正,0表示负。有效位数位24位,其中一位是实数符号位。

         将一个float型转化为内存存储格式的步骤为:

        (1)先将这个实数的绝对值化为二进制格式,注意实数的整数部分和小数部分的二进制方法在上面已经探讨过了。 
     (2)将这个二进制格式实数的小数点左移或右移n位,直到小数点移动到第一个有效数字的右边。 
     (3)从小数点右边第一位开始数出二十三位数字放入第22到第0位。 
     (4)如果实数是正的,则在第31位放入“0”,否则放入“1”。 
     (5)如果n 是左移得到的,说明指数是正的,第30位放入“1”。如果n是右移得到的或n=0,则第30位放入“0”。 
     (6)如果n是左移得到的,则将n减去1后化为二进制,并在左边加“0”补足七位,放入第29到第23位。如果n是右移得到的或n=0,则将n化为二进制后在左边加“0”补足七位,再各位求反,再放入第29到第23位。

          举例说明: 11.9的内存存储格式

       (1) 将11.9化为二进制后大约是" 1011. 1110011001100110011001100..."。

       (2) 将小数点左移三位到第一个有效位右侧: "1. 011 11100110011001100110 "。 保证有效位数24位,右侧多余的截取(误差在这里产生了 )。

       (3) 这已经有了二十四位有效数字,将最左边一位“1”去掉,得到“ 011 11100110011001100110 ”共23bit。将它放入float存储结构的第22到第0位。

       (4) 因为11.9是正数,因此在第31位实数符号位放入“0”。

       (5) 由于我们把小数点左移,因此在第30位指数符号位放入“1”。

       (6) 因为我们是把小数点左移3位,因此将3减去1得2,化为二进制,并补足7位得到0000010,放入第29到第23位。

           最后表示11.9为: 0 1 0000010 011 11100110011001100110

           再举一个例子:0.2356的内存存储格式
      (1)将0.2356化为二进制后大约是0.00111100010100000100100000。 
      (2)将小数点右移三位得到1.11100010100000100100000。 
      (3)从小数点右边数出二十三位有效数字,即11100010100000100100000放
入第22到第0位。 
      (4)由于0.2356是正的,所以在第31位放入“0”。 
      (5)由于我们把小数点右移了,所以在第30位放入“0”。 
      (6)因为小数点被右移了3位,所以将3化为二进制,在左边补“0”补足七
位,得到0000011,各位取反,得到1111100,放入第29到第23位。 


           最后表示0.2356为:0 0 1111100 11100010100000100100000

          将一个内存存储的float二进制格式转化为十进制的步骤: 
     (1)将第22位到第0位的二进制数写出来,在最左边补一位“1”,得到二十四位有效数字。将小数点点在最左边那个“1”的右边。 
     (2)取出第29到第23位所表示的值n。当30位是“0”时将n各位求反。当30位是“1”时将n增1。 
     (3)将小数点左移n位(当30位是“0”时)或右移n位(当30位是“1”时),得到一个二进制表示的实数。 
     (4)将这个二进制实数化为十进制,并根据第31位是“0”还是“1”加上正号或负号即可。

 

3、浮点型的减法运算

浮点加减运算过程比定点运算过程复杂。完成浮点加减运算的操作过程大体分为四步:

(1) 0操作数的检查;
        如果判断两个需要加减的浮点数有一个为0,即可得知运算结果而没有必要再进行有序的一些列操作。 

(2) 比较阶码(指数位)大小并完成对阶;
    两浮点数进行加减,首先要看两数的 指数位 是否相同,即小数点位置是否对齐。若两数 指数位 相同,表示小数点是对齐的,就可以进行尾数的加减运算。反之,若两数阶码不同,表示小数点位置没有对齐,此时必须使两数的阶码相同,这个过程叫做对阶 。

    如何对 阶(假设两浮点数的指数位为 Ex 和 Ey ):
    通过尾数的移位以改变 Ex 或 Ey ,使之相等。 由 于浮点表示的数多是规格化的,尾数左移会引起最高有位的丢失,造成很大误差;而尾数右移虽引起最低有效位的丢失,但造成的误差较小,因此,对阶操作规定使 尾数右移,尾数右移后使阶码作相应增加,其数值保持不变。很显然,一个增加后的阶码与另一个相等,所增加的阶码一定是小阶。因此在对阶时,总是使小阶向大阶看齐 ,即小阶的尾数向右移位 ( 相当于小数点左移 ) ,每右移一位,其阶码加 1 ,直到两数的阶码相等为止,右移的位数等于阶差 △ E 。 

(3) 尾数(有效数位)进行加或减运算;

(4) 结果规格化并进行舍入处理。

 

 

java在计算浮点数的时候,由于二进制无法精确表示0.1的值(就好比十进制无法精确表示1/3一样),所以一般会对小数格式化处理.

但是如果涉及到金钱的项目,一点点误差都不能有,必须使用精确运算的时候,就可以使用BigDecimal方法计算.

但是在使用中还需要注意一个问题:

复制代码
//直接使用double类型数据进行运算
System.out.println(0.05+0.01);
//使用BigDecimal的double参数的构造器
BigDecimal bd1 = new BigDecimal(0.05);
BigDecimal bd2 = new BigDecimal(0.01);
System.out.println(bd1.add(bd2));
//使用BigDecimal的String参数的构造器
BigDecimal bd3 = new BigDecimal("0.05");
BigDecimal bd4 = new BigDecimal("0.01");
System.out.println(bd3.add(bd4));
复制代码

这三个输出结果是不一样的:

所以在计算的时候,应该先把数字转换成String类型的,才能得到最精确的值.

 

附常用的方法:

add  加

subtract  减

multiply  乘

divide 除

abs 绝对值

getScale  根据一个规则取几位小数

pow  几次方

 

中文API链接:

http://www.apihome.cn/api/java/BigDecimal.html

以上是关于BigDecimal 精度 和准确度问题的主要内容,如果未能解决你的问题,请参考以下文章

BigDecimal精度丢失问题

BigDecimal类处理高精度计算

bigdecimal长度包含小数位吗

Double与BigDecimal 精度问题

BigDecimal 高精度计算 熟悉扩展,java除法保留小数问题

Java 使用BigDecimal类处理高精度计算