不掌握这些坑,你敢用bigdecimal吗?(代码片段)

程序新视界 程序新视界     2022-10-21     131

关键词:

背景

一直从事金融相关项目,所以对BigDecimal再熟悉不过了,也曾看到很多同学因为不知道、不了解或使用不当导致资损事件发生。

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

BigDecimal概述

Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理。

一般情况下,对于不需要准确计算精度的数字,可以直接使用Float和Double处理,但是Double.valueOf(String) 和Float.valueOf(String)会丢失精度。所以如果需要精确计算的结果,则必须使用BigDecimal类来操作。

BigDecimal对象提供了传统的+、-、*、/等算术运算符对应的方法,通过这些方法进行相应的操作。BigDecimal都是不可变的(immutable)的, 在进行每一次四则运算时,都会产生一个新的对象 ,所以在做加减乘除运算时要记得要保存操作后的值。

BigDecimal的4个坑

在使用BigDecimal时,有4种使用场景下的坑,你一定要了解一下,如果使用不当,必定很惨。掌握这些案例,当别人写出有坑的代码,你也能够一眼识别出来,大牛就是这么练成的。

第一:浮点类型的坑

在学习了解BigDecimal的坑之前,先来说一个老生常谈的问题:如果使用Float、Double等浮点类型进行计算时,有可能得到的是一个近似值,而不是精确的值。

比如下面的代码:

	@Test
	public void test0()
		float a = 1;
		float b = 0.9f;
		System.out.println(a - b);
	

结果是多少?0.1吗?不是,执行上面代码执行的结果是0.100000024。之所以产生这样的结果,是因为0.1的二进制表示是无限循环的。由于计算机的资源是有限的,所以是没办法用二进制精确的表示 0.1,只能用「近似值」来表示,就是在有限的精度情况下,最大化接近 0.1 的二进制数,于是就会造成精度缺失的情况

关于上述的现象大家都知道,不再详细展开。同时,还会得出结论在科学计数法时可考虑使用浮点类型,但如果是涉及到金额计算要使用BigDecimal来计算。

那么,BigDecimal就一定能避免上述的浮点问题吗?来看下面的示例:

	@Test
	public void test1()
		BigDecimal a = new BigDecimal(0.01);
		BigDecimal b = BigDecimal.valueOf(0.01);
		System.out.println("a = " + a);
		System.out.println("b = " + b);
	

上述单元测试中的代码,a和b结果分别是什么?

a = 0.01000000000000000020816681711721685132943093776702880859375
b = 0.01

上面的实例说明,即便是使用BigDecimal,结果依旧会出现精度问题。这就涉及到创建BigDecimal对象时,如果有初始值,是采用new BigDecimal的形式,还是通过BigDecimal#valueOf方法了。

之所以会出现上述现象,是因为new BigDecimal时,传入的0.1已经是浮点类型了,鉴于上面说的这个值只是近似值,在使用new BigDecimal时就把这个近似值完整的保留下来了。

而BigDecimal#valueOf则不同,它的源码实现如下:

    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));
    

在valueOf内部,使用Double#toString方法,将浮点类型的值转换成了字符串,因此就不存在精度丢失问题了。

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

这里延伸一下,BigDecimal常见的构造方法有如下几种:

BigDecimal(int)       创建一个具有参数所指定整数值的对象。
BigDecimal(double)    创建一个具有参数所指定双精度值的对象。
BigDecimal(long)      创建一个具有参数所指定长整数值的对象。
BigDecimal(String)    创建一个具有参数所指定以字符串表示的数值的对象。

其中涉及到参数类型为double的构造方法,会出现上述的问题,使用时需特别留意。

第二:浮点精度的坑

如果比较两个BigDecimal的值是否相等,你会如何比较?使用equals方法还是compareTo方法呢?

先来看一个示例:

	@Test
	public void test2()
		BigDecimal a = new BigDecimal("0.01");
		BigDecimal b = new BigDecimal("0.010");
		System.out.println(a.equals(b));
		System.out.println(a.compareTo(b));
	

乍一看感觉可能相等,但实际上它们的本质并不相同。

equals方法是基于BigDecimal实现的equals方法来进行比较的,直观印象就是比较两个对象是否相同,那么代码是如何实现的呢?

    @Override
    public boolean equals(Object x) 
        if (!(x instanceof BigDecimal))
            return false;
        BigDecimal xDec = (BigDecimal) x;
        if (x == this)
            return true;
        if (scale != xDec.scale)
            return false;
        long s = this.intCompact;
        long xs = xDec.intCompact;
        if (s != INFLATED) 
            if (xs == INFLATED)
                xs = compactValFor(xDec.intVal);
            return xs == s;
         else if (xs != INFLATED)
            return xs == compactValFor(this.intVal);

        return this.inflated().equals(xDec.inflated());
    

仔细阅读代码可以看出,equals方法不仅比较了值是否相等,还比较了精度是否相同。上述示例中,由于两者的精度不同,所以equals方法的结果当然是false了。而compareTo方法实现了Comparable接口,真正比较的是值的大小,返回的值为-1(小于),0(等于),1(大于)。

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

另外,这种场景在比较0值的时候比较常见,比如比较BigDecimal(“0”)、BigDecimal(“0.0”)、BigDecimal(“0.00”),此时一定要使用compareTo方法进行比较。

第三:设置精度的坑

在项目中看到好多同学通过BigDecimal进行计算时不设置计算结果的精度和舍入模式,真是着急人,虽然大多数情况下不会出现什么问题。但下面的场景就不一定了:

	@Test
	public void test3()
		BigDecimal a = new BigDecimal("1.0");
		BigDecimal b = new BigDecimal("3.0");
		a.divide(b);
	

执行上述代码的结果是什么?ArithmeticException异常

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

	at java.math.BigDecimal.divide(BigDecimal.java:1690)
	...

这个异常的发生在官方文档中也有说明:

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 test3()
		BigDecimal a = new BigDecimal("1.0");
		BigDecimal b = new BigDecimal("3.0");
		BigDecimal c = a.divide(b, 2,RoundingMode.HALF_UP);
		System.out.println(c);
	

执行上述代码,输入结果为0.33。

基本结论:在使用BigDecimal进行(所有)运算时,一定要明确指定精度和舍入模式

拓展一下,舍入模式定义在RoundingMode枚举类中,共有8种:

  • RoundingMode.UP:舍入远离零的舍入模式。在丢弃非零部分之前始终增加数字(始终对非零舍弃部分前面的数字加1)。注意,此舍入模式始终不会减少计算值的大小。
  • RoundingMode.DOWN:接近零的舍入模式。在丢弃某部分之前始终不增加数字(从不对舍弃部分前面的数字加1,即截短)。注意,此舍入模式始终不会增加计算值的大小。
  • RoundingMode.CEILING:接近正无穷大的舍入模式。如果 BigDecimal 为正,则舍入行为与 ROUNDUP 相同;如果为负,则舍入行为与 ROUNDDOWN 相同。注意,此舍入模式始终不会减少计算值。
  • RoundingMode.FLOOR:接近负无穷大的舍入模式。如果 BigDecimal 为正,则舍入行为与 ROUNDDOWN 相同;如果为负,则舍入行为与 ROUNDUP 相同。注意,此舍入模式始终不会增加计算值。
  • RoundingMode.HALF_UP:向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为向上舍入的舍入模式。如果舍弃部分 >= 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同。注意,这是我们在小学时学过的舍入模式(四舍五入)。
  • RoundingMode.HALF_DOWN:向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为上舍入的舍入模式。如果舍弃部分 > 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同(五舍六入)。
  • RoundingMode.HALF_EVEN:向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则向相邻的偶数舍入。如果舍弃部分左边的数字为奇数,则舍入行为与 ROUNDHALFUP 相同;如果为偶数,则舍入行为与 ROUNDHALF_DOWN 相同。注意,在重复进行一系列计算时,此舍入模式可以将累加错误减到最小。此舍入模式也称为“银行家舍入法”,主要在美国使用。四舍六入,五分两种情况。如果前一位为奇数,则入位,否则舍去。以下例子为保留小数点1位,那么这种舍入方式下的结果。1.15 ==> 1.2 ,1.25 ==> 1.2
  • RoundingMode.UNNECESSARY:断言请求的操作具有精确的结果,因此不需要舍入。如果对获得精确结果的操作指定此舍入模式,则抛出ArithmeticException。

通常我们使用的四舍五入即RoundingMode.HALF_UP。

第四:三种字符串输出的坑

当使用BigDecimal之后,需要转换成String类型,你是如何操作的?直接toString?

先来看看下面的代码:

@Test
public void test4()
	BigDecimal a = BigDecimal.valueOf(35634535255456719.22345634534124578902);
	System.out.println(a.toString());

执行的结果是上述对应的值吗?并不是:

3.563453525545672E+16

也就是说,本来想打印字符串的,结果打印出来的是科学计数法的值。

这里我们需要了解BigDecimal转换字符串的三个方法

  • toPlainString():不使用任何科学计数法;
  • toString():在必要的时候使用科学计数法;
  • toEngineeringString() :在必要的时候使用工程计数法。类似于科学计数法,只不过指数的幂都是3的倍数,这样方便工程上的应用,因为在很多单位转换的时候都是10^3;

三种方法展示结果示例如下:

基本结论:根据数据结果展示格式不同,采用不同的字符串输出方法,通常使用比较多的方法为toPlainString()

另外,NumberFormat类的format()方法可以使用BigDecimal对象作为其参数,可以利用BigDecimal对超出16位有效数字的货币值,百分值,以及一般数值进行格式化控制。

使用示例如下:

NumberFormat currency = NumberFormat.getCurrencyInstance(); //建立货币格式化引用
NumberFormat percent = NumberFormat.getPercentInstance();  //建立百分比格式化引用
percent.setMaximumFractionDigits(3); //百分比小数点最多3位

BigDecimal loanAmount = new BigDecimal("15000.48"); //金额
BigDecimal interestRate = new BigDecimal("0.008"); //利率
BigDecimal interest = loanAmount.multiply(interestRate); //相乘

System.out.println("金额:\\t" + currency.format(loanAmount));
System.out.println("利率:\\t" + percent.format(interestRate));
System.out.println("利息:\\t" + currency.format(interest));

输出结果如下:

金额: ¥15,000.48 
利率: 0.8% 
利息: ¥120.00

小结

本篇文章介绍了BigDecimal使用中场景的坑,以及基于这些坑我们得出的“最佳实践”。虽然某些场景下推荐使用BigDecimal,它能够达到更好的精度,但性能相较于double和float,还是有一定的损失的,特别在处理庞大,复杂的运算时尤为明显。故一般精度的计算没必要使用BigDecimal。而必须使用时,一定要规避上述的坑。

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan


程序新视界”,一个100%技术干货的公众号

不掌握这些坑,你敢用bigdecimal吗?(代码片段)

背景一直从事金融相关项目,所以对BigDecimal再熟悉不过了,也曾看到很多同学因为不知道、不了解或使用不当导致资损事件发生。所以,如果你从事金融相关项目,或者你的项目中涉及到金额的计算,那么你... 查看详情

手一挥钱就没,微信亚马逊刷掌支付最新进展,你敢用吗?

随着移动支付的不断发展,越来越多的生物特征数据被应用在了支付方式上。继“刷指纹”、“刷脸”后,“刷掌纹”也在试图跻身主流支付方式之列。近日,据外媒报道,亚马逊正在加利福尼亚州大规模地推广... 查看详情

bigdecimal类型除法问题

坑:bigdecimal类型做除法运算时,结果为整数或有限小数时候不存在问题,若结果无法整除,为无限小数时报错错误代码:Bigdecimal b=a.divide(c).setScale(5,ROUND_HALF_DOWN); 错误提示:Non-terminatingdecimalexpansion;noexactrepresentabledecimalresult"错误... 查看详情

java中如何比较两个bigdecimal以及bigdecimal的坑(代码片段)

Java中如何比较两个BigDecimal以及BigDecimal的坑一、背景我们经常要比较两个数字是否相等,两个数字可以是整数、小数,我们也知道浮点数如float、double在java里是不准确的,那就会使用到BigDecimal来比较,那有些坑就... 查看详情

[poi2012]bon-vouchers----你敢模拟吗?(代码片段)

链接:https://www.luogu.org/problemnew/show/P3536 题意:定义n个数为幸运数字,一共有n批人,设第i批人有x个,则它们会依次取走余下的x的倍数中最小的x个,问哪些人去走了幸运数字 题解:考虑暴力吧。枚举每一天,从第一个... 查看详情

在座的python爬虫工程师,你敢爬律师事务所站点吗?(代码片段)

文章目录⛳️实战场景⛳️反爬实战⛳️反爬总结⛳️实战场景本次要分析的站点是credit.acla.org.cn/,一个律师群体常去的站点,作为一个爬虫工程师,这简直是送自己去喝茶。该站点反爬手段特别多,分析起来也特别有趣。⛳... 查看详情

在座的python爬虫工程师,你敢爬律师事务所站点吗?(代码片段)

文章目录⛳️实战场景⛳️反爬实战⛳️反爬总结⛳️实战场景本次要分析的站点是credit.acla.org.cn/,一个律师群体常去的站点,作为一个爬虫工程师,这简直是送自己去喝茶。该站点反爬手段特别多,分析起来也特别有趣。⛳... 查看详情

计网基础--什么什么,做后端开发你敢说不熟悉计算机网络?(代码片段)

...议的体系结构互联网概述计算机网络由若干个节点和连接这些节点的链路组成,网络中的节点可以是计算机、集线器、交换机或路由器等。网络之间可以通过路由器互相连接起来,这就构成了一个覆盖范围更大的计算机... 查看详情

appium入坑必备--不写代码的自动化测试,你不心动吗?(代码片段)

       上一章我们讲了appium的一些基本操作,也是app测试中一些比较常用的一些方法。本章我们讲讲另外两种工具的使用,最后一种会让你心动!元素定位及工具使用-------必须学会的技能app定位的基本操作-------... 查看详情

python实战!四行python代码就能知道你那的天气,你敢信吗?

今天给大家带来的Python实战项目是四行Python代码获取所在城市的天气预报,我们隐隐听到唏嘘声,不信四行Python代码可以获取是吗?那我们一起来看看:使用Python获取天气预报,想想是件很简单的事情。无非是发送一个HTTP请求,... 查看详情

html中fontbodytabletrtd的属性有哪些?这些代码编辑时排序是怎样排列的?有啥规律能快速掌握吗

本人在自学html,现在还没接触到css,谢谢。<head><metahttp-equiv="Content-Type"content="text/html;charset=gb2312"/><title>无标题文档</title></head><body><tablewidth="998"border="0"><tr><tds... 查看详情

编程经验分享:大神学编程居然从抄代码开始!你敢信?

...机原理,计算机网络、数据库原理、以及操作系统原理,这些知识都是以理论为基础,注重的理解能力。另一部分主要以实践为主,就是平常当工具用的,例如linux日常命令工具、数据库SQL操作、还有写代码,这些知识学起来没... 查看详情

必读丨新手程序员最容易踩的“坑”,你踩过几个?

...工作的负担。对于没有使用该框架经验的开发人员来说,掌握框架的API提供的所有功能非常困难。因此,他们常常会重新实现API中已有的某些代码。没有经验的开发人员更有可能踩这个坑的原因有两个:?第一,由于缺乏经验,... 查看详情

认识升级|系列1|富人思维

...首先,我们来说,什么是富人?有钱?有车?有房?NO,这些都太表面了。给个结论好了:富人:财务自由、时间自由、身心灵自由的人所谓,富人,就是自由的人,想不做什么就不做什么的人。有同学说,我现在上班,就是想... 查看详情

bigdecimal详解(代码片段)

BigDecimal详解BigDecimal介绍BigDecimal常见方法创建加减乘除大小比较保留几位小数BigDecimal等值比较问题BigDecimal工具类分享总结《阿里巴巴Java开发手册》中提到:“为了避免精度丢失,可以使用BigDecimal来进行浮点数的运算”... 查看详情

掌握gradle,还需要掌握这些知识--groovymop(代码片段)

Groovy:MOP一文打尽写在最前Groovy已经不再是一门新出现的语言,而笔者是在2013年左右接触到它的,并且在2017年时,有机会尝试使用它编写了基于SpringBoot的后端项目。但说来惭愧,在很长的一段时间里,我... 查看详情

bigdecimal(代码片段)

BigDecimaljava.math.BigDecimal为了解决java中浮点数运算不精确,用这个类可以很好的解决常用构造器建议使用String类型构造方法,否则使用double类型进行初始化可能还是不精确,因为double本身就不够精确构造器说明BigDecimal(Stringval)val必须... 查看详情

详解javascript中的this

...象,或者…有人甚至因为坑大而不用this。其实如果完全掌握了this的工作原理,自然就不会走进这些坑。来看下以下这些情况中的this分别会指向什么:1.全局代码中的thisJavaScriptalert(this)//window全 查看详情