项目中使用了 BigDecimal 差点被开除

背景

从事金融相关项目,对BigDecimal应该是再熟悉不过了,也有很多人因为不知道、不了解或使用不当导致资损事件发生。

所以,如果你从事金融相关项目,或者你的项目中涉及到金额的计算,那么你一定要花时间看看这篇文章,全面学习一下BigDecimal

概述

一般情况下,对于不需要准确计算精度的数字,可以直接使用FloatDouble处理,但是 FloatDouble 会导致精度丢失。所以在需要精确计算结果的项目中,则必须使用BigDecimal类来操作。虽然,BigDecimalFloatDouble 能够保证精度问题,但是使用不当也会踩坑。

四个BigDecimal 中容易踩的坑

1、创建 BigDecimal 的坑

BigDecimal 中提供了多种创建方式,可以通过new 直接创建,也可以通过 BigDecimal#valueOf 创建。这两种方式使用不当,也会导致精度问题。如下:

    @Test
    public void test(){

        BigDecimal b1 = new BigDecimal(0.1);
        BigDecimal b2 = BigDecimal.valueOf(0.1);

        System.out.println("b1=" + b1);
        System.out.println("b2=" + b2);

    }

输出结果:

b1=0.1000000000000000055511151231257827021181583404541015625
b2=0.1

上面示例中 b1 还是出现了精度的问题。造成这种问题的原因是 0.1 这个数字计算机是无法精确表示的,送给 BigDecimal 的时候就已经丢精度了,而 BigDecimal#valueOf 的实现却完全不同。如下源码所示,BigDecimal#valueOf 中是把浮点数转换成了字符串来构造的BigDecimal,因此避免了问题。

public static BigDecimal valueOf(double val) {
    // Reminder: a zero double returns '0.0', so we cannot fastpath
    // to use the constant ZERO.  This might be important enough to
    // justify a factory approach, a cache, or a few private
    // constants, later.
    return new BigDecimal(Double.toString(val));
}

结论
第一,在使用BigDecimal构造函数时,尽量传递字符串而非浮点类型;
第二,如果无法满足第一条,则可采用BigDecimal#valueOf方法来构造初始化值。

2、等值比较的坑

一般在比较两个值是否相等时,都是用equals 方法,但是,在BigDecimal 中使用equals可能会导致结果错误,BigDecimal 中提供了 compareTo 方法,在很多时候需要使用compareTo比较两个值。如下所示:

    @Test
    public void testEquals(){

        BigDecimal b1 = new BigDecimal("1.0");
        BigDecimal b2 = new BigDecimal("1.00");

        System.out.println(b1.equals(b2));
        System.out.println(b1.compareTo(b2));

    }

输出结果:

false
0

出现此种结果的原因是,equals不仅比较了值相等,还比较了精度是否相同。示例中,由于两个值的精度不同,所有结果也就不相同。而 compareTo 是值比较值的大小。返回的值为-1(小于),0(等于),1(大于)。

结论
如果比较两个BigDecimal值的大小,采用其实现的compareTo方法;
如果严格限制精度的比较,那么则可考虑使用equals方法。

3、无限精度的坑

BigDecimal 并不代表无限精度,当在两个数除不尽的时候,就会出现无限精度的坑,如下所示:

    @Test
    public void testDivide(){
        BigDecimal b1 = new BigDecimal("1.0");
        BigDecimal b2 = new BigDecimal("3.0");

        b1.divide(b2);

    }

输出结果:

java.lang.ArithmeticException: Non-terminating decimal expansion;
 no exact representable decimal result.

在官方文档中有如下说明:

If the quotient has a nonterminating decimal expansion and the operation is specified to return an exact result, an ArithmeticException is thrown. Otherwise, the exact result of the division is returned, as done for other operations.

大致意思就是,如果在除法(divide)运算过程中,如果商是一个无限小数(0.333…),而操作的结果预期是一个精确的数字,那么将会抛出ArithmeticException异常。

此种情况,只需要在使用 divide方法时指定结果的精度即可:

    @Test
    public void testDivide(){
        BigDecimal b1 = new BigDecimal("1.0");
        BigDecimal b2 = new BigDecimal("3.0");

        BigDecimal divide = b1.divide(b2, 2, RoundingMode.HALF_UP);

        System.out.println(divide);

    }

结论
在使用BigDecimal进行(所有)运算时,尽量指定精度和舍入模式。

4、输出字符串的坑

BigDecimal 转换成字符串时,有可能输出非你预期的结果。如下所示:

    @Test
    public void testString(){
        BigDecimal a = BigDecimal.valueOf(332334535345456700.12345634534534578901);
        System.out.println(a.toString());
    }

输出结果:

3.323345353454567E+17

可以看到结果已经被转换成了科学计数法,可能这个并不是预期的结果 ,BigDecimal有三个方法可以转为相应的字符串类型,切记不要用错:

String toString();     // 有必要时使用科学计数法
String toPlainString();   // 不使用科学计数法
String toEngineeringString();  // 工程计算中经常使用的记录数字的方法,与科学计数法类似,但要求10的幂必须是3的倍数

小结

在金融项目中,一般都是推荐使用BigDecimal,来避免精度的丢失。但是BigDecimal使用不当也会踩坑。本章内容主要介绍了在使用BigDecimal时经常容易踩的坑。虽然某些场景下推荐使用BigDecimal,它能够达到更好的精度,但性能相较于doublefloat,还是有一定的损失的,特别在处理庞大,复杂的运算时尤为明显。故一般精度的计算没必要使用BigDecimal。而必须使用时,一定要规避上述的坑。

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

推荐阅读更多精彩内容