Bug场景

sku价格比较

    @Overridepublic 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

转载于:https://www.cnblogs.com/qiushizhu/p/3983985.html

非精确运算和精确运算相关推荐

  1. 关于浮点型加减乘除运算不精确的问题

    关于浮点型加减乘除运算不精确的问题 先举一个遇到这个错误的项目例子: 之前做一个小模块,由于后端接口还没有完成,需要自己搭建node服务,返回数据,功能需求是实时更新的,这个小模块中本人没有使用web ...

  2. 类选择器选择非唯一属性无法精确取值的问题

    类选择器选择非唯一属性无法精确取值的问题 一.总结 一句话总结:因为做的操作并不是精确选取,因为执行了两次选择器,所以肯定不对啊. 二.类选择器选择非唯一属性无法精确取值的问题 1.截图 2.代码 1 ...

  3. 布尔运算符的结果false true !非运算 -o或运算 -a与运算

    shell 布尔运算符,优先级由高到底 !非运算 -a与运算 -o或运算

  4. 二进制与运算、或运算、非运算

    与运算 "与"运算是计算机中一种基本的逻辑运算方式,符号表示为&,运算法则为遇0得0.也就是说只要有0,结果即为0. 举例 或运算 "或"运算符号表示为 ...

  5. Hive常用运算(关系运算)、逻辑运算与数学运算、数值运算、日期函数、条件函数、字符串函数

    hive 常用运算 第一部分:关系运算 Hive支持的关系运算符 •常见的关系运算符 •等值比较: = •不等值比较: <> •小于比较: < •小于等于比较: <= •大于比 ...

  6. 序列的卷积运算与相关运算——MATLAB

    一.实验目的 1.掌握有限长序列线性卷积的编程计算原理,并能够利用Matlab或C语言编写算法程序进行线性卷积运算的程序实现; 2.学会线性卷积函数和线性相关函数的使用方法,并能利用二者进行有限长序列 ...

  7. MySQL关系运算和连接运算,数据库的关系运算和完整性约束

    对关系数据库进行查询统计时,需要查询到用户感兴趣的数据,这就需要对关系及关系间进行一定的运算.本篇主要讲述关系运算和关系的完整性约束,理解关系操作的含义,了解传统的集合运算,掌握关系代数中基本关系运算 ...

  8. [GO语言基础] 四.算术运算、逻辑运算、赋值运算、位运算及编程练习

    作为网络安全初学者,会遇到采用Go语言开发的恶意样本.因此从今天开始从零讲解Golang编程语言,一方面是督促自己不断前行且学习新知识:另一方面是分享与读者,希望大家一起进步.前文介绍了Golang的 ...

  9. 开运算和闭运算的异同

    例一:毛刺在往外凸的面上 策略1:分割出黑色部分,然后通过开运算去掉毛刺,再通过原黑色部分区域减去开运算之后的区域,得到毛刺部分的区域. 1 read_image (Tu, 'C:/Users/xia ...

  10. python 幂运算_python幂运算

    广告关闭 腾讯云11.11云上盛惠 ,精选热门产品助力上云,云服务器首年88元起,买的越多返的越多,最高返5000元! "**"运算这个"**"比较简单,就是标 ...

最新文章

  1. 一脸懵逼学习Hadoop中的MapReduce程序中自定义分组的实现
  2. scipy 图像处理(scipy.misc、scipy.ndimage)、matplotlib 图像处理
  3. linux mysql主主复制_MySQL主从复制与主主复制
  4. vspythonqt混合_Qt混合Python开发技术:Python介绍、混合过程和Demo
  5. strconv---用来基本类型之间的转换
  6. AngularJs 1.5 $location获取url参数
  7. 第十九章 TCP的交互数据流
  8. 8路抢答器c语言程序,多路抢答器c程序(原创)
  9. Java教师工资习题
  10. void main java_详细讲解Java中的main()方法
  11. 前后落差大用什么词语_描写心理落差大词语
  12. 数据结构1800试题(第2章)
  13. python猜单词游戏心得_【Python】猜单词游戏
  14. Windows10更新导致共享打印机无法连接
  15. 2022-2028年中国特种食用油行业市场运营格局及前景战略分析报告
  16. mac按键难回弹(按下去软软的)
  17. L1-039 古风排版 (20 分)
  18. 达芬奇调色 Blackmagic Design DaVinci Resolve Studio 17 中文版,整合剪辑、视觉特效、动态图形、调色和音频后期制作
  19. yolov8 OpenCV DNN 部署 推理报错
  20. MAC彻底删除库乐队,清空音乐创作所占内存

热门文章

  1. Qt深入:不能不知道的Type、Attribute和Flags
  2. C# 获得当前目录和执行目录的一些方法
  3. jsp网页上实现计算圆面积小程序
  4. java sftp 密钥_通过密钥 SFTP (二):启用没有 Shell 访问权限的 SFTP 账户
  5. Hystrix-超时机制和断路器模式
  6. (62)Verilog HDL模块例化system Verilog模块
  7. (20)Verilog HDL并行块:fork-join
  8. FPGA同步复位设计代码
  9. 1 D触发器verilog与Systemverilog编码
  10. 以太网头数据和802.3的区别