1. 引子
平常在代码中,从不缺少使用浮点数的地方。浮点数可以使用float和double类型进行定义。默认都是使用的double类型,如果需要声明为float类型,需要显示地加F或者f,比如Float fNumber = 1.234F。往常个人在使用的时候,使用float居多(在java的基本类型中,Float是32位,4个字节;Double是64位,8个字节;所以float相对来说节省内存),近来踩了两个坑,觉得还是double比较省心。记录如下。
2. 问题1 – 精度错误
2.1 表现形式问题一示例如下。
需要将一组对象转换成json,使用的json包是json-lib 2.2版本。
图2.1 json-lib包引用
图2.2 问题1示例
图2.3 问题1结果
可以看到在将浮点数转换成json数据的时候,Float类型的浮点数会出现精度错误的问题。
2.2 问题原因
排查原因,发现在json-lib的库里面,对于Float类型的数据,是先将其转换成Double类型之后,然后再进行处理的。处理代码如下。
图2.4 json-lib对于Float的处理
Float的doubleValue方法是直接进行类型转换,即return (double)value。
Float和Double在内存中的存储长度不同,将Float转换成Double类型的时候,如果直接取Float的doubleVlaue方法,会进行位数的补全;特别是在有小数部分的时候,这样是不能保证精度的,这样就引起了我们现在的问题。
2.3 解决方法
解决问题的方法有两个思路。
- 在整个系统中以double类型存储,替换掉现有的以float类型存储的方式;
- 找到float类型转换成double类型的安全的方法。
第一个思路,可以彻底的解决这个问题,但是对于一个系统来说,进行全局替换需要慎重,所以暂时不进行这样的修复;
第二个思路,可以有以下几种方案。
- 因为我们的精度要求保留到小数点之后的两位即可,所以可以将原值value乘以100,然后取long值,将结果再除以100.0转换成double类型;
- 使用BigDecimal;
- 先将float类型的数值转换成String,然后将String转换成Double类型的即可。比较了一下,调用Double.valueOf(String value)方法和直接新建Double对象(new Double(String value))都是可以的;但是前者只需要创建对象一次,后者需要新建两个Double对象,为了节省开销,使用了第一个方法进行了处理。
解决问题的代码如下。
图2.5 问题解决
先判断要处理的值是float类型,然后调用Double的valueOf方法进行处理。
3. 问题2 – 大数表示问题
3.1 表现形式
解决了上面这个json输出的问题之后,又发现了一个问题。对于一些较大的数值,比如9999999.99;最终的输出结果为1.0E7,而不是预期的数值。
3.2 原因分析
按理来说,float存储的范围应该远大于这个数值,经过一系列的调试,发现是float类型转换成String的时候出现的问题。进一步深入,发现是float类型的存储结构导致的。
在java中,float类型存储的长度是32bit,double类型的存储长度是64bit。如下表所示。
表3.1 float和double在java中的存储
符号位(bit) | 指数位(bit) | 尾数位(bit) | |
Float(32bit) | 1 | 8 | 23 |
Double(64bit) | 1 | 11 | 52 |
其中:
- 符号位表示数值的正负;
- 指数位表示数值的取值范围,以补码形式存储;
- 尾数位表示数值的精度。
对于float来说,2^23=8388608;所以float类型的数值,如果有效数字的数值超过了8388608这个值,系统会自动找一个近似的结果进行代替。这也是我们这里存储9999999.99的时候,结果异常的原因。这个值在存储的时候就已经被处理成了1.0E7,所以不论采用什么方式进行处理,都不会有改善的。这里也说明,float类型,可以保证6-7位的有效数字(6位及以下是绝对可以保证的,但是7位的时候,这个有效数字的数值如果超过了8388608,就无法进行表示了)。
对于double类型,2^52=4503599627370496;所以double类型的有效数字在15-16位。
所以我们遇到的问题2,目前是没有办法进行解决的。后续还是安排使用double类型替换float类型。
4. 小结
本文从两个问题说明了float类型的“坑爹”之处,如果不是对业务以及业务的发展特别有把握,还是建议使用double类型。