1. BigDecimal1.1 背景一直从事金融相关项目,所以对BigDecimal再熟悉不过了,也曾看到很多时候因为不知道、不了解或使用不当导致资损事件发生。所以,如果从事金融相关项目,或者在项目中涉及到金额的计算,那么你一定要花时间看
一直从事金融相关项目,所以对BigDecimal再熟悉不过了,也曾看到很多时候因为不知道、不了解或使用不当导致资损事件发生。
所以,如果从事金融相关项目,或者在项目中涉及到金额的计算,那么你一定要花时间看看这篇文章,全面学习一下BigDecimal。
Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理。
一般情况下,对于不需要准确计算精度的数字,可以直接使用Float和Double处理,但是Double.valueOf(String) 和Float.valueOf(String)会丢失精度。所以如果需要精确计算的结果,则必须使用BigDecimal类来操作。
BigDecimal所创建的是对象,我们不能使用传统的+、-、*、/等算术运算符直接对其对象进行数**算,而必须调用其相对应的方法。方法中的参数也必须是BigDecimal的对象。构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象。
看如下代码:
publicstaticvoidmain(String[] args) throws Exception { System.out.println(0.2 + 0.1); System.out.println(0.3 - 0.1); System.out.println(0.2 * 0.1); System.out.println(0.3 / 0.1);}
打印结果:
为何会这样:
原因:不论是float 还是double都是浮点数,而计算机是二进制的,浮点数会失去一定的精确度。
根本原因:十进制值通常没有完全相同的二进制表示形式;十进制数的二进制表示形式可能不精确。只能无限接近于那个值。
但是,在项目中,我们不可能让这种情况出现,特别是金融项目,因为涉及金额的计算都必须十分精确,否则,如果你的微信账户余额显示193.99999999999998,那是一种怎么样的体验?
float 和double为什么不精确:
首先,计算机是只认识二进制的,即 0 和 1,这个大家一定都知道。
那么,所有数字,包括整数和小数,想要在计算机中存储和展示,都需要转成二进制。
十进制整数转成二进制很简单,通常采用 ”除 2 取余,逆序排列” 即可,如 10 的二进制为 1010
但是,小数的二进制如何表示呢?
十进制小数转成二进制,一般采用 ”乘 2 取整,顺序排列”方法 ,如 0.625 转成二进制的表示为 0.101。
具体做法是:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的整数部分为零,或者整数部分为1,此时0或1为二进制的最后一位。或者达到所要求的精度为止
但是,并不是所有小数都能转成二进制,如 0.1 就不能直接用二进制表示,他的二进制是 0.000110011001100… 一个无限循环小数
这里我们使用乘2 取整,顺序排列的方法来计算一下
0.1*2=0.2----->取整数部分00.2*2=0.4----->取整数部分00.4*2=0.8----->取整数部分00.8*2=1.6----->取整数部分10.6*2=1.2----->取整数部分10.2*2=0.2----->取整数部分0(从此处又开始新的一轮循环)所以0.1的二进制为0.0001100110011无限循
所以,计算机是没办法用二进制精确的表示 0.1 的。也就是说,在计算机中,很多小数没办法精确的使用二进制表示出来,这也就是为什么float 和double的值并不是精确的原因了,其只是在一定精度范围内表示一个浮点数。
方法 | 类型 | 描述 |
public BigDecimal(int val) | 构造函数 | int类型的值生成BigDecimal对象 |
public BigDecimal(long val) | 构造函数 | long类型的值生成BigDecimal对象 |
public BigDecimal(String val) | 静态方法 | String类型的值转换为BigDecimal类型 |
public static BigDecimal valueOf(double val) | 静态方法 | double类型的值转换为BigDecimal类型 |
public static BigDecimal valueOf(long val) | 静态方法 | long类型(包含int类型)的值转换为BigDecimal类型 |
通常建议优先使用String构造方法。当double必须用作BigDecimal的源时,请使用Double.toString(double)转成String,然后使用String构造方法,或使用BigDecimal的静态方法valueOf:
BigDecimal b = new BigDecimal("12.3");BigDecimalc = BigDecimal.valueOf(7.34325);BigDecimal d = new BigDecimal(Double.toString(7.34325));System.out.println(b);System.out.println(c);System.out.println(d);
打印结果:
运算法则 | 对应方法 |
加法 | public BigDecimal add(BigDecimal value) |
减法 | public BigDecimal subtract(BigDecimal value) |
乘法 | public BigDecimal multiply(BigDecimal value) |
除法 | public BigDecimal divide(BigDecimal value) |
代码示例:
publicstaticvoidmain(String[] args) throws Exception { System.out.println("计算加法: " + BigDecimal.valueOf(1.9).add(BigDecimal.valueOf(0.2))); System.out.println("计算减法: " + BigDecimal.valueOf(1.9).subtract(BigDecimal.valueOf(1.5))); System.out.println("计算乘法: " + BigDecimal.valueOf(1.9).multiply(BigDecimal.valueOf(0.2))); System.out.println("计算除法: " + BigDecimal.valueOf(1.9).divide(BigDecimal.valueOf(0.2)));}
打印结果:
注意点:
public BigDecimal setScale(int newScale, int roundingMode)
用于格式化小数的方法,靠前个值表示保留几位小数,第二个值表示格式化的类型。
8种类型如下:
格式化类型 | 描述 |
ROUND_DOWN | 舍弃多余位数,如1.55会格式化为1.5,-1.55会格式化为-1.5 |
ROUND_UP | 进位处理,如1.52会格式化为1.6,-1.52会格式化为-1.6 |
ROUND_HALF_UP | 四舍五入,如果舍弃部分>= .5,则进位 |
ROUND_HALF_DOWN | 五舍六入,如果舍弃部分> .5,则进位 |
ROUND_CEILING | 正无穷大方向舍入模式。如果值为正数,则与ROUND_UP模式相同;如果值为负数,则与ROUND_DOWN模式相同 |
ROUND_FLOOR | 负无穷大方向舍入模式。如果值为正数,则与ROUND_DOWN模式相同;如果值为负数,则与ROUND_UP模式相同 |
ROUND_UNNECESSARY | 确认值的小数位数是否与传入靠前个参数(保留小数的位数)相等,如果符合则返回值,如果不符抛出异常 |
ROUND_HALF_EVEN | 如果舍弃部门左边的数字为奇数,则与ROUND_HALF_UP模式相同,如果为偶数则与ROUND_HALF_DOWN模式相同 |
代码示例:
publicstaticvoidmain(String[] args) { BigDecimal a = BigDecimal.valueOf(5.445); System.out.println("5.445舍弃多余位数:" + a.setScale(2, BigDecimal.ROUND_DOWN)); System.out.println("5.445进位处理:" + a.setScale(2, BigDecimal.ROUND_UP)); System.out.println("5.445四舍五入(舍弃部分>= .5,进位):" + a.setScale(2, BigDecimal.ROUND_HALF_UP)); System.out.println("5.445四舍五入(舍弃部分未> .5,舍弃):" + a.setScale(2, BigDecimal.ROUND_HALF_DOWN)); System.out.println("5.446四舍五入(舍弃部分> .5,进位):" + BigDecimal.valueOf(5.446).setScale(2, BigDecimal.ROUND_HALF_DOWN));}
打印结果:
publicintcompareTo(BigDecimal val)
BigDecimal类提供的比较值的方法,注意比较的两个值均不能为空。
a.compareTo(b)得到结果 1, 0, -1。
比较结果 | 描述 |
1 | a 大于b |
0 | a 等于b |
-1 | a 小于b |
代码示例:
publicstaticvoidmain(String[] args) { BigDecimal a = BigDecimal.valueOf(1); BigDecimal b = BigDecimal.valueOf(2); BigDecimal c = BigDecimal.valueOf(1); BigDecimal d = BigDecimal.ZERO; System.out.println("1和2比较结果:" + a.compareTo(b)); System.out.println("1和1比较结果:" + a.compareTo(c)); System.out.println("1和0比较判断:" + (a.compareTo(d) > 0) );}
打印结果:
注意:实际业务中,不会单纯的比较谁打谁小,而是类似于第三条去判断一个布尔值。
代码 | 类型 | 描述 |
BigDecimal.ZERO | 常量 | 初始化一个为0的BigDecimal对象 |
BigDecimal.ONE | 常量 | 初始化一个为1的BigDecimal对象 |
BigDecimal.TEN | 常量 | 初始化一个为10的BigDecimal对象 |
public BigDecimal abs() | 方法 | 求绝对值,不管正数还是负数,都得到正数 |
public BigDecimal negate() | 方法 | 求相反数,正变负,负变正 |
public BigDecimal pow(int n) | 方法 | 求乘方,如BigDecimal.valueOf(2).pow(3)的值为8 |
public BigDecimal max(BigDecimal val) | 方法 | 两值比较,返回最大值 |
public BigDecimal min(BigDecimal val) | 方法 | 两值比较,返回最小值 |
在使用BigDecimal时,有4种使用场景下的坑,你一定要了解一下,如果使用不当,必定很惨。掌握这些案例,当别人写出有坑的代码,你也能够一眼识别出来,大牛就是这么练成的。
在学习了解BigDecimal的坑之前,先来说一个老生常谈的问题:如果使用Float、Double等浮点类型进行计算时,有可能得到的是一个近似值,而不是精确的值。
比如下面的代码:
@Test publicvoidtest0(){ float a = 1; float b = 0.9f; System.out.println(a - b); }
结果是多少?0.1吗?不是,执行上面代码执行的结果是0.100000024。之所以产生这样的结果,是因为0.1的二进制表示是无限循环的。由于计算机的资源是有限的,所以是没办法用二进制精确的表示 0.1,只能用「近似值」来表示,就是在有限的精度情况下,最大化接近 0.1 的二进制数,于是就会造成精度缺失的情况。
那么,BigDecimal就一定能避免上述的浮点问题吗?来看下面的示例:
@Test publicvoidtest1(){ BigDecimal a = new BigDecimal(0.01); BigDecimal b = BigDecimal.valueOf(0.01); System.out.println("a = " + a); System.out.println("b = " + b); }
结果:
a = 0.01000000000000000020816681711721685132943093776702880859375b = 0.01
上面的实例说明,即便是使用BigDecimal,结果依旧会出现精度问题。这就涉及到创建BigDecimal对象时,如果有初始值,是采用new BigDecimal的形式,还是通过BigDecimal#valueOf方法了。
之所以会出现上述现象,是因为new BigDecimal时,传入的0.1已经是浮点类型了,鉴于上面说的这个值只是近似值,在使用new BigDecimal时就把这个近似值完整的保留下来了。
而BigDecimal#valueOf则不同,它的源码实现如下:
publicstatic 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.returnnew BigDecimal(Double.toString(val)); }
在valueOf内部,使用Double#toString方法,将浮点类型的值转换成了字符串,因此就不存在精度丢失问题了。
此时就得出一个基本的结论:靠前,在使用BigDecimal构造函数时,尽量传递字符串而非浮点类型;第二,如果无法满足靠前条,则可采用BigDecimal#valueOf方法来构造初始化值。
如果比较两个BigDecimal的值是否相等,你会如何比较?使用equals方法还是compareTo方法呢?
先来看一个示例:
@Test publicvoidtest2(){ BigDecimal a = new BigDecimal("0.01"); BigDecimal b = new BigDecimal("0.010"); System.out.println(a.equals(b)); System.out.println(a.compareTo(b)); }
@Overridepublic boolean equals(Object x) { if (!(x instanceof BigDecimal)) returnfalse; BigDecimal xDec = (BigDecimal) x; if (x == this) returntrue; if (scale != xDec.scale) returnfalse; long s = this.intCompact; long xs = xDec.intCompact; if (s != INFLATED) { if (xs == INFLATED) xs = compactValFor(xDec.intVal); return xs == s; } elseif (xs != INFLATED) return xs == compactValFor(this.intVal); returnthis.inflated().equals(xDec.inflated());}
equals方法不仅比较了值是否相等,还比较了精度是否相同。上述示例中,由于两者的精度不同,所以equals方法的结果当然是false了。而compareTo方法实现了Comparable接口,真正比较的是值的大小,返回的值为-1(小于),0(等于),1(大于)。
基本结论:通常情况,如果比较两个BigDecimal值的大小,采用其实现的compareTo方法;如果严格限制精度的比较,那么则可考虑使用equals方法。
在项目中看到好多同学通过BigDecimal进行计算时不设置计算结果的精度和舍入模式,真是着急人,虽然大多数情况下不会出现什么问题。但下面的场景就不一定了:
@Testpublicvoidtest3(){ BigDecimal a = new BigDecimal("1.0"); BigDecimal b = new BigDecimal("3.0"); a.divide(b); }
执行上述代码的结果是什么?ArithmeticException异常!
总结:如果在除法(divide)运算过程中,如果商是一个无限小数(0.333…),而操作的结果预期是一个精确的数字,那么将会抛出ArithmeticException异常。
此时,只需在使用divide方法时指定结果的精度即可:
@Test publicvoidtest3(){ 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进行(所有)运算时,一定要明确指定精度和舍入模式。
当使用BigDecimal之后,需要转换成String类型,你是如何操作的?直接toString?
先来看看下面的代码:
@Testpublicvoidtest4(){ BigDecimal a = BigDecimal.valueOf(35634535255456719.22345634534124578902); System.out.println(a.toString());}
执行的结果是上述对应的值吗?并不是:3.563453525545672E+16
也就是说,本来想打印字符串的,结果打印出来的是科学计数法的值。
这里我们需要了解BigDecimal转换字符串的三个方法
基本结论:根据数据结果展示格式不同,采用不同的字符串输出方法,通常使用比较多的方法为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除法取整的方法)'的探讨分享,希望大家通过阅读小编的文章之后能够有所收获!如果大家觉得小编的文章不错的话,可以多多分享给有需要的人。