为什么关注数字精度?
在大部分场景,我们默认整数或者保留两位小数位,分别对应Int和Double,而没有进一步去了解其精度,因为大部分应用,这样的精度和数据类型是足够应付的,但是在某些科学计数及特殊的商业范畴,可能需要更高精度的数字表达,这就要进一步了解数字的精度。大部分语言都提供两种基本精度类型,一种是float、一种是double ;实际上如果小数位如果是固定的,float和double可以想办法转成用int和long来表示,显示给用户的时候再按小数位整除,比如金额一般要求到分就可以了,那我们可以把数据设置成long,显示的时候再除100。无论如何,如果涉及大数字、高精度的运算,可能还是饶不开使用更高精度要求的数据处理,比如:
bigdata*(smalldata*smalldata),可能理想的结果是一个正常值,但因为精度丢失原因,两个smalldata*smalldata可能会变成0,而让整个计算结果相差太大,当然这种场景可能在特定的行业、物理等方面可能会碰到。
精度丢失问题
来一段java代码:
float a=1f
float b=3f
System.out.println(a/b)
System.out.println(0.33333331f)
结果是
0.33333334
0.3333333
public class FloatDoubleTest {
public static void main(String[] args) {
float f = 20014999;
double d = f;
double d2 = 20014999;
System.out.println("f=" + f);
System.out.println("d=" + d);
System.out.println("d2=" + d2);
}
}
f=2.0015E7
d=2.0015E7
d2=2.0014999E7
这两个案例都发现单精度float出现了精度丢失问题,而且丢失后的数据也没有按理想中的比如四舍五入给存保留最后一位。我们必须理解精度有数据范围的一面,但更重要的是其精度范围的一面。
Java的单精度和双精度都遵循IEEE 754标准,可进一步去了维基百科大致了解下IEEE 754标准http://zh.wikipedia.org/wiki/IEEE_754。
从标准来看,可总结出单双精度和数据范围与精度范围。
1. 数据范围
float和double的范围是由指数的位数来决定的。
float的指数位有8位,而double的指数位有11位,分布如下:
float:
1bit(符号位) 8bits(指数位) 23bits(尾数位)
double:
1bit(符号位) 11bits(指数位) 52bits(尾数位)
因此,float的指数范围为-127~+128,而double的指数范围为-1023~+1024,并且指数位是按补码的形式来划分的。
其中负指数决定了浮点数所能表达的绝对值最小的非零数;而正指数决定了浮点数所能表达的绝对值最大的数,也即决定了浮点数的取值范围。
float的范围为-2^128 ~ +2^128,也即-3.40E+38 ~ +3.40E+38;double的范围为-2^1024 ~ +2^1024,也即-1.79E+308 ~ +1.79E+308。
2. 数据精度
float和double的精度是由尾数的位数来决定的。浮点数在内存中是按科学计数法来存储的,其整数部分始终是一个隐含着的“1”,由于它是不变的,故不能对精度造成影响。
float:2^23 = 8388608,一共七位,这意味着最多能有7位有效数字,但绝对能保证的为6位,也即float的精度为6~7位有效数字;
double:2^52 = 4503599627370496,一共16位,同理,double的精度为15~16位
3.范围与精度制约关系
从上面我们知道了单双精度float与double各自的数据范围与精度,以float为例,如果我们控制6-7位的精度在小数点的位置上,会觉的这个是正常的应用场所。但不幸的是,实际上精度只控制高位数6-7位的精度,也就是说如果你的数字整数部分就是个大整数,那就先控制好整数部分的精度,如果小数部分超过6-7位,整数部分的精度也会丢失,更别说小数部分了。举个极端的例子,如下代码:
System.out.println(Float.MAX_VALUE);
System.out.println(Float.MAX_VALUE-3000000f);
输出结果:
3.4028235E38
3.4028235E38
两者是相等的,原因大家应该都明白了,39位整数,300万的精度差已丢失,我想现在回头再来看一开始的两段代码,问题和原因也都明白了。
这里还有一点,就是丢失精度的时候,并非按四舍五入等更合理的算法,是因为这里的元位是二进制,规则是如果超过尾数位就会直接丢失。可以参考这篇博文:http://singleant.iteye.com/blog/713890
JAVA的BigDecimal
Java引入另外一种大数字类型BigDecimal,通过设置setScale可以设置精度控制,与float及double不同,这里的精度专指小数部分的精度。
BigDecimal有多种构造方法,与精度相关最就要的三个构造方法:
BigDecimal(float param)
BigDecimal(double param)
BigDecimal(String param)
不同的参数,在构造的时候会形成默认的精度,比如float,其能控制的精度6-7位,如果参数整数部分超过7位,这时候BigDecimal构造出来的值直接就去掉了小数部分,scale也自然变0,而如果数字较小,构造出来的方法因为其很长的小数位,这时scale也变的很长。double类型的参数与float构造相似,因此看起来用double与float构造出来的方法的精度是不确定的,一般情况下采用String构造,比如
new BigDecimal(“111.111001”)
scale:6,小数后6位
new BigDecimal(“111.11100”)
new BigDecimal(“111.111”)
scale:3,多余的0不算。
可以再次设置setScale来提高精度,比如
BigDecimal a new BigDecimal(“111.111”)
a.setScale(6);//提高到6位精度
但不能降低精度,比如设成a.setScale(2),运行时会有异常,因为降低精度必须采用一种近似取值的策略,因此如果降低精度的话,BigDecimal提供了另外一个参数,a.setScale(int newScale,int roundingMode)。
另外BigDecimal提供了一些计算的方法,还有一个专门用来比较两数是否相等的方法compareTo。
从功能上来说BigDecimal完全可以替代float和double来做运算使用,并且能提供精度更大的数据运算。而且BigDecimal的精度专指小数部分,对于入门者来说更容易理解。
遗憾的是BigDecimal在大数据量运算时,效率上会不如直接使用float、double;这是从网上引用的测试案例和测试数据:
public class BigDecimalEfficiency {
public static int REPEAT_TIMES = 1000000;
public static double computeByBigDecimal(double a, double b) {
BigDecimal result = BigDecimal.valueOf(0);
BigDecimal decimalA = BigDecimal.valueOf(a);
BigDecimal decimalB = BigDecimal.valueOf(b);
for (int i = 0; i < REPEAT_TIMES; i++) {
result = result.add(decimalA.multiply(decimalB));
}
return result.doubleValue();
}
public static double computeByDouble(double a, double b) {
double result = 0;
for (int i = 0; i < REPEAT_TIMES; i++) {
result += a * b;
}
return result;
}
public static void main(String[] args) {
long test = System.nanoTime();
long start1 = System.nanoTime();
double result1 = computeByBigDecimal(0.120000000034, 11.22);
long end1 = System.nanoTime();
long start2 = System.nanoTime();
double result2 = computeByDouble(0.120000000034, 11.22);
long end2 = System.nanoTime();
long timeUsed1 = (end1 - start1);
long timeUsed2 = (end2 - start2);
System.out.println("result by BigDecimal:" + result1);
System.out.println("time used:" + timeUsed1);
System.out.println("result by Double:" + result2);
System.out.println("time used:" + timeUsed2);
System.out.println("timeUsed1/timeUsed2=" + timeUsed1 / timeUsed2);
}
}
输出结果:
result by BigDecimal:1346400.00038148
time used:365847335
result by Double:1346400.000387465
time used:5361855
timeUsed1/timeUsed2=68
相差68倍,不在一个数量级上的差距。因此BigDecimal虽然好用,除非你的需求超过了double的精度,否则还是少用BigDecimal。
选择思路
首先java本身不是执行高效率的语言,因此如果集中于数据应算的话,可以考虑比如C、Fortran等语言是否更加合适。
如已经不能脱离java,那就要好好分析实际需求,一般情况下double的精度是足够一般商业业务应用的。通常情况下double有15-16位,12位整数+(2-3)小数这样的精度是足够的,但也有例外,比如一般发票的单据,金额一般就是小数点后2位,我们整数给他12位,而商品单价有时候会要求很长的小数位,那如果考虑到商品单价也有可能很高,这时候double就可能存在精度丢失问题,这种情况下再选择BigDecimal。总之选择的时候要兼顾实际需求和执行效率。