背景:
java系统,MySql数据库,定义有些数据格式为Decimal(24,2),即最多整数22位,小数2位,或者Decimal(24,4),即最多整数20位,小数4位的数字。系统内部操作使用BigDecimal来记录和操作这样的数据,并无不妥,也不会丢失数据,但是当要将这样的数据导出的excel,问题出现了,为了便于用户使用excel对数据(可能是金额,数量)作分析,所以我们保证单元格写的还是数字。
以金额为例,虽然使用了formater:”#,##0.00”来格式化数字,使之显示为形如”52,441,314.20”这个样的字符串,但这个单元格的内部value还是”52441314.20”,还是数字。
系统中,调用poi框架写这样的一个数字的代码如下:
cell = row.createCell(idx, CellType.NUMERIC);
cell.setCellValue(((BigDecimal) val).doubleValue());
cell.setCellStyle(cellStyles.getMoneyStyle());
第一行创建一个数字格式的单元格。
第二行是将系统内部的数据写到单元格中去。
第三行是设置单元格的格式引用(多说一句,一个excel对一种单元格格式尽量使用同一个单元格格式变量,即尽量复用单元格格式,因为存放单元格格式是需要代价的)。
我们所使用的POI3.15仅支持下面一些setValue的方式,毫无疑问,这里只能先将BigDecimal转换成double再存放进去。
然后,问题开始出现了,当要写入的val值出现形如”5244524452445244.13”这样的大数字(18,2),在系统允许范围内,内部计算使用BigDecimal也没问题。但是当想要导出到excel的时候,问题出现了,被写入的数据是:
5.244524452445240E15。
明显的丢失了精度。经过简单排查,很快发现了问题就是出现在了BigDecinal.doubleValue()这个方法上面。
BigDecimal | 5244524452445244.13 |
---|---|
Double | 5.244524452445244E15 |
正是Double的值成了科学计数法提醒了我,虽然double类型可以具有宽阔的表值空间:-1.7*10(-308)~1.7*10(308)。但是它能维持的精度可能没有这里需要的高。
double结构分析:
看一下double的格式,一个double数是64位,它分为三个区域:
1 | 11 | 52 |
---|---|---|
符号位 | 指数位 | 尾数位 |
简单举一个例子,十进制数字9二进制是:1001,
十进制数字0.625的二进制是:0.101;
所以十进制数字9.625的十进制数是1001.101,也就是1.001101*2^3。double是移位存储(这个概念不清楚请自行百度)的,所以这个数字存放进double就是:
符号位 | 指数位 | 尾数位 |
---|---|---|
0 | 1…100 | 0011010…0 |
63 | 62…52 | 51…0 |
因为移位计算后,科学计数法首位都是1,不用存,所以double 52位的尾数能表达出53位的精度。
范围
11bit的指数位的表达区间是-2^10到2^10
所以double的表值范围是:
−2(210)