java中精确计算,double与BigDecimal的取舍
相信java程序员都知道double是一种不能用作精确计算的类型,因为它会有精度损失,而要想规避精度损失,大家都会想到BigDecimal,这是JDK提供的类,确实能解决精度问题,但是它并不是完美的,它有如下三个缺点:
- 慢
- 乱
- 也不是那么准确
一、慢有多慢?
所有人都知道,BigDecimal作为对象,new出来是有成本的,肯定比基本数据类型会慢一点,但具体慢到什么程度呢?我写了一段求加和的代码如下:
double sum = 0.0;
long a = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
double d = Math.random();
sum += d;
}
long b = System.currentTimeMillis();
System.out.println(b - a);
System.out.println(sum);
Double sum = 0.0;
long a = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
Double d = Math.random();
sum += d;
}
long b = System.currentTimeMillis();
System.out.println(b - a);
System.out.println(sum);
BigDecimal sum = new BigDecimal("0");
long a = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
BigDecimal d = new BigDecimal(Math.random());
sum = sum.add(d);
}
long b = System.currentTimeMillis();
System.out.println(b - a);
System.out.println(sum);
这三段代码分别是用基本数据类型、包装数据类型和BigDecimal来计算一个和,我的电脑上分别运行这三段代码,当times是10000的时候,前两个是1ms执行时间,而第三个是30ms,当times是100000000的时候,前两个是差不多2秒(但是结果显然不正确),而第三个是差不多40秒,这是20~30倍的执行效率差距,所以BigDecimal绝不能滥用。至于为什么包装类的创建几乎没有感觉到呢?自动拆装箱不需要时间吗?这个就涉及到一些底层的优化策略了,在此不做深究,先了解一下结论,不用太纠结基本类型和包装类型。
二、乱又怎样?
乱,在某些人看来,并不能称为问题,但我是有些不能接受的,比如一个简单的物理公式:距离=速度时间+(加速度时间的平方)/2(即:S=vt+1/2at^2),用double来写就跟公式本身很像,而用BigDecimal,即使是这样简单的公式,也让人看得云里雾里,不知所云,代码如下:
double s;
double v = 2323.346852;
double a = 102.1523684;
double t = 20.004;
s = v * t + (a * Math.pow(t, 2)) / 2;
System.out.println(s); // 66914.87711409896
BigDecimal s;
BigDecimal v = new BigDecimal(2323.346852);
BigDecimal a = new BigDecimal(102.1523684);
BigDecimal t = new BigDecimal(20.004);
// 写这个可得留神,稍微一个不注意,错个括号,那可就差大了,
// 有的人可能想要提取变量,可能会好点,但很多时候就像这个例子,
// 最复杂的部分a*t^2/2,提取出来的变量叫什么名能够见名知意?可能效果跟不提取也没啥区别。
s = v.multiply(t).add(a.multiply(t.pow(2)).divide(new BigDecimal(2)));
System.out.println(s); // 66914.8771140989556179216886351698979346...........还没完
三、JDK的类能不准?
关于BigDecimal计算不准确的问题,项目中已经多次遇到了,分为两方面,一方面是BigDecimal的用法不对,本文中上面所列举的所有关于BigDecimal的代码,用法都是错的;另一方面是即使用对了结果也未必是你想要的。
先说第一个用法的问题,很多程序员在使用BigDecimal时会用BigDecimal(double val)这个构造方法,JDK的文档中说得非常明白“The results of this constructor can be somewhat unpredictable”,“The String constructor, on the other hand, is perfectly predictable”(double的构造方法是垃圾,String的构造方法是完美的),而很少有人在被这个构造方法坑掉之前查阅这个文档,我们用String构造重新执行一下算距离的公式
BigDecimal s;
BigDecimal v = new BigDecimal("2323.346852");
BigDecimal a = new BigDecimal("102.1523684");
BigDecimal t = new BigDecimal("20.004");
s = v.multiply(t).add(a.multiply(t.pow(2)).divide(new BigDecimal("2")));
System.out.println(s); // 66914.8771140989472(完结)
可以看出,用double构造方法来计算,精度几乎没有比double提升多少,但开销多了30倍。可见对知识的一知半解有的时候比完全不懂更差。
有的时候,现实是很残酷的,我们用了正确的方法使用了BigDecimal而结果依然可能是不尽如人意的,比如下面这个场景:
部门 | 人员 | 考勤(分钟) |
---|---|---|
1 | Jack | 3073.32 |
1 | Robin | 3073.32 |
1 | 小白 | 3073.32 |
2 | Robin | 3073.32 |
2 | 小白 | 3073.32 |
2 | Jack | 3073.32 |
假设某个公司有三个人,两个部门,而这三个人来回在这两个部门之间调动,现在要统计每个人的总考勤,每个部门的总考勤,还有整个公司的总考勤,并且是要以小时为单位,保留两位小数,这个需求一看就很明确,每个人的考勤总和、每个部门的考勤总和、公司的总考勤应该是相等的,但不幸的是,经过计算可能会得到下面的结果:
部门 | 考勤(分钟) | 考勤(小时) | 人员 | 考勤(分钟) | 考勤(小时) |
---|---|---|---|---|---|
1 | 9219.96 | 153.67 | Jack | 6146.64 | 102.44 |
2 | 9219.96 | 153.67 | Robin | 6146.64 | 102.44 |
- | - | - | 小白 | 6146.64 | 102.44 |
合计 | - | 307.34 | 合计 | - | 307.32 |
显然,每个人的考勤总和、每个部门的考勤总和已经不相等了,而这跟你用double还是BigDecimal无关。更令人绝望的是整个公司的合计是18439.92分钟,保留小数后是307.33小时,三个本应该相等的数,全都不相等。
可能有的朋友已经看出了端倪,这不是一个技术问题,这已经是一个业务了,如果这个项目的需求就是这样的,那作为开发人员必须据理力争,痛陈这个不合理性,不要再做任何技术尝试,不管是用double还是BigDecimal都是在浪费时间。