剖析金额不能用浮点数表示的原因
2016-09-13 21:28
225 查看
近期参与到了一个金融项目,开发十分的谨慎。先抛出我有问题的代码,作用是把以分为单位的金额转成以元为单位的字符串。
很自信的以为这行代码简洁明了的完成了使命。同事review了我的代码后,指出这段代码会造成精度丢失的问题。先演示一个demo,构造一个浮点数丢失精度的场景。
程序输出结果
输出结果足以颠覆三观。一个双精度浮点数,加了10亿之后,居然没有发生任何变化!如果在金融项目里发生这种事,何止是直接被fire掉,蹲监狱都是可能的。这类 问题其根本原因在于浮点数在计算机内部的表示方法。这种IEEE标准表示法兼顾了数据的精度和大小。
图片摘自网上。学过《计算机组成原理》的同学都知道,32位的浮点数由3部分组成:1比特的符号位,8比特的阶码(exponent,指数),23比特的尾数(Mantissa,尾数)。这个结构会表示成一个小数点左边为1,以底数为2的科学计数法表示的二进制小数。浮点数的能表示的数据大小范围由阶码决定,但是能够表示的精度完全取决于尾数的长度。long的最大值是2的64次方减1,需要63个二进制位表示,即便是double,52位的尾数也无法完整的表示long的最大值。不能表示的部分也就只能被舍去了。对于金额,舍去不能表示的部分,损失也就产生了。
了解了浮点数表示机制后,丢失精度的现象也就不难理解了。但是,这只是浮点数不能表示金额的原因之一。还有一个深刻的原因与进制转换有关。十进制的0.1在二进制下将是一个无线循环小数。同样,给出一个能体现这个问题的demo。
程序输出结果:
10个浮点数0.1相加最后并没有得到1。如果一个小数不是2的负整数次幂,用浮点数表示必然产生浮点误差。做一次延伸,A进制下的有限小数,B进制下极有可能是无限小数。这种情形下,十进制小数转换成尾数长度固定的浮点数,误差也将产生。
综上,浮点数不精确的根本原因在于尾数部分的位数是固定的,一旦需要表示的数字的精度高于浮点数的精度,那么必然产生误差!这在处理金融数据的情况下是绝对不允许存在的。
对于金融项目,误差是不能容忍的。那么用什么数据类型才能精确的表示金额?JDK提供了一个BigDecimal的类,这个类可以表示任意精度的数字。有ACM经验的同学对这个类的底层实现应该不陌生,用int数组模拟大数。各大OJ平台都有长整数加减乘除的题目,八大基本数据类型都无法解决这类题目,唯一可行的解就是用数组模拟长整数。
不仅金融项目对浮点误差是零容忍的,国防军工航天项目亦是如此!1991的海湾战争,沙特的爱国者导弹因为浮点误差产生了0.3秒的误差,不仅没能拦截伊拉克的飞毛腿导弹,而且因为0.3秒的时间误差,导致了700余米的位移误差,炸毁了美军自己的军营,28名美国大兵出师未捷身先死,更讽刺的是死于浮点误差而非枪林弹雨(详见《CSAPP》修订版第二章习题32)。
回头再看自己犯的错误,不禁一身冷汗,这种代码要是上到正式环境了,恐怕会为公司带来不少的损失!优秀的程序员不会栽在同一个陷阱,把这个经验记下来,分享给大家。
能够快速的理解这个问题,也得益于本科时学习了当时认为对编程根本没卵用的《计算机组成原理》。修过的课程和阅读过的书,都在潜移默化的帮助你我写出鲁棒性更好的代码。
long adjustFee; String.valueOf(adjustFee / 100.0);
很自信的以为这行代码简洁明了的完成了使命。同事review了我的代码后,指出这段代码会造成精度丢失的问题。先演示一个demo,构造一个浮点数丢失精度的场景。
@Test public void addTest() { long l = Long.MAX_VALUE; double d = l / 1.0; double clone = d; System.out.println(d); for (int i = 0; i < 1000000000; i++) { clone += 1; } System.out.println(clone); System.out.println(clone == d); }
程序输出结果
9.223372036854776E18 9.223372036854776E18 true
输出结果足以颠覆三观。一个双精度浮点数,加了10亿之后,居然没有发生任何变化!如果在金融项目里发生这种事,何止是直接被fire掉,蹲监狱都是可能的。这类 问题其根本原因在于浮点数在计算机内部的表示方法。这种IEEE标准表示法兼顾了数据的精度和大小。
图片摘自网上。学过《计算机组成原理》的同学都知道,32位的浮点数由3部分组成:1比特的符号位,8比特的阶码(exponent,指数),23比特的尾数(Mantissa,尾数)。这个结构会表示成一个小数点左边为1,以底数为2的科学计数法表示的二进制小数。浮点数的能表示的数据大小范围由阶码决定,但是能够表示的精度完全取决于尾数的长度。long的最大值是2的64次方减1,需要63个二进制位表示,即便是double,52位的尾数也无法完整的表示long的最大值。不能表示的部分也就只能被舍去了。对于金额,舍去不能表示的部分,损失也就产生了。
了解了浮点数表示机制后,丢失精度的现象也就不难理解了。但是,这只是浮点数不能表示金额的原因之一。还有一个深刻的原因与进制转换有关。十进制的0.1在二进制下将是一个无线循环小数。同样,给出一个能体现这个问题的demo。
public class MyTest { public static void main(String[] args) { float increment = 0.1f; float expected = 1; float sum = 0; for (int i = 0; i < 10; i++) { sum += increment; System.out.println(sum); } if (expected == sum) { System.out.println("equal"); } else { System.out.println("not equal "); } } }
程序输出结果:
0.1 0.2 0.3 0.4 0.5 0.6 0.70000005 0.8000001 0.9000001 1.0000001 not equal
10个浮点数0.1相加最后并没有得到1。如果一个小数不是2的负整数次幂,用浮点数表示必然产生浮点误差。做一次延伸,A进制下的有限小数,B进制下极有可能是无限小数。这种情形下,十进制小数转换成尾数长度固定的浮点数,误差也将产生。
综上,浮点数不精确的根本原因在于尾数部分的位数是固定的,一旦需要表示的数字的精度高于浮点数的精度,那么必然产生误差!这在处理金融数据的情况下是绝对不允许存在的。
对于金融项目,误差是不能容忍的。那么用什么数据类型才能精确的表示金额?JDK提供了一个BigDecimal的类,这个类可以表示任意精度的数字。有ACM经验的同学对这个类的底层实现应该不陌生,用int数组模拟大数。各大OJ平台都有长整数加减乘除的题目,八大基本数据类型都无法解决这类题目,唯一可行的解就是用数组模拟长整数。
不仅金融项目对浮点误差是零容忍的,国防军工航天项目亦是如此!1991的海湾战争,沙特的爱国者导弹因为浮点误差产生了0.3秒的误差,不仅没能拦截伊拉克的飞毛腿导弹,而且因为0.3秒的时间误差,导致了700余米的位移误差,炸毁了美军自己的军营,28名美国大兵出师未捷身先死,更讽刺的是死于浮点误差而非枪林弹雨(详见《CSAPP》修订版第二章习题32)。
回头再看自己犯的错误,不禁一身冷汗,这种代码要是上到正式环境了,恐怕会为公司带来不少的损失!优秀的程序员不会栽在同一个陷阱,把这个经验记下来,分享给大家。
能够快速的理解这个问题,也得益于本科时学习了当时认为对编程根本没卵用的《计算机组成原理》。修过的课程和阅读过的书,都在潜移默化的帮助你我写出鲁棒性更好的代码。
相关文章推荐
- 剖析金额不能用浮点数表示的原因
- ftp能够连接,但是不能传数据原因剖析
- 金额,重量,成绩不使用浮点数来表示,而使用整形
- 浮点数不能进行相等性运算的原因
- BitmapCutter实现截取图片功能不能拖动原因剖析
- C语言为什么不能精确表示浮点数
- 浮点数在计算机中不能准确表示范例
- 罗马帝国开创了辉煌的人类文明,但他们的数字表示法的确有些繁琐,尤其在表示大数的时候,现在看起来简直不能忍受,所以在现代很少使用了。之所以这样,不是因为发明表示法的人的智力的问题,而是因为一个宗教的原因
- 基于Bootstrap实现的下拉菜单手机端不能选择菜单项的原因附解决办法
- 浅析jquery ajax异步调用方法中不能给全局变量赋值的原因及解决方法
- 结构体不能赋值的原因.CListCtrl不响应右键。想法回溯。
- 控件不能与用户交互的原因
- VS2015在win10上编译的程序不能在Win7上运行的原因
- 数据库日志超过 4G 数据库不能正常工作的原因
- vps日志不能被nginx创建原因检查
- 浮点数在内存中的表示
- 去掉VS2005中VC8的警告错误:warning C4819: 该文件包含不能在当前代码页(936)中表示的字符...
- ffmpeg av_open_input_file 不能打开文件原因
- 如果插上网线,可以登录QQ,但不能浏览网页,这是什么原因造成的?
- String被设计成不可变和不能被继承的原因