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都是在浪费时间。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容