`
lynnwong
  • 浏览: 36699 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

非精确运算和精确运算

    博客分类:
  • java
阅读更多

非精确运算和精确运算


Bug场景

sku价格比较

    @Override
    public boolean checkSKUEqualPrice(ItemDO itemDO) {
        boolean result = true;
        List<ItemSkuDO> list=itemDO.getSkuList();
        //如果为空就是非sku商品
        if(list==null || list.isEmpty()){
            return result;
        }
        long tmpPrice = list.get(0).getPrice();
        for(ItemSkuDO sku:list){
             if (sku.getPrice()!=tmpPrice) {
                result= false;//存在不同的价格,校验失败
            }
        }
        return result && (tmpPrice == itemDO.getPrice()*100);
    }

如果熟悉价格比较的同学一眼就能看出这个bug的问题所在

return result && (tmpPrice == itemDO.getPrice()*100);

其中tmpPrice是以分为单位的价格long型,而item.getPrice()是double型的价格,在乘以100的操作后,将会返回一个近似double值,例如23.45*100,这个结果就不一定是2345,而是一个近似接近这个的值的数字。按照这种方法去比较,不能做到精确相等,bug就自然而然地产生了。 修改后的代码如下

@Override
public boolean checkSKUEqualPrice(ItemDO itemDO) {
    boolean result = true;
    List<ItemSkuDO> list=itemDO.getSkuList();
    //如果为空就是非sku商品
    if(list==null || list.isEmpty()){
        return result;
    }
    long tmpPrice = list.get(0).getPrice();
    for(ItemSkuDO sku:list){
         if (sku.getPrice()!=tmpPrice) {
            result= false;//存在不同的价格,校验失败
        }
    }
    return result && (tmpPrice == itemDO.getReservePriceLong());
}

通过取一个同样以分为单位的long型数来保证精确匹配。

这个bug算是个引子,接下来详细介绍下近似计算和精确计算在java中是如何实现的。

实数的表达方式

在介绍java的近似计算和精确计算之前,先简单介绍下一些实数的表达方式。

  • 定点数 定点数(Fixed Point Number),在这种表达方式中,小数点固定的位于实数所有数字中间的某个位置。货币的表达就可以使用这种方式,比如 99.00 或者 00.99 可以用于表达具有四位精度(Precision),小数点后有两位的货币值。由于小数点位置固定,所以可以直接用四位数值来表达相应的数值。SQL 中的 NUMBER 数据类型就是利用定点数来定义的。定点数表达法的缺点在于其形式过于僵硬,固定的小数点位置决定了固定位数的整数部分和小数部分,不利于同时表达特别大的数或者特别小的数。
  • 有理数 用两个整数的比值来表达实数。
  • 浮点数 利用科学计数法来表达实数,即用一个尾数(Mantissa ),一个基数(Base),一个指数(Exponent)以及一个表示正负的符号来表达实数。比如 123.45 用十进制科学计数法可以表达为 1.2345 × 10的2次方 ,其中 1.2345 为尾数,10 为基数,2 为指数。浮点数利用指数达到了浮动小数点的效果,从而可以灵活地表达更大范围的实数。 同样的数值可以有多种浮点数表达方式,比如上面例子中的 123.45 可以表达为 12.345 × 10的1次方,0.12345 × 10的3次方或者 1.2345 × 10的2次方。因为这种多样性,有必要对其加以规范化以达到统一表达的目标。规范的(Normalized)浮点数表达方式具有如下形式:

    ±d.dd...d × β 的e方 , (0 ≤ d i < β),其中 d.dd...d 即尾数,β 为基数,e 为指数。
    

    比如二进制数 1001.101 ,其规范浮点数表达为 1.001101 × 2的3次方

实数在计算机中的表达方式

计算机中是用有限的连续字节保存浮点数的。保存这些浮点数当然必须有特定的格式,Java 平台上的浮点数类型float 和 double 采纳了 IEEE 754 标准中所定义的单精度 32 位浮点数和双精度 64 位浮点数的格式。

举例:

1001.101对应于十进制的 9.625,按照标准可以表达成 1.001101 × 2的3次方

那么在计算机中是如何存储的,以单精度为例

  • 符号位 1

  • 指数存在正负的情况,指数可以为正数,也可以为负数。为了处理负指数的情况,实际的指数值按要求需要加上一个偏差(Bias)值作为保存在指数域中的值,单精度数的偏差值为 127,而双精度数的偏差值为 1023。例子中的指数在计算机中存储为 3+127= 130 二进制表示成 1000 0010

  • 尾数 IEEE 标准要求浮点数必须是规范的。这意味着尾数的小数点左侧必须为 1,因此我们在保存尾数的时候,可以省略小数点前面这个 1,从而腾出一个二进制位来保存更多的尾数。这样我们实际上用 23 位长的尾数域表达了 24 位的尾数。 例子中的尾数表示为00110100000000000000000

为什么浮点数不能保证精度

我们来看下实数和计算机存储的浮点数后之间的相互转化后,就能够明白为什么计算机中存储的浮点数不能保证精度了。

假定我们有一个 32 位的数据,用十六进制表示为 0xC0B40000,并且我们知道它实际上是一个单精度的浮点数。为了得到该浮点数实际表达的实数,我们首先将它变换为二进制形式:

  C    0    B    4    0    0    0    0
1100 0000 1011 0100 0000 0000 0000 0000

接着按照浮点数的格式切分为相应的域:

1  10000001 01101000000000000000000

符号域 1 意味着负数;指数域为 129 意味着实际的指数为 2 (减去偏差值 127);尾数域为 01101 意味着实际的二进制尾数为 1.01101 (加上隐含的小数点前面的 1)。所以,实际的实数为:

-1.01101 × 2的2次方
-5.625

接下来再看看实数向计算机中存储的浮点数转变的例子。

假定我们需要将实数 -9.625 表达为单精度的浮点数格式。方法是首先将它用二进制浮点数表达,然后变换为相应的浮点数格式。 首先,将小数点左侧的整数部分变换为其二进制形式,9 的二进制性形式为 1001。处理小数部分的算法是将我们的小数部分乘以基数 2,记录乘积结果的整数部分,接着将结果的小数部分继续乘以 2,并不断继续该过程:

0.625 × 2 = 1.25        1
0.25  × 2 = 0.5         0
0.5   × 2 = 1           1
0

当最后的结果为零时,结束这个过程。这时右侧的一列数字就是我们所需的二进制小数部分,即 0.101。这样,我们就得到了完整的二进制形式 1001.101。用规范浮点数表达为 1.001101 × 2的3次方。

因为是负数,所以符号域为 1。指数为 3,所以指数域为 3 + 127 = 130,即二进制的 10000010。尾数省略掉小数点左侧的 1 之后为 001101,右侧用零补齐。最终结果为:

1 10000010 00110100000000000000000

在上面这个我们有意选择的示例中,不断的将产生的小数部分乘以 2 的过程掩盖了一个事实。该过程结束的标志是小数部分乘以 2 的结果为 1,不难想象,很多小数根本不能经过有限次这样的过程而得到结果(比如最简单的 0.1)。我们已经知道浮点数尾数域的位数是有限的,为此,浮点数的处理办法是持续该过程直到由此得到的尾数足以填满尾数域,之后对多余的位进行舍入。换句话说,除了我们之前讲到的精度问题之外,十进制到二进制的变换也并不能保证总是精确的,而只能是近似值。事实上,只有很少一部分十进制小数具有精确的二进制浮点数表达。再加上浮点数运算过程中的误差累积,结果是很多我们看来非常简单的十进制运算在计算机上却往往出人意料。这就是最常见的浮点运算的"不准确"问题。

至此浮点数运算不准确性终于浮出水面。 那么,如果我们需要进行精确的运算呢?比如,电子商务中,一分钱都不能算错的情况下。接下俩就会介绍下java提供了什么样的机制来保证精确运算。

BigDecimal使用

对于精确计算,java提供了BigDecimal来保证,详细的BigDecimal使用可以参考JDK介绍,下面简单介绍下,使用BigDecimal的过程中可能会遇到的问题。

在使用BigDecimal类来进行计算的时候,主要分为以下步骤:

  1. 构建BigDecimal对象。
  2. 通过调用BigDecimal的加,减,乘,除等相应的方法进行算术运算。
  3. 把BigDecimal对象转换成float,double,int等类型。

BigDecimal构造

想了解BigDecimal的构造,先看两个构造函数的例子

 BigDecimal aDouble =new BigDecimal(1.22);
 System.out.println("construct with a double value: " + aDouble);
 BigDecimal aString = new BigDecimal("1.22");
 System.out.println("construct with a String value: " + aString);

你认为输出结果会是什么呢?如果你没有认为第一个会输出1.22,那么恭喜你答对了,输出结果如下:

construct with a doublevalue:1.2199999999999999733546474089962430298328399658203125
construct with a String value: 1.22

对于JDK的构造函数,有这样的描述

  1. 参数类型为double的构造方法的结果有一定的不可预知性。有人可能认为在Java中写入newBigDecimal(0.1)所创建的BigDecimal正好等于 0.1(非标度值 1,其标度为 1),但是它实际上等于0.1000000000000000055511151231257827021181583404541015625。这是因为0.1无法准确地表示为 double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样,传入到构造方法的值不会正好等于 0.1(虽然表面上等于该值)。
  2. 另一方面,String 构造方法是完全可预知的:写入 newBigDecimal("0.1") 将创建一个 BigDecimal,它正好等于预期的 0.1。因此,比较而言,通常建议优先使用String构造方法。
  3. 当double必须用作BigDecimal的源时,请注意,此构造方法提供了一个准确转换;它不提供与以下操作相同的结果:先使用Double.toString(double)方法,然后使用BigDecimal(String)构造方法,将double转换为String。要获取该结果,请使用staticvalueOf(double)方法。

不可变性

BigInteger与BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以a.add(b);虽然做了加法操作,但是a并没有保存加操作后的值,正确的用法应该是a=a.add(b);

BigDecimal和浮点型的性能比较

精确计算和非精确计算可以通过以下代码执行的结果来直接体现

1. import java.math.BigDecimal;  
2.   
3. public class BigDecimalEfficiency {  
4.   
5.     public static int REPEAT_TIMES = 1000000;  
6.   
7.     public static double computeByBigDecimal(double a, double b) {  
8.         BigDecimal result = BigDecimal.valueOf(0);  
9.         BigDecimal decimalA = BigDecimal.valueOf(a);  
10.         BigDecimal decimalB = BigDecimal.valueOf(b);  
11.         for (int i = 0; i < REPEAT_TIMES; i++) {  
12.             result = result.add(decimalA.multiply(decimalB));  
13.         }  
14.         return result.doubleValue();  
15.     }  
16.   
17.     public static double computeByDouble(double a, double b) {  
18.         double result = 0;  
19.         for (int i = 0; i < REPEAT_TIMES; i++) {  
20.             result += a * b;  
21.         }  
22.         return result;  
23.     }  
24.   
25.     public static void main(String[] args) {  
26.         long test = System.nanoTime();  
27.         long start1 = System.nanoTime();  
28.         double result1 = computeByBigDecimal(0.120000000034, 11.22);  
29.         long end1 = System.nanoTime();  
30.         long start2 = System.nanoTime();  
31.         double result2 = computeByDouble(0.120000000034, 11.22);  
32.         long end2 = System.nanoTime();  
33.   
34.         long timeUsed1 = (end1 - start1);  
35.         long timeUsed2 = (end2 - start2);  
36.         System.out.println("result by BigDecimal:" + result1);  
37.         System.out.println("time used:" + timeUsed1);  
38.         System.out.println("result by Double:" + result2);  
39.         System.out.println("time used:" + timeUsed2);  
40.   
41.         System.out.println("timeUsed1/timeUsed2=" + timeUsed1 / timeUsed2);  
42.     }  
43. }  

结果:

result by BigDecimal:1346400.00038148
time used:572537012
result by Double:1346400.000387465
time used:5280000
timeUsed1/timeUsed2=108

可见,二者的执行效率还是相差非常大的。

参考文献

http://www.cjsdn.net/Doc/JDK60/java/math/BigDecimal.html

http://singleant.iteye.com/blog/1159884

http://blog.csdn.net/jackiehff/article/details/8582449

http://blog.csdn.net/cppptr/article/details/573372

  • 大小: 6 KB
  • 大小: 21.8 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics