背景
事情是这样的,由于业务中需要将几百个浮点数求和,因为以前都会将浮点数的值持久化在数据库中,求Sum这样的操作都是依赖数据库聚合函数Sum,从来都没有考虑过求和之后的值是否会精度损失的问题,也都是正确的。
但在这次项目中,需要将这些浮点数持久化在本地文件中,所以数值求和就不能依赖数据库了,只能自己写一个数值累加的函数,本来想这个事儿根本不算个事儿,写个for循环累加分分钟就搞定了,没有想到最后在单元测试的时候总是和标准值对不上,总是少个几分或者多几分,初步判断应该是在数值累加精度损失造成的。
为了彻底搞明白这个问题,对浮点数相关的存储格式学习了一遍。只有对底层的存储格式有了解才能对实际场景中产生的诡异现象作一个圆满的解释。
先看一个问题
执行以下代码:
System.out.println(Float.parseFloat("132541.35")); BigDecimal f = new BigDecimal(("132541.35")); System.out.println(f.floatValue());
结果:
132541.34
132541.34
无论是primitive还是BigDecimal精度都损失了,如果使用一个精度已经损失的值进行累加操作,最后的总和肯定是不对的,正所谓失之毫厘谬之千里。
为什么会精度损失?
要解释为什么会精度损失就要,就要找到其背后的本质,那就需要先看看计算机中是怎么保存浮点数的。
先到百度上搜先关的技术文章,找到一篇看似比较权威的博客(http://blog.csdn.net/rsp19801226/article/details/3085343)到的最核心的就是float和里面说double在内存中存贮格式,如下:
Float存储格式:
Double存储格式:
`
先看看“132541.35”这个浮点数在计算机中是怎么保存的。计算机中只能保存二进制,所以我们需要将它转成二进制。
“132541.35”分为整数部分,和小数部分。
十进制整数部分转二进制
整数部分转二进制是将整数部分进行迭代除2取余,直到商为0为止,将每次迭代之后的余数连接起来就行了。
132541的二进制表示:
十进制小数部分转二进制
十进制小数转成二进制小数,是需要对小数部分进行迭代乘2,每次迭代整数部分如果是1,则取小数部分进行下一次迭代,直到小数部分为0为止。
如上图所示,当得到0.4的小数部分之后,之后的每次迭代乘以2之后永远也没有办法得到整数1,就是以“0110”无限循环,所以0.35 不能用有限位的二进制表示,这就像1/3是无法用有限位的十进制表示是一样的道理,随着0.3333的尾数精度增加,只能无限趋近于1/3的真实值而不可能相等。
完整的“132541.35”二进制表示:
按照之前的说明,float是用32位存储的,前一位是符号位,中间8位为指数位,最后23位为尾数位。
以下是“132541.35”在计算机中存储结构:
第一位是符号位1代表正数,中间的指数段8位用移码来表示17,至于17怎么转成移码这里不深究了,23位尾数段,用来表示小数的部分也只用到了6位(底色是黄色的部分),其余的精度硬生生的被截断了。
试着将“010110”再转成十进制小数看看,1/4+1/16+1/32=0.34375 与0.35比较,精度损失了不少,这也就解释了最开头的那个问题产生的原因。如果要得到正确满意的结果,只能用双精度double了,因为双精度在尾数部分可以保存52位,可以保存比float更长的尾数部分。这样可以适当提高浮点数的精度。
但是问题是无论是单精度还是双精度,如果整数部分本身就是一个非常大的值的话那都会挤压小数部分的精度。
在实际的业务场景中,如果只是涉及到两个浮点数之间的相加求和,那还能得到满意的结果。如果是多个浮点数求和话就,随着累加值整数部分不断变大,小数部分的精度损失会越来越厉害。
解决之道
提高精度
可以总结出一个经验,就是在日常中,如果我们需要对浮点数进行累加操作,在明确整数部分的累加值不会特别大时候,需要全部使用double类型来操作。
使用BigDecimal
如果,最后的累加值会比较大,那就需要使用BigDecimal类进行累加,因为它不会因为整数部分变大而影响小数部分的精度。但是,由于BigDecimal是不可变对象,当两个BigDecimal对象累加之后会生成一个新的BigDecimal对象,本来如果用primitive值累加的话全部是在栈上完成的,如果用bigDecimal会在堆内存中产生大量临时对象,这样对VM(java)的young区的垃圾回收可能产生一定压力。
将浮点数先转成Integer
原理很简单,就是业务系统中保存的浮点数一般是表示金额,而金额一般都精确到分,所以在反序列化数据时候先将数据乘上100,转成以分为单位的int数值,然后再进行累加操作。这样带来的好处是累加过程中没有精度损失的烦恼,而且int值的累加操作理论上来说会比浮点的累加操作快不少,终端显示的时候只要再除以100就可以了。
但是特别注意的是,如果业务上使用这种方式处理浮点数操作的话,一定要注意,真实值被乘以两次100的问题,特别是在右多个函数嵌套的时候,乘两次100的问题极易发生,如果这个值的意义是“商家应付款”那将成为一个灾难。
如果要规避这样的问题,可以构建一个包装类,实际上我在工程中就构建了一个为浮点数的包装类,有了包装类可以有效避免乘两次,和除两次的问题。
包装类代码如下:
mport java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; public class TisMoney { // 分 private int fen = 0; private static final String[] ZERO_ARRAY = new String[] { "00", "0", StringUtils.EMPTY }; public static TisMoney create() { return new TisMoney(); } public static TisMoney create(String doubleValue) { return new TisMoney(doubleValue); } private TisMoney() { this.fen = 0; } private static final Pattern DECI_PATTERN = Pattern .compile("(\\-?)(\\d+)(\\.(\\d{1,2}))?"); private TisMoney(String doubleValue) { Matcher m = DECI_PATTERN.matcher(doubleValue); if (m.find()) { String decimal = m.group(4); this.fen = Integer .parseInt(m.group(2) + StringUtils.trimToEmpty(m.group(4)) + ZERO_ARRAY[StringUtils.length(decimal)]); if ("-".equals(m.group(1))) { this.fen *= -1; } return; } } public void addCoefficient(BigDecimal value) { this.fen *= value.doubleValue(); } public int getFen() { return this.fen; } public int intValue() { return (fen / 100); } public void add(TisMoney fen) { this.fen += fen.fen; } public String format() { int decim = Math.abs(fen % 100); return ((fen < 0) ? "-" : "") + (Math.abs(fen) / 100) + "." + ((decim < 10 && decim > 0) ? "0" : "") + decim; } @Override public String toString() { return format(); } }
有了强类型保护,使用起来就方便多了。
祝君玩得愉快!
相关推荐
浮点数(单精度浮点数,双精度浮点数) 浮点数是一种数字表示方法,用于近似表示任意实数。在计算机中,浮点数由一个整数或定点数(即尾数)乘以某个基数(通常是 2)的整数次幂得到。这种表示方法类似于基数为 10 ...
本文将深入探讨十六进制到浮点数的转换,特别是在LabVIEW环境下的实现。LabVIEW(Laboratory Virtual Instrument Engineering Workbench)是由美国国家仪器公司(NI)开发的一种图形化编程语言,广泛应用于工程、...
此外,当涉及到32位整数和32位浮点数之间的转换时,整数转浮点数时可能会因为浮点数的精度而产生非预期的结果,而浮点数转整数时可能因四舍五入规则产生误差。因此,在编写程序时,应充分考虑这些潜在问题,并进行...
`float_2_char.zip` 提供的代码着重解决了单片机中浮点数(`float`)与字符型(`char`)之间的转换问题,这对于节省存储空间、提高通信效率以及降低错误率非常有用。下面将详细介绍浮点数与字符型之间的转换原理和...
在数字信号处理领域,浮点数运算在许多高级计算任务中扮演着重要角色。TMS320C3x是一款由德州仪器(TI)推出的数字信号处理器(DSP),它支持浮点运算,使得复杂的数学计算得以高效执行。本文将深入探讨TMS320C3x中...
在计算机科学中,浮点数是一种用于表示数值的近似方式,主要分为单精度浮点数和双精度浮点数。这些数据类型广泛应用于各种计算,特别是在需要处理大量精确度和范围的数学运算中,例如科学计算、图像处理和游戏开发。...
单片机浮点数设计是嵌入式系统开发中的一个重要环节,特别是在资源有限的环境中,如微控制器(MCU)等。浮点数运算在科学计算、传感器数据处理以及复杂算法实现等方面扮演着关键角色。然而,传统的单片机往往不直接...
- **类型转换**:将输入的十六进制字符串转换为二进制数组,再进一步转换为浮点数,需要使用到`Convert.ToInt32`或`BitConverter`类。 - **错误处理**:考虑到可能出现的无效输入或溢出情况,需加入适当的错误检查和...
2. **HEX到浮点数**: 反向过程,从HEX字符串解析出二进制表示,再根据IEEE 754的规则计算出浮点数。这需要正确解读符号位、指数和尾数,考虑到指数的偏移值和尾数的规格化。 在标签中,“HEX”和“浮点数”进一步...
浮点数转换在计算机科学中是一项基础且重要的概念,特别是在编程和数字处理领域。浮点数,顾名思义,是指可以表示小数部分的数值,与整数相对。它们在计算机内部通常以二进制浮点数的形式存储,遵循IEEE 754标准。这...
本文将详细介绍如何使用S7-200SMART进行双精度浮点数到单精度浮点数的转换,并提供相应的库文件及使用说明。 1. **浮点数类型**: 浮点数在计算机中分为单精度浮点数(32位,IEEE 754标准)和双精度浮点数(64位,...
在处理数值计算时,有时我们需要将不同精度的浮点数进行转换,如将64位浮点数转换为32位浮点数。这个过程涉及到浮点数的表示方式、数据类型的转换以及可能的精度损失问题。 64位浮点数,也称为双精度浮点数(Double...
1. **浮点数表示**:浮点数在内存中以IEEE 754标准存储,分为单精度(32位)和双精度(64位)两种形式。打印时需要将其转换为人类可读的十进制格式。 2. **浮点数转换**:在资源有限的嵌入式系统中,我们可能需要...
浮点数的DFA识别算法是一种在计算机编程中用于解析和验证输入字符串是否符合浮点数格式的方法。DFA(确定有限状态自动机)是一种计算模型,它通过一系列预定义的状态转换来处理输入序列,最终达到判断输入是否合法的...
浮点数在计算机科学中扮演着至关重要的角色,特别是在数值计算、图形处理和科学计算等领域。浮点数的表示方式是基于二进制的,但为了方便人类阅读和理解,通常我们使用十进制形式。浮点数与二进制之间的转换是理解和...
- get_float_hex函数:结合上述两个函数,实现将十进制浮点数转换为符合IEEE 754标准的二进制,再转换为十六进制表示。 具体的JavaScript代码实现中,定义了上述的DecToBinTail和DecToBinHead函数来辅助完成二进制...
例如,0.1在二进制中无法精确表示,所以当你将0.1转换为浮点数然后再转换回十进制时,可能会得到略小于0.1的结果。这就是著名的浮点数舍入误差问题,它在计算领域是一个重要的概念,需要在设计算法和编写代码时予以...
浮点数与二进制转换在计算机科学中是至关重要的概念,特别是在通信开发、数据存储和计算领域。本文将深入探讨浮点数和二进制之间的转换,以及如何利用工具进行这种转换。 浮点数是一种表示实数的方式,它允许在有限...
在C语言中,将浮点数转换为字符串是一项常见的任务,尤其在需要将数值数据输出到文件或屏幕上时。这个过程通常涉及到`printf`函数家族的使用,它们能够按照指定的格式将各种类型的数据转化为可读的字符串。本文将...
在三菱PLC编程中,浮点数运算指令是非常重要的功能,因为工业控制中的许多参数和变量都需要进行浮点数运算。在三菱PLC中,浮点数运算指令能够处理32位的浮点数,并进行比较、变换、四则运算、开方运算以及三角函数...